thingsboard-developers

Merge pull request #794 from thingsboard/develop/2.0 TB

5/28/2018 1:00:25 PM

Changes

application/pom.xml 57(+9 -48)

application/src/main/data/json/demo/plugins/demo_device_messaging_rpc_plugin.json 13(+0 -13)

application/src/main/data/json/demo/plugins/demo_email_plugin.json 28(+0 -28)

application/src/main/data/json/demo/plugins/demo_time_rpc_plugin.json 11(+0 -11)

application/src/main/data/json/demo/rules/demo_alarm_rule.json 46(+0 -46)

application/src/main/data/json/demo/rules/demo_gettime_rpc_rule.json 35(+0 -35)

application/src/main/data/json/demo/rules/demo_messaging_rpc_rule.json 38(+0 -38)

application/src/main/data/json/system/plugins/system_rpc_plugin.json 11(+0 -11)

application/src/main/data/json/system/plugins/system_telemetry_plugin.json 9(+0 -9)

application/src/main/data/json/system/rules/system_telemetry_rule.json 29(+0 -29)

application/src/main/java/org/thingsboard/server/actors/plugin/PluginActor.java 151(+0 -151)

application/src/main/java/org/thingsboard/server/actors/plugin/PluginActorMessageProcessor.java 251(+0 -251)

application/src/main/java/org/thingsboard/server/actors/plugin/PluginCallbackMessage.java 53(+0 -53)

application/src/main/java/org/thingsboard/server/actors/plugin/PluginProcessingContext.java 546(+0 -546)

application/src/main/java/org/thingsboard/server/actors/plugin/RuleToPluginMsgWrapper.java 66(+0 -66)

application/src/main/java/org/thingsboard/server/actors/plugin/SharedPluginProcessingContext.java 141(+0 -141)

application/src/main/java/org/thingsboard/server/actors/rule/ChainProcessingContext.java 117(+0 -117)

application/src/main/java/org/thingsboard/server/actors/rule/RuleActor.java 90(+0 -90)

application/src/main/java/org/thingsboard/server/actors/rule/RuleActorMessageProcessor.java 354(+0 -354)

application/src/main/java/org/thingsboard/server/actors/rule/RuleActorMetaData.java 107(+0 -107)

application/src/main/java/org/thingsboard/server/actors/rule/RuleProcessingContext.java 115(+0 -115)

application/src/main/java/org/thingsboard/server/actors/shared/rule/RuleManager.java 135(+0 -135)

application/src/main/java/org/thingsboard/server/controller/plugin/PluginApiController.java 106(+0 -106)

application/src/main/java/org/thingsboard/server/controller/PluginController.java 240(+0 -240)

application/src/main/java/org/thingsboard/server/controller/RuleController.java 239(+0 -239)

application/src/main/java/org/thingsboard/server/service/cluster/rpc/RpcSessionCreationFuture.java 63(+0 -63)

application/src/test/java/org/thingsboard/server/actors/DefaultActorServiceTest.java 245(+0 -245)

application/src/test/java/org/thingsboard/server/actors/DummySessionID.java 63(+0 -63)

application/src/test/java/org/thingsboard/server/controller/BasePluginControllerTest.java 232(+0 -232)

application/src/test/java/org/thingsboard/server/controller/BaseRuleControllerTest.java 247(+0 -247)

common/pom.xml 2(+1 -1)

dao/pom.xml 25(+10 -15)

dao/src/main/java/org/thingsboard/server/dao/plugin/BasePluginService.java 261(+0 -261)

dao/src/main/java/org/thingsboard/server/dao/plugin/CassandraBasePluginDao.java 120(+0 -120)

dao/src/main/java/org/thingsboard/server/dao/plugin/PluginService.java 55(+0 -55)

dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleService.java 300(+0 -300)

dao/src/main/java/org/thingsboard/server/dao/rule/CassandraBaseRuleDao.java 107(+0 -107)

dao/src/main/java/org/thingsboard/server/dao/sql/plugin/JpaBasePluginDao.java 132(+0 -132)

dao/src/main/java/org/thingsboard/server/dao/sql/plugin/PluginMetaDataRepository.java 51(+0 -51)

dao/src/main/java/org/thingsboard/server/dao/sql/rule/JpaBaseRuleDao.java 122(+0 -122)

dao/src/test/java/org/thingsboard/server/dao/service/plugin/BasePluginServiceTest.java 114(+0 -114)

dao/src/test/java/org/thingsboard/server/dao/service/rule/BaseRuleServiceTest.java 163(+0 -163)

dao/src/test/java/org/thingsboard/server/dao/sql/plugin/JpaBasePluginDaoTest.java 102(+0 -102)

dao/src/test/java/org/thingsboard/server/dao/sql/rule/JpaBaseRuleDaoTest.java 156(+0 -156)

extensions/extension-kafka/pom.xml 97(+0 -97)

extensions/extension-kafka/src/assembly/extension.xml 39(+0 -39)

extensions/extension-kafka/src/main/java/org/thingsboard/server/extensions/kafka/action/KafkaActionMsg.java 28(+0 -28)

extensions/extension-kafka/src/main/java/org/thingsboard/server/extensions/kafka/action/KafkaActionPayload.java 34(+0 -34)

extensions/extension-kafka/src/main/java/org/thingsboard/server/extensions/kafka/action/KafkaPluginAction.java 45(+0 -45)

extensions/extension-kafka/src/main/java/org/thingsboard/server/extensions/kafka/action/KafkaPluginActionConfiguration.java 26(+0 -26)

extensions/extension-kafka/src/main/java/org/thingsboard/server/extensions/kafka/plugin/KafkaMsgHandler.java 63(+0 -63)

extensions/extension-kafka/src/main/java/org/thingsboard/server/extensions/kafka/plugin/KafkaPlugin.java 94(+0 -94)

extensions/extension-kafka/src/main/java/org/thingsboard/server/extensions/kafka/plugin/KafkaPluginConfiguration.java 34(+0 -34)

extensions/extension-kafka/src/main/resources/KafkaActionDescriptor.json 34(+0 -34)

extensions/extension-kafka/src/main/resources/KafkaPluginDescriptor.json 80(+0 -80)

extensions/extension-kafka/src/test/java/org/thingsboard/server/extensions/kafka/KafkaDemoClient.java 131(+0 -131)

extensions/extension-kafka/src/test/resources/logback.xml 10(+0 -10)

extensions/extension-mqtt/pom.xml 98(+0 -98)

extensions/extension-mqtt/src/assembly/extension.xml 34(+0 -34)

extensions/extension-mqtt/src/main/java/org/thingsboard/server/extensions/mqtt/action/MqttActionMsg.java 28(+0 -28)

extensions/extension-mqtt/src/main/java/org/thingsboard/server/extensions/mqtt/action/MqttActionPayload.java 34(+0 -34)

extensions/extension-mqtt/src/main/java/org/thingsboard/server/extensions/mqtt/action/MqttPluginAction.java 43(+0 -43)

extensions/extension-mqtt/src/main/java/org/thingsboard/server/extensions/mqtt/action/MqttPluginActionConfiguration.java 26(+0 -26)

extensions/extension-mqtt/src/main/java/org/thingsboard/server/extensions/mqtt/plugin/MqttMsgHandler.java 70(+0 -70)

extensions/extension-mqtt/src/main/java/org/thingsboard/server/extensions/mqtt/plugin/MqttPlugin.java 130(+0 -130)

extensions/extension-mqtt/src/main/java/org/thingsboard/server/extensions/mqtt/plugin/MqttPluginConfiguration.java 28(+0 -28)

extensions/extension-mqtt/src/main/resources/MqttActionDescriptor.json 32(+0 -32)

extensions/extension-mqtt/src/main/resources/MqttPluginDescriptor.json 48(+0 -48)

extensions/extension-rabbitmq/pom.xml 139(+0 -139)

extensions/extension-rabbitmq/src/assembly/extension.xml 34(+0 -34)

extensions/extension-rabbitmq/src/main/java/org/thingsboard/server/extensions/rabbitmq/action/RabbitMqActionMsg.java 31(+0 -31)

extensions/extension-rabbitmq/src/main/java/org/thingsboard/server/extensions/rabbitmq/action/RabbitMqActionPayload.java 39(+0 -39)

extensions/extension-rabbitmq/src/main/java/org/thingsboard/server/extensions/rabbitmq/action/RabbitMqPluginAction.java 49(+0 -49)

extensions/extension-rabbitmq/src/main/java/org/thingsboard/server/extensions/rabbitmq/action/RabbitMqPluginActionConfiguration.java 32(+0 -32)

extensions/extension-rabbitmq/src/main/java/org/thingsboard/server/extensions/rabbitmq/plugin/RabbitMqMsgHandler.java 86(+0 -86)

extensions/extension-rabbitmq/src/main/java/org/thingsboard/server/extensions/rabbitmq/plugin/RabbitMqPlugin.java 109(+0 -109)

extensions/extension-rabbitmq/src/main/java/org/thingsboard/server/extensions/rabbitmq/plugin/RabbitMqPluginConfiguration.java 47(+0 -47)

extensions/extension-rabbitmq/src/main/resources/RabbitMqActionDescriptor.json 77(+0 -77)

extensions/extension-rabbitmq/src/main/resources/RabbitMqPluginDescriptor.json 79(+0 -79)

extensions/extension-rabbitmq/src/test/java/org/thingsboard/server/extensions/rabbitmq/RabbitMqDemoClient.java 56(+0 -56)

extensions/extension-rest-api-call/pom.xml 98(+0 -98)

extensions/extension-rest-api-call/src/assembly/extension.xml 34(+0 -34)

extensions/extension-rest-api-call/src/main/java/org/thingsboard/server/extensions/rest/action/RestApiCallActionMsg.java 28(+0 -28)

extensions/extension-rest-api-call/src/main/java/org/thingsboard/server/extensions/rest/action/RestApiCallActionPayload.java 37(+0 -37)

extensions/extension-rest-api-call/src/main/java/org/thingsboard/server/extensions/rest/action/RestApiCallPluginAction.java 51(+0 -51)

extensions/extension-rest-api-call/src/main/java/org/thingsboard/server/extensions/rest/action/RestApiCallPluginActionConfiguration.java 28(+0 -28)

extensions/extension-rest-api-call/src/main/java/org/thingsboard/server/extensions/rest/plugin/RestApiCallMsgHandler.java 67(+0 -67)

extensions/extension-rest-api-call/src/main/java/org/thingsboard/server/extensions/rest/plugin/RestApiCallPlugin.java 98(+0 -98)

extensions/extension-rest-api-call/src/main/java/org/thingsboard/server/extensions/rest/plugin/RestApiCallPluginConfiguration.java 37(+0 -37)

extensions/extension-rest-api-call/src/main/resources/RestApiCallActionDescriptor.json 61(+0 -61)

extensions/extension-rest-api-call/src/main/resources/RestApiCallPluginDescriptor.json 106(+0 -106)

extensions/extension-rest-api-call/src/test/java/org/thingsboard/server/extensions/rest/RestApiCallDemoClient.java 70(+0 -70)

extensions/extension-sns/pom.xml 81(+0 -81)

extensions/extension-sns/src/assembly/extension.xml 37(+0 -37)

extensions/extension-sns/src/main/java/org/thingsboard/server/extensions/sns/action/SnsTopicActionMsg.java 31(+0 -31)

extensions/extension-sns/src/main/java/org/thingsboard/server/extensions/sns/action/SnsTopicActionPayload.java 37(+0 -37)

extensions/extension-sns/src/main/java/org/thingsboard/server/extensions/sns/action/SnsTopicPluginAction.java 45(+0 -45)

extensions/extension-sns/src/main/java/org/thingsboard/server/extensions/sns/action/SnsTopicPluginActionConfiguration.java 30(+0 -30)

extensions/extension-sns/src/main/java/org/thingsboard/server/extensions/sns/plugin/SnsMessageHandler.java 63(+0 -63)

extensions/extension-sns/src/main/java/org/thingsboard/server/extensions/sns/plugin/SnsPlugin.java 79(+0 -79)

extensions/extension-sns/src/main/java/org/thingsboard/server/extensions/sns/plugin/SnsPluginConfiguration.java 30(+0 -30)

extensions/extension-sns/src/main/resources/SnsPluginDescriptor.json 30(+0 -30)

extensions/extension-sns/src/main/resources/SnsTopicActionDescriptor.json 34(+0 -34)

extensions/extension-sqs/pom.xml 81(+0 -81)

extensions/extension-sqs/src/assembly/extension.xml 37(+0 -37)

extensions/extension-sqs/src/main/java/org/thingsboard/server/extensions/sqs/action/fifo/SqsFifoQueueActionMsg.java 31(+0 -31)

extensions/extension-sqs/src/main/java/org/thingsboard/server/extensions/sqs/action/fifo/SqsFifoQueueActionPayload.java 39(+0 -39)

extensions/extension-sqs/src/main/java/org/thingsboard/server/extensions/sqs/action/fifo/SqsFifoQueuePluginAction.java 49(+0 -49)

extensions/extension-sqs/src/main/java/org/thingsboard/server/extensions/sqs/action/fifo/SqsFifoQueuePluginActionConfiguration.java 30(+0 -30)

extensions/extension-sqs/src/main/java/org/thingsboard/server/extensions/sqs/action/standard/SqsStandardQueueActionMsg.java 31(+0 -31)

extensions/extension-sqs/src/main/java/org/thingsboard/server/extensions/sqs/action/standard/SqsStandardQueueActionPayload.java 39(+0 -39)

extensions/extension-sqs/src/main/java/org/thingsboard/server/extensions/sqs/action/standard/SqsStandardQueuePluginAction.java 46(+0 -46)

extensions/extension-sqs/src/main/java/org/thingsboard/server/extensions/sqs/action/standard/SqsStandardQueuePluginActionConfiguration.java 32(+0 -32)

extensions/extension-sqs/src/main/java/org/thingsboard/server/extensions/sqs/plugin/SqsMessageHandler.java 84(+0 -84)

extensions/extension-sqs/src/main/java/org/thingsboard/server/extensions/sqs/plugin/SqsPlugin.java 78(+0 -78)

extensions/extension-sqs/src/main/java/org/thingsboard/server/extensions/sqs/plugin/SqsPluginConfiguration.java 30(+0 -30)

extensions/extension-sqs/src/main/resources/SqsFifoQueueActionDescriptor.json 34(+0 -34)

extensions/extension-sqs/src/main/resources/SqsPluginDescriptor.json 30(+0 -30)

extensions/extension-sqs/src/main/resources/SqsStandardQueueActionDescriptor.json 41(+0 -41)

extensions/extension-sqs/src/test/java/org/thingsboard/server/extensions/sqs/SqsDemoClient.java 69(+0 -69)

extensions/extension-sqs/src/test/resources/logback.xml 10(+0 -10)

extensions/pom.xml 46(+0 -46)

extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/AbstractPlugin.java 88(+0 -88)

extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/handlers/DefaultRestMsgHandler.java 72(+0 -72)

extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/handlers/DefaultRuleMsgHandler.java 63(+0 -63)

extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/handlers/DefaultWebsocketMsgHandler.java 104(+0 -104)

extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/msg/AbstractPluginToRuleMsg.java 63(+0 -63)

extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/msg/AbstractRuleToPluginMsg.java 72(+0 -72)

extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/msg/PluginToRuleMsg.java 64(+0 -64)

extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/msg/RuleToPluginMsg.java 61(+0 -61)

extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/Plugin.java 54(+0 -54)

extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/PluginApiCallSecurityContext.java 80(+0 -80)

extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/PluginContext.java 128(+0 -128)

extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/rest/BasicPluginRestMsg.java 63(+0 -63)

extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/rest/RestRequest.java 87(+0 -87)

extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/ws/BasicPluginWebsocketSessionRef.java 111(+0 -111)

extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/ws/msg/AbstractPluginWebSocketMsg.java 66(+0 -66)

extensions-api/src/main/resources/EmptyJsonDescriptor.json 12(+0 -12)

extensions-core/src/main/java/org/thingsboard/server/extensions/core/action/mail/SendMailAction.java 109(+0 -109)

extensions-core/src/main/java/org/thingsboard/server/extensions/core/action/rpc/RpcPluginAction.java 68(+0 -68)

extensions-core/src/main/java/org/thingsboard/server/extensions/core/action/rpc/ServerSideRpcCallAction.java 107(+0 -107)

extensions-core/src/main/java/org/thingsboard/server/extensions/core/action/telemetry/TelemetryPluginAction.java 84(+0 -84)

extensions-core/src/main/java/org/thingsboard/server/extensions/core/action/template/AbstractTemplatePluginAction.java 87(+0 -87)

extensions-core/src/main/java/org/thingsboard/server/extensions/core/filter/BasicJsFilter.java 78(+0 -78)

extensions-core/src/main/java/org/thingsboard/server/extensions/core/filter/DeviceAttributesFilter.java 57(+0 -57)

extensions-core/src/main/java/org/thingsboard/server/extensions/core/filter/DeviceTypeFilter.java 52(+0 -52)

extensions-core/src/main/java/org/thingsboard/server/extensions/core/filter/DeviceTypeFilterConfiguration.java 33(+0 -33)

extensions-core/src/main/java/org/thingsboard/server/extensions/core/filter/MethodNameFilter.java 56(+0 -56)

extensions-core/src/main/java/org/thingsboard/server/extensions/core/filter/MethodNameFilterConfiguration.java 33(+0 -33)

extensions-core/src/main/java/org/thingsboard/server/extensions/core/filter/MsgTypeFilter.java 67(+0 -67)

extensions-core/src/main/java/org/thingsboard/server/extensions/core/filter/MsgTypeFilterConfiguration.java 28(+0 -28)

extensions-core/src/main/java/org/thingsboard/server/extensions/core/filter/NashornJsEvaluator.java 131(+0 -131)

extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/KeyValuePluginProperties.java 27(+0 -27)

extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/mail/MailPlugin.java 129(+0 -129)

extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/mail/MailPluginConfiguration.java 33(+0 -33)

extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/messaging/DeviceMessagingPlugin.java 69(+0 -69)

extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/messaging/DeviceMessagingPluginConfiguration.java 30(+0 -30)

extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/messaging/DeviceMessagingRuleMsgHandler.java 228(+0 -228)

extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/messaging/PendingRpcRequestMetadata.java 37(+0 -37)

extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/rpc/cmd/RpcRequest.java 28(+0 -28)

extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/rpc/handlers/RpcRestMsgHandler.java 161(+0 -161)

extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/rpc/handlers/RpcRuleMsgHandler.java 102(+0 -102)

extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/rpc/LocalRequestMetaData.java 30(+0 -30)

extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/rpc/RpcManager.java 69(+0 -69)

extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/rpc/RpcPlugin.java 86(+0 -86)

extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/rpc/RpcPluginConfiguration.java 26(+0 -26)

extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/AttributeData.java 48(+0 -48)

extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/cmd/AttributesSubscriptionCmd.java 32(+0 -32)

extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/cmd/GetHistoryCmd.java 40(+0 -40)

extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/cmd/SubscriptionCmd.java 42(+0 -42)

extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/cmd/TelemetryPluginCmd.java 29(+0 -29)

extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/cmd/TelemetryPluginCmdsWrapper.java 58(+0 -58)

extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/cmd/TimeseriesSubscriptionCmd.java 41(+0 -41)

extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/handlers/BiPluginCallBack.java 74(+0 -74)

extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/handlers/TelemetryFeature.java 29(+0 -29)

extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/handlers/TelemetryRestMsgHandler.java 441(+0 -441)

extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/handlers/TelemetryRpcMsgHandler.java 300(+0 -300)

extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/handlers/TelemetryRuleMsgHandler.java 152(+0 -152)

extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/handlers/TelemetryWebsocketMsgHandler.java 395(+0 -395)

extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/sub/Subscription.java 78(+0 -78)

extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/sub/SubscriptionErrorCode.java 50(+0 -50)

extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/sub/SubscriptionState.java 69(+0 -69)

extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/sub/SubscriptionType.java 23(+0 -23)

extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/sub/SubscriptionUpdate.java 93(+0 -93)

extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/SubscriptionManager.java 365(+0 -365)

extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/TelemetryStoragePlugin.java 106(+0 -106)

extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/TsData.java 42(+0 -42)

extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/time/TimePlugin.java 92(+0 -92)

extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/time/TimePluginConfiguration.java 26(+0 -26)

extensions-core/src/main/java/org/thingsboard/server/extensions/core/processor/AlarmDeduplicationProcessor.java 84(+0 -84)

extensions-core/src/main/java/org/thingsboard/server/extensions/core/processor/AlarmDeduplicationProcessorConfiguration.java 29(+0 -29)

extensions-core/src/main/java/org/thingsboard/server/extensions/core/processor/AlarmProcessor.java 250(+0 -250)

extensions-core/src/main/java/org/thingsboard/server/extensions/core/processor/AlarmProcessorConfiguration.java 39(+0 -39)

extensions-core/src/main/java/org/thingsboard/server/extensions/core/utils/VelocityUtils.java 105(+0 -105)

extensions-core/src/main/proto/telemetry.proto 82(+0 -82)

extensions-core/src/main/resources/AlarmDeduplicationProcessorDescriptor.json 30(+0 -30)

extensions-core/src/main/resources/AlarmProcessorDescriptor.json 121(+0 -121)

extensions-core/src/main/resources/DeviceMessagingPluginDescriptor.json 37(+0 -37)

extensions-core/src/main/resources/DeviceTypeFilterDescriptor.json 28(+0 -28)

extensions-core/src/main/resources/JsFilterDescriptor.json 21(+0 -21)

extensions-core/src/main/resources/MailPluginData.json 16(+0 -16)

extensions-core/src/main/resources/MailPluginDescriptor.json 61(+0 -61)

extensions-core/src/main/resources/MethodNameFilterDescriptor.json 28(+0 -28)

extensions-core/src/main/resources/MsgTypeFilterDescriptor.json 40(+0 -40)

extensions-core/src/main/resources/RpcPluginData.json 3(+0 -3)

extensions-core/src/main/resources/RpcPluginDescriptor.json 18(+0 -18)

extensions-core/src/main/resources/SendMailActionData.json 7(+0 -7)

extensions-core/src/main/resources/SendMailActionDescriptor.json 55(+0 -55)

extensions-core/src/main/resources/ServerSideRpcCallActionDescriptor.json 57(+0 -57)

extensions-core/src/main/resources/TelemetryPluginActionDescriptor.json 50(+0 -50)

extensions-core/src/main/resources/TimePluginDescriptor.json 16(+0 -16)

extensions-core/src/test/java/org/thingsboard/server/extensions/core/filter/DeviceAttributesFilterTest.java 129(+0 -129)

netty-mqtt/pom.xml 99(+99 -0)

pom.xml 97(+31 -66)

resume.bat 18(+0 -18)

rule-engine/pom.xml 41(+41 -0)

tools/pom.xml 2(+1 -1)

ui/package.json 3(+2 -1)

ui/pom.xml 2(+1 -1)

ui/server.js 20(+20 -0)

ui/src/app/api/plugin.service.js 218(+0 -218)

ui/src/app/api/rule.service.js 186(+0 -186)

ui/src/app/component/component.directive.js 75(+0 -75)

ui/src/app/component/component.tpl.html 58(+0 -58)

ui/src/app/component/component-dialog.controller.js 105(+0 -105)

ui/src/app/component/component-dialog.service.js 60(+0 -60)

ui/src/app/component/component-dialog.tpl.html 79(+0 -79)

ui/src/app/component/index.js 28(+0 -28)

ui/src/app/components/plugin-select.directive.js 115(+0 -115)

ui/src/app/components/plugin-select.scss 37(+0 -37)

ui/src/app/components/plugin-select.tpl.html 44(+0 -44)

ui/src/app/plugin/add-plugin.tpl.html 48(+0 -48)

ui/src/app/plugin/index.js 36(+0 -36)

ui/src/app/plugin/plugin.controller.js 218(+0 -218)

ui/src/app/plugin/plugin.directive.js 94(+0 -94)

ui/src/app/plugin/plugin.routes.js 46(+0 -46)

ui/src/app/plugin/plugin.scss 18(+0 -18)

ui/src/app/plugin/plugin-card.tpl.html 19(+0 -19)

ui/src/app/plugin/plugin-fieldset.tpl.html 90(+0 -90)

ui/src/app/plugin/plugins.tpl.html 77(+0 -77)

ui/src/app/rule/add-rule.tpl.html 48(+0 -48)

ui/src/app/rule/index.js 40(+0 -40)

ui/src/app/rule/rule.controller.js 210(+0 -210)

ui/src/app/rule/rule.directive.js 191(+0 -191)

ui/src/app/rule/rule.routes.js 46(+0 -46)

ui/src/app/rule/rule.scss 55(+0 -55)

ui/src/app/rule/rule-card.tpl.html 19(+0 -19)

ui/src/app/rule/rule-fieldset.tpl.html 219(+0 -219)

ui/src/app/rule/rules.tpl.html 77(+0 -77)

Details

application/pom.xml 57(+9 -48)

diff --git a/application/pom.xml b/application/pom.xml
index 8449246..e0b6b98 100644
--- a/application/pom.xml
+++ b/application/pom.xml
@@ -20,10 +20,9 @@
     <modelVersion>4.0.0</modelVersion>
     <parent>
         <groupId>org.thingsboard</groupId>
-        <version>1.4.1-SNAPSHOT</version>
+        <version>2.0.0-SNAPSHOT</version>
         <artifactId>thingsboard</artifactId>
     </parent>
-    <groupId>org.thingsboard</groupId>
     <artifactId>application</artifactId>
     <packaging>jar</packaging>
 
@@ -50,12 +49,12 @@
             <classifier>linux-x86_64</classifier>
         </dependency>
         <dependency>
-            <groupId>org.thingsboard</groupId>
-            <artifactId>extensions-api</artifactId>
+            <groupId>org.thingsboard.rule-engine</groupId>
+            <artifactId>rule-engine-api</artifactId>
         </dependency>
         <dependency>
-            <groupId>org.thingsboard</groupId>
-            <artifactId>extensions-core</artifactId>
+            <groupId>org.thingsboard.rule-engine</groupId>
+            <artifactId>rule-engine-components</artifactId>
         </dependency>
         <dependency>
             <groupId>org.thingsboard.common</groupId>
@@ -257,6 +256,10 @@
             <groupId>org.hsqldb</groupId>
             <artifactId>hsqldb</artifactId>
         </dependency>
+        <dependency>
+            <groupId>org.javadelight</groupId>
+            <artifactId>delight-nashorn-sandbox</artifactId>
+        </dependency>
     </dependencies>
 
     <build>
@@ -464,48 +467,6 @@
                 <artifactId>maven-dependency-plugin</artifactId>
                 <executions>
                     <execution>
-                        <id>copy-extensions</id>
-                        <phase>package</phase>
-                        <goals>
-                            <goal>copy</goal>
-                        </goals>
-                        <configuration>
-                            <outputDirectory>${project.build.directory}/extensions</outputDirectory>
-                            <artifactItems>
-                                <artifactItem>
-                                    <groupId>org.thingsboard.extensions</groupId>
-                                    <artifactId>extension-rabbitmq</artifactId>
-                                    <classifier>extension</classifier>
-                                </artifactItem>
-                                <artifactItem>
-                                    <groupId>org.thingsboard.extensions</groupId>
-                                    <artifactId>extension-rest-api-call</artifactId>
-                                    <classifier>extension</classifier>
-                                </artifactItem>
-                                <artifactItem>
-                                    <groupId>org.thingsboard.extensions</groupId>
-                                    <artifactId>extension-kafka</artifactId>
-                                    <classifier>extension</classifier>
-                                </artifactItem>
-                                <artifactItem>
-                                    <groupId>org.thingsboard.extensions</groupId>
-                                    <artifactId>extension-mqtt</artifactId>
-                                    <classifier>extension</classifier>
-                                </artifactItem>
-                                <artifactItem>
-                                    <groupId>org.thingsboard.extensions</groupId>
-                                    <artifactId>extension-sqs</artifactId>
-                                    <classifier>extension</classifier>
-                                </artifactItem>
-                                <artifactItem>
-                                    <groupId>org.thingsboard.extensions</groupId>
-                                    <artifactId>extension-sns</artifactId>
-                                    <classifier>extension</classifier>
-                                </artifactItem>
-                            </artifactItems>
-                        </configuration>
-                    </execution>
-                    <execution>
                         <id>copy-winsw-service</id>
                         <phase>package</phase>
                         <goals>
diff --git a/application/src/main/conf/thingsboard.conf b/application/src/main/conf/thingsboard.conf
index a6e404d..2baee7d 100644
--- a/application/src/main/conf/thingsboard.conf
+++ b/application/src/main/conf/thingsboard.conf
@@ -14,7 +14,7 @@
 # limitations under the License.
 #
 
-export JAVA_OPTS="$JAVA_OPTS -Dplatform=@pkg.platform@"
+export JAVA_OPTS="$JAVA_OPTS -Dplatform=@pkg.platform@ -Dinstall.data_dir=@pkg.installFolder@"
 export LOG_FILENAME=${pkg.name}.out
 export LOADER_PATH=${pkg.installFolder}/conf,${pkg.installFolder}/extensions
 export SQL_DATA_FOLDER=${pkg.installFolder}/data/sql
diff --git a/application/src/main/data/json/tenant/rule_chains/root_rule_chain.json b/application/src/main/data/json/tenant/rule_chains/root_rule_chain.json
new file mode 100644
index 0000000..225ccdc
--- /dev/null
+++ b/application/src/main/data/json/tenant/rule_chains/root_rule_chain.json
@@ -0,0 +1,98 @@
+{
+  "ruleChain": {
+    "additionalInfo": null,
+    "name": "Root Rule Chain",
+    "firstRuleNodeId": null,
+    "root": true,
+    "debugMode": false,
+    "configuration": null
+  },
+  "metadata": {
+    "firstNodeIndex": 2,
+    "nodes": [
+      {
+        "additionalInfo": {
+          "layoutX": 824,
+          "layoutY": 156
+        },
+        "type": "org.thingsboard.rule.engine.telemetry.TbMsgTimeseriesNode",
+        "name": "SaveTS",
+        "debugMode": false,
+        "configuration": {
+          "defaultTTL": 0
+        }
+      },
+      {
+        "additionalInfo": {
+          "layoutX": 825,
+          "layoutY": 52
+        },
+        "type": "org.thingsboard.rule.engine.telemetry.TbMsgAttributesNode",
+        "name": "save client attributes",
+        "debugMode": false,
+        "configuration": {
+          "scope": "CLIENT_SCOPE"
+        }
+      },
+      {
+        "additionalInfo": {
+          "layoutX": 347,
+          "layoutY": 149
+        },
+        "type": "org.thingsboard.rule.engine.filter.TbMsgTypeSwitchNode",
+        "name": "Message Type Switch",
+        "debugMode": false,
+        "configuration": {
+          "version": 0
+        }
+      },
+      {
+        "additionalInfo": {
+          "layoutX": 825,
+          "layoutY": 266
+        },
+        "type": "org.thingsboard.rule.engine.action.TbLogNode",
+        "name": "Log RPC",
+        "debugMode": false,
+        "configuration": {
+          "jsScript": "return '\\nIncoming message:\\n' + JSON.stringify(msg) + '\\nIncoming metadata:\\n' + JSON.stringify(metadata);"
+        }
+      },
+      {
+        "additionalInfo": {
+          "layoutX": 825,
+          "layoutY": 379
+        },
+        "type": "org.thingsboard.rule.engine.action.TbLogNode",
+        "name": "Log Other",
+        "debugMode": false,
+        "configuration": {
+          "jsScript": "return '\\nIncoming message:\\n' + JSON.stringify(msg) + '\\nIncoming metadata:\\n' + JSON.stringify(metadata);"
+        }
+      }
+    ],
+    "connections": [
+      {
+        "fromIndex": 2,
+        "toIndex": 4,
+        "type": "Other"
+      },
+      {
+        "fromIndex": 2,
+        "toIndex": 1,
+        "type": "Post attributes"
+      },
+      {
+        "fromIndex": 2,
+        "toIndex": 0,
+        "type": "Post telemetry"
+      },
+      {
+        "fromIndex": 2,
+        "toIndex": 3,
+        "type": "RPC Request"
+      }
+    ],
+    "ruleChainConnections": null
+  }
+}
\ No newline at end of file
diff --git a/application/src/main/data/upgrade/2.0.0/schema_update.cql b/application/src/main/data/upgrade/2.0.0/schema_update.cql
new file mode 100644
index 0000000..5f46878
--- /dev/null
+++ b/application/src/main/data/upgrade/2.0.0/schema_update.cql
@@ -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.
+--
+
+CREATE TABLE IF NOT EXISTS thingsboard.msg_queue (
+    node_id         timeuuid,
+    cluster_partition    bigint,
+    ts_partition       bigint,
+    ts              bigint,
+    msg             blob,
+	PRIMARY KEY ((node_id, cluster_partition, ts_partition), ts))
+WITH CLUSTERING ORDER BY (ts DESC)
+AND compaction = {
+    'class': 'org.apache.cassandra.db.compaction.DateTieredCompactionStrategy',
+    'min_threshold': '5',
+    'base_time_seconds': '43200',
+    'max_window_size_seconds': '43200',
+    'tombstone_threshold': '0.9',
+    'unchecked_tombstone_compaction': 'true'
+};
+
+CREATE TABLE IF NOT EXISTS thingsboard.msg_ack_queue (
+    node_id         timeuuid,
+    cluster_partition    bigint,
+    ts_partition       bigint,
+    msg_id              timeuuid,
+	PRIMARY KEY ((node_id, cluster_partition, ts_partition), msg_id))
+WITH CLUSTERING ORDER BY (msg_id DESC)
+AND compaction = {
+    'class': 'org.apache.cassandra.db.compaction.DateTieredCompactionStrategy',
+    'min_threshold': '5',
+    'base_time_seconds': '43200',
+    'max_window_size_seconds': '43200',
+    'tombstone_threshold': '0.9',
+    'unchecked_tombstone_compaction': 'true'
+};
+
+CREATE TABLE IF NOT EXISTS thingsboard.processed_msg_partitions (
+    node_id         timeuuid,
+    cluster_partition    bigint,
+    ts_partition       bigint,
+	PRIMARY KEY ((node_id, cluster_partition), ts_partition))
+WITH CLUSTERING ORDER BY (ts_partition DESC)
+AND compaction = {
+    'class': 'org.apache.cassandra.db.compaction.DateTieredCompactionStrategy',
+    'min_threshold': '5',
+    'base_time_seconds': '43200',
+    'max_window_size_seconds': '43200',
+    'tombstone_threshold': '0.9',
+    'unchecked_tombstone_compaction': 'true'
+};
+
+CREATE TABLE IF NOT EXISTS  thingsboard.rule_chain (
+    id uuid,
+    tenant_id uuid,
+    name text,
+    search_text text,
+    first_rule_node_id uuid,
+    root boolean,
+    debug_mode boolean,
+    configuration text,
+    additional_info text,
+    PRIMARY KEY (id, tenant_id)
+);
+
+CREATE MATERIALIZED VIEW IF NOT EXISTS thingsboard.rule_chain_by_tenant_and_search_text AS
+    SELECT *
+    from thingsboard.rule_chain
+    WHERE tenant_id IS NOT NULL AND search_text IS NOT NULL AND id IS NOT NULL
+    PRIMARY KEY ( tenant_id, search_text, id )
+    WITH CLUSTERING ORDER BY ( search_text ASC, id DESC );
+
+CREATE TABLE IF NOT EXISTS  thingsboard.rule_node (
+    id uuid,
+    rule_chain_id uuid,
+    type text,
+    name text,
+    debug_mode boolean,
+    search_text text,
+    configuration text,
+    additional_info text,
+    PRIMARY KEY (id)
+);
+
+DROP MATERIALIZED VIEW IF EXISTS thingsboard.rule_by_plugin_token;
+DROP MATERIALIZED VIEW IF EXISTS thingsboard.rule_by_tenant_and_search_text;
+DROP MATERIALIZED VIEW IF EXISTS thingsboard.plugin_by_api_token;
+DROP MATERIALIZED VIEW IF EXISTS thingsboard.plugin_by_tenant_and_search_text;
+
+DROP TABLE IF EXISTS thingsboard.rule;
+DROP TABLE IF EXISTS thingsboard.plugin;
diff --git a/application/src/main/data/upgrade/2.0.0/schema_update.sql b/application/src/main/data/upgrade/2.0.0/schema_update.sql
new file mode 100644
index 0000000..f16e334
--- /dev/null
+++ b/application/src/main/data/upgrade/2.0.0/schema_update.sql
@@ -0,0 +1,44 @@
+--
+-- 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.
+--
+
+CREATE TABLE IF NOT EXISTS rule_chain (
+    id varchar(31) NOT NULL CONSTRAINT rule_chain_pkey PRIMARY KEY,
+    additional_info varchar,
+    configuration varchar(10000000),
+    name varchar(255),
+    first_rule_node_id varchar(31),
+    root boolean,
+    debug_mode boolean,
+    search_text varchar(255),
+    tenant_id varchar(31)
+);
+
+CREATE TABLE IF NOT EXISTS rule_node (
+    id varchar(31) NOT NULL CONSTRAINT rule_node_pkey PRIMARY KEY,
+    rule_chain_id varchar(31),
+    additional_info varchar,
+    configuration varchar(10000000),
+    type varchar(255),
+    name varchar(255),
+    debug_mode boolean,
+    search_text varchar(255)
+);
+
+DROP TABLE rule;
+DROP TABLE plugin;
+
+DELETE FROM alarm WHERE originator_type = 3 OR originator_type = 4;
+UPDATE alarm SET originator_type = (originator_type - 2) where  originator_type > 2;
diff --git a/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java b/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java
index 77953c9..d33734e 100644
--- a/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java
+++ b/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java
@@ -21,22 +21,27 @@ import akka.actor.Scheduler;
 import com.fasterxml.jackson.databind.JsonNode;
 import com.fasterxml.jackson.databind.ObjectMapper;
 import com.fasterxml.jackson.databind.node.ObjectNode;
+import com.google.common.util.concurrent.FutureCallback;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
 import com.typesafe.config.Config;
 import com.typesafe.config.ConfigFactory;
 import lombok.Getter;
 import lombok.Setter;
+import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.beans.factory.annotation.Value;
 import org.springframework.stereotype.Component;
+import org.thingsboard.rule.engine.api.MailService;
 import org.thingsboard.server.actors.service.ActorService;
 import org.thingsboard.server.common.data.DataConstants;
 import org.thingsboard.server.common.data.Event;
 import org.thingsboard.server.common.data.id.EntityId;
 import org.thingsboard.server.common.data.id.TenantId;
 import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent;
+import org.thingsboard.server.common.msg.TbMsg;
 import org.thingsboard.server.common.msg.cluster.ServerAddress;
 import org.thingsboard.server.common.transport.auth.DeviceAuthService;
-import org.thingsboard.server.controller.plugin.PluginWebSocketMsgEndpoint;
 import org.thingsboard.server.dao.alarm.AlarmService;
 import org.thingsboard.server.dao.asset.AssetService;
 import org.thingsboard.server.dao.attributes.AttributesService;
@@ -44,118 +49,222 @@ import org.thingsboard.server.dao.audit.AuditLogService;
 import org.thingsboard.server.dao.customer.CustomerService;
 import org.thingsboard.server.dao.device.DeviceService;
 import org.thingsboard.server.dao.event.EventService;
-import org.thingsboard.server.dao.plugin.PluginService;
 import org.thingsboard.server.dao.relation.RelationService;
-import org.thingsboard.server.dao.rule.RuleService;
+import org.thingsboard.server.dao.rule.RuleChainService;
 import org.thingsboard.server.dao.tenant.TenantService;
 import org.thingsboard.server.dao.timeseries.TimeseriesService;
+import org.thingsboard.server.dao.user.UserService;
 import org.thingsboard.server.service.cluster.discovery.DiscoveryService;
 import org.thingsboard.server.service.cluster.routing.ClusterRoutingService;
 import org.thingsboard.server.service.cluster.rpc.ClusterRpcService;
 import org.thingsboard.server.service.component.ComponentDiscoveryService;
-
+import org.thingsboard.server.service.encoding.DataDecodingEncodingService;
+import org.thingsboard.server.service.executors.DbCallbackExecutorService;
+import org.thingsboard.server.service.executors.ExternalCallExecutorService;
+import org.thingsboard.server.service.mail.MailExecutorService;
+import org.thingsboard.server.service.queue.MsgQueueService;
+import org.thingsboard.server.service.rpc.DeviceRpcService;
+import org.thingsboard.server.service.script.JsExecutorService;
+import org.thingsboard.server.service.script.JsSandboxService;
+import org.thingsboard.server.service.state.DeviceStateService;
+import org.thingsboard.server.service.telemetry.TelemetrySubscriptionService;
+
+import javax.annotation.Nullable;
+import java.io.IOException;
 import java.io.PrintWriter;
 import java.io.StringWriter;
 import java.util.Optional;
 
+@Slf4j
 @Component
 public class ActorSystemContext {
     private static final String AKKA_CONF_FILE_NAME = "actor-system.conf";
 
     protected final ObjectMapper mapper = new ObjectMapper();
 
-    @Getter @Setter private ActorService actorService;
+    @Getter
+    @Setter
+    private ActorService actorService;
+
+    @Autowired
+    @Getter
+    private DiscoveryService discoveryService;
+
+    @Autowired
+    @Getter
+    @Setter
+    private ComponentDiscoveryService componentService;
+
+    @Autowired
+    @Getter
+    private ClusterRoutingService routingService;
+
+    @Autowired
+    @Getter
+    private ClusterRpcService rpcService;
+
+    @Autowired
+    @Getter
+    private DataDecodingEncodingService encodingService;
+
+    @Autowired
+    @Getter
+    private DeviceAuthService deviceAuthService;
+
+    @Autowired
+    @Getter
+    private DeviceService deviceService;
+
+    @Autowired
+    @Getter
+    private AssetService assetService;
+
+    @Autowired
+    @Getter
+    private TenantService tenantService;
+
+    @Autowired
+    @Getter
+    private CustomerService customerService;
 
     @Autowired
-    @Getter private DiscoveryService discoveryService;
+    @Getter
+    private UserService userService;
 
     @Autowired
-    @Getter @Setter private ComponentDiscoveryService componentService;
+    @Getter
+    private RuleChainService ruleChainService;
 
     @Autowired
-    @Getter private ClusterRoutingService routingService;
+    @Getter
+    private TimeseriesService tsService;
 
     @Autowired
-    @Getter private ClusterRpcService rpcService;
+    @Getter
+    private AttributesService attributesService;
 
     @Autowired
-    @Getter private DeviceAuthService deviceAuthService;
+    @Getter
+    private EventService eventService;
 
     @Autowired
-    @Getter private DeviceService deviceService;
+    @Getter
+    private AlarmService alarmService;
 
     @Autowired
-    @Getter private AssetService assetService;
+    @Getter
+    private RelationService relationService;
 
     @Autowired
-    @Getter private TenantService tenantService;
+    @Getter
+    private AuditLogService auditLogService;
 
     @Autowired
-    @Getter private CustomerService customerService;
+    @Getter
+    private TelemetrySubscriptionService tsSubService;
 
     @Autowired
-    @Getter private RuleService ruleService;
+    @Getter
+    private DeviceRpcService deviceRpcService;
 
     @Autowired
-    @Getter private PluginService pluginService;
+    @Getter
+    private JsSandboxService jsSandbox;
 
     @Autowired
-    @Getter private TimeseriesService tsService;
+    @Getter
+    private JsExecutorService jsExecutor;
 
     @Autowired
-    @Getter private AttributesService attributesService;
+    @Getter
+    private MailExecutorService mailExecutor;
 
     @Autowired
-    @Getter private EventService eventService;
+    @Getter
+    private DbCallbackExecutorService dbCallbackExecutor;
 
     @Autowired
-    @Getter private AlarmService alarmService;
+    @Getter
+    private ExternalCallExecutorService externalCallExecutorService;
 
     @Autowired
-    @Getter private RelationService relationService;
+    @Getter
+    private MailService mailService;
 
     @Autowired
-    @Getter private AuditLogService auditLogService;
+    @Getter
+    private MsgQueueService msgQueueService;
 
     @Autowired
-    @Getter @Setter private PluginWebSocketMsgEndpoint wsMsgEndpoint;
+    @Getter
+    private DeviceStateService deviceStateService;
+
+    @Value("${cluster.partition_id}")
+    @Getter
+    private long queuePartitionId;
+
+    @Value("${actors.session.max_concurrent_sessions_per_device:1}")
+    @Getter
+    private long maxConcurrentSessionsPerDevice;
 
     @Value("${actors.session.sync.timeout}")
-    @Getter private long syncSessionTimeout;
+    @Getter
+    private long syncSessionTimeout;
 
-    @Value("${actors.plugin.termination.delay}")
-    @Getter private long pluginActorTerminationDelay;
+    @Value("${actors.queue.enabled}")
+    @Getter
+    private boolean queuePersistenceEnabled;
 
-    @Value("${actors.plugin.processing.timeout}")
-    @Getter private long pluginProcessingTimeout;
+    @Value("${actors.queue.timeout}")
+    @Getter
+    private long queuePersistenceTimeout;
 
-    @Value("${actors.plugin.error_persist_frequency}")
-    @Getter private long pluginErrorPersistFrequency;
+    @Value("${actors.client_side_rpc.timeout}")
+    @Getter
+    private long clientSideRpcTimeout;
 
-    @Value("${actors.rule.termination.delay}")
-    @Getter private long ruleActorTerminationDelay;
+    @Value("${actors.rule.chain.error_persist_frequency}")
+    @Getter
+    private long ruleChainErrorPersistFrequency;
 
-    @Value("${actors.rule.error_persist_frequency}")
-    @Getter private long ruleErrorPersistFrequency;
+    @Value("${actors.rule.node.error_persist_frequency}")
+    @Getter
+    private long ruleNodeErrorPersistFrequency;
 
     @Value("${actors.statistics.enabled}")
-    @Getter private boolean statisticsEnabled;
+    @Getter
+    private boolean statisticsEnabled;
 
     @Value("${actors.statistics.persist_frequency}")
-    @Getter private long statisticsPersistFrequency;
+    @Getter
+    private long statisticsPersistFrequency;
 
     @Value("${actors.tenant.create_components_on_init}")
-    @Getter private boolean tenantComponentsInitEnabled;
+    @Getter
+    private boolean tenantComponentsInitEnabled;
 
-    @Getter @Setter private ActorSystem actorSystem;
+    @Value("${actors.rule.allow_system_mail_service}")
+    @Getter
+    private boolean allowSystemMailService;
 
-    @Getter @Setter private ActorRef appActor;
+    @Getter
+    @Setter
+    private ActorSystem actorSystem;
 
-    @Getter @Setter private ActorRef sessionManagerActor;
+    @Getter
+    @Setter
+    private ActorRef appActor;
 
-    @Getter @Setter private ActorRef statsActor;
+    @Getter
+    @Setter
+    private ActorRef sessionManagerActor;
 
-    @Getter private final Config config;
+    @Getter
+    @Setter
+    private ActorRef statsActor;
+
+    @Getter
+    private final Config config;
 
     public ActorSystemContext() {
         config = ConfigFactory.parseResources(AKKA_CONF_FILE_NAME).withFallback(ConfigFactory.load());
@@ -187,7 +296,7 @@ public class ActorSystemContext {
         eventService.save(event);
     }
 
-    private String toString(Exception e) {
+    private String toString(Throwable e) {
         StringWriter sw = new StringWriter();
         e.printStackTrace(new PrintWriter(sw));
         return sw.toString();
@@ -207,4 +316,72 @@ public class ActorSystemContext {
     private JsonNode toBodyJson(ServerAddress server, String method, String body) {
         return mapper.createObjectNode().put("server", server.toString()).put("method", method).put("error", body);
     }
+
+    public String getServerAddress() {
+        return discoveryService.getCurrentServer().getServerAddress().toString();
+    }
+
+    public void persistDebugInput(TenantId tenantId, EntityId entityId, TbMsg tbMsg, String relationType) {
+        persistDebugAsync(tenantId, entityId, "IN", tbMsg, relationType, null);
+    }
+
+    public void persistDebugInput(TenantId tenantId, EntityId entityId, TbMsg tbMsg, String relationType, Throwable error) {
+        persistDebugAsync(tenantId, entityId, "IN", tbMsg, relationType, error);
+    }
+
+    public void persistDebugOutput(TenantId tenantId, EntityId entityId, TbMsg tbMsg, String relationType, Throwable error) {
+        persistDebugAsync(tenantId, entityId, "OUT", tbMsg, relationType, error);
+    }
+
+    public void persistDebugOutput(TenantId tenantId, EntityId entityId, TbMsg tbMsg, String relationType) {
+        persistDebugAsync(tenantId, entityId, "OUT", tbMsg, relationType, null);
+    }
+
+    private void persistDebugAsync(TenantId tenantId, EntityId entityId, String type, TbMsg tbMsg, String relationType, Throwable error) {
+        try {
+            Event event = new Event();
+            event.setTenantId(tenantId);
+            event.setEntityId(entityId);
+            event.setType(DataConstants.DEBUG_RULE_NODE);
+
+            String metadata = mapper.writeValueAsString(tbMsg.getMetaData().getData());
+
+            ObjectNode node = mapper.createObjectNode()
+                    .put("type", type)
+                    .put("server", getServerAddress())
+                    .put("entityId", tbMsg.getOriginator().getId().toString())
+                    .put("entityName", tbMsg.getOriginator().getEntityType().name())
+                    .put("msgId", tbMsg.getId().toString())
+                    .put("msgType", tbMsg.getType())
+                    .put("dataType", tbMsg.getDataType().name())
+                    .put("relationType", relationType)
+                    .put("data", tbMsg.getData())
+                    .put("metadata", metadata);
+
+            if (error != null) {
+                node = node.put("error", toString(error));
+            }
+
+            event.setBody(node);
+            ListenableFuture<Event> future = eventService.saveAsync(event);
+            Futures.addCallback(future, new FutureCallback<Event>() {
+                @Override
+                public void onSuccess(@Nullable Event event) {
+
+                }
+
+                @Override
+                public void onFailure(Throwable th) {
+                    log.error("Could not save debug Event for Node", th);
+                }
+            });
+        } catch (IOException ex) {
+            log.warn("Failed to persist rule node debug message", ex);
+        }
+    }
+
+    public static Exception toException(Throwable error) {
+        return Exception.class.isInstance(error) ? (Exception) error : new Exception(error);
+    }
+
 }
diff --git a/application/src/main/java/org/thingsboard/server/actors/app/AppActor.java b/application/src/main/java/org/thingsboard/server/actors/app/AppActor.java
index b475277..d453e59 100644
--- a/application/src/main/java/org/thingsboard/server/actors/app/AppActor.java
+++ b/application/src/main/java/org/thingsboard/server/actors/app/AppActor.java
@@ -15,55 +15,51 @@
  */
 package org.thingsboard.server.actors.app;
 
-import akka.actor.*;
+import akka.actor.ActorRef;
+import akka.actor.LocalActorRef;
+import akka.actor.OneForOneStrategy;
+import akka.actor.Props;
+import akka.actor.SupervisorStrategy;
 import akka.actor.SupervisorStrategy.Directive;
+import akka.actor.Terminated;
 import akka.event.Logging;
 import akka.event.LoggingAdapter;
 import akka.japi.Function;
 import org.thingsboard.server.actors.ActorSystemContext;
-import org.thingsboard.server.actors.plugin.PluginTerminationMsg;
-import org.thingsboard.server.actors.service.ContextAwareActor;
+import org.thingsboard.server.actors.ruleChain.RuleChainManagerActor;
 import org.thingsboard.server.actors.service.ContextBasedCreator;
 import org.thingsboard.server.actors.service.DefaultActorService;
-import org.thingsboard.server.actors.shared.plugin.PluginManager;
-import org.thingsboard.server.actors.shared.plugin.SystemPluginManager;
-import org.thingsboard.server.actors.shared.rule.RuleManager;
-import org.thingsboard.server.actors.shared.rule.SystemRuleManager;
-import org.thingsboard.server.actors.tenant.RuleChainDeviceMsg;
+import org.thingsboard.server.actors.shared.rulechain.SystemRuleChainManager;
 import org.thingsboard.server.actors.tenant.TenantActor;
 import org.thingsboard.server.common.data.Tenant;
-import org.thingsboard.server.common.data.id.PluginId;
-import org.thingsboard.server.common.data.id.RuleId;
 import org.thingsboard.server.common.data.id.TenantId;
 import org.thingsboard.server.common.data.page.PageDataIterable;
-import org.thingsboard.server.common.msg.cluster.ClusterEventMsg;
-import org.thingsboard.server.common.msg.device.ToDeviceActorMsg;
+import org.thingsboard.server.common.msg.TbActorMsg;
+import org.thingsboard.server.common.msg.aware.TenantAwareMsg;
+import org.thingsboard.server.common.msg.cluster.SendToClusterMsg;
+import org.thingsboard.server.common.msg.cluster.ServerAddress;
+import org.thingsboard.server.common.msg.core.BasicActorSystemToDeviceSessionActorMsg;
+import org.thingsboard.server.common.msg.device.DeviceToDeviceActorMsg;
 import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg;
+import org.thingsboard.server.common.msg.system.ServiceToRuleEngineMsg;
 import org.thingsboard.server.dao.model.ModelConstants;
 import org.thingsboard.server.dao.tenant.TenantService;
-import org.thingsboard.server.extensions.api.device.ToDeviceActorNotificationMsg;
-import org.thingsboard.server.extensions.api.plugins.msg.ToPluginActorMsg;
-import org.thingsboard.server.extensions.api.rules.ToRuleActorMsg;
 import scala.concurrent.duration.Duration;
 
 import java.util.HashMap;
 import java.util.Map;
 import java.util.Optional;
 
-public class AppActor extends ContextAwareActor {
+public class AppActor extends RuleChainManagerActor {
 
     private final LoggingAdapter logger = Logging.getLogger(getContext().system(), this);
 
     public static final TenantId SYSTEM_TENANT = new TenantId(ModelConstants.NULL_UUID);
-    private final RuleManager ruleManager;
-    private final PluginManager pluginManager;
     private final TenantService tenantService;
     private final Map<TenantId, ActorRef> tenantActors;
 
     private AppActor(ActorSystemContext systemContext) {
-        super(systemContext);
-        this.ruleManager = new SystemRuleManager(systemContext);
-        this.pluginManager = new SystemPluginManager(systemContext);
+        super(systemContext, new SystemRuleChainManager(systemContext));
         this.tenantService = systemContext.getTenantService();
         this.tenantActors = new HashMap<>();
     }
@@ -77,8 +73,7 @@ public class AppActor extends ContextAwareActor {
     public void preStart() {
         logger.info("Starting main system actor.");
         try {
-            ruleManager.init(this.context());
-            pluginManager.init(this.context());
+            initRuleChains();
 
             if (systemContext.isTenantComponentsInitEnabled()) {
                 PageDataIterable<Tenant> tenantIterator = new PageDataIterable<>(tenantService::findTenants, ENTITY_PACK_LIMIT);
@@ -96,93 +91,89 @@ public class AppActor extends ContextAwareActor {
     }
 
     @Override
-    public void onReceive(Object msg) throws Exception {
-        logger.debug("Received message: {}", msg);
-        if (msg instanceof ToDeviceActorMsg) {
-            processDeviceMsg((ToDeviceActorMsg) msg);
-        } else if (msg instanceof ToPluginActorMsg) {
-            onToPluginMsg((ToPluginActorMsg) msg);
-        } else if (msg instanceof ToRuleActorMsg) {
-            onToRuleMsg((ToRuleActorMsg) msg);
-        } else if (msg instanceof ToDeviceActorNotificationMsg) {
-            onToDeviceActorMsg((ToDeviceActorNotificationMsg) msg);
-        } else if (msg instanceof Terminated) {
-            processTermination((Terminated) msg);
-        } else if (msg instanceof ClusterEventMsg) {
-            broadcast(msg);
-        } else if (msg instanceof ComponentLifecycleMsg) {
-            onComponentLifecycleMsg((ComponentLifecycleMsg) msg);
-        } else if (msg instanceof PluginTerminationMsg) {
-            onPluginTerminated((PluginTerminationMsg) msg);
-        } else {
-            logger.warning("Unknown message: {}!", msg);
+    protected boolean process(TbActorMsg msg) {
+        switch (msg.getMsgType()) {
+            case SEND_TO_CLUSTER_MSG:
+                onPossibleClusterMsg((SendToClusterMsg) msg);
+                break;
+            case CLUSTER_EVENT_MSG:
+                broadcast(msg);
+                break;
+            case COMPONENT_LIFE_CYCLE_MSG:
+                onComponentLifecycleMsg((ComponentLifecycleMsg) msg);
+                break;
+            case SERVICE_TO_RULE_ENGINE_MSG:
+                onServiceToRuleEngineMsg((ServiceToRuleEngineMsg) msg);
+                break;
+            case DEVICE_SESSION_TO_DEVICE_ACTOR_MSG:
+            case DEVICE_ATTRIBUTES_UPDATE_TO_DEVICE_ACTOR_MSG:
+            case DEVICE_CREDENTIALS_UPDATE_TO_DEVICE_ACTOR_MSG:
+            case DEVICE_NAME_OR_TYPE_UPDATE_TO_DEVICE_ACTOR_MSG:
+            case DEVICE_RPC_REQUEST_TO_DEVICE_ACTOR_MSG:
+            case SERVER_RPC_RESPONSE_TO_DEVICE_ACTOR_MSG:
+                onToDeviceActorMsg((TenantAwareMsg) msg);
+                break;
+            case ACTOR_SYSTEM_TO_DEVICE_SESSION_ACTOR_MSG:
+                onToDeviceSessionMsg((BasicActorSystemToDeviceSessionActorMsg) msg);
+            default:
+                return false;
         }
+        return true;
     }
 
-    private void onPluginTerminated(PluginTerminationMsg msg) {
-        pluginManager.remove(msg.getId());
+    private void onToDeviceSessionMsg(BasicActorSystemToDeviceSessionActorMsg msg) {
+        systemContext.getSessionManagerActor().tell(msg, self());
     }
 
-    private void broadcast(Object msg) {
-        pluginManager.broadcast(msg);
-        tenantActors.values().forEach(actorRef -> actorRef.tell(msg, ActorRef.noSender()));
+    private void onPossibleClusterMsg(SendToClusterMsg msg) {
+        Optional<ServerAddress> address = systemContext.getRoutingService().resolveById(msg.getEntityId());
+        if (address.isPresent()) {
+            systemContext.getRpcService().tell(
+                    systemContext.getEncodingService().convertToProtoDataMessage(address.get(), msg.getMsg()));
+        } else {
+            self().tell(msg.getMsg(), ActorRef.noSender());
+        }
     }
 
-    private void onToRuleMsg(ToRuleActorMsg msg) {
-        ActorRef target;
+    private void onServiceToRuleEngineMsg(ServiceToRuleEngineMsg msg) {
         if (SYSTEM_TENANT.equals(msg.getTenantId())) {
-            target = ruleManager.getOrCreateRuleActor(this.context(), msg.getRuleId());
+            //TODO: ashvayka handle this.
         } else {
-            target = getOrCreateTenantActor(msg.getTenantId());
+            getOrCreateTenantActor(msg.getTenantId()).tell(msg, self());
         }
-        target.tell(msg, ActorRef.noSender());
     }
 
-    private void onToPluginMsg(ToPluginActorMsg msg) {
-        ActorRef target;
-        if (SYSTEM_TENANT.equals(msg.getPluginTenantId())) {
-            target = pluginManager.getOrCreatePluginActor(this.context(), msg.getPluginId());
-        } else {
-            target = getOrCreateTenantActor(msg.getPluginTenantId());
-        }
-        target.tell(msg, ActorRef.noSender());
+    @Override
+    protected void broadcast(Object msg) {
+        super.broadcast(msg);
+        tenantActors.values().forEach(actorRef -> actorRef.tell(msg, ActorRef.noSender()));
     }
 
     private void onComponentLifecycleMsg(ComponentLifecycleMsg msg) {
-        ActorRef target = null;
+        ActorRef target;
         if (SYSTEM_TENANT.equals(msg.getTenantId())) {
-            Optional<PluginId> pluginId = msg.getPluginId();
-            Optional<RuleId> ruleId = msg.getRuleId();
-            if (pluginId.isPresent()) {
-                target = pluginManager.getOrCreatePluginActor(this.context(), pluginId.get());
-            } else if (ruleId.isPresent()) {
-                Optional<ActorRef> ref = ruleManager.update(this.context(), ruleId.get(), msg.getEvent());
-                if (ref.isPresent()) {
-                    target = ref.get();
-                } else {
-                    logger.debug("Failed to find actor for rule: [{}]", ruleId);
-                    return;
-                }
-            }
+            target = getEntityActorRef(msg.getEntityId());
         } else {
             target = getOrCreateTenantActor(msg.getTenantId());
         }
         if (target != null) {
             target.tell(msg, ActorRef.noSender());
+        } else {
+            logger.debug("Invalid component lifecycle msg: {}", msg);
         }
     }
 
-    private void onToDeviceActorMsg(ToDeviceActorNotificationMsg msg) {
+    private void onToDeviceActorMsg(TenantAwareMsg msg) {
         getOrCreateTenantActor(msg.getTenantId()).tell(msg, ActorRef.noSender());
     }
 
-    private void processDeviceMsg(ToDeviceActorMsg toDeviceActorMsg) {
-        TenantId tenantId = toDeviceActorMsg.getTenantId();
+    private void processDeviceMsg(DeviceToDeviceActorMsg deviceToDeviceActorMsg) {
+        TenantId tenantId = deviceToDeviceActorMsg.getTenantId();
         ActorRef tenantActor = getOrCreateTenantActor(tenantId);
-        if (toDeviceActorMsg.getPayload().getMsgType().requiresRulesProcessing()) {
-            tenantActor.tell(new RuleChainDeviceMsg(toDeviceActorMsg, ruleManager.getRuleChain(this.context())), context().self());
+        if (deviceToDeviceActorMsg.getPayload().getMsgType().requiresRulesProcessing()) {
+//            tenantActor.tell(new RuleChainDeviceMsg(deviceToDeviceActorMsg, ruleManager.getRuleChain(this.context())), context().self());
         } else {
-            tenantActor.tell(toDeviceActorMsg, context().self());
+            tenantActor.tell(deviceToDeviceActorMsg, context().self());
         }
     }
 
diff --git a/application/src/main/java/org/thingsboard/server/actors/device/DeviceActor.java b/application/src/main/java/org/thingsboard/server/actors/device/DeviceActor.java
index 861c405..99d0045 100644
--- a/application/src/main/java/org/thingsboard/server/actors/device/DeviceActor.java
+++ b/application/src/main/java/org/thingsboard/server/actors/device/DeviceActor.java
@@ -17,61 +17,73 @@ package org.thingsboard.server.actors.device;
 
 import akka.event.Logging;
 import akka.event.LoggingAdapter;
+import org.thingsboard.rule.engine.api.msg.DeviceAttributesEventNotificationMsg;
+import org.thingsboard.rule.engine.api.msg.DeviceNameOrTypeUpdateMsg;
 import org.thingsboard.server.actors.ActorSystemContext;
-import org.thingsboard.server.actors.rule.RulesProcessedMsg;
 import org.thingsboard.server.actors.service.ContextAwareActor;
 import org.thingsboard.server.actors.service.ContextBasedCreator;
-import org.thingsboard.server.actors.tenant.RuleChainDeviceMsg;
 import org.thingsboard.server.common.data.id.DeviceId;
 import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.msg.TbActorMsg;
 import org.thingsboard.server.common.msg.cluster.ClusterEventMsg;
-import org.thingsboard.server.common.msg.device.ToDeviceActorMsg;
-import org.thingsboard.server.extensions.api.device.DeviceAttributesEventNotificationMsg;
-import org.thingsboard.server.extensions.api.device.DeviceCredentialsUpdateNotificationMsg;
-import org.thingsboard.server.extensions.api.device.DeviceNameOrTypeUpdateMsg;
-import org.thingsboard.server.extensions.api.device.ToDeviceActorNotificationMsg;
-import org.thingsboard.server.extensions.api.plugins.msg.*;
+import org.thingsboard.server.common.msg.device.DeviceToDeviceActorMsg;
+import org.thingsboard.server.common.msg.timeout.DeviceActorClientSideRpcTimeoutMsg;
+import org.thingsboard.server.common.msg.timeout.DeviceActorQueueTimeoutMsg;
+import org.thingsboard.server.common.msg.timeout.DeviceActorServerSideRpcTimeoutMsg;
+import org.thingsboard.server.service.rpc.ToDeviceRpcRequestActorMsg;
+import org.thingsboard.server.service.rpc.ToServerRpcResponseActorMsg;
 
 public class DeviceActor extends ContextAwareActor {
 
     private final LoggingAdapter logger = Logging.getLogger(getContext().system(), this);
 
-    private final TenantId tenantId;
-    private final DeviceId deviceId;
     private final DeviceActorMessageProcessor processor;
 
     private DeviceActor(ActorSystemContext systemContext, TenantId tenantId, DeviceId deviceId) {
         super(systemContext);
-        this.tenantId = tenantId;
-        this.deviceId = deviceId;
-        this.processor = new DeviceActorMessageProcessor(systemContext, logger, deviceId);
+        this.processor = new DeviceActorMessageProcessor(systemContext, logger, tenantId, deviceId);
     }
 
     @Override
-    public void onReceive(Object msg) throws Exception {
-        if (msg instanceof RuleChainDeviceMsg) {
-            processor.process(context(), (RuleChainDeviceMsg) msg);
-        } else if (msg instanceof RulesProcessedMsg) {
-            processor.onRulesProcessedMsg(context(), (RulesProcessedMsg) msg);
-        } else if (msg instanceof ToDeviceActorMsg) {
-            processor.process(context(), (ToDeviceActorMsg) msg);
-        } else if (msg instanceof ToDeviceActorNotificationMsg) {
-            if (msg instanceof DeviceAttributesEventNotificationMsg) {
+    protected boolean process(TbActorMsg msg) {
+        switch (msg.getMsgType()) {
+            case CLUSTER_EVENT_MSG:
+                processor.processClusterEventMsg((ClusterEventMsg) msg);
+                break;
+            case DEVICE_SESSION_TO_DEVICE_ACTOR_MSG:
+                processor.process(context(), (DeviceToDeviceActorMsg) msg);
+                break;
+            case DEVICE_ATTRIBUTES_UPDATE_TO_DEVICE_ACTOR_MSG:
                 processor.processAttributesUpdate(context(), (DeviceAttributesEventNotificationMsg) msg);
-            } else if (msg instanceof ToDeviceRpcRequestPluginMsg) {
-                processor.processRpcRequest(context(), (ToDeviceRpcRequestPluginMsg) msg);
-            } else if (msg instanceof DeviceCredentialsUpdateNotificationMsg){
+                break;
+            case DEVICE_CREDENTIALS_UPDATE_TO_DEVICE_ACTOR_MSG:
                 processor.processCredentialsUpdate();
-            } else if (msg instanceof DeviceNameOrTypeUpdateMsg){
+                break;
+            case DEVICE_NAME_OR_TYPE_UPDATE_TO_DEVICE_ACTOR_MSG:
                 processor.processNameOrTypeUpdate((DeviceNameOrTypeUpdateMsg) msg);
-            }
-        } else if (msg instanceof TimeoutMsg) {
-            processor.processTimeout(context(), (TimeoutMsg) msg);
-        } else if (msg instanceof ClusterEventMsg) {
-            processor.processClusterEventMsg((ClusterEventMsg) msg);
-        } else {
-            logger.debug("[{}][{}] Unknown msg type.", tenantId, deviceId, msg.getClass().getName());
+                break;
+            case DEVICE_RPC_REQUEST_TO_DEVICE_ACTOR_MSG:
+                processor.processRpcRequest(context(), (ToDeviceRpcRequestActorMsg) msg);
+                break;
+            case SERVER_RPC_RESPONSE_TO_DEVICE_ACTOR_MSG:
+                processor.processToServerRPCResponse(context(), (ToServerRpcResponseActorMsg) msg);
+                break;
+            case DEVICE_ACTOR_SERVER_SIDE_RPC_TIMEOUT_MSG:
+                processor.processServerSideRpcTimeout(context(), (DeviceActorServerSideRpcTimeoutMsg) msg);
+                break;
+            case DEVICE_ACTOR_CLIENT_SIDE_RPC_TIMEOUT_MSG:
+                processor.processClientSideRpcTimeout(context(), (DeviceActorClientSideRpcTimeoutMsg) msg);
+                break;
+            case DEVICE_ACTOR_QUEUE_TIMEOUT_MSG:
+                processor.processQueueTimeout(context(), (DeviceActorQueueTimeoutMsg) msg);
+                break;
+            case RULE_ENGINE_QUEUE_PUT_ACK_MSG:
+                processor.processQueueAck(context(), (RuleEngineQueuePutAckMsg) msg);
+                break;
+            default:
+                return false;
         }
+        return true;
     }
 
     public static class ActorCreator extends ContextBasedCreator<DeviceActor> {
diff --git a/application/src/main/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessor.java b/application/src/main/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessor.java
index 21112bf..85b3285 100644
--- a/application/src/main/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessor.java
+++ b/application/src/main/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessor.java
@@ -18,37 +18,75 @@ package org.thingsboard.server.actors.device;
 import akka.actor.ActorContext;
 import akka.actor.ActorRef;
 import akka.event.LoggingAdapter;
+import com.datastax.driver.core.utils.UUIDs;
+import com.google.common.util.concurrent.FutureCallback;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.gson.Gson;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParser;
+import org.thingsboard.rule.engine.api.RpcError;
+import org.thingsboard.rule.engine.api.msg.DeviceAttributesEventNotificationMsg;
+import org.thingsboard.rule.engine.api.msg.DeviceNameOrTypeUpdateMsg;
 import org.thingsboard.server.actors.ActorSystemContext;
-import org.thingsboard.server.actors.rule.*;
 import org.thingsboard.server.actors.shared.AbstractContextAwareMsgProcessor;
-import org.thingsboard.server.actors.tenant.RuleChainDeviceMsg;
 import org.thingsboard.server.common.data.DataConstants;
 import org.thingsboard.server.common.data.Device;
 import org.thingsboard.server.common.data.id.DeviceId;
 import org.thingsboard.server.common.data.id.SessionId;
+import org.thingsboard.server.common.data.id.TenantId;
 import org.thingsboard.server.common.data.kv.AttributeKey;
 import org.thingsboard.server.common.data.kv.AttributeKvEntry;
+import org.thingsboard.server.common.data.kv.KvEntry;
+import org.thingsboard.server.common.data.rpc.ToDeviceRpcRequestBody;
+import org.thingsboard.server.common.msg.TbMsg;
+import org.thingsboard.server.common.msg.TbMsgDataType;
+import org.thingsboard.server.common.msg.TbMsgMetaData;
 import org.thingsboard.server.common.msg.cluster.ClusterEventMsg;
 import org.thingsboard.server.common.msg.cluster.ServerAddress;
-import org.thingsboard.server.common.msg.core.*;
-import org.thingsboard.server.common.msg.device.ToDeviceActorMsg;
+import org.thingsboard.server.common.msg.core.ActorSystemToDeviceSessionActorMsg;
+import org.thingsboard.server.common.msg.core.AttributesUpdateNotification;
+import org.thingsboard.server.common.msg.core.AttributesUpdateRequest;
+import org.thingsboard.server.common.msg.core.BasicActorSystemToDeviceSessionActorMsg;
+import org.thingsboard.server.common.msg.core.BasicCommandAckResponse;
+import org.thingsboard.server.common.msg.core.BasicGetAttributesResponse;
+import org.thingsboard.server.common.msg.core.BasicStatusCodeResponse;
+import org.thingsboard.server.common.msg.core.GetAttributesRequest;
+import org.thingsboard.server.common.msg.core.RuleEngineError;
+import org.thingsboard.server.common.msg.core.RuleEngineErrorMsg;
+import org.thingsboard.server.common.msg.core.SessionCloseMsg;
+import org.thingsboard.server.common.msg.core.SessionCloseNotification;
+import org.thingsboard.server.common.msg.core.SessionOpenMsg;
+import org.thingsboard.server.common.msg.core.TelemetryUploadRequest;
+import org.thingsboard.server.common.msg.core.ToDeviceRpcRequestMsg;
+import org.thingsboard.server.common.msg.core.ToDeviceRpcResponseMsg;
+import org.thingsboard.server.common.msg.core.ToServerRpcRequestMsg;
+import org.thingsboard.server.common.msg.device.DeviceToDeviceActorMsg;
 import org.thingsboard.server.common.msg.kv.BasicAttributeKVMsg;
+import org.thingsboard.server.common.msg.rpc.ToDeviceRpcRequest;
 import org.thingsboard.server.common.msg.session.FromDeviceMsg;
-import org.thingsboard.server.common.msg.session.MsgType;
+import org.thingsboard.server.common.msg.session.SessionMsgType;
 import org.thingsboard.server.common.msg.session.SessionType;
 import org.thingsboard.server.common.msg.session.ToDeviceMsg;
-import org.thingsboard.server.extensions.api.device.*;
-import org.thingsboard.server.extensions.api.plugins.msg.FromDeviceRpcResponse;
-import org.thingsboard.server.extensions.api.plugins.msg.RpcError;
-import org.thingsboard.server.extensions.api.plugins.msg.TimeoutIntMsg;
-import org.thingsboard.server.extensions.api.plugins.msg.TimeoutMsg;
-import org.thingsboard.server.extensions.api.plugins.msg.ToDeviceRpcRequest;
-import org.thingsboard.server.extensions.api.plugins.msg.ToDeviceRpcRequestBody;
-import org.thingsboard.server.extensions.api.plugins.msg.ToDeviceRpcRequestPluginMsg;
-import org.thingsboard.server.extensions.api.plugins.msg.ToPluginRpcResponseDeviceMsg;
-
-import java.util.*;
-import java.util.concurrent.ExecutionException;
+import org.thingsboard.server.common.msg.timeout.DeviceActorClientSideRpcTimeoutMsg;
+import org.thingsboard.server.common.msg.timeout.DeviceActorQueueTimeoutMsg;
+import org.thingsboard.server.common.msg.timeout.DeviceActorServerSideRpcTimeoutMsg;
+import org.thingsboard.server.service.rpc.FromDeviceRpcResponse;
+import org.thingsboard.server.service.rpc.ToDeviceRpcRequestActorMsg;
+import org.thingsboard.server.service.rpc.ToServerRpcResponseActorMsg;
+
+import javax.annotation.Nullable;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.UUID;
 import java.util.concurrent.TimeoutException;
 import java.util.function.Consumer;
 import java.util.function.Predicate;
@@ -59,46 +97,46 @@ import java.util.stream.Collectors;
  */
 public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcessor {
 
+    private final TenantId tenantId;
     private final DeviceId deviceId;
     private final Map<SessionId, SessionInfo> sessions;
     private final Map<SessionId, SessionInfo> attributeSubscriptions;
     private final Map<SessionId, SessionInfo> rpcSubscriptions;
+    private final Map<Integer, ToDeviceRpcRequestMetadata> toDeviceRpcPendingMap;
+    private final Map<Integer, ToServerRpcRequestMetadata> toServerRpcPendingMap;
+    private final Map<UUID, PendingSessionMsgData> pendingMsgs;
 
-    private final Map<Integer, ToDeviceRpcRequestMetadata> rpcPendingMap;
+    private final Gson gson = new Gson();
+    private final JsonParser jsonParser = new JsonParser();
 
     private int rpcSeq = 0;
     private String deviceName;
     private String deviceType;
-    private DeviceAttributes deviceAttributes;
+    private TbMsgMetaData defaultMetaData;
 
-    public DeviceActorMessageProcessor(ActorSystemContext systemContext, LoggingAdapter logger, DeviceId deviceId) {
+    public DeviceActorMessageProcessor(ActorSystemContext systemContext, LoggingAdapter logger, TenantId tenantId, DeviceId deviceId) {
         super(systemContext, logger);
+        this.tenantId = tenantId;
         this.deviceId = deviceId;
-        this.sessions = new HashMap<>();
+        this.sessions = new LinkedHashMap<>();
         this.attributeSubscriptions = new HashMap<>();
         this.rpcSubscriptions = new HashMap<>();
-        this.rpcPendingMap = new HashMap<>();
+        this.toDeviceRpcPendingMap = new HashMap<>();
+        this.toServerRpcPendingMap = new HashMap<>();
+        this.pendingMsgs = new HashMap<>();
         initAttributes();
     }
 
     private void initAttributes() {
-        //TODO: add invalidation of deviceType cache.
         Device device = systemContext.getDeviceService().findDeviceById(deviceId);
         this.deviceName = device.getName();
         this.deviceType = device.getType();
-        this.deviceAttributes = new DeviceAttributes(fetchAttributes(DataConstants.CLIENT_SCOPE),
-                fetchAttributes(DataConstants.SERVER_SCOPE), fetchAttributes(DataConstants.SHARED_SCOPE));
+        this.defaultMetaData = new TbMsgMetaData();
+        this.defaultMetaData.putValue("deviceName", deviceName);
+        this.defaultMetaData.putValue("deviceType", deviceType);
     }
 
-    private void refreshAttributes(DeviceAttributesEventNotificationMsg msg) {
-        if (msg.isDeleted()) {
-            msg.getDeletedKeys().forEach(key -> deviceAttributes.remove(key));
-        } else {
-            deviceAttributes.update(msg.getScope(), msg.getValues());
-        }
-    }
-
-    void processRpcRequest(ActorContext context, ToDeviceRpcRequestPluginMsg msg) {
+    void processRpcRequest(ActorContext context, ToDeviceRpcRequestActorMsg msg) {
         ToDeviceRpcRequest request = msg.getMsg();
         ToDeviceRpcRequestBody body = request.getBody();
         ToDeviceRpcRequestMsg rpcRequest = new ToDeviceRpcRequestMsg(
@@ -116,7 +154,7 @@ public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcesso
         boolean sent = rpcSubscriptions.size() > 0;
         Set<SessionId> syncSessionSet = new HashSet<>();
         rpcSubscriptions.entrySet().forEach(sub -> {
-            ToDeviceSessionActorMsg response = new BasicToDeviceSessionActorMsg(rpcRequest, sub.getKey());
+            ActorSystemToDeviceSessionActorMsg response = new BasicActorSystemToDeviceSessionActorMsg(rpcRequest, sub.getKey());
             sendMsgToSessionActor(response, sub.getValue().getServer());
             if (SessionType.SYNC == sub.getValue().getType()) {
                 syncSessionSet.add(sub.getKey());
@@ -125,9 +163,8 @@ public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcesso
         syncSessionSet.forEach(rpcSubscriptions::remove);
 
         if (request.isOneway() && sent) {
-            ToPluginRpcResponseDeviceMsg responsePluginMsg = toPluginRpcResponseMsg(msg, (String) null);
-            context.parent().tell(responsePluginMsg, ActorRef.noSender());
             logger.debug("[{}] Rpc command response sent [{}]!", deviceId, request.getId());
+            systemContext.getDeviceRpcService().processRpcResponseFromDevice(new FromDeviceRpcResponse(msg.getMsg().getId(), msg.getServerAddress(), null, null));
         } else {
             registerPendingRpcRequest(context, msg, sent, rpcRequest, timeout);
         }
@@ -139,24 +176,46 @@ public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcesso
 
     }
 
-    private void registerPendingRpcRequest(ActorContext context, ToDeviceRpcRequestPluginMsg msg, boolean sent, ToDeviceRpcRequestMsg rpcRequest, long timeout) {
-        rpcPendingMap.put(rpcRequest.getRequestId(), new ToDeviceRpcRequestMetadata(msg, sent));
-        TimeoutIntMsg timeoutMsg = new TimeoutIntMsg(rpcRequest.getRequestId(), timeout);
+    private void registerPendingRpcRequest(ActorContext context, ToDeviceRpcRequestActorMsg msg, boolean sent, ToDeviceRpcRequestMsg rpcRequest, long timeout) {
+        toDeviceRpcPendingMap.put(rpcRequest.getRequestId(), new ToDeviceRpcRequestMetadata(msg, sent));
+        DeviceActorServerSideRpcTimeoutMsg timeoutMsg = new DeviceActorServerSideRpcTimeoutMsg(rpcRequest.getRequestId(), timeout);
         scheduleMsgWithDelay(context, timeoutMsg, timeoutMsg.getTimeout());
     }
 
-    public void processTimeout(ActorContext context, TimeoutMsg msg) {
-        ToDeviceRpcRequestMetadata requestMd = rpcPendingMap.remove(msg.getId());
+    void processServerSideRpcTimeout(ActorContext context, DeviceActorServerSideRpcTimeoutMsg msg) {
+        ToDeviceRpcRequestMetadata requestMd = toDeviceRpcPendingMap.remove(msg.getId());
         if (requestMd != null) {
             logger.debug("[{}] RPC request [{}] timeout detected!", deviceId, msg.getId());
-            ToPluginRpcResponseDeviceMsg responsePluginMsg = toPluginRpcResponseMsg(requestMd.getMsg(), requestMd.isSent() ? RpcError.TIMEOUT : RpcError.NO_ACTIVE_CONNECTION);
-            context.parent().tell(responsePluginMsg, ActorRef.noSender());
+            systemContext.getDeviceRpcService().processRpcResponseFromDevice(new FromDeviceRpcResponse(requestMd.getMsg().getMsg().getId(),
+                    requestMd.getMsg().getServerAddress(), null, requestMd.isSent() ? RpcError.TIMEOUT : RpcError.NO_ACTIVE_CONNECTION));
+        }
+    }
+
+    void processQueueTimeout(ActorContext context, DeviceActorQueueTimeoutMsg msg) {
+        PendingSessionMsgData data = pendingMsgs.remove(msg.getId());
+        if (data != null) {
+            logger.debug("[{}] Queue put [{}] timeout detected!", deviceId, msg.getId());
+            ToDeviceMsg toDeviceMsg = new RuleEngineErrorMsg(data.getSessionMsgType(), RuleEngineError.QUEUE_PUT_TIMEOUT);
+            sendMsgToSessionActor(new BasicActorSystemToDeviceSessionActorMsg(toDeviceMsg, data.getSessionId()), data.getServerAddress());
+        }
+    }
+
+    void processQueueAck(ActorContext context, RuleEngineQueuePutAckMsg msg) {
+        PendingSessionMsgData data = pendingMsgs.remove(msg.getId());
+        if (data != null && data.isReplyOnQueueAck()) {
+            int remainingAcks = data.getAckMsgCount() - 1;
+            data.setAckMsgCount(remainingAcks);
+            logger.debug("[{}] Queue put [{}] ack detected. Remaining acks: {}!", deviceId, msg.getId(), remainingAcks);
+            if (remainingAcks == 0) {
+                ToDeviceMsg toDeviceMsg = BasicStatusCodeResponse.onSuccess(data.getSessionMsgType(), data.getRequestId());
+                sendMsgToSessionActor(new BasicActorSystemToDeviceSessionActorMsg(toDeviceMsg, data.getSessionId()), data.getServerAddress());
+            }
         }
     }
 
     private void sendPendingRequests(ActorContext context, SessionId sessionId, SessionType type, Optional<ServerAddress> server) {
-        if (!rpcPendingMap.isEmpty()) {
-            logger.debug("[{}] Pushing {} pending RPC messages to new async session [{}]", deviceId, rpcPendingMap.size(), sessionId);
+        if (!toDeviceRpcPendingMap.isEmpty()) {
+            logger.debug("[{}] Pushing {} pending RPC messages to new async session [{}]", deviceId, toDeviceRpcPendingMap.size(), sessionId);
             if (type == SessionType.SYNC) {
                 logger.debug("[{}] Cleanup sync rpc session [{}]", deviceId, sessionId);
                 rpcSubscriptions.remove(sessionId);
@@ -166,41 +225,196 @@ public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcesso
         }
         Set<Integer> sentOneWayIds = new HashSet<>();
         if (type == SessionType.ASYNC) {
-            rpcPendingMap.entrySet().forEach(processPendingRpc(context, sessionId, server, sentOneWayIds));
+            toDeviceRpcPendingMap.entrySet().forEach(processPendingRpc(context, sessionId, server, sentOneWayIds));
         } else {
-            rpcPendingMap.entrySet().stream().findFirst().ifPresent(processPendingRpc(context, sessionId, server, sentOneWayIds));
+            toDeviceRpcPendingMap.entrySet().stream().findFirst().ifPresent(processPendingRpc(context, sessionId, server, sentOneWayIds));
         }
 
-        sentOneWayIds.forEach(rpcPendingMap::remove);
+        sentOneWayIds.forEach(toDeviceRpcPendingMap::remove);
     }
 
     private Consumer<Map.Entry<Integer, ToDeviceRpcRequestMetadata>> processPendingRpc(ActorContext context, SessionId sessionId, Optional<ServerAddress> server, Set<Integer> sentOneWayIds) {
         return entry -> {
+            ToDeviceRpcRequestActorMsg requestActorMsg = entry.getValue().getMsg();
             ToDeviceRpcRequest request = entry.getValue().getMsg().getMsg();
             ToDeviceRpcRequestBody body = request.getBody();
             if (request.isOneway()) {
                 sentOneWayIds.add(entry.getKey());
-                ToPluginRpcResponseDeviceMsg responsePluginMsg = toPluginRpcResponseMsg(entry.getValue().getMsg(), (String) null);
-                context.parent().tell(responsePluginMsg, ActorRef.noSender());
+                systemContext.getDeviceRpcService().processRpcResponseFromDevice(new FromDeviceRpcResponse(request.getId(), requestActorMsg.getServerAddress(), null, null));
             }
             ToDeviceRpcRequestMsg rpcRequest = new ToDeviceRpcRequestMsg(
                     entry.getKey(),
                     body.getMethod(),
                     body.getParams()
             );
-            ToDeviceSessionActorMsg response = new BasicToDeviceSessionActorMsg(rpcRequest, sessionId);
+            ActorSystemToDeviceSessionActorMsg response = new BasicActorSystemToDeviceSessionActorMsg(rpcRequest, sessionId);
             sendMsgToSessionActor(response, server);
         };
     }
 
-    void process(ActorContext context, ToDeviceActorMsg msg) {
+    void process(ActorContext context, DeviceToDeviceActorMsg msg) {
         processSubscriptionCommands(context, msg);
         processRpcResponses(context, msg);
         processSessionStateMsgs(msg);
+
+        SessionMsgType sessionMsgType = msg.getPayload().getMsgType();
+        if (sessionMsgType.requiresRulesProcessing()) {
+            switch (sessionMsgType) {
+                case GET_ATTRIBUTES_REQUEST:
+                    handleGetAttributesRequest(msg);
+                    break;
+                case POST_ATTRIBUTES_REQUEST:
+                    handlePostAttributesRequest(context, msg);
+                    reportActivity();
+                    break;
+                case POST_TELEMETRY_REQUEST:
+                    handlePostTelemetryRequest(context, msg);
+                    reportActivity();
+                    break;
+                case TO_SERVER_RPC_REQUEST:
+                    handleClientSideRPCRequest(context, msg);
+                    reportActivity();
+                    break;
+            }
+        }
+    }
+
+    private void reportActivity() {
+        systemContext.getDeviceStateService().onDeviceActivity(deviceId);
+    }
+
+    private void reportSessionOpen() {
+        systemContext.getDeviceStateService().onDeviceConnect(deviceId);
+    }
+
+    private void reportSessionClose() {
+        systemContext.getDeviceStateService().onDeviceDisconnect(deviceId);
+    }
+
+    private void handleGetAttributesRequest(DeviceToDeviceActorMsg src) {
+        GetAttributesRequest request = (GetAttributesRequest) src.getPayload();
+        ListenableFuture<List<AttributeKvEntry>> clientAttributesFuture = getAttributeKvEntries(deviceId, DataConstants.CLIENT_SCOPE, request.getClientAttributeNames());
+        ListenableFuture<List<AttributeKvEntry>> sharedAttributesFuture = getAttributeKvEntries(deviceId, DataConstants.SHARED_SCOPE, request.getClientAttributeNames());
+
+        Futures.addCallback(Futures.allAsList(Arrays.asList(clientAttributesFuture, sharedAttributesFuture)), new FutureCallback<List<List<AttributeKvEntry>>>() {
+            @Override
+            public void onSuccess(@Nullable List<List<AttributeKvEntry>> result) {
+                BasicGetAttributesResponse response = BasicGetAttributesResponse.onSuccess(request.getMsgType(),
+                        request.getRequestId(), BasicAttributeKVMsg.from(result.get(0), result.get(1)));
+                sendMsgToSessionActor(new BasicActorSystemToDeviceSessionActorMsg(response, src.getSessionId()), src.getServerAddress());
+            }
+
+            @Override
+            public void onFailure(Throwable t) {
+                if (t instanceof Exception) {
+                    ToDeviceMsg toDeviceMsg = BasicStatusCodeResponse.onError(SessionMsgType.GET_ATTRIBUTES_REQUEST, request.getRequestId(), (Exception) t);
+                    sendMsgToSessionActor(new BasicActorSystemToDeviceSessionActorMsg(toDeviceMsg, src.getSessionId()), src.getServerAddress());
+                } else {
+                    logger.error("[{}] Failed to process attributes request", deviceId, t);
+                }
+            }
+        });
+    }
+
+    private ListenableFuture<List<AttributeKvEntry>> getAttributeKvEntries(DeviceId deviceId, String scope, Optional<Set<String>> names) {
+        if (names.isPresent()) {
+            if (!names.get().isEmpty()) {
+                return systemContext.getAttributesService().find(deviceId, scope, names.get());
+            } else {
+                return systemContext.getAttributesService().findAll(deviceId, scope);
+            }
+        } else {
+            return Futures.immediateFuture(Collections.emptyList());
+        }
+    }
+
+    private void handlePostAttributesRequest(ActorContext context, DeviceToDeviceActorMsg src) {
+        AttributesUpdateRequest request = (AttributesUpdateRequest) src.getPayload();
+
+        JsonObject json = new JsonObject();
+        for (AttributeKvEntry kv : request.getAttributes()) {
+            kv.getBooleanValue().ifPresent(v -> json.addProperty(kv.getKey(), v));
+            kv.getLongValue().ifPresent(v -> json.addProperty(kv.getKey(), v));
+            kv.getDoubleValue().ifPresent(v -> json.addProperty(kv.getKey(), v));
+            kv.getStrValue().ifPresent(v -> json.addProperty(kv.getKey(), v));
+        }
+
+        TbMsg tbMsg = new TbMsg(UUIDs.timeBased(), SessionMsgType.POST_ATTRIBUTES_REQUEST.name(), deviceId, defaultMetaData.copy(), TbMsgDataType.JSON, gson.toJson(json), null, null, 0L);
+        PendingSessionMsgData msgData = new PendingSessionMsgData(src.getSessionId(), src.getServerAddress(),
+                SessionMsgType.POST_ATTRIBUTES_REQUEST, request.getRequestId(), true, 1);
+        pushToRuleEngineWithTimeout(context, tbMsg, msgData);
+    }
+
+    private void handlePostTelemetryRequest(ActorContext context, DeviceToDeviceActorMsg src) {
+        TelemetryUploadRequest request = (TelemetryUploadRequest) src.getPayload();
+
+        Map<Long, List<KvEntry>> tsData = request.getData();
+
+        PendingSessionMsgData msgData = new PendingSessionMsgData(src.getSessionId(), src.getServerAddress(),
+                SessionMsgType.POST_TELEMETRY_REQUEST, request.getRequestId(), true, tsData.size());
+
+        for (Map.Entry<Long, List<KvEntry>> entry : tsData.entrySet()) {
+            JsonObject json = new JsonObject();
+            for (KvEntry kv : entry.getValue()) {
+                kv.getBooleanValue().ifPresent(v -> json.addProperty(kv.getKey(), v));
+                kv.getLongValue().ifPresent(v -> json.addProperty(kv.getKey(), v));
+                kv.getDoubleValue().ifPresent(v -> json.addProperty(kv.getKey(), v));
+                kv.getStrValue().ifPresent(v -> json.addProperty(kv.getKey(), v));
+            }
+            TbMsgMetaData metaData = defaultMetaData.copy();
+            metaData.putValue("ts", entry.getKey() + "");
+            TbMsg tbMsg = new TbMsg(UUIDs.timeBased(), SessionMsgType.POST_TELEMETRY_REQUEST.name(), deviceId, metaData, TbMsgDataType.JSON, gson.toJson(json), null, null, 0L);
+            pushToRuleEngineWithTimeout(context, tbMsg, msgData);
+        }
+    }
+
+    private void handleClientSideRPCRequest(ActorContext context, DeviceToDeviceActorMsg src) {
+        ToServerRpcRequestMsg request = (ToServerRpcRequestMsg) src.getPayload();
+
+        JsonObject json = new JsonObject();
+        json.addProperty("method", request.getMethod());
+        json.add("params", jsonParser.parse(request.getParams()));
+
+        TbMsgMetaData requestMetaData = defaultMetaData.copy();
+        requestMetaData.putValue("requestId", Integer.toString(request.getRequestId()));
+        TbMsg tbMsg = new TbMsg(UUIDs.timeBased(), SessionMsgType.TO_SERVER_RPC_REQUEST.name(), deviceId, requestMetaData, TbMsgDataType.JSON, gson.toJson(json), null, null, 0L);
+        PendingSessionMsgData msgData = new PendingSessionMsgData(src.getSessionId(), src.getServerAddress(), SessionMsgType.TO_SERVER_RPC_REQUEST, request.getRequestId(), false, 1);
+        pushToRuleEngineWithTimeout(context, tbMsg, msgData);
+
+        scheduleMsgWithDelay(context, new DeviceActorClientSideRpcTimeoutMsg(request.getRequestId(), systemContext.getClientSideRpcTimeout()), systemContext.getClientSideRpcTimeout());
+        toServerRpcPendingMap.put(request.getRequestId(), new ToServerRpcRequestMetadata(src.getSessionId(), src.getSessionType(), src.getServerAddress()));
+    }
+
+    public void processClientSideRpcTimeout(ActorContext context, DeviceActorClientSideRpcTimeoutMsg msg) {
+        ToServerRpcRequestMetadata data = toServerRpcPendingMap.remove(msg.getId());
+        if (data != null) {
+            logger.debug("[{}] Client side RPC request [{}] timeout detected!", deviceId, msg.getId());
+            ToDeviceMsg toDeviceMsg = new RuleEngineErrorMsg(SessionMsgType.TO_SERVER_RPC_REQUEST, RuleEngineError.TIMEOUT);
+            sendMsgToSessionActor(new BasicActorSystemToDeviceSessionActorMsg(toDeviceMsg, data.getSessionId()), data.getServer());
+        }
+    }
+
+    void processToServerRPCResponse(ActorContext context, ToServerRpcResponseActorMsg msg) {
+        ToServerRpcRequestMetadata data = toServerRpcPendingMap.remove(msg.getMsg().getRequestId());
+        if (data != null) {
+            sendMsgToSessionActor(new BasicActorSystemToDeviceSessionActorMsg(msg.getMsg(), data.getSessionId()), data.getServer());
+        }
+    }
+
+    private void pushToRuleEngineWithTimeout(ActorContext context, TbMsg tbMsg, PendingSessionMsgData pendingMsgData) {
+        SessionMsgType sessionMsgType = pendingMsgData.getSessionMsgType();
+        int requestId = pendingMsgData.getRequestId();
+        if (systemContext.isQueuePersistenceEnabled()) {
+            pendingMsgs.put(tbMsg.getId(), pendingMsgData);
+            scheduleMsgWithDelay(context, new DeviceActorQueueTimeoutMsg(tbMsg.getId(), systemContext.getQueuePersistenceTimeout()), systemContext.getQueuePersistenceTimeout());
+        } else {
+            ActorSystemToDeviceSessionActorMsg response = new BasicActorSystemToDeviceSessionActorMsg(BasicStatusCodeResponse.onSuccess(sessionMsgType, requestId), pendingMsgData.getSessionId());
+            sendMsgToSessionActor(response, pendingMsgData.getServerAddress());
+        }
+        context.parent().tell(new DeviceActorToRuleEngineMsg(context.self(), tbMsg), context.self());
     }
 
     void processAttributesUpdate(ActorContext context, DeviceAttributesEventNotificationMsg msg) {
-        refreshAttributes(msg);
         if (attributeSubscriptions.size() > 0) {
             ToDeviceMsg notification = null;
             if (msg.isDeleted()) {
@@ -221,7 +435,7 @@ public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcesso
             if (notification != null) {
                 ToDeviceMsg finalNotification = notification;
                 attributeSubscriptions.entrySet().forEach(sub -> {
-                    ToDeviceSessionActorMsg response = new BasicToDeviceSessionActorMsg(finalNotification, sub.getKey());
+                    ActorSystemToDeviceSessionActorMsg response = new BasicActorSystemToDeviceSessionActorMsg(finalNotification, sub.getKey());
                     sendMsgToSessionActor(response, sub.getValue().getServer());
                 });
             }
@@ -230,50 +444,30 @@ public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcesso
         }
     }
 
-    void process(ActorContext context, RuleChainDeviceMsg srcMsg) {
-        ChainProcessingMetaData md = new ChainProcessingMetaData(srcMsg.getRuleChain(),
-                srcMsg.getToDeviceActorMsg(), new DeviceMetaData(deviceId, deviceName, deviceType, deviceAttributes), context.self());
-        ChainProcessingContext ctx = new ChainProcessingContext(md);
-        if (ctx.getChainLength() > 0) {
-            RuleProcessingMsg msg = new RuleProcessingMsg(ctx);
-            ActorRef ruleActorRef = ctx.getCurrentActor();
-            ruleActorRef.tell(msg, ActorRef.noSender());
-        } else {
-            context.self().tell(new RulesProcessedMsg(ctx), context.self());
-        }
-    }
-
-    void processRpcResponses(ActorContext context, ToDeviceActorMsg msg) {
+    private void processRpcResponses(ActorContext context, DeviceToDeviceActorMsg msg) {
         SessionId sessionId = msg.getSessionId();
         FromDeviceMsg inMsg = msg.getPayload();
-        if (inMsg.getMsgType() == MsgType.TO_DEVICE_RPC_RESPONSE) {
+        if (inMsg.getMsgType() == SessionMsgType.TO_DEVICE_RPC_RESPONSE) {
             logger.debug("[{}] Processing rpc command response [{}]", deviceId, sessionId);
             ToDeviceRpcResponseMsg responseMsg = (ToDeviceRpcResponseMsg) inMsg;
-            ToDeviceRpcRequestMetadata requestMd = rpcPendingMap.remove(responseMsg.getRequestId());
+            ToDeviceRpcRequestMetadata requestMd = toDeviceRpcPendingMap.remove(responseMsg.getRequestId());
             boolean success = requestMd != null;
             if (success) {
-                ToPluginRpcResponseDeviceMsg responsePluginMsg = toPluginRpcResponseMsg(requestMd.getMsg(), responseMsg.getData());
-                Optional<ServerAddress> pluginServerAddress = requestMd.getMsg().getServerAddress();
-                if (pluginServerAddress.isPresent()) {
-                    systemContext.getRpcService().tell(pluginServerAddress.get(), responsePluginMsg);
-                    logger.debug("[{}] Rpc command response sent to remote plugin actor [{}]!", deviceId, requestMd.getMsg().getMsg().getId());
-                } else {
-                    context.parent().tell(responsePluginMsg, ActorRef.noSender());
-                    logger.debug("[{}] Rpc command response sent to local plugin actor [{}]!", deviceId, requestMd.getMsg().getMsg().getId());
-                }
+                systemContext.getDeviceRpcService().processRpcResponseFromDevice(new FromDeviceRpcResponse(requestMd.getMsg().getMsg().getId(),
+                        requestMd.getMsg().getServerAddress(), responseMsg.getData(), null));
             } else {
                 logger.debug("[{}] Rpc command response [{}] is stale!", deviceId, responseMsg.getRequestId());
             }
             if (msg.getSessionType() == SessionType.SYNC) {
                 BasicCommandAckResponse response = success
-                        ? BasicCommandAckResponse.onSuccess(MsgType.TO_DEVICE_RPC_REQUEST, responseMsg.getRequestId())
-                        : BasicCommandAckResponse.onError(MsgType.TO_DEVICE_RPC_REQUEST, responseMsg.getRequestId(), new TimeoutException());
-                sendMsgToSessionActor(new BasicToDeviceSessionActorMsg(response, msg.getSessionId()), msg.getServerAddress());
+                        ? BasicCommandAckResponse.onSuccess(SessionMsgType.TO_DEVICE_RPC_REQUEST, responseMsg.getRequestId())
+                        : BasicCommandAckResponse.onError(SessionMsgType.TO_DEVICE_RPC_REQUEST, responseMsg.getRequestId(), new TimeoutException());
+                sendMsgToSessionActor(new BasicActorSystemToDeviceSessionActorMsg(response, msg.getSessionId()), msg.getServerAddress());
             }
         }
     }
 
-    public void processClusterEventMsg(ClusterEventMsg msg) {
+    void processClusterEventMsg(ClusterEventMsg msg) {
         if (!msg.isAdded()) {
             logger.debug("[{}] Clearing attributes/rpc subscription for server [{}]", deviceId, msg.getServerAddress());
             Predicate<Map.Entry<SessionId, SessionInfo>> filter = e -> e.getValue().getServer()
@@ -283,102 +477,79 @@ public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcesso
         }
     }
 
-    private ToPluginRpcResponseDeviceMsg toPluginRpcResponseMsg(ToDeviceRpcRequestPluginMsg requestMsg, String data) {
-        return toPluginRpcResponseMsg(requestMsg, data, null);
-    }
-
-    private ToPluginRpcResponseDeviceMsg toPluginRpcResponseMsg(ToDeviceRpcRequestPluginMsg requestMsg, RpcError error) {
-        return toPluginRpcResponseMsg(requestMsg, null, error);
-    }
-
-    private ToPluginRpcResponseDeviceMsg toPluginRpcResponseMsg(ToDeviceRpcRequestPluginMsg requestMsg, String data, RpcError error) {
-        return new ToPluginRpcResponseDeviceMsg(
-                requestMsg.getPluginId(),
-                requestMsg.getPluginTenantId(),
-                new FromDeviceRpcResponse(requestMsg.getMsg().getId(),
-                        data,
-                        error
-                )
-        );
-    }
-
-    void onRulesProcessedMsg(ActorContext context, RulesProcessedMsg msg) {
-        ChainProcessingContext ctx = msg.getCtx();
-        ToDeviceActorMsg inMsg = ctx.getInMsg();
-        SessionId sid = inMsg.getSessionId();
-        ToDeviceSessionActorMsg response;
-        if (ctx.getResponse() != null) {
-            response = new BasicToDeviceSessionActorMsg(ctx.getResponse(), sid);
-        } else {
-            response = new BasicToDeviceSessionActorMsg(ctx.getError(), sid);
-        }
-        sendMsgToSessionActor(response, inMsg.getServerAddress());
-    }
-
-    private void processSubscriptionCommands(ActorContext context, ToDeviceActorMsg msg) {
+    private void processSubscriptionCommands(ActorContext context, DeviceToDeviceActorMsg msg) {
         SessionId sessionId = msg.getSessionId();
         SessionType sessionType = msg.getSessionType();
         FromDeviceMsg inMsg = msg.getPayload();
-        if (inMsg.getMsgType() == MsgType.SUBSCRIBE_ATTRIBUTES_REQUEST) {
+        if (inMsg.getMsgType() == SessionMsgType.SUBSCRIBE_ATTRIBUTES_REQUEST) {
             logger.debug("[{}] Registering attributes subscription for session [{}]", deviceId, sessionId);
             attributeSubscriptions.put(sessionId, new SessionInfo(sessionType, msg.getServerAddress()));
-        } else if (inMsg.getMsgType() == MsgType.UNSUBSCRIBE_ATTRIBUTES_REQUEST) {
+        } else if (inMsg.getMsgType() == SessionMsgType.UNSUBSCRIBE_ATTRIBUTES_REQUEST) {
             logger.debug("[{}] Canceling attributes subscription for session [{}]", deviceId, sessionId);
             attributeSubscriptions.remove(sessionId);
-        } else if (inMsg.getMsgType() == MsgType.SUBSCRIBE_RPC_COMMANDS_REQUEST) {
+        } else if (inMsg.getMsgType() == SessionMsgType.SUBSCRIBE_RPC_COMMANDS_REQUEST) {
             logger.debug("[{}] Registering rpc subscription for session [{}][{}]", deviceId, sessionId, sessionType);
             rpcSubscriptions.put(sessionId, new SessionInfo(sessionType, msg.getServerAddress()));
             sendPendingRequests(context, sessionId, sessionType, msg.getServerAddress());
-        } else if (inMsg.getMsgType() == MsgType.UNSUBSCRIBE_RPC_COMMANDS_REQUEST) {
+        } else if (inMsg.getMsgType() == SessionMsgType.UNSUBSCRIBE_RPC_COMMANDS_REQUEST) {
             logger.debug("[{}] Canceling rpc subscription for session [{}][{}]", deviceId, sessionId, sessionType);
             rpcSubscriptions.remove(sessionId);
         }
     }
 
-    private void processSessionStateMsgs(ToDeviceActorMsg msg) {
+    private void processSessionStateMsgs(DeviceToDeviceActorMsg msg) {
         SessionId sessionId = msg.getSessionId();
         FromDeviceMsg inMsg = msg.getPayload();
         if (inMsg instanceof SessionOpenMsg) {
             logger.debug("[{}] Processing new session [{}]", deviceId, sessionId);
+            if (sessions.size() >= systemContext.getMaxConcurrentSessionsPerDevice()) {
+                SessionId sessionIdToRemove = sessions.keySet().stream().findFirst().orElse(null);
+                if (sessionIdToRemove != null) {
+                    closeSession(sessionIdToRemove, sessions.remove(sessionIdToRemove));
+                }
+            }
             sessions.put(sessionId, new SessionInfo(SessionType.ASYNC, msg.getServerAddress()));
+            if (sessions.size() == 1) {
+                reportSessionOpen();
+            }
         } else if (inMsg instanceof SessionCloseMsg) {
             logger.debug("[{}] Canceling subscriptions for closed session [{}]", deviceId, sessionId);
             sessions.remove(sessionId);
             attributeSubscriptions.remove(sessionId);
             rpcSubscriptions.remove(sessionId);
+            if (sessions.isEmpty()) {
+                reportSessionClose();
+            }
         }
     }
 
-    private void sendMsgToSessionActor(ToDeviceSessionActorMsg response, Optional<ServerAddress> sessionAddress) {
+    private void sendMsgToSessionActor(ActorSystemToDeviceSessionActorMsg response, Optional<ServerAddress> sessionAddress) {
         if (sessionAddress.isPresent()) {
             ServerAddress address = sessionAddress.get();
             logger.debug("{} Forwarding msg: {}", address, response);
-            systemContext.getRpcService().tell(sessionAddress.get(), response);
+            systemContext.getRpcService().tell(systemContext.getEncodingService()
+                    .convertToProtoDataMessage(sessionAddress.get(), response));
         } else {
             systemContext.getSessionManagerActor().tell(response, ActorRef.noSender());
         }
     }
 
-    private List<AttributeKvEntry> fetchAttributes(String scope) {
-        try {
-            //TODO: replace this with async operation. Happens only during actor creation, but is still criticla for performance,
-            return systemContext.getAttributesService().findAll(this.deviceId, scope).get();
-        } catch (InterruptedException | ExecutionException e) {
-            logger.warning("[{}] Failed to fetch attributes for scope: {}", deviceId, scope);
-            throw new RuntimeException(e);
-        }
-    }
-
-    public void processCredentialsUpdate() {
-        sessions.forEach((k, v) -> {
-            sendMsgToSessionActor(new BasicToDeviceSessionActorMsg(new SessionCloseNotification(), k), v.getServer());
-        });
+    void processCredentialsUpdate() {
+        sessions.forEach(this::closeSession);
         attributeSubscriptions.clear();
         rpcSubscriptions.clear();
     }
 
-    public void processNameOrTypeUpdate(DeviceNameOrTypeUpdateMsg msg) {
+    private void closeSession(SessionId sessionId, SessionInfo sessionInfo) {
+        sendMsgToSessionActor(new BasicActorSystemToDeviceSessionActorMsg(new SessionCloseNotification(), sessionId), sessionInfo.getServer());
+    }
+
+    void processNameOrTypeUpdate(DeviceNameOrTypeUpdateMsg msg) {
         this.deviceName = msg.getDeviceName();
         this.deviceType = msg.getDeviceType();
+        this.defaultMetaData = new TbMsgMetaData();
+        this.defaultMetaData.putValue("deviceName", deviceName);
+        this.defaultMetaData.putValue("deviceType", deviceType);
     }
+
 }
diff --git a/application/src/main/java/org/thingsboard/server/actors/device/PendingSessionMsgData.java b/application/src/main/java/org/thingsboard/server/actors/device/PendingSessionMsgData.java
new file mode 100644
index 0000000..23ad966
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/actors/device/PendingSessionMsgData.java
@@ -0,0 +1,40 @@
+/**
+ * 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.server.actors.device;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import org.thingsboard.server.common.data.id.SessionId;
+import org.thingsboard.server.common.msg.cluster.ServerAddress;
+import org.thingsboard.server.common.msg.session.SessionMsgType;
+
+import java.util.Optional;
+
+/**
+ * Created by ashvayka on 17.04.18.
+ */
+@Data
+@AllArgsConstructor
+public final class PendingSessionMsgData {
+
+    private final SessionId sessionId;
+    private final Optional<ServerAddress> serverAddress;
+    private final SessionMsgType sessionMsgType;
+    private final int requestId;
+    private final boolean replyOnQueueAck;
+    private int ackMsgCount;
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/actors/device/SessionInfo.java b/application/src/main/java/org/thingsboard/server/actors/device/SessionInfo.java
index e039b96..04c457c 100644
--- a/application/src/main/java/org/thingsboard/server/actors/device/SessionInfo.java
+++ b/application/src/main/java/org/thingsboard/server/actors/device/SessionInfo.java
@@ -16,7 +16,6 @@
 package org.thingsboard.server.actors.device;
 
 import lombok.Data;
-import org.thingsboard.server.common.data.id.SessionId;
 import org.thingsboard.server.common.msg.cluster.ServerAddress;
 import org.thingsboard.server.common.msg.session.SessionType;
 
diff --git a/application/src/main/java/org/thingsboard/server/actors/device/ToDeviceRpcRequestMetadata.java b/application/src/main/java/org/thingsboard/server/actors/device/ToDeviceRpcRequestMetadata.java
index 01342fd..8a4262c 100644
--- a/application/src/main/java/org/thingsboard/server/actors/device/ToDeviceRpcRequestMetadata.java
+++ b/application/src/main/java/org/thingsboard/server/actors/device/ToDeviceRpcRequestMetadata.java
@@ -16,13 +16,13 @@
 package org.thingsboard.server.actors.device;
 
 import lombok.Data;
-import org.thingsboard.server.extensions.api.plugins.msg.ToDeviceRpcRequestPluginMsg;
+import org.thingsboard.server.service.rpc.ToDeviceRpcRequestActorMsg;
 
 /**
  * @author Andrew Shvayka
  */
 @Data
 public class ToDeviceRpcRequestMetadata {
-    private final ToDeviceRpcRequestPluginMsg msg;
+    private final ToDeviceRpcRequestActorMsg msg;
     private final boolean sent;
 }
diff --git a/application/src/main/java/org/thingsboard/server/actors/rpc/BasicRpcSessionListener.java b/application/src/main/java/org/thingsboard/server/actors/rpc/BasicRpcSessionListener.java
index 4fa2227..14bb636 100644
--- a/application/src/main/java/org/thingsboard/server/actors/rpc/BasicRpcSessionListener.java
+++ b/application/src/main/java/org/thingsboard/server/actors/rpc/BasicRpcSessionListener.java
@@ -17,43 +17,23 @@ package org.thingsboard.server.actors.rpc;
 
 import akka.actor.ActorRef;
 import lombok.extern.slf4j.Slf4j;
-import org.springframework.util.SerializationUtils;
-import org.springframework.util.StringUtils;
-import org.thingsboard.server.actors.ActorSystemContext;
 import org.thingsboard.server.actors.service.ActorService;
-import org.thingsboard.server.common.data.id.DeviceId;
-import org.thingsboard.server.common.data.id.PluginId;
-import org.thingsboard.server.common.data.id.TenantId;
-import org.thingsboard.server.common.msg.cluster.ServerAddress;
-import org.thingsboard.server.common.msg.cluster.ToAllNodesMsg;
-import org.thingsboard.server.common.msg.core.ToDeviceSessionActorMsg;
-import org.thingsboard.server.common.msg.device.ToDeviceActorMsg;
-import org.thingsboard.server.extensions.api.device.ToDeviceActorNotificationMsg;
-import org.thingsboard.server.extensions.api.plugins.msg.*;
-import org.thingsboard.server.extensions.api.plugins.rpc.PluginRpcMsg;
-import org.thingsboard.server.extensions.api.plugins.rpc.RpcMsg;
 import org.thingsboard.server.gen.cluster.ClusterAPIProtos;
 import org.thingsboard.server.service.cluster.rpc.GrpcSession;
 import org.thingsboard.server.service.cluster.rpc.GrpcSessionListener;
 
-import java.io.Serializable;
-import java.util.UUID;
-
 /**
  * @author Andrew Shvayka
  */
 @Slf4j
 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 ActorRef manager;
     private final ActorRef self;
 
-    public BasicRpcSessionListener(ActorSystemContext context, ActorRef manager, ActorRef self) {
-        this.context = context;
-        this.service = context.getActorService();
+    public BasicRpcSessionListener(ActorService service, ActorRef manager, ActorRef self) {
+        this.service = service;
         this.manager = manager;
         this.self = self;
     }
@@ -73,47 +53,11 @@ public class BasicRpcSessionListener implements GrpcSessionListener {
     }
 
     @Override
-    public void onToPluginRpcMsg(GrpcSession session, ClusterAPIProtos.ToPluginRpcMessage msg) {
-        if (log.isTraceEnabled()) {
-            log.trace("{} session [{}] received plugin msg {}", getType(session), session.getRemoteServer(), msg);
-        }
-        service.onMsg(convert(session.getRemoteServer(), msg));
-    }
-
-    @Override
-    public void onToDeviceActorRpcMsg(GrpcSession session, ClusterAPIProtos.ToDeviceActorRpcMessage msg) {
-        log.trace("{} session [{}] received device actor msg {}", getType(session), session.getRemoteServer(), msg);
-        service.onMsg((ToDeviceActorMsg) deserialize(msg.getData().toByteArray()));
-    }
-
-    @Override
-    public void onToDeviceActorNotificationRpcMsg(GrpcSession session, ClusterAPIProtos.ToDeviceActorNotificationRpcMessage msg) {
-        log.trace("{} session [{}] received device actor notification msg {}", getType(session), session.getRemoteServer(), msg);
-        service.onMsg((ToDeviceActorNotificationMsg) deserialize(msg.getData().toByteArray()));
-    }
-
-    @Override
-    public void onToDeviceSessionActorRpcMsg(GrpcSession session, ClusterAPIProtos.ToDeviceSessionActorRpcMessage msg) {
-        log.trace(SESSION_RECEIVED_SESSION_ACTOR_MSG, getType(session), session.getRemoteServer(), msg);
-        service.onMsg((ToDeviceSessionActorMsg) deserialize(msg.getData().toByteArray()));
-    }
-
-    @Override
-    public void onToDeviceRpcRequestRpcMsg(GrpcSession session, ClusterAPIProtos.ToDeviceRpcRequestRpcMessage msg) {
-        log.trace(SESSION_RECEIVED_SESSION_ACTOR_MSG, getType(session), session.getRemoteServer(), msg);
-        service.onMsg(deserialize(session.getRemoteServer(), msg));
-    }
-
-    @Override
-    public void onFromDeviceRpcResponseRpcMsg(GrpcSession session, ClusterAPIProtos.ToPluginRpcResponseRpcMessage msg) {
-        log.trace(SESSION_RECEIVED_SESSION_ACTOR_MSG, getType(session), session.getRemoteServer(), msg);
-        service.onMsg(deserialize(session.getRemoteServer(), msg));
-    }
-
-    @Override
-    public void onToAllNodesRpcMessage(GrpcSession session, ClusterAPIProtos.ToAllNodesRpcMessage msg) {
-        log.trace(SESSION_RECEIVED_SESSION_ACTOR_MSG, getType(session), session.getRemoteServer(), msg);
-        service.onMsg((ToAllNodesMsg) deserialize(msg.getData().toByteArray()));
+    public void onReceiveClusterGrpcMsg(GrpcSession session, ClusterAPIProtos.ClusterMessage clusterMessage) {
+        log.trace("{} Service [{}] received session actor msg {}", getType(session),
+                session.getRemoteServer(),
+                clusterMessage);
+        service.onReceivedMsg(session.getRemoteServer(), clusterMessage);
     }
 
     @Override
@@ -127,45 +71,5 @@ public class BasicRpcSessionListener implements GrpcSessionListener {
         return session.isClient() ? "Client" : "Server";
     }
 
-    private static PluginRpcMsg convert(ServerAddress serverAddress, ClusterAPIProtos.ToPluginRpcMessage msg) {
-        ClusterAPIProtos.PluginAddress address = msg.getAddress();
-        TenantId tenantId = new TenantId(toUUID(address.getTenantId()));
-        PluginId pluginId = new PluginId(toUUID(address.getPluginId()));
-        RpcMsg rpcMsg = new RpcMsg(serverAddress, msg.getClazz(), msg.getData().toByteArray());
-        return new PluginRpcMsg(tenantId, pluginId, rpcMsg);
-    }
-
-    private static UUID toUUID(ClusterAPIProtos.Uid uid) {
-        return new UUID(uid.getPluginUuidMsb(), uid.getPluginUuidLsb());
-    }
-
-    private static ToDeviceRpcRequestPluginMsg deserialize(ServerAddress serverAddress, ClusterAPIProtos.ToDeviceRpcRequestRpcMessage msg) {
-        ClusterAPIProtos.PluginAddress address = msg.getAddress();
-        TenantId pluginTenantId = new TenantId(toUUID(address.getTenantId()));
-        PluginId pluginId = new PluginId(toUUID(address.getPluginId()));
-
-        TenantId deviceTenantId = new TenantId(toUUID(msg.getDeviceTenantId()));
-        DeviceId deviceId = new DeviceId(toUUID(msg.getDeviceId()));
-
-        ToDeviceRpcRequestBody requestBody = new ToDeviceRpcRequestBody(msg.getMethod(), msg.getParams());
-        ToDeviceRpcRequest request = new ToDeviceRpcRequest(toUUID(msg.getMsgId()), null, deviceTenantId, deviceId, msg.getOneway(), msg.getExpTime(), requestBody);
-
-        return new ToDeviceRpcRequestPluginMsg(serverAddress, pluginId, pluginTenantId, request);
-    }
-
-    private static ToPluginRpcResponseDeviceMsg deserialize(ServerAddress serverAddress, ClusterAPIProtos.ToPluginRpcResponseRpcMessage msg) {
-        ClusterAPIProtos.PluginAddress address = msg.getAddress();
-        TenantId pluginTenantId = new TenantId(toUUID(address.getTenantId()));
-        PluginId pluginId = new PluginId(toUUID(address.getPluginId()));
-
-        RpcError error = !StringUtils.isEmpty(msg.getError()) ? RpcError.valueOf(msg.getError()) : null;
-        FromDeviceRpcResponse response = new FromDeviceRpcResponse(toUUID(msg.getMsgId()), msg.getResponse(), error);
-        return new ToPluginRpcResponseDeviceMsg(pluginId, pluginTenantId, response);
-    }
-
-    @SuppressWarnings("unchecked")
-    private static <T extends Serializable> T deserialize(byte[] data) {
-        return (T) SerializationUtils.deserialize(data);
-    }
 
 }
diff --git a/application/src/main/java/org/thingsboard/server/actors/rpc/RpcBroadcastMsg.java b/application/src/main/java/org/thingsboard/server/actors/rpc/RpcBroadcastMsg.java
index 3718a22..2dd949e 100644
--- a/application/src/main/java/org/thingsboard/server/actors/rpc/RpcBroadcastMsg.java
+++ b/application/src/main/java/org/thingsboard/server/actors/rpc/RpcBroadcastMsg.java
@@ -23,5 +23,5 @@ import org.thingsboard.server.gen.cluster.ClusterAPIProtos;
  */
 @Data
 public final class RpcBroadcastMsg {
-    private final ClusterAPIProtos.ToRpcServerMessage msg;
+    private final ClusterAPIProtos.ClusterMessage msg;
 }
diff --git a/application/src/main/java/org/thingsboard/server/actors/rpc/RpcManagerActor.java b/application/src/main/java/org/thingsboard/server/actors/rpc/RpcManagerActor.java
index 9290a8f..3f3f70b 100644
--- a/application/src/main/java/org/thingsboard/server/actors/rpc/RpcManagerActor.java
+++ b/application/src/main/java/org/thingsboard/server/actors/rpc/RpcManagerActor.java
@@ -23,12 +23,17 @@ import org.thingsboard.server.actors.ActorSystemContext;
 import org.thingsboard.server.actors.service.ContextAwareActor;
 import org.thingsboard.server.actors.service.ContextBasedCreator;
 import org.thingsboard.server.actors.service.DefaultActorService;
+import org.thingsboard.server.common.msg.TbActorMsg;
 import org.thingsboard.server.common.msg.cluster.ClusterEventMsg;
 import org.thingsboard.server.common.msg.cluster.ServerAddress;
 import org.thingsboard.server.gen.cluster.ClusterAPIProtos;
 import org.thingsboard.server.service.cluster.discovery.ServerInstance;
 
-import java.util.*;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.Map;
+import java.util.Queue;
+import java.util.UUID;
 
 /**
  * @author Andrew Shvayka
@@ -39,7 +44,7 @@ public class RpcManagerActor extends ContextAwareActor {
 
     private final Map<ServerAddress, SessionActorInfo> sessionActors;
 
-    private final Map<ServerAddress, Queue<ClusterAPIProtos.ToRpcServerMessage>> pendingMsgs;
+    private final Map<ServerAddress, Queue<ClusterAPIProtos.ClusterMessage>> pendingMsgs;
 
     private final ServerAddress instance;
 
@@ -57,9 +62,15 @@ public class RpcManagerActor extends ContextAwareActor {
     }
 
     @Override
+    protected boolean process(TbActorMsg msg) {
+        //TODO Move everything here, to work with TbActorMsg
+        return false;
+    }
+
+    @Override
     public void onReceive(Object msg) throws Exception {
-        if (msg instanceof RpcSessionTellMsg) {
-            onMsg((RpcSessionTellMsg) msg);
+        if (msg instanceof ClusterAPIProtos.ClusterMessage) {
+            onMsg((ClusterAPIProtos.ClusterMessage) msg);
         } else if (msg instanceof RpcBroadcastMsg) {
             onMsg((RpcBroadcastMsg) msg);
         } else if (msg instanceof RpcSessionCreateRequestMsg) {
@@ -77,24 +88,30 @@ public class RpcManagerActor extends ContextAwareActor {
 
     private void onMsg(RpcBroadcastMsg msg) {
         log.debug("Forwarding msg to session actors {}", msg);
-        sessionActors.keySet().forEach(address -> onMsg(new RpcSessionTellMsg(address, msg.getMsg())));
+        sessionActors.keySet().forEach(address -> onMsg(msg.getMsg()));
         pendingMsgs.values().forEach(queue -> queue.add(msg.getMsg()));
     }
 
-    private void onMsg(RpcSessionTellMsg msg) {
-        ServerAddress address = msg.getServerAddress();
-        SessionActorInfo session = sessionActors.get(address);
-        if (session != null) {
-            log.debug("{} Forwarding msg to session actor", address);
-            session.actor.tell(msg, ActorRef.noSender());
-        } else {
-            log.debug("{} Storing msg to pending queue", address);
-            Queue<ClusterAPIProtos.ToRpcServerMessage> queue = pendingMsgs.get(address);
-            if (queue == null) {
-                queue = new LinkedList<>();
-                pendingMsgs.put(address, queue);
+    private void onMsg(ClusterAPIProtos.ClusterMessage msg) {
+        if (msg.hasServerAddress()) {
+            ServerAddress address = new ServerAddress(msg.getServerAddress().getHost(),
+                    msg.getServerAddress().getPort());
+            SessionActorInfo session = sessionActors.get(address);
+            if (session != null) {
+                log.debug("{} Forwarding msg to session actor", address);
+                session.getActor().tell(msg, ActorRef.noSender());
+            } else {
+                log.debug("{} Storing msg to pending queue", address);
+                Queue<ClusterAPIProtos.ClusterMessage> queue = pendingMsgs.get(address);
+                if (queue == null) {
+                    queue = new LinkedList<>();
+                    pendingMsgs.put(new ServerAddress(
+                            msg.getServerAddress().getHost(), msg.getServerAddress().getPort()), queue);
+                }
+                queue.add(msg);
             }
-            queue.add(msg.getMsg());
+        } else {
+            logger.warning("Cluster msg doesn't have set Server Address [{}]", msg);
         }
     }
 
@@ -141,7 +158,7 @@ public class RpcManagerActor extends ContextAwareActor {
     private void onSessionClose(boolean reconnect, ServerAddress remoteAddress) {
         log.debug("[{}] session closed. Should reconnect: {}", remoteAddress, reconnect);
         SessionActorInfo sessionRef = sessionActors.get(remoteAddress);
-        if (context().sender().equals(sessionRef.actor)) {
+        if (context().sender() != null && context().sender().equals(sessionRef.actor)) {
             sessionActors.remove(remoteAddress);
             pendingMsgs.remove(remoteAddress);
             if (reconnect) {
@@ -160,10 +177,10 @@ public class RpcManagerActor extends ContextAwareActor {
     private void register(ServerAddress remoteAddress, UUID uuid, ActorRef sender) {
         sessionActors.put(remoteAddress, new SessionActorInfo(uuid, sender));
         log.debug("[{}][{}] Registering session actor.", remoteAddress, uuid);
-        Queue<ClusterAPIProtos.ToRpcServerMessage> data = pendingMsgs.remove(remoteAddress);
+        Queue<ClusterAPIProtos.ClusterMessage> data = pendingMsgs.remove(remoteAddress);
         if (data != null) {
             log.debug("[{}][{}] Forwarding {} pending messages.", remoteAddress, uuid, data.size());
-            data.forEach(msg -> sender.tell(new RpcSessionTellMsg(remoteAddress, msg), ActorRef.noSender()));
+            data.forEach(msg -> sender.tell(new RpcSessionTellMsg(msg), ActorRef.noSender()));
         } else {
             log.debug("[{}][{}] No pending messages to forward.", remoteAddress, uuid);
         }
diff --git a/application/src/main/java/org/thingsboard/server/actors/rpc/RpcSessionActor.java b/application/src/main/java/org/thingsboard/server/actors/rpc/RpcSessionActor.java
index db029fa..c9cf869 100644
--- a/application/src/main/java/org/thingsboard/server/actors/rpc/RpcSessionActor.java
+++ b/application/src/main/java/org/thingsboard/server/actors/rpc/RpcSessionActor.java
@@ -23,6 +23,7 @@ import io.grpc.stub.StreamObserver;
 import org.thingsboard.server.actors.ActorSystemContext;
 import org.thingsboard.server.actors.service.ContextAwareActor;
 import org.thingsboard.server.actors.service.ContextBasedCreator;
+import org.thingsboard.server.common.msg.TbActorMsg;
 import org.thingsboard.server.common.msg.cluster.ServerAddress;
 import org.thingsboard.server.gen.cluster.ClusterAPIProtos;
 import org.thingsboard.server.gen.cluster.ClusterRpcServiceGrpc;
@@ -31,6 +32,8 @@ import org.thingsboard.server.service.cluster.rpc.GrpcSessionListener;
 
 import java.util.UUID;
 
+import static org.thingsboard.server.gen.cluster.ClusterAPIProtos.MessageType.CONNECT_RPC_MESSAGE;
+
 /**
  * @author Andrew Shvayka
  */
@@ -48,16 +51,22 @@ public class RpcSessionActor extends ContextAwareActor {
     }
 
     @Override
+    protected boolean process(TbActorMsg msg) {
+        //TODO Move everything here, to work with TbActorMsg
+        return false;
+    }
+
+    @Override
     public void onReceive(Object msg) throws Exception {
-        if (msg instanceof RpcSessionTellMsg) {
-            tell((RpcSessionTellMsg) msg);
+        if (msg instanceof ClusterAPIProtos.ClusterMessage) {
+            tell((ClusterAPIProtos.ClusterMessage) msg);
         } else if (msg instanceof RpcSessionCreateRequestMsg) {
             initSession((RpcSessionCreateRequestMsg) msg);
         }
     }
 
-    private void tell(RpcSessionTellMsg msg) {
-        session.sendMsg(msg.getMsg());
+    private void tell(ClusterAPIProtos.ClusterMessage msg) {
+        session.sendMsg(msg);
     }
 
     @Override
@@ -69,7 +78,7 @@ public class RpcSessionActor extends ContextAwareActor {
     private void initSession(RpcSessionCreateRequestMsg msg) {
         log.info("[{}] Initializing session", context().self());
         ServerAddress remoteServer = msg.getRemoteAddress();
-        listener = new BasicRpcSessionListener(systemContext, context().parent(), context().self());
+        listener = new BasicRpcSessionListener(systemContext.getActorService(), context().parent(), context().self());
         if (msg.getRemoteAddress() == null) {
             // Server session
             session = new GrpcSession(listener);
@@ -84,7 +93,7 @@ public class RpcSessionActor extends ContextAwareActor {
             session.initInputStream();
 
             ClusterRpcServiceGrpc.ClusterRpcServiceStub stub = ClusterRpcServiceGrpc.newStub(channel);
-            StreamObserver<ClusterAPIProtos.ToRpcServerMessage> outputStream = stub.handlePluginMsgs(session.getInputStream());
+            StreamObserver<ClusterAPIProtos.ClusterMessage> outputStream = stub.handleMsgs(session.getInputStream());
 
             session.setOutputStream(outputStream);
             session.initOutputStream();
@@ -108,11 +117,10 @@ public class RpcSessionActor extends ContextAwareActor {
         }
     }
 
-    private ClusterAPIProtos.ToRpcServerMessage toConnectMsg() {
+    private ClusterAPIProtos.ClusterMessage toConnectMsg() {
         ServerAddress instance = systemContext.getDiscoveryService().getCurrentServer().getServerAddress();
-        return ClusterAPIProtos.ToRpcServerMessage.newBuilder().setConnectMsg(
-                ClusterAPIProtos.ConnectRpcMessage.newBuilder().setServerAddress(
-                        ClusterAPIProtos.ServerAddress.newBuilder().setHost(instance.getHost()).setPort(instance.getPort()).build()).build()).build();
-
+        return ClusterAPIProtos.ClusterMessage.newBuilder().setMessageType(CONNECT_RPC_MESSAGE).setServerAddress(
+                ClusterAPIProtos.ServerAddress.newBuilder().setHost(instance.getHost())
+                        .setPort(instance.getPort()).build()).build();
     }
 }
diff --git a/application/src/main/java/org/thingsboard/server/actors/rpc/RpcSessionCreateRequestMsg.java b/application/src/main/java/org/thingsboard/server/actors/rpc/RpcSessionCreateRequestMsg.java
index 5bcf1d6..0c1136e 100644
--- a/application/src/main/java/org/thingsboard/server/actors/rpc/RpcSessionCreateRequestMsg.java
+++ b/application/src/main/java/org/thingsboard/server/actors/rpc/RpcSessionCreateRequestMsg.java
@@ -30,6 +30,6 @@ public final class RpcSessionCreateRequestMsg {
 
     private final UUID msgUid;
     private final ServerAddress remoteAddress;
-    private final StreamObserver<ClusterAPIProtos.ToRpcServerMessage> responseObserver;
+    private final StreamObserver<ClusterAPIProtos.ClusterMessage> responseObserver;
 
 }
diff --git a/application/src/main/java/org/thingsboard/server/actors/rpc/RpcSessionTellMsg.java b/application/src/main/java/org/thingsboard/server/actors/rpc/RpcSessionTellMsg.java
index 5a61044..50db43f 100644
--- a/application/src/main/java/org/thingsboard/server/actors/rpc/RpcSessionTellMsg.java
+++ b/application/src/main/java/org/thingsboard/server/actors/rpc/RpcSessionTellMsg.java
@@ -16,7 +16,6 @@
 package org.thingsboard.server.actors.rpc;
 
 import lombok.Data;
-import org.thingsboard.server.common.msg.cluster.ServerAddress;
 import org.thingsboard.server.gen.cluster.ClusterAPIProtos;
 
 /**
@@ -24,6 +23,5 @@ import org.thingsboard.server.gen.cluster.ClusterAPIProtos;
  */
 @Data
 public final class RpcSessionTellMsg {
-    private final ServerAddress serverAddress;
-    private final ClusterAPIProtos.ToRpcServerMessage msg;
+    private final ClusterAPIProtos.ClusterMessage msg;
 }
diff --git a/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java b/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java
new file mode 100644
index 0000000..bcadd0c
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java
@@ -0,0 +1,241 @@
+/**
+ * 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.server.actors.ruleChain;
+
+import akka.actor.ActorRef;
+import com.datastax.driver.core.utils.UUIDs;
+import org.thingsboard.rule.engine.api.ListeningExecutor;
+import org.thingsboard.rule.engine.api.MailService;
+import org.thingsboard.rule.engine.api.RuleEngineDeviceRpcRequest;
+import org.thingsboard.rule.engine.api.RuleEngineDeviceRpcResponse;
+import org.thingsboard.rule.engine.api.RuleEngineRpcService;
+import org.thingsboard.rule.engine.api.RuleEngineTelemetryService;
+import org.thingsboard.rule.engine.api.ScriptEngine;
+import org.thingsboard.rule.engine.api.TbContext;
+import org.thingsboard.rule.engine.api.TbRelationTypes;
+import org.thingsboard.server.actors.ActorSystemContext;
+import org.thingsboard.server.common.data.id.DeviceId;
+import org.thingsboard.server.common.data.id.EntityId;
+import org.thingsboard.server.common.data.id.RuleNodeId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.rpc.ToDeviceRpcRequestBody;
+import org.thingsboard.server.common.data.rule.RuleNode;
+import org.thingsboard.server.common.msg.TbMsg;
+import org.thingsboard.server.common.msg.TbMsgMetaData;
+import org.thingsboard.server.common.msg.rpc.ToDeviceRpcRequest;
+import org.thingsboard.server.dao.alarm.AlarmService;
+import org.thingsboard.server.dao.asset.AssetService;
+import org.thingsboard.server.dao.attributes.AttributesService;
+import org.thingsboard.server.dao.customer.CustomerService;
+import org.thingsboard.server.dao.device.DeviceService;
+import org.thingsboard.server.dao.relation.RelationService;
+import org.thingsboard.server.dao.rule.RuleChainService;
+import org.thingsboard.server.dao.timeseries.TimeseriesService;
+import org.thingsboard.server.dao.user.UserService;
+import org.thingsboard.server.service.script.RuleNodeJsScriptEngine;
+import scala.concurrent.duration.Duration;
+
+import java.util.Collections;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+import java.util.function.Consumer;
+
+/**
+ * Created by ashvayka on 19.03.18.
+ */
+class DefaultTbContext implements TbContext {
+
+    private final ActorSystemContext mainCtx;
+    private final RuleNodeCtx nodeCtx;
+
+    public DefaultTbContext(ActorSystemContext mainCtx, RuleNodeCtx nodeCtx) {
+        this.mainCtx = mainCtx;
+        this.nodeCtx = nodeCtx;
+    }
+
+    @Override
+    public void tellNext(TbMsg msg, String relationType) {
+        tellNext(msg, Collections.singleton(relationType), null);
+    }
+
+    @Override
+    public void tellNext(TbMsg msg, Set<String> relationTypes) {
+        tellNext(msg, relationTypes, null);
+    }
+
+    @Override
+    public void tellNext(TbMsg msg, String relationType, Throwable th) {
+        tellNext(msg, Collections.singleton(relationType), th);
+    }
+
+    private void tellNext(TbMsg msg, Set<String> relationTypes, Throwable th) {
+        if (nodeCtx.getSelf().isDebugMode()) {
+            relationTypes.forEach(relationType -> mainCtx.persistDebugOutput(nodeCtx.getTenantId(), nodeCtx.getSelf().getId(), msg, relationType, th));
+        }
+        nodeCtx.getChainActor().tell(new RuleNodeToRuleChainTellNextMsg(nodeCtx.getSelf().getId(), relationTypes, msg), nodeCtx.getSelfActor());
+    }
+
+    @Override
+    public void tellSelf(TbMsg msg, long delayMs) {
+        //TODO: add persistence layer
+        scheduleMsgWithDelay(new RuleNodeToSelfMsg(msg), delayMs, nodeCtx.getSelfActor());
+    }
+
+    private void scheduleMsgWithDelay(Object msg, long delayInMs, ActorRef target) {
+        mainCtx.getScheduler().scheduleOnce(Duration.create(delayInMs, TimeUnit.MILLISECONDS), target, msg, mainCtx.getActorSystem().dispatcher(), nodeCtx.getSelfActor());
+    }
+
+    @Override
+    public void tellFailure(TbMsg msg, Throwable th) {
+        if (nodeCtx.getSelf().isDebugMode()) {
+            mainCtx.persistDebugOutput(nodeCtx.getTenantId(), nodeCtx.getSelf().getId(), msg, TbRelationTypes.FAILURE, th);
+        }
+        nodeCtx.getChainActor().tell(new RuleNodeToRuleChainTellNextMsg(nodeCtx.getSelf().getId(), Collections.singleton(TbRelationTypes.FAILURE), msg), nodeCtx.getSelfActor());
+    }
+
+    @Override
+    public void updateSelf(RuleNode self) {
+        nodeCtx.setSelf(self);
+    }
+
+    @Override
+    public TbMsg newMsg(String type, EntityId originator, TbMsgMetaData metaData, String data) {
+        return new TbMsg(UUIDs.timeBased(), type, originator, metaData.copy(), data, nodeCtx.getSelf().getRuleChainId(), nodeCtx.getSelf().getId(), mainCtx.getQueuePartitionId());
+    }
+
+    @Override
+    public TbMsg transformMsg(TbMsg origMsg, String type, EntityId originator, TbMsgMetaData metaData, String data) {
+        return new TbMsg(origMsg.getId(), type, originator, metaData.copy(), data, origMsg.getRuleChainId(), origMsg.getRuleNodeId(), mainCtx.getQueuePartitionId());
+    }
+
+    @Override
+    public RuleNodeId getSelfId() {
+        return nodeCtx.getSelf().getId();
+    }
+
+    @Override
+    public TenantId getTenantId() {
+        return nodeCtx.getTenantId();
+    }
+
+    @Override
+    public ListeningExecutor getJsExecutor() {
+        return mainCtx.getJsExecutor();
+    }
+
+    @Override
+    public ListeningExecutor getMailExecutor() {
+        return mainCtx.getMailExecutor();
+    }
+
+    @Override
+    public ListeningExecutor getDbCallbackExecutor() {
+        return mainCtx.getDbCallbackExecutor();
+    }
+
+    @Override
+    public ListeningExecutor getExternalCallExecutor() {
+        return mainCtx.getExternalCallExecutorService();
+    }
+
+    @Override
+    public ScriptEngine createJsScriptEngine(String script, String... argNames) {
+        return new RuleNodeJsScriptEngine(mainCtx.getJsSandbox(), script, argNames);
+    }
+
+    @Override
+    public AttributesService getAttributesService() {
+        return mainCtx.getAttributesService();
+    }
+
+    @Override
+    public CustomerService getCustomerService() {
+        return mainCtx.getCustomerService();
+    }
+
+    @Override
+    public UserService getUserService() {
+        return mainCtx.getUserService();
+    }
+
+    @Override
+    public AssetService getAssetService() {
+        return mainCtx.getAssetService();
+    }
+
+    @Override
+    public DeviceService getDeviceService() {
+        return mainCtx.getDeviceService();
+    }
+
+    @Override
+    public AlarmService getAlarmService() {
+        return mainCtx.getAlarmService();
+    }
+
+    @Override
+    public RuleChainService getRuleChainService() {
+        return mainCtx.getRuleChainService();
+    }
+
+    @Override
+    public TimeseriesService getTimeseriesService() {
+        return mainCtx.getTsService();
+    }
+
+    @Override
+    public RuleEngineTelemetryService getTelemetryService() {
+        return mainCtx.getTsSubService();
+    }
+
+    @Override
+    public RelationService getRelationService() {
+        return mainCtx.getRelationService();
+    }
+
+    @Override
+    public MailService getMailService() {
+        if (mainCtx.isAllowSystemMailService()) {
+            return mainCtx.getMailService();
+        } else {
+            throw new RuntimeException("Access to System Mail Service is forbidden!");
+        }
+    }
+
+    @Override
+    public RuleEngineRpcService getRpcService() {
+        return new RuleEngineRpcService() {
+            @Override
+            public void sendRpcReply(DeviceId deviceId, int requestId, String body) {
+                mainCtx.getDeviceRpcService().sendRpcReplyToDevice(nodeCtx.getTenantId(), deviceId, requestId, body);
+            }
+
+            @Override
+            public void sendRpcRequest(RuleEngineDeviceRpcRequest src, Consumer<RuleEngineDeviceRpcResponse> consumer) {
+                ToDeviceRpcRequest request = new ToDeviceRpcRequest(UUIDs.timeBased(), nodeCtx.getTenantId(), src.getDeviceId(),
+                        src.isOneway(), System.currentTimeMillis() + src.getTimeout(), new ToDeviceRpcRequestBody(src.getMethod(), src.getBody()));
+                mainCtx.getDeviceRpcService().processRpcRequestToDevice(request, response -> {
+                    consumer.accept(RuleEngineDeviceRpcResponse.builder()
+                            .deviceId(src.getDeviceId())
+                            .requestId(src.getRequestId())
+                            .error(response.getError())
+                            .response(response.getResponse())
+                            .build());
+                });
+            }
+        };
+    }
+}
diff --git a/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainActor.java b/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainActor.java
new file mode 100644
index 0000000..3ba646a
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainActor.java
@@ -0,0 +1,97 @@
+/**
+ * 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.server.actors.ruleChain;
+
+import akka.actor.OneForOneStrategy;
+import akka.actor.SupervisorStrategy;
+import org.thingsboard.server.actors.ActorSystemContext;
+import org.thingsboard.server.actors.device.DeviceActorToRuleEngineMsg;
+import org.thingsboard.server.actors.service.ComponentActor;
+import org.thingsboard.server.actors.service.ContextBasedCreator;
+import org.thingsboard.server.common.data.id.RuleChainId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.msg.TbActorMsg;
+import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg;
+import org.thingsboard.server.common.msg.system.ServiceToRuleEngineMsg;
+import scala.concurrent.duration.Duration;
+
+public class RuleChainActor extends ComponentActor<RuleChainId, RuleChainActorMessageProcessor> {
+
+    private RuleChainActor(ActorSystemContext systemContext, TenantId tenantId, RuleChainId ruleChainId) {
+        super(systemContext, tenantId, ruleChainId);
+        setProcessor(new RuleChainActorMessageProcessor(tenantId, ruleChainId, systemContext,
+                logger, context().parent(), context().self()));
+    }
+
+    @Override
+    protected boolean process(TbActorMsg msg) {
+        switch (msg.getMsgType()) {
+            case COMPONENT_LIFE_CYCLE_MSG:
+                onComponentLifecycleMsg((ComponentLifecycleMsg) msg);
+                break;
+            case SERVICE_TO_RULE_ENGINE_MSG:
+                processor.onServiceToRuleEngineMsg((ServiceToRuleEngineMsg) msg);
+                break;
+            case DEVICE_ACTOR_TO_RULE_ENGINE_MSG:
+                processor.onDeviceActorToRuleEngineMsg((DeviceActorToRuleEngineMsg) msg);
+                break;
+            case RULE_TO_RULE_CHAIN_TELL_NEXT_MSG:
+                processor.onTellNext((RuleNodeToRuleChainTellNextMsg) msg);
+                break;
+            case RULE_CHAIN_TO_RULE_CHAIN_MSG:
+                processor.onRuleChainToRuleChainMsg((RuleChainToRuleChainMsg) msg);
+                break;
+            case CLUSTER_EVENT_MSG:
+                break;
+            default:
+                return false;
+        }
+        return true;
+    }
+
+    public static class ActorCreator extends ContextBasedCreator<RuleChainActor> {
+        private static final long serialVersionUID = 1L;
+
+        private final TenantId tenantId;
+        private final RuleChainId ruleChainId;
+
+        public ActorCreator(ActorSystemContext context, TenantId tenantId, RuleChainId pluginId) {
+            super(context);
+            this.tenantId = tenantId;
+            this.ruleChainId = pluginId;
+        }
+
+        @Override
+        public RuleChainActor create() throws Exception {
+            return new RuleChainActor(context, tenantId, ruleChainId);
+        }
+    }
+
+    @Override
+    protected long getErrorPersistFrequency() {
+        return systemContext.getRuleChainErrorPersistFrequency();
+    }
+
+    @Override
+    public SupervisorStrategy supervisorStrategy() {
+        return strategy;
+    }
+
+    private final SupervisorStrategy strategy = new OneForOneStrategy(3, Duration.create("1 minute"), t -> {
+        logAndPersist("Unknown Failure", ActorSystemContext.toException(t));
+        return SupervisorStrategy.resume();
+    });
+}
diff --git a/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainActorMessageProcessor.java b/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainActorMessageProcessor.java
new file mode 100644
index 0000000..7d560db
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainActorMessageProcessor.java
@@ -0,0 +1,297 @@
+/**
+ * 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.server.actors.ruleChain;
+
+import akka.actor.ActorContext;
+import akka.actor.ActorRef;
+import akka.actor.Props;
+import akka.event.LoggingAdapter;
+import com.datastax.driver.core.utils.UUIDs;
+import org.thingsboard.server.actors.ActorSystemContext;
+import org.thingsboard.server.actors.device.DeviceActorToRuleEngineMsg;
+import org.thingsboard.server.actors.device.RuleEngineQueuePutAckMsg;
+import org.thingsboard.server.actors.service.DefaultActorService;
+import org.thingsboard.server.actors.shared.ComponentMsgProcessor;
+import org.thingsboard.server.common.data.EntityType;
+import org.thingsboard.server.common.data.id.EntityId;
+import org.thingsboard.server.common.data.id.RuleChainId;
+import org.thingsboard.server.common.data.id.RuleNodeId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent;
+import org.thingsboard.server.common.data.plugin.ComponentLifecycleState;
+import org.thingsboard.server.common.data.relation.EntityRelation;
+import org.thingsboard.server.common.data.rule.RuleChain;
+import org.thingsboard.server.common.data.rule.RuleNode;
+import org.thingsboard.server.common.msg.TbMsg;
+import org.thingsboard.server.common.msg.cluster.ClusterEventMsg;
+import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg;
+import org.thingsboard.server.common.msg.system.ServiceToRuleEngineMsg;
+import org.thingsboard.server.dao.rule.RuleChainService;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+/**
+ * @author Andrew Shvayka
+ */
+public class RuleChainActorMessageProcessor extends ComponentMsgProcessor<RuleChainId> {
+
+    private static final long DEFAULT_CLUSTER_PARTITION = 0L;
+    private final ActorRef parent;
+    private final ActorRef self;
+    private final Map<RuleNodeId, RuleNodeCtx> nodeActors;
+    private final Map<RuleNodeId, List<RuleNodeRelation>> nodeRoutes;
+    private final RuleChainService service;
+
+    private RuleNodeId firstId;
+    private RuleNodeCtx firstNode;
+    private boolean started;
+
+    RuleChainActorMessageProcessor(TenantId tenantId, RuleChainId ruleChainId, ActorSystemContext systemContext
+            , LoggingAdapter logger, ActorRef parent, ActorRef self) {
+        super(systemContext, logger, tenantId, ruleChainId);
+        this.parent = parent;
+        this.self = self;
+        this.nodeActors = new HashMap<>();
+        this.nodeRoutes = new HashMap<>();
+        this.service = systemContext.getRuleChainService();
+    }
+
+    @Override
+    public void start(ActorContext context) throws Exception {
+        if (!started) {
+            RuleChain ruleChain = service.findRuleChainById(entityId);
+            List<RuleNode> ruleNodeList = service.getRuleChainNodes(entityId);
+            // Creating and starting the actors;
+            for (RuleNode ruleNode : ruleNodeList) {
+                ActorRef ruleNodeActor = createRuleNodeActor(context, ruleNode);
+                nodeActors.put(ruleNode.getId(), new RuleNodeCtx(tenantId, self, ruleNodeActor, ruleNode));
+            }
+            initRoutes(ruleChain, ruleNodeList);
+            reprocess(ruleNodeList);
+            started = true;
+        } else {
+            onUpdate(context);
+        }
+    }
+
+    private void reprocess(List<RuleNode> ruleNodeList) {
+        for (RuleNode ruleNode : ruleNodeList) {
+            for (TbMsg tbMsg : queue.findUnprocessed(tenantId, ruleNode.getId().getId(), systemContext.getQueuePartitionId())) {
+                pushMsgToNode(nodeActors.get(ruleNode.getId()), tbMsg, "");
+            }
+        }
+        if (firstNode != null) {
+            for (TbMsg tbMsg : queue.findUnprocessed(tenantId, entityId.getId(), systemContext.getQueuePartitionId())) {
+                pushMsgToNode(firstNode, tbMsg, "");
+            }
+        }
+    }
+
+    @Override
+    public void onUpdate(ActorContext context) throws Exception {
+        RuleChain ruleChain = service.findRuleChainById(entityId);
+        List<RuleNode> ruleNodeList = service.getRuleChainNodes(entityId);
+
+        for (RuleNode ruleNode : ruleNodeList) {
+            RuleNodeCtx existing = nodeActors.get(ruleNode.getId());
+            if (existing == null) {
+                ActorRef ruleNodeActor = createRuleNodeActor(context, ruleNode);
+                nodeActors.put(ruleNode.getId(), new RuleNodeCtx(tenantId, self, ruleNodeActor, ruleNode));
+            } else {
+                existing.setSelf(ruleNode);
+                existing.getSelfActor().tell(new ComponentLifecycleMsg(tenantId, existing.getSelf().getId(), ComponentLifecycleEvent.UPDATED), self);
+            }
+        }
+
+        Set<RuleNodeId> existingNodes = ruleNodeList.stream().map(RuleNode::getId).collect(Collectors.toSet());
+        List<RuleNodeId> removedRules = nodeActors.keySet().stream().filter(node -> !existingNodes.contains(node)).collect(Collectors.toList());
+        removedRules.forEach(ruleNodeId -> {
+            RuleNodeCtx removed = nodeActors.remove(ruleNodeId);
+            removed.getSelfActor().tell(new ComponentLifecycleMsg(tenantId, removed.getSelf().getId(), ComponentLifecycleEvent.DELETED), self);
+        });
+
+        initRoutes(ruleChain, ruleNodeList);
+        reprocess(ruleNodeList);
+    }
+
+    @Override
+    public void stop(ActorContext context) throws Exception {
+        nodeActors.values().stream().map(RuleNodeCtx::getSelfActor).forEach(context::stop);
+        nodeActors.clear();
+        nodeRoutes.clear();
+        context.stop(self);
+        started = false;
+    }
+
+    @Override
+    public void onClusterEventMsg(ClusterEventMsg msg) throws Exception {
+
+    }
+
+    private ActorRef createRuleNodeActor(ActorContext context, RuleNode ruleNode) {
+        String dispatcherName = tenantId.getId().equals(EntityId.NULL_UUID) ?
+                DefaultActorService.SYSTEM_RULE_DISPATCHER_NAME : DefaultActorService.TENANT_RULE_DISPATCHER_NAME;
+        return context.actorOf(
+                Props.create(new RuleNodeActor.ActorCreator(systemContext, tenantId, entityId, ruleNode.getId()))
+                        .withDispatcher(dispatcherName), ruleNode.getId().toString());
+    }
+
+    private void initRoutes(RuleChain ruleChain, List<RuleNode> ruleNodeList) {
+        nodeRoutes.clear();
+        // Populating the routes map;
+        for (RuleNode ruleNode : ruleNodeList) {
+            List<EntityRelation> relations = service.getRuleNodeRelations(ruleNode.getId());
+            if (relations.size() == 0) {
+                nodeRoutes.put(ruleNode.getId(), Collections.emptyList());
+            } else {
+                for (EntityRelation relation : relations) {
+                    if (relation.getTo().getEntityType() == EntityType.RULE_NODE) {
+                        RuleNodeCtx ruleNodeCtx = nodeActors.get(new RuleNodeId(relation.getTo().getId()));
+                        if (ruleNodeCtx == null) {
+                            throw new IllegalArgumentException("Rule Node [" + relation.getFrom() + "] has invalid relation to Rule node [" + relation.getTo() + "]");
+                        }
+                    }
+                    nodeRoutes.computeIfAbsent(ruleNode.getId(), k -> new ArrayList<>())
+                            .add(new RuleNodeRelation(ruleNode.getId(), relation.getTo(), relation.getType()));
+                }
+            }
+        }
+
+        firstId = ruleChain.getFirstRuleNodeId();
+        firstNode = nodeActors.get(ruleChain.getFirstRuleNodeId());
+        state = ComponentLifecycleState.ACTIVE;
+    }
+
+    void onServiceToRuleEngineMsg(ServiceToRuleEngineMsg envelope) {
+        checkActive();
+        if (firstNode != null) {
+            putToQueue(enrichWithRuleChainId(envelope.getTbMsg()), msg -> pushMsgToNode(firstNode, msg, ""));
+        }
+    }
+
+    void onDeviceActorToRuleEngineMsg(DeviceActorToRuleEngineMsg envelope) {
+        checkActive();
+        if (firstNode != null) {
+            putToQueue(enrichWithRuleChainId(envelope.getTbMsg()), msg -> {
+                pushMsgToNode(firstNode, msg, "");
+                envelope.getCallbackRef().tell(new RuleEngineQueuePutAckMsg(msg.getId()), self);
+            });
+        }
+    }
+
+    void onRuleChainToRuleChainMsg(RuleChainToRuleChainMsg envelope) {
+        checkActive();
+        if (envelope.isEnqueue()) {
+            if (firstNode != null) {
+                putToQueue(enrichWithRuleChainId(envelope.getMsg()), msg -> pushMsgToNode(firstNode, msg, envelope.getFromRelationType()));
+            }
+        } else {
+            if (firstNode != null) {
+                pushMsgToNode(firstNode, envelope.getMsg(), envelope.getFromRelationType());
+            } else {
+                TbMsg msg = envelope.getMsg();
+                EntityId ackId = msg.getRuleNodeId() != null ? msg.getRuleNodeId() : msg.getRuleChainId();
+                queue.ack(tenantId, envelope.getMsg(), ackId.getId(), msg.getClusterPartition());
+            }
+        }
+    }
+
+    void onTellNext(RuleNodeToRuleChainTellNextMsg envelope) {
+        checkActive();
+        RuleNodeId originator = envelope.getOriginator();
+        List<RuleNodeRelation> relations = nodeRoutes.get(originator).stream()
+                .filter(r -> contains(envelope.getRelationTypes(), r.getType()))
+                .collect(Collectors.toList());
+
+        TbMsg msg = envelope.getMsg();
+        int relationsCount = relations.size();
+        EntityId ackId = msg.getRuleNodeId() != null ? msg.getRuleNodeId() : msg.getRuleChainId();
+        if (relationsCount == 0) {
+            queue.ack(tenantId, msg, ackId.getId(), msg.getClusterPartition());
+        } else if (relationsCount == 1) {
+            for (RuleNodeRelation relation : relations) {
+                pushToTarget(msg, relation.getOut(), relation.getType());
+            }
+        } else {
+            for (RuleNodeRelation relation : relations) {
+                EntityId target = relation.getOut();
+                switch (target.getEntityType()) {
+                    case RULE_NODE:
+                        enqueueAndForwardMsgCopyToNode(msg, target, relation.getType());
+                        break;
+                    case RULE_CHAIN:
+                        enqueueAndForwardMsgCopyToChain(msg, target, relation.getType());
+                        break;
+                }
+            }
+            //TODO: Ideally this should happen in async way when all targets confirm that the copied messages are successfully written to corresponding target queues.
+            queue.ack(tenantId, msg, ackId.getId(), msg.getClusterPartition());
+        }
+    }
+
+    private boolean contains(Set<String> relationTypes, String type) {
+        if (relationTypes == null) {
+            return true;
+        }
+        for (String relationType : relationTypes) {
+            if (relationType.equalsIgnoreCase(type)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    private void enqueueAndForwardMsgCopyToChain(TbMsg msg, EntityId target, String fromRelationType) {
+        RuleChainId targetRCId = new RuleChainId(target.getId());
+        TbMsg copyMsg = msg.copy(UUIDs.timeBased(), targetRCId, null, DEFAULT_CLUSTER_PARTITION);
+        parent.tell(new RuleChainToRuleChainMsg(new RuleChainId(target.getId()), entityId, copyMsg, fromRelationType, true), self);
+    }
+
+    private void enqueueAndForwardMsgCopyToNode(TbMsg msg, EntityId target, String fromRelationType) {
+        RuleNodeId targetId = new RuleNodeId(target.getId());
+        RuleNodeCtx targetNodeCtx = nodeActors.get(targetId);
+        TbMsg copy = msg.copy(UUIDs.timeBased(), entityId, targetId, DEFAULT_CLUSTER_PARTITION);
+        putToQueue(copy, queuedMsg -> pushMsgToNode(targetNodeCtx, queuedMsg, fromRelationType));
+    }
+
+    private void pushToTarget(TbMsg msg, EntityId target, String fromRelationType) {
+        switch (target.getEntityType()) {
+            case RULE_NODE:
+                pushMsgToNode(nodeActors.get(new RuleNodeId(target.getId())), msg, fromRelationType);
+                break;
+            case RULE_CHAIN:
+                parent.tell(new RuleChainToRuleChainMsg(new RuleChainId(target.getId()), entityId, msg, fromRelationType, false), self);
+                break;
+        }
+    }
+
+    private void pushMsgToNode(RuleNodeCtx nodeCtx, TbMsg msg, String fromRelationType) {
+        if (nodeCtx != null) {
+            nodeCtx.getSelfActor().tell(new RuleChainToRuleNodeMsg(new DefaultTbContext(systemContext, nodeCtx), msg, fromRelationType), self);
+        }
+    }
+
+    private TbMsg enrichWithRuleChainId(TbMsg tbMsg) {
+        // We don't put firstNodeId because it may change over time;
+        return new TbMsg(tbMsg.getId(), tbMsg.getType(), tbMsg.getOriginator(), tbMsg.getMetaData().copy(), tbMsg.getData(), entityId, null, systemContext.getQueuePartitionId());
+    }
+}
diff --git a/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainManagerActor.java b/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainManagerActor.java
new file mode 100644
index 0000000..47ee796
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainManagerActor.java
@@ -0,0 +1,57 @@
+/**
+ * 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.server.actors.ruleChain;
+
+import akka.actor.ActorRef;
+import org.thingsboard.server.actors.ActorSystemContext;
+import org.thingsboard.server.actors.service.ContextAwareActor;
+import org.thingsboard.server.actors.shared.rulechain.RuleChainManager;
+import org.thingsboard.server.common.data.id.EntityId;
+import org.thingsboard.server.common.data.id.RuleChainId;
+import org.thingsboard.server.dao.rule.RuleChainService;
+
+/**
+ * Created by ashvayka on 15.03.18.
+ */
+public abstract class RuleChainManagerActor extends ContextAwareActor {
+
+    protected final RuleChainManager ruleChainManager;
+    protected final RuleChainService ruleChainService;
+
+    public RuleChainManagerActor(ActorSystemContext systemContext, RuleChainManager ruleChainManager) {
+        super(systemContext);
+        this.ruleChainManager = ruleChainManager;
+        this.ruleChainService = systemContext.getRuleChainService();
+    }
+
+    protected void initRuleChains() {
+        ruleChainManager.init(this.context());
+    }
+
+    protected ActorRef getEntityActorRef(EntityId entityId) {
+        ActorRef target = null;
+        switch (entityId.getEntityType()) {
+            case RULE_CHAIN:
+                target = ruleChainManager.getOrCreateActor(this.context(), (RuleChainId) entityId);
+                break;
+        }
+        return target;
+    }
+
+    protected void broadcast(Object msg) {
+        ruleChainManager.broadcast(msg);
+    }
+}
diff --git a/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainToRuleChainMsg.java b/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainToRuleChainMsg.java
new file mode 100644
index 0000000..8b13747
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainToRuleChainMsg.java
@@ -0,0 +1,40 @@
+/**
+ * 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.server.actors.ruleChain;
+
+import lombok.Data;
+import org.thingsboard.server.common.data.id.RuleChainId;
+import org.thingsboard.server.common.msg.MsgType;
+import org.thingsboard.server.common.msg.TbActorMsg;
+import org.thingsboard.server.common.msg.TbMsg;
+
+/**
+ * Created by ashvayka on 19.03.18.
+ */
+@Data
+public final class RuleChainToRuleChainMsg implements TbActorMsg {
+
+    private final RuleChainId target;
+    private final RuleChainId source;
+    private final TbMsg msg;
+    private final String fromRelationType;
+    private final boolean enqueue;
+
+    @Override
+    public MsgType getMsgType() {
+        return MsgType.RULE_CHAIN_TO_RULE_CHAIN_MSG;
+    }
+}
diff --git a/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainToRuleNodeMsg.java b/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainToRuleNodeMsg.java
new file mode 100644
index 0000000..abcddc9
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainToRuleNodeMsg.java
@@ -0,0 +1,38 @@
+/**
+ * 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.server.actors.ruleChain;
+
+import lombok.Data;
+import org.thingsboard.rule.engine.api.TbContext;
+import org.thingsboard.server.common.msg.MsgType;
+import org.thingsboard.server.common.msg.TbActorMsg;
+import org.thingsboard.server.common.msg.TbMsg;
+
+/**
+ * Created by ashvayka on 19.03.18.
+ */
+@Data
+final class RuleChainToRuleNodeMsg implements TbActorMsg {
+
+    private final TbContext ctx;
+    private final TbMsg msg;
+    private final String fromRelationType;
+
+    @Override
+    public MsgType getMsgType() {
+        return MsgType.RULE_CHAIN_TO_RULE_MSG;
+    }
+}
diff --git a/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleNodeActor.java b/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleNodeActor.java
new file mode 100644
index 0000000..f7ca0d8
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleNodeActor.java
@@ -0,0 +1,109 @@
+/**
+ * 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.server.actors.ruleChain;
+
+import org.thingsboard.server.actors.ActorSystemContext;
+import org.thingsboard.server.actors.service.ComponentActor;
+import org.thingsboard.server.actors.service.ContextBasedCreator;
+import org.thingsboard.server.common.data.id.RuleChainId;
+import org.thingsboard.server.common.data.id.RuleNodeId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.msg.TbActorMsg;
+import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg;
+
+public class RuleNodeActor extends ComponentActor<RuleNodeId, RuleNodeActorMessageProcessor> {
+
+    private final RuleChainId ruleChainId;
+
+    private RuleNodeActor(ActorSystemContext systemContext, TenantId tenantId, RuleChainId ruleChainId, RuleNodeId ruleNodeId) {
+        super(systemContext, tenantId, ruleNodeId);
+        this.ruleChainId = ruleChainId;
+        setProcessor(new RuleNodeActorMessageProcessor(tenantId, ruleChainId, ruleNodeId, systemContext,
+                logger, context().parent(), context().self()));
+    }
+
+    @Override
+    protected boolean process(TbActorMsg msg) {
+        switch (msg.getMsgType()) {
+            case COMPONENT_LIFE_CYCLE_MSG:
+                onComponentLifecycleMsg((ComponentLifecycleMsg) msg);
+                break;
+            case RULE_CHAIN_TO_RULE_MSG:
+                onRuleChainToRuleNodeMsg((RuleChainToRuleNodeMsg) msg);
+                break;
+            case RULE_TO_SELF_ERROR_MSG:
+                onRuleNodeToSelfErrorMsg((RuleNodeToSelfErrorMsg) msg);
+                break;
+            case RULE_TO_SELF_MSG:
+                onRuleNodeToSelfMsg((RuleNodeToSelfMsg) msg);
+                break;
+            default:
+                return false;
+        }
+        return true;
+    }
+
+    private void onRuleNodeToSelfMsg(RuleNodeToSelfMsg msg) {
+        logger.debug("[{}] Going to process rule msg: {}", id, msg.getMsg());
+        try {
+            processor.onRuleToSelfMsg(msg);
+            increaseMessagesProcessedCount();
+        } catch (Exception e) {
+            logAndPersist("onRuleMsg", e);
+        }
+    }
+
+    private void onRuleChainToRuleNodeMsg(RuleChainToRuleNodeMsg msg) {
+        logger.debug("[{}] Going to process rule msg: {}", id, msg.getMsg());
+        try {
+            processor.onRuleChainToRuleNodeMsg(msg);
+            increaseMessagesProcessedCount();
+        } catch (Exception e) {
+            logAndPersist("onRuleMsg", e);
+        }
+    }
+
+    private void onRuleNodeToSelfErrorMsg(RuleNodeToSelfErrorMsg msg) {
+        logAndPersist("onRuleMsg", ActorSystemContext.toException(msg.getError()));
+    }
+
+    public static class ActorCreator extends ContextBasedCreator<RuleNodeActor> {
+        private static final long serialVersionUID = 1L;
+
+        private final TenantId tenantId;
+        private final RuleChainId ruleChainId;
+        private final RuleNodeId ruleNodeId;
+
+        public ActorCreator(ActorSystemContext context, TenantId tenantId, RuleChainId ruleChainId, RuleNodeId ruleNodeId) {
+            super(context);
+            this.tenantId = tenantId;
+            this.ruleChainId = ruleChainId;
+            this.ruleNodeId = ruleNodeId;
+
+        }
+
+        @Override
+        public RuleNodeActor create() throws Exception {
+            return new RuleNodeActor(context, tenantId, ruleChainId, ruleNodeId);
+        }
+    }
+
+    @Override
+    protected long getErrorPersistFrequency() {
+        return systemContext.getRuleNodeErrorPersistFrequency();
+    }
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleNodeActorMessageProcessor.java b/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleNodeActorMessageProcessor.java
new file mode 100644
index 0000000..acb171d
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleNodeActorMessageProcessor.java
@@ -0,0 +1,121 @@
+/**
+ * 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.server.actors.ruleChain;
+
+import akka.actor.ActorContext;
+import akka.actor.ActorRef;
+import akka.event.LoggingAdapter;
+import org.thingsboard.rule.engine.api.TbContext;
+import org.thingsboard.rule.engine.api.TbNode;
+import org.thingsboard.rule.engine.api.TbNodeConfiguration;
+import org.thingsboard.server.actors.ActorSystemContext;
+import org.thingsboard.server.actors.shared.ComponentMsgProcessor;
+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.plugin.ComponentLifecycleState;
+import org.thingsboard.server.common.data.rule.RuleNode;
+import org.thingsboard.server.common.msg.cluster.ClusterEventMsg;
+import org.thingsboard.server.dao.rule.RuleChainService;
+
+/**
+ * @author Andrew Shvayka
+ */
+public class RuleNodeActorMessageProcessor extends ComponentMsgProcessor<RuleNodeId> {
+
+    private final ActorRef parent;
+    private final ActorRef self;
+    private final RuleChainService service;
+    private RuleNode ruleNode;
+    private TbNode tbNode;
+    private TbContext defaultCtx;
+
+    RuleNodeActorMessageProcessor(TenantId tenantId, RuleChainId ruleChainId, RuleNodeId ruleNodeId, ActorSystemContext systemContext
+            , LoggingAdapter logger, ActorRef parent, ActorRef self) {
+        super(systemContext, logger, tenantId, ruleNodeId);
+        this.parent = parent;
+        this.self = self;
+        this.service = systemContext.getRuleChainService();
+        this.ruleNode = systemContext.getRuleChainService().findRuleNodeById(entityId);
+        this.defaultCtx = new DefaultTbContext(systemContext, new RuleNodeCtx(tenantId, parent, self, ruleNode));
+    }
+
+    @Override
+    public void start(ActorContext context) throws Exception {
+        tbNode = initComponent(ruleNode);
+        state = ComponentLifecycleState.ACTIVE;
+    }
+
+    @Override
+    public void onUpdate(ActorContext context) throws Exception {
+        RuleNode newRuleNode = systemContext.getRuleChainService().findRuleNodeById(entityId);
+        boolean restartRequired = !(ruleNode.getType().equals(newRuleNode.getType())
+                && ruleNode.getConfiguration().equals(newRuleNode.getConfiguration()));
+        this.ruleNode = newRuleNode;
+        this.defaultCtx.updateSelf(newRuleNode);
+        if (restartRequired) {
+            if (tbNode != null) {
+                tbNode.destroy();
+            }
+            start(context);
+        }
+    }
+
+    @Override
+    public void stop(ActorContext context) throws Exception {
+        if (tbNode != null) {
+            tbNode.destroy();
+        }
+        context.stop(self);
+    }
+
+    @Override
+    public void onClusterEventMsg(ClusterEventMsg msg) throws Exception {
+
+    }
+
+    public void onRuleToSelfMsg(RuleNodeToSelfMsg msg) throws Exception {
+        checkActive();
+        if (ruleNode.isDebugMode()) {
+            systemContext.persistDebugInput(tenantId, entityId, msg.getMsg(), "Self");
+        }
+        try {
+            tbNode.onMsg(defaultCtx, msg.getMsg());
+        } catch (Exception e) {
+            defaultCtx.tellFailure(msg.getMsg(), e);
+        }
+    }
+
+    void onRuleChainToRuleNodeMsg(RuleChainToRuleNodeMsg msg) throws Exception {
+        checkActive();
+        if (ruleNode.isDebugMode()) {
+            systemContext.persistDebugInput(tenantId, entityId, msg.getMsg(), msg.getFromRelationType());
+        }
+        try {
+            tbNode.onMsg(msg.getCtx(), msg.getMsg());
+        } catch (Exception e) {
+            msg.getCtx().tellFailure(msg.getMsg(), e);
+        }
+    }
+
+    private TbNode initComponent(RuleNode ruleNode) throws Exception {
+        Class<?> componentClazz = Class.forName(ruleNode.getType());
+        TbNode tbNode = (TbNode) (componentClazz.newInstance());
+        tbNode.init(defaultCtx, new TbNodeConfiguration(ruleNode.getConfiguration()));
+        return tbNode;
+    }
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleNodeToRuleChainTellNextMsg.java b/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleNodeToRuleChainTellNextMsg.java
new file mode 100644
index 0000000..c0a475c
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleNodeToRuleChainTellNextMsg.java
@@ -0,0 +1,41 @@
+/**
+ * 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.server.actors.ruleChain;
+
+import lombok.Data;
+import org.thingsboard.server.common.data.id.RuleNodeId;
+import org.thingsboard.server.common.msg.MsgType;
+import org.thingsboard.server.common.msg.TbActorMsg;
+import org.thingsboard.server.common.msg.TbMsg;
+
+import java.util.Set;
+
+/**
+ * Created by ashvayka on 19.03.18.
+ */
+@Data
+final class RuleNodeToRuleChainTellNextMsg implements TbActorMsg {
+
+    private final RuleNodeId originator;
+    private final Set<String> relationTypes;
+    private final TbMsg msg;
+
+    @Override
+    public MsgType getMsgType() {
+        return MsgType.RULE_TO_RULE_CHAIN_TELL_NEXT_MSG;
+    }
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleNodeToSelfErrorMsg.java b/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleNodeToSelfErrorMsg.java
new file mode 100644
index 0000000..e6248f1
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleNodeToSelfErrorMsg.java
@@ -0,0 +1,37 @@
+/**
+ * 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.server.actors.ruleChain;
+
+import lombok.Data;
+import org.thingsboard.server.common.msg.MsgType;
+import org.thingsboard.server.common.msg.TbActorMsg;
+import org.thingsboard.server.common.msg.TbMsg;
+
+/**
+ * Created by ashvayka on 19.03.18.
+ */
+@Data
+final class RuleNodeToSelfErrorMsg implements TbActorMsg {
+
+    private final TbMsg msg;
+    private final Throwable error;
+
+    @Override
+    public MsgType getMsgType() {
+        return MsgType.RULE_TO_SELF_ERROR_MSG;
+    }
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/actors/service/ActorService.java b/application/src/main/java/org/thingsboard/server/actors/service/ActorService.java
index baae376..f7e80e4 100644
--- a/application/src/main/java/org/thingsboard/server/actors/service/ActorService.java
+++ b/application/src/main/java/org/thingsboard/server/actors/service/ActorService.java
@@ -16,21 +16,24 @@
 package org.thingsboard.server.actors.service;
 
 import org.thingsboard.server.common.data.id.DeviceId;
-import org.thingsboard.server.common.data.id.PluginId;
-import org.thingsboard.server.common.data.id.RuleId;
+import org.thingsboard.server.common.data.id.EntityId;
 import org.thingsboard.server.common.data.id.TenantId;
 import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent;
+import org.thingsboard.server.common.msg.cluster.SendToClusterMsg;
+import org.thingsboard.server.common.msg.system.ServiceToRuleEngineMsg;
 import org.thingsboard.server.common.transport.SessionMsgProcessor;
 import org.thingsboard.server.service.cluster.discovery.DiscoveryServiceListener;
 import org.thingsboard.server.service.cluster.rpc.RpcMsgListener;
 
-public interface ActorService extends SessionMsgProcessor, WebSocketMsgProcessor, RestMsgProcessor, RpcMsgListener, DiscoveryServiceListener {
+public interface ActorService extends SessionMsgProcessor, RpcMsgListener, DiscoveryServiceListener {
 
-    void onPluginStateChange(TenantId tenantId, PluginId pluginId, ComponentLifecycleEvent state);
+    void onEntityStateChange(TenantId tenantId, EntityId entityId, ComponentLifecycleEvent state);
 
-    void onRuleStateChange(TenantId tenantId, RuleId ruleId, ComponentLifecycleEvent state);
+    void onMsg(SendToClusterMsg msg);
 
     void onCredentialsUpdate(TenantId tenantId, DeviceId deviceId);
 
     void onDeviceNameOrTypeUpdate(TenantId tenantId, DeviceId deviceId, String deviceName, String deviceType);
+
+    void onMsg(ServiceToRuleEngineMsg serviceToRuleEngineMsg);
 }
diff --git a/application/src/main/java/org/thingsboard/server/actors/service/ComponentActor.java b/application/src/main/java/org/thingsboard/server/actors/service/ComponentActor.java
index 76b9be9..6aa68d3 100644
--- a/application/src/main/java/org/thingsboard/server/actors/service/ComponentActor.java
+++ b/application/src/main/java/org/thingsboard/server/actors/service/ComponentActor.java
@@ -54,7 +54,7 @@ public abstract class ComponentActor<T extends EntityId, P extends ComponentMsgP
     @Override
     public void preStart() {
         try {
-            processor.start();
+            processor.start(context());
             logLifecycleEvent(ComponentLifecycleEvent.STARTED);
             if (systemContext.isStatisticsEnabled()) {
                 scheduleStatsPersistTick();
@@ -78,7 +78,7 @@ public abstract class ComponentActor<T extends EntityId, P extends ComponentMsgP
     @Override
     public void postStop() {
         try {
-            processor.stop();
+            processor.stop(context());
             logLifecycleEvent(ComponentLifecycleEvent.STOPPED);
         } catch (Exception e) {
             logger.warning("[{}][{}] Failed to stop {} processor: {}", tenantId, id, id.getEntityType(), e.getMessage());
@@ -141,7 +141,6 @@ public abstract class ComponentActor<T extends EntityId, P extends ComponentMsgP
         messagesProcessed++;
     }
 
-
     protected void logAndPersist(String method, Exception e) {
         logAndPersist(method, e, false);
     }
diff --git a/application/src/main/java/org/thingsboard/server/actors/service/ContextAwareActor.java b/application/src/main/java/org/thingsboard/server/actors/service/ContextAwareActor.java
index 825c971..3624127 100644
--- a/application/src/main/java/org/thingsboard/server/actors/service/ContextAwareActor.java
+++ b/application/src/main/java/org/thingsboard/server/actors/service/ContextAwareActor.java
@@ -16,9 +16,13 @@
 package org.thingsboard.server.actors.service;
 
 import akka.actor.UntypedActor;
+import akka.event.Logging;
+import akka.event.LoggingAdapter;
 import org.thingsboard.server.actors.ActorSystemContext;
+import org.thingsboard.server.common.msg.TbActorMsg;
 
 public abstract class ContextAwareActor extends UntypedActor {
+    protected final LoggingAdapter logger = Logging.getLogger(getContext().system(), this);
 
     public static final int ENTITY_PACK_LIMIT = 1024;
 
@@ -28,4 +32,24 @@ public abstract class ContextAwareActor extends UntypedActor {
         super();
         this.systemContext = systemContext;
     }
+
+    @Override
+    public void onReceive(Object msg) throws Exception {
+        if (logger.isDebugEnabled()) {
+            logger.debug("Processing msg: {}", msg);
+        }
+        if (msg instanceof TbActorMsg) {
+            try {
+                if (!process((TbActorMsg) msg)) {
+                    logger.warning("Unknown message: {}!", msg);
+                }
+            } catch (Exception e) {
+                throw e;
+            }
+        } else {
+            logger.warning("Unknown message: {}!", msg);
+        }
+    }
+
+    protected abstract boolean process(TbActorMsg msg);
 }
diff --git a/application/src/main/java/org/thingsboard/server/actors/service/ContextBasedCreator.java b/application/src/main/java/org/thingsboard/server/actors/service/ContextBasedCreator.java
index e6d7ff8..b5f1f61 100644
--- a/application/src/main/java/org/thingsboard/server/actors/service/ContextBasedCreator.java
+++ b/application/src/main/java/org/thingsboard/server/actors/service/ContextBasedCreator.java
@@ -15,9 +15,8 @@
  */
 package org.thingsboard.server.actors.service;
 
-import org.thingsboard.server.actors.ActorSystemContext;
-
 import akka.japi.Creator;
+import org.thingsboard.server.actors.ActorSystemContext;
 
 public abstract class ContextBasedCreator<T> implements Creator<T> {
 
diff --git a/application/src/main/java/org/thingsboard/server/actors/service/DefaultActorService.java b/application/src/main/java/org/thingsboard/server/actors/service/DefaultActorService.java
index bb84a30..3507f24 100644
--- a/application/src/main/java/org/thingsboard/server/actors/service/DefaultActorService.java
+++ b/application/src/main/java/org/thingsboard/server/actors/service/DefaultActorService.java
@@ -19,35 +19,32 @@ import akka.actor.ActorRef;
 import akka.actor.ActorSystem;
 import akka.actor.Props;
 import akka.actor.Terminated;
+import com.google.protobuf.ByteString;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
+import org.thingsboard.rule.engine.api.msg.DeviceCredentialsUpdateNotificationMsg;
+import org.thingsboard.rule.engine.api.msg.DeviceNameOrTypeUpdateMsg;
 import org.thingsboard.server.actors.ActorSystemContext;
 import org.thingsboard.server.actors.app.AppActor;
 import org.thingsboard.server.actors.rpc.RpcBroadcastMsg;
 import org.thingsboard.server.actors.rpc.RpcManagerActor;
 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.stats.StatsActor;
 import org.thingsboard.server.common.data.id.DeviceId;
-import org.thingsboard.server.common.data.id.PluginId;
-import org.thingsboard.server.common.data.id.RuleId;
+import org.thingsboard.server.common.data.id.EntityId;
 import org.thingsboard.server.common.data.id.TenantId;
 import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent;
+import org.thingsboard.server.common.msg.TbActorMsg;
 import org.thingsboard.server.common.msg.aware.SessionAwareMsg;
 import org.thingsboard.server.common.msg.cluster.ClusterEventMsg;
+import org.thingsboard.server.common.msg.cluster.SendToClusterMsg;
 import org.thingsboard.server.common.msg.cluster.ServerAddress;
 import org.thingsboard.server.common.msg.cluster.ToAllNodesMsg;
-import org.thingsboard.server.common.msg.core.ToDeviceSessionActorMsg;
-import org.thingsboard.server.extensions.api.device.DeviceNameOrTypeUpdateMsg;
-import org.thingsboard.server.common.msg.device.ToDeviceActorMsg;
 import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg;
-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.ws.msg.PluginWebsocketMsg;
+import org.thingsboard.server.common.msg.system.ServiceToRuleEngineMsg;
+import org.thingsboard.server.gen.cluster.ClusterAPIProtos;
 import org.thingsboard.server.service.cluster.discovery.DiscoveryService;
 import org.thingsboard.server.service.cluster.discovery.ServerInstance;
 import org.thingsboard.server.service.cluster.rpc.ClusterRpcService;
@@ -57,7 +54,8 @@ import scala.concurrent.duration.Duration;
 
 import javax.annotation.PostConstruct;
 import javax.annotation.PreDestroy;
-import java.util.Optional;
+
+import static org.thingsboard.server.gen.cluster.ClusterAPIProtos.MessageType.CLUSTER_ACTOR_MESSAGE;
 
 @Service
 @Slf4j
@@ -93,7 +91,7 @@ public class DefaultActorService implements ActorService {
 
     @PostConstruct
     public void initActorSystem() {
-        log.info("Initializing Actor system. {}", actorContext.getRuleService());
+        log.info("Initializing Actor system. {}", actorContext.getRuleChainService());
         actorContext.setActorService(this);
         system = ActorSystem.create(ACTOR_SYSTEM_NAME, actorContext.getConfig());
         actorContext.setActorSystem(system);
@@ -129,72 +127,17 @@ public class DefaultActorService implements ActorService {
     }
 
     @Override
-    public void process(SessionAwareMsg msg) {
-        log.debug("Processing session aware msg: {}", msg);
-        sessionManagerActor.tell(msg, ActorRef.noSender());
-    }
-
-    @Override
-    public void process(PluginWebsocketMsg<?> msg) {
-        log.debug("Processing websocket msg: {}", msg);
-        appActor.tell(msg, ActorRef.noSender());
-    }
-
-    @Override
-    public void process(PluginRestMsg msg) {
-        log.debug("Processing rest msg: {}", msg);
-        appActor.tell(msg, ActorRef.noSender());
-    }
-
-    @Override
-    public void onMsg(ToPluginActorMsg msg) {
-        log.trace("Processing plugin rpc msg: {}", msg);
-        appActor.tell(msg, ActorRef.noSender());
-    }
-
-    @Override
-    public void onMsg(ToDeviceActorMsg msg) {
-        log.trace("Processing device rpc msg: {}", msg);
+    public void onMsg(SendToClusterMsg msg) {
         appActor.tell(msg, ActorRef.noSender());
     }
 
     @Override
-    public void onMsg(ToDeviceActorNotificationMsg msg) {
-        log.trace("Processing notification rpc msg: {}", msg);
-        appActor.tell(msg, ActorRef.noSender());
-    }
-
-    @Override
-    public void onMsg(ToDeviceSessionActorMsg msg) {
-        log.trace("Processing session rpc msg: {}", msg);
+    public void process(SessionAwareMsg msg) {
+        log.debug("Processing session aware msg: {}", msg);
         sessionManagerActor.tell(msg, ActorRef.noSender());
     }
 
     @Override
-    public void onMsg(ToAllNodesMsg msg) {
-        log.trace("Processing broadcast rpc msg: {}", msg);
-        appActor.tell(msg, ActorRef.noSender());
-    }
-
-    @Override
-    public void onMsg(RpcSessionCreateRequestMsg msg) {
-        log.trace("Processing session create msg: {}", msg);
-        rpcManagerActor.tell(msg, ActorRef.noSender());
-    }
-
-    @Override
-    public void onMsg(RpcSessionTellMsg msg) {
-        log.trace("Processing session rpc msg: {}", msg);
-        rpcManagerActor.tell(msg, ActorRef.noSender());
-    }
-
-    @Override
-    public void onMsg(RpcBroadcastMsg msg) {
-        log.trace("Processing broadcast rpc msg: {}", msg);
-        rpcManagerActor.tell(msg, ActorRef.noSender());
-    }
-
-    @Override
     public void onServerAdded(ServerInstance server) {
         log.trace("Processing onServerAdded msg: {}", server);
         broadcast(new ClusterEventMsg(server.getServerAddress(), true));
@@ -212,42 +155,37 @@ public class DefaultActorService implements ActorService {
     }
 
     @Override
-    public void onPluginStateChange(TenantId tenantId, PluginId pluginId, ComponentLifecycleEvent state) {
-        log.trace("[{}] Processing onPluginStateChange event: {}", pluginId, state);
-        broadcast(ComponentLifecycleMsg.forPlugin(tenantId, pluginId, state));
-    }
-
-    @Override
-    public void onRuleStateChange(TenantId tenantId, RuleId ruleId, ComponentLifecycleEvent state) {
-        log.trace("[{}] Processing onRuleStateChange event: {}", ruleId, state);
-        broadcast(ComponentLifecycleMsg.forRule(tenantId, ruleId, state));
+    public void onEntityStateChange(TenantId tenantId, EntityId entityId, ComponentLifecycleEvent state) {
+        log.trace("[{}] Processing {} state change event: {}", tenantId, entityId.getEntityType(), state);
+        broadcast(new ComponentLifecycleMsg(tenantId, entityId, state));
     }
 
     @Override
     public void onCredentialsUpdate(TenantId tenantId, DeviceId deviceId) {
         DeviceCredentialsUpdateNotificationMsg msg = new DeviceCredentialsUpdateNotificationMsg(tenantId, deviceId);
-        Optional<ServerAddress> address = actorContext.getRoutingService().resolveById(deviceId);
-        if (address.isPresent()) {
-            rpcService.tell(address.get(), msg);
-        } else {
-            onMsg(msg);
-        }
+        appActor.tell(new SendToClusterMsg(deviceId, msg), ActorRef.noSender());
     }
 
     @Override
     public void onDeviceNameOrTypeUpdate(TenantId tenantId, DeviceId deviceId, String deviceName, String deviceType) {
         log.trace("[{}] Processing onDeviceNameOrTypeUpdate event, deviceName: {}, deviceType: {}", deviceId, deviceName, deviceType);
         DeviceNameOrTypeUpdateMsg msg = new DeviceNameOrTypeUpdateMsg(tenantId, deviceId, deviceName, deviceType);
-        Optional<ServerAddress> address = actorContext.getRoutingService().resolveById(deviceId);
-        if (address.isPresent()) {
-            rpcService.tell(address.get(), msg);
-        } else {
-            onMsg(msg);
-        }
+        appActor.tell(new SendToClusterMsg(deviceId, msg), ActorRef.noSender());
+    }
+
+    @Override
+    public void onMsg(ServiceToRuleEngineMsg msg) {
+        appActor.tell(msg, ActorRef.noSender());
     }
 
     public void broadcast(ToAllNodesMsg msg) {
-        rpcService.broadcast(msg);
+        actorContext.getEncodingService().encode(msg);
+        rpcService.broadcast(new RpcBroadcastMsg(ClusterAPIProtos.ClusterMessage
+                .newBuilder()
+                .setPayload(ByteString
+                        .copyFrom(actorContext.getEncodingService().encode(msg)))
+                .setMessageType(CLUSTER_ACTOR_MESSAGE)
+                .build()));
         appActor.tell(msg, ActorRef.noSender());
     }
 
@@ -256,4 +194,64 @@ public class DefaultActorService implements ActorService {
         this.sessionManagerActor.tell(msg, ActorRef.noSender());
         this.rpcManagerActor.tell(msg, ActorRef.noSender());
     }
+
+    @Override
+    public void onReceivedMsg(ServerAddress source, ClusterAPIProtos.ClusterMessage msg) {
+        ServerAddress serverAddress = new ServerAddress(source.getHost(), source.getPort());
+        log.info("Received msg [{}] from [{}]", msg.getMessageType().name(), serverAddress);
+        if(log.isDebugEnabled()){
+            log.info("MSG: ", msg);
+        }
+        switch (msg.getMessageType()) {
+            case CLUSTER_ACTOR_MESSAGE:
+                java.util.Optional<TbActorMsg> decodedMsg = actorContext.getEncodingService()
+                        .decode(msg.getPayload().toByteArray());
+                if (decodedMsg.isPresent()) {
+                    appActor.tell(decodedMsg.get(), ActorRef.noSender());
+                } else {
+                    log.error("Error during decoding cluster proto message");
+                }
+                break;
+            case TO_ALL_NODES_MSG:
+                //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;
+            case CLUSTER_RPC_FROM_DEVICE_RESPONSE_MESSAGE:
+                actorContext.getDeviceRpcService().processRemoteResponseFromDevice(serverAddress, msg.getPayload().toByteArray());
+                break;
+        }
+    }
+
+    @Override
+    public void onSendMsg(ClusterAPIProtos.ClusterMessage msg) {
+        rpcManagerActor.tell(msg, ActorRef.noSender());
+    }
+
+    @Override
+    public void onRpcSessionCreateRequestMsg(RpcSessionCreateRequestMsg msg) {
+        rpcManagerActor.tell(msg, ActorRef.noSender());
+    }
+
+    @Override
+    public void onBroadcastMsg(RpcBroadcastMsg msg) {
+        rpcManagerActor.tell(msg, ActorRef.noSender());
+    }
+
 }
diff --git a/application/src/main/java/org/thingsboard/server/actors/session/AbstractSessionActorMsgProcessor.java b/application/src/main/java/org/thingsboard/server/actors/session/AbstractSessionActorMsgProcessor.java
index 96526dd..469cda9 100644
--- a/application/src/main/java/org/thingsboard/server/actors/session/AbstractSessionActorMsgProcessor.java
+++ b/application/src/main/java/org/thingsboard/server/actors/session/AbstractSessionActorMsgProcessor.java
@@ -15,36 +15,42 @@
  */
 package org.thingsboard.server.actors.session;
 
+import akka.actor.ActorContext;
+import akka.actor.ActorRef;
+import akka.event.LoggingAdapter;
 import org.thingsboard.server.actors.ActorSystemContext;
 import org.thingsboard.server.actors.shared.AbstractContextAwareMsgProcessor;
 import org.thingsboard.server.actors.shared.SessionTimeoutMsg;
 import org.thingsboard.server.common.data.id.DeviceId;
 import org.thingsboard.server.common.data.id.SessionId;
 import org.thingsboard.server.common.msg.cluster.ClusterEventMsg;
+import org.thingsboard.server.common.msg.cluster.SendToClusterMsg;
 import org.thingsboard.server.common.msg.cluster.ServerAddress;
-import org.thingsboard.server.common.msg.device.BasicToDeviceActorMsg;
-import org.thingsboard.server.common.msg.device.ToDeviceActorMsg;
-import org.thingsboard.server.common.msg.session.*;
+import org.thingsboard.server.common.msg.device.BasicDeviceToDeviceActorMsg;
+import org.thingsboard.server.common.msg.device.DeviceToDeviceActorMsg;
+import org.thingsboard.server.common.msg.session.AdaptorToSessionActorMsg;
+import org.thingsboard.server.common.msg.session.FromDeviceMsg;
+import org.thingsboard.server.common.msg.session.SessionContext;
+import org.thingsboard.server.common.msg.session.SessionCtrlMsg;
+import org.thingsboard.server.common.msg.session.SessionType;
+import org.thingsboard.server.common.msg.session.ToDeviceMsg;
+import org.thingsboard.server.common.msg.session.TransportToDeviceSessionActorMsg;
 import org.thingsboard.server.common.msg.session.ctrl.SessionCloseMsg;
 
-import akka.actor.ActorContext;
-import akka.actor.ActorRef;
-import akka.event.LoggingAdapter;
-
 import java.util.Optional;
 
 abstract class AbstractSessionActorMsgProcessor extends AbstractContextAwareMsgProcessor {
 
     protected final SessionId sessionId;
     protected SessionContext sessionCtx;
-    protected ToDeviceActorMsg toDeviceActorMsgPrototype;
+    protected DeviceToDeviceActorMsg deviceToDeviceActorMsgPrototype;
 
     protected AbstractSessionActorMsgProcessor(ActorSystemContext ctx, LoggingAdapter logger, SessionId sessionId) {
         super(ctx, logger);
         this.sessionId = sessionId;
     }
 
-    protected abstract void processToDeviceActorMsg(ActorContext ctx, ToDeviceActorSessionMsg msg);
+    protected abstract void processToDeviceActorMsg(ActorContext ctx, TransportToDeviceSessionActorMsg msg);
 
     protected abstract void processTimeoutMsg(ActorContext context, SessionTimeoutMsg msg);
 
@@ -62,47 +68,44 @@ abstract class AbstractSessionActorMsgProcessor extends AbstractContextAwareMsgP
     protected void cleanupSession(ActorContext ctx) {
     }
 
-    protected void updateSessionCtx(ToDeviceActorSessionMsg msg, SessionType type) {
+    protected void updateSessionCtx(TransportToDeviceSessionActorMsg msg, SessionType type) {
         sessionCtx = msg.getSessionMsg().getSessionContext();
-        toDeviceActorMsgPrototype = new BasicToDeviceActorMsg(msg, type);
+        deviceToDeviceActorMsgPrototype = new BasicDeviceToDeviceActorMsg(msg, type);
     }
 
-    protected ToDeviceActorMsg toDeviceMsg(ToDeviceActorSessionMsg msg) {
+    protected DeviceToDeviceActorMsg toDeviceMsg(TransportToDeviceSessionActorMsg msg) {
         AdaptorToSessionActorMsg adaptorMsg = msg.getSessionMsg();
-        return new BasicToDeviceActorMsg(toDeviceActorMsgPrototype, adaptorMsg.getMsg());
+        return new BasicDeviceToDeviceActorMsg(deviceToDeviceActorMsgPrototype, adaptorMsg.getMsg());
     }
 
-    protected Optional<ToDeviceActorMsg> toDeviceMsg(FromDeviceMsg msg) {
-        if (toDeviceActorMsgPrototype != null) {
-            return Optional.of(new BasicToDeviceActorMsg(toDeviceActorMsgPrototype, msg));
+    protected Optional<DeviceToDeviceActorMsg> toDeviceMsg(FromDeviceMsg msg) {
+        if (deviceToDeviceActorMsgPrototype != null) {
+            return Optional.of(new BasicDeviceToDeviceActorMsg(deviceToDeviceActorMsgPrototype, msg));
         } else {
             return Optional.empty();
         }
     }
 
-    protected Optional<ServerAddress> forwardToAppActor(ActorContext ctx, ToDeviceActorMsg toForward) {
+    protected Optional<ServerAddress> forwardToAppActor(ActorContext ctx, DeviceToDeviceActorMsg toForward) {
         Optional<ServerAddress> address = systemContext.getRoutingService().resolveById(toForward.getDeviceId());
         forwardToAppActor(ctx, toForward, address);
         return address;
     }
 
-    protected Optional<ServerAddress> forwardToAppActorIfAdressChanged(ActorContext ctx, ToDeviceActorMsg toForward, Optional<ServerAddress> oldAddress) {
+    protected Optional<ServerAddress> forwardToAppActorIfAddressChanged(ActorContext ctx, DeviceToDeviceActorMsg toForward, Optional<ServerAddress> oldAddress) {
+
         Optional<ServerAddress> newAddress = systemContext.getRoutingService().resolveById(toForward.getDeviceId());
         if (!newAddress.equals(oldAddress)) {
-            if (newAddress.isPresent()) {
-                systemContext.getRpcService().tell(newAddress.get(),
-                        toForward.toOtherAddress(systemContext.getRoutingService().getCurrentServer()));
-            } else {
-                getAppActor().tell(toForward, ctx.self());
-            }
+            getAppActor().tell(new SendToClusterMsg(toForward.getDeviceId(), toForward
+                    .toOtherAddress(systemContext.getRoutingService().getCurrentServer())), ctx.self());
         }
         return newAddress;
     }
 
-    protected void forwardToAppActor(ActorContext ctx, ToDeviceActorMsg toForward, Optional<ServerAddress> address) {
+    protected void forwardToAppActor(ActorContext ctx, DeviceToDeviceActorMsg toForward, Optional<ServerAddress> address) {
         if (address.isPresent()) {
-            systemContext.getRpcService().tell(address.get(),
-                    toForward.toOtherAddress(systemContext.getRoutingService().getCurrentServer()));
+            systemContext.getRpcService().tell(systemContext.getEncodingService().convertToProtoDataMessage(address.get(),
+                    toForward.toOtherAddress(systemContext.getRoutingService().getCurrentServer())));
         } else {
             getAppActor().tell(toForward, ctx.self());
         }
@@ -114,6 +117,6 @@ abstract class AbstractSessionActorMsgProcessor extends AbstractContextAwareMsgP
     }
 
     public DeviceId getDeviceId() {
-        return toDeviceActorMsgPrototype.getDeviceId();
+        return deviceToDeviceActorMsgPrototype.getDeviceId();
     }
 }
diff --git a/application/src/main/java/org/thingsboard/server/actors/session/ASyncMsgProcessor.java b/application/src/main/java/org/thingsboard/server/actors/session/ASyncMsgProcessor.java
index ab75cdb..a8f14fe 100644
--- a/application/src/main/java/org/thingsboard/server/actors/session/ASyncMsgProcessor.java
+++ b/application/src/main/java/org/thingsboard/server/actors/session/ASyncMsgProcessor.java
@@ -15,19 +15,26 @@
  */
 package org.thingsboard.server.actors.session;
 
+import akka.actor.ActorContext;
+import akka.event.LoggingAdapter;
 import org.thingsboard.server.actors.ActorSystemContext;
 import org.thingsboard.server.actors.shared.SessionTimeoutMsg;
 import org.thingsboard.server.common.data.id.SessionId;
 import org.thingsboard.server.common.msg.cluster.ClusterEventMsg;
 import org.thingsboard.server.common.msg.cluster.ServerAddress;
-import org.thingsboard.server.common.msg.core.*;
+import org.thingsboard.server.common.msg.core.AttributesSubscribeMsg;
+import org.thingsboard.server.common.msg.core.ResponseMsg;
+import org.thingsboard.server.common.msg.core.RpcSubscribeMsg;
 import org.thingsboard.server.common.msg.core.SessionCloseMsg;
-import org.thingsboard.server.common.msg.device.ToDeviceActorMsg;
-import org.thingsboard.server.common.msg.session.*;
-
-import akka.actor.ActorContext;
-import akka.event.LoggingAdapter;
-import org.thingsboard.server.common.msg.session.ctrl.*;
+import org.thingsboard.server.common.msg.core.SessionOpenMsg;
+import org.thingsboard.server.common.msg.device.DeviceToDeviceActorMsg;
+import org.thingsboard.server.common.msg.session.BasicSessionActorToAdaptorMsg;
+import org.thingsboard.server.common.msg.session.FromDeviceMsg;
+import org.thingsboard.server.common.msg.session.FromDeviceRequestMsg;
+import org.thingsboard.server.common.msg.session.SessionMsgType;
+import org.thingsboard.server.common.msg.session.SessionType;
+import org.thingsboard.server.common.msg.session.ToDeviceMsg;
+import org.thingsboard.server.common.msg.session.TransportToDeviceSessionActorMsg;
 import org.thingsboard.server.common.msg.session.ex.SessionException;
 
 import java.util.HashMap;
@@ -37,7 +44,7 @@ import java.util.Optional;
 class ASyncMsgProcessor extends AbstractSessionActorMsgProcessor {
 
     private boolean firstMsg = true;
-    private Map<Integer, ToDeviceActorMsg> pendingMap = new HashMap<>();
+    private Map<Integer, DeviceToDeviceActorMsg> pendingMap = new HashMap<>();
     private Optional<ServerAddress> currentTargetServer;
     private boolean subscribedToAttributeUpdates;
     private boolean subscribedToRpcCommands;
@@ -47,14 +54,16 @@ class ASyncMsgProcessor extends AbstractSessionActorMsgProcessor {
     }
 
     @Override
-    protected void processToDeviceActorMsg(ActorContext ctx, ToDeviceActorSessionMsg msg) {
+    protected void processToDeviceActorMsg(ActorContext ctx, TransportToDeviceSessionActorMsg msg) {
         updateSessionCtx(msg, SessionType.ASYNC);
+        DeviceToDeviceActorMsg pendingMsg = toDeviceMsg(msg);
+        FromDeviceMsg fromDeviceMsg = pendingMsg.getPayload();
         if (firstMsg) {
-            toDeviceMsg(new SessionOpenMsg()).ifPresent(m -> forwardToAppActor(ctx, m));
+            if (fromDeviceMsg.getMsgType() != SessionMsgType.SESSION_OPEN) {
+                toDeviceMsg(new SessionOpenMsg()).ifPresent(m -> forwardToAppActor(ctx, m));
+            }
             firstMsg = false;
         }
-        ToDeviceActorMsg pendingMsg = toDeviceMsg(msg);
-        FromDeviceMsg fromDeviceMsg = pendingMsg.getPayload();
         switch (fromDeviceMsg.getMsgType()) {
             case POST_TELEMETRY_REQUEST:
             case POST_ATTRIBUTES_REQUEST:
@@ -86,8 +95,8 @@ class ASyncMsgProcessor extends AbstractSessionActorMsgProcessor {
     @Override
     public void processToDeviceMsg(ActorContext context, ToDeviceMsg msg) {
         try {
-            if (msg.getMsgType() != MsgType.SESSION_CLOSE) {
-                switch (msg.getMsgType()) {
+            if (msg.getSessionMsgType() != SessionMsgType.SESSION_CLOSE) {
+                switch (msg.getSessionMsgType()) {
                     case STATUS_CODE_RESPONSE:
                     case GET_ATTRIBUTES_RESPONSE:
                         ResponseMsg responseMsg = (ResponseMsg) msg;
diff --git a/application/src/main/java/org/thingsboard/server/actors/session/SessionActor.java b/application/src/main/java/org/thingsboard/server/actors/session/SessionActor.java
index 37827d6..05926c1 100644
--- a/application/src/main/java/org/thingsboard/server/actors/session/SessionActor.java
+++ b/application/src/main/java/org/thingsboard/server/actors/session/SessionActor.java
@@ -17,22 +17,21 @@ package org.thingsboard.server.actors.session;
 
 import akka.actor.OneForOneStrategy;
 import akka.actor.SupervisorStrategy;
-import akka.japi.Function;
+import akka.event.Logging;
+import akka.event.LoggingAdapter;
 import org.thingsboard.server.actors.ActorSystemContext;
 import org.thingsboard.server.actors.service.ContextAwareActor;
 import org.thingsboard.server.actors.service.ContextBasedCreator;
 import org.thingsboard.server.actors.shared.SessionTimeoutMsg;
 import org.thingsboard.server.common.data.id.SessionId;
+import org.thingsboard.server.common.msg.TbActorMsg;
 import org.thingsboard.server.common.msg.cluster.ClusterEventMsg;
-import org.thingsboard.server.common.msg.core.ToDeviceSessionActorMsg;
-import org.thingsboard.server.common.msg.session.ToDeviceActorSessionMsg;
+import org.thingsboard.server.common.msg.core.ActorSystemToDeviceSessionActorMsg;
 import org.thingsboard.server.common.msg.session.SessionCtrlMsg;
 import org.thingsboard.server.common.msg.session.SessionMsg;
 import org.thingsboard.server.common.msg.session.SessionType;
+import org.thingsboard.server.common.msg.session.TransportToDeviceSessionActorMsg;
 import org.thingsboard.server.common.msg.session.ctrl.SessionCloseMsg;
-
-import akka.event.Logging;
-import akka.event.LoggingAdapter;
 import scala.concurrent.duration.Duration;
 
 public class SessionActor extends ContextAwareActor {
@@ -61,33 +60,38 @@ public class SessionActor extends ContextAwareActor {
     }
 
     @Override
-    public void onReceive(Object msg) throws Exception {
-        logger.debug("[{}] Processing: {}.", sessionId, msg);
-        if (msg instanceof ToDeviceActorSessionMsg) {
-            processDeviceMsg((ToDeviceActorSessionMsg) msg);
-        } else if (msg instanceof ToDeviceSessionActorMsg) {
-            processToDeviceMsg((ToDeviceSessionActorMsg) msg);
-        } else if (msg instanceof SessionTimeoutMsg) {
-            processTimeoutMsg((SessionTimeoutMsg) msg);
-        } else if (msg instanceof SessionCtrlMsg) {
-            processSessionCtrlMsg((SessionCtrlMsg) msg);
-        } else if (msg instanceof ClusterEventMsg) {
-            processClusterEvent((ClusterEventMsg) msg);
-        } else {
-            logger.warning("[{}] Unknown msg: {}", sessionId, msg);
+    protected boolean process(TbActorMsg msg) {
+        switch (msg.getMsgType()) {
+            case TRANSPORT_TO_DEVICE_SESSION_ACTOR_MSG:
+                processTransportToSessionMsg((TransportToDeviceSessionActorMsg) msg);
+                break;
+            case ACTOR_SYSTEM_TO_DEVICE_SESSION_ACTOR_MSG:
+                processActorsToSessionMsg((ActorSystemToDeviceSessionActorMsg) msg);
+                break;
+            case SESSION_TIMEOUT_MSG:
+                processTimeoutMsg((SessionTimeoutMsg) msg);
+                break;
+            case SESSION_CTRL_MSG:
+                processSessionCloseMsg((SessionCtrlMsg) msg);
+                break;
+            case CLUSTER_EVENT_MSG:
+                processClusterEvent((ClusterEventMsg) msg);
+                break;
+            default: return false;
         }
+        return true;
     }
 
     private void processClusterEvent(ClusterEventMsg msg) {
         processor.processClusterEvent(context(), msg);
     }
 
-    private void processDeviceMsg(ToDeviceActorSessionMsg msg) {
+    private void processTransportToSessionMsg(TransportToDeviceSessionActorMsg msg) {
         initProcessor(msg);
         processor.processToDeviceActorMsg(context(), msg);
     }
 
-    private void processToDeviceMsg(ToDeviceSessionActorMsg msg) {
+    private void processActorsToSessionMsg(ActorSystemToDeviceSessionActorMsg msg) {
         processor.processToDeviceMsg(context(), msg.getMsg());
     }
 
@@ -99,7 +103,7 @@ public class SessionActor extends ContextAwareActor {
         }
     }
 
-    private void processSessionCtrlMsg(SessionCtrlMsg msg) {
+    private void processSessionCloseMsg(SessionCtrlMsg msg) {
         if (processor != null) {
             processor.processSessionCtrlMsg(context(), msg);
         } else if (msg instanceof SessionCloseMsg) {
@@ -109,7 +113,7 @@ public class SessionActor extends ContextAwareActor {
         }
     }
 
-    private void initProcessor(ToDeviceActorSessionMsg msg) {
+    private void initProcessor(TransportToDeviceSessionActorMsg msg) {
         if (processor == null) {
             SessionMsg sessionMsg = (SessionMsg) msg.getSessionMsg();
             if (sessionMsg.getSessionContext().getSessionType() == SessionType.SYNC) {
diff --git a/application/src/main/java/org/thingsboard/server/actors/session/SessionManagerActor.java b/application/src/main/java/org/thingsboard/server/actors/session/SessionManagerActor.java
index 9d67dab..624a9f4 100644
--- a/application/src/main/java/org/thingsboard/server/actors/session/SessionManagerActor.java
+++ b/application/src/main/java/org/thingsboard/server/actors/session/SessionManagerActor.java
@@ -15,26 +15,29 @@
  */
 package org.thingsboard.server.actors.session;
 
-import java.util.HashMap;
-import java.util.Map;
-import java.util.UUID;
-
-import akka.actor.*;
+import akka.actor.ActorRef;
+import akka.actor.InvalidActorNameException;
+import akka.actor.LocalActorRef;
+import akka.actor.Props;
+import akka.actor.Terminated;
+import akka.event.Logging;
+import akka.event.LoggingAdapter;
 import org.thingsboard.server.actors.ActorSystemContext;
 import org.thingsboard.server.actors.service.ContextAwareActor;
 import org.thingsboard.server.actors.service.ContextBasedCreator;
 import org.thingsboard.server.actors.service.DefaultActorService;
 import org.thingsboard.server.actors.shared.SessionTimeoutMsg;
 import org.thingsboard.server.common.data.id.SessionId;
+import org.thingsboard.server.common.msg.TbActorMsg;
 import org.thingsboard.server.common.msg.aware.SessionAwareMsg;
-
-import akka.event.Logging;
-import akka.event.LoggingAdapter;
 import org.thingsboard.server.common.msg.cluster.ClusterEventMsg;
+import org.thingsboard.server.common.msg.core.ActorSystemToDeviceSessionActorMsg;
 import org.thingsboard.server.common.msg.core.SessionCloseMsg;
-import org.thingsboard.server.common.msg.core.ToDeviceSessionActorMsg;
 import org.thingsboard.server.common.msg.session.SessionCtrlMsg;
 
+import java.util.HashMap;
+import java.util.Map;
+
 public class SessionManagerActor extends ContextAwareActor {
 
     private static final int INITIAL_SESSION_MAP_SIZE = 1024;
@@ -49,6 +52,12 @@ public class SessionManagerActor extends ContextAwareActor {
     }
 
     @Override
+    protected boolean process(TbActorMsg msg) {
+        //TODO Move everything here, to work with TbActorMsg
+        return false;
+    }
+
+    @Override
     public void onReceive(Object msg) throws Exception {
         if (msg instanceof SessionCtrlMsg) {
             onSessionCtrlMsg((SessionCtrlMsg) msg);
@@ -97,7 +106,7 @@ public class SessionManagerActor extends ContextAwareActor {
     }
 
     private void forwardToSessionActor(SessionAwareMsg msg) {
-        if (msg instanceof ToDeviceSessionActorMsg || msg instanceof SessionCloseMsg) {
+        if (msg instanceof ActorSystemToDeviceSessionActorMsg || msg instanceof SessionCloseMsg) {
             String sessionIdStr = msg.getSessionId().toUidStr();
             ActorRef sessionActor = sessionActors.get(sessionIdStr);
             if (sessionActor != null) {
diff --git a/application/src/main/java/org/thingsboard/server/actors/session/SyncMsgProcessor.java b/application/src/main/java/org/thingsboard/server/actors/session/SyncMsgProcessor.java
index d696503..cf8df13 100644
--- a/application/src/main/java/org/thingsboard/server/actors/session/SyncMsgProcessor.java
+++ b/application/src/main/java/org/thingsboard/server/actors/session/SyncMsgProcessor.java
@@ -15,24 +15,26 @@
  */
 package org.thingsboard.server.actors.session;
 
+import akka.actor.ActorContext;
+import akka.event.LoggingAdapter;
 import org.thingsboard.server.actors.ActorSystemContext;
 import org.thingsboard.server.actors.shared.SessionTimeoutMsg;
 import org.thingsboard.server.common.data.id.SessionId;
 import org.thingsboard.server.common.msg.cluster.ClusterEventMsg;
 import org.thingsboard.server.common.msg.cluster.ServerAddress;
-import org.thingsboard.server.common.msg.device.ToDeviceActorMsg;
-import org.thingsboard.server.common.msg.session.*;
-import org.thingsboard.server.common.msg.session.ToDeviceActorSessionMsg;
+import org.thingsboard.server.common.msg.device.DeviceToDeviceActorMsg;
+import org.thingsboard.server.common.msg.session.BasicSessionActorToAdaptorMsg;
+import org.thingsboard.server.common.msg.session.SessionContext;
+import org.thingsboard.server.common.msg.session.SessionType;
+import org.thingsboard.server.common.msg.session.ToDeviceMsg;
+import org.thingsboard.server.common.msg.session.TransportToDeviceSessionActorMsg;
 import org.thingsboard.server.common.msg.session.ctrl.SessionCloseMsg;
 import org.thingsboard.server.common.msg.session.ex.SessionException;
 
-import akka.actor.ActorContext;
-import akka.event.LoggingAdapter;
-
 import java.util.Optional;
 
 class SyncMsgProcessor extends AbstractSessionActorMsgProcessor {
-    private ToDeviceActorMsg pendingMsg;
+    private DeviceToDeviceActorMsg pendingMsg;
     private Optional<ServerAddress> currentTargetServer;
     private boolean pendingResponse;
 
@@ -41,7 +43,7 @@ class SyncMsgProcessor extends AbstractSessionActorMsgProcessor {
     }
 
     @Override
-    protected void processToDeviceActorMsg(ActorContext ctx, ToDeviceActorSessionMsg msg) {
+    protected void processToDeviceActorMsg(ActorContext ctx, TransportToDeviceSessionActorMsg msg) {
         updateSessionCtx(msg, SessionType.SYNC);
         pendingMsg = toDeviceMsg(msg);
         pendingResponse = true;
@@ -73,7 +75,7 @@ class SyncMsgProcessor extends AbstractSessionActorMsgProcessor {
     @Override
     public void processClusterEvent(ActorContext context, ClusterEventMsg msg) {
         if (pendingResponse) {
-            Optional<ServerAddress> newTargetServer = forwardToAppActorIfAdressChanged(context, pendingMsg, currentTargetServer);
+            Optional<ServerAddress> newTargetServer = forwardToAppActorIfAddressChanged(context, pendingMsg, currentTargetServer);
             if (logger.isDebugEnabled()) {
                 if (!newTargetServer.equals(currentTargetServer)) {
                     if (newTargetServer.isPresent()) {
diff --git a/application/src/main/java/org/thingsboard/server/actors/shared/AbstractContextAwareMsgProcessor.java b/application/src/main/java/org/thingsboard/server/actors/shared/AbstractContextAwareMsgProcessor.java
index 73b221f..8864486 100644
--- a/application/src/main/java/org/thingsboard/server/actors/shared/AbstractContextAwareMsgProcessor.java
+++ b/application/src/main/java/org/thingsboard/server/actors/shared/AbstractContextAwareMsgProcessor.java
@@ -19,18 +19,13 @@ import akka.actor.ActorContext;
 import akka.actor.ActorRef;
 import akka.actor.Scheduler;
 import akka.event.LoggingAdapter;
-import com.fasterxml.jackson.databind.JsonNode;
 import com.fasterxml.jackson.databind.ObjectMapper;
 import lombok.AllArgsConstructor;
 import lombok.Data;
 import org.thingsboard.server.actors.ActorSystemContext;
-import org.thingsboard.server.common.data.plugin.ComponentDescriptor;
-import org.thingsboard.server.common.data.plugin.ComponentType;
-import org.thingsboard.server.extensions.api.component.*;
 import scala.concurrent.ExecutionContextExecutor;
 import scala.concurrent.duration.Duration;
 
-import java.io.IOException;
 import java.util.concurrent.TimeUnit;
 
 public abstract class AbstractContextAwareMsgProcessor {
@@ -76,53 +71,6 @@ public abstract class AbstractContextAwareMsgProcessor {
         getScheduler().scheduleOnce(Duration.create(delayInMs, TimeUnit.MILLISECONDS), target, msg, getSystemDispatcher(), null);
     }
 
-    protected <T extends ConfigurableComponent> T initComponent(JsonNode componentNode) throws Exception {
-        ComponentConfiguration configuration = new ComponentConfiguration(
-                componentNode.get("clazz").asText(),
-                componentNode.get("name").asText(),
-                mapper.writeValueAsString(componentNode.get("configuration"))
-        );
-        logger.info("Initializing [{}][{}] component", configuration.getName(), configuration.getClazz());
-        ComponentDescriptor componentDescriptor = systemContext.getComponentService().getComponent(configuration.getClazz())
-                .orElseThrow(() -> new InstantiationException("Component Not found!"));
-        return initComponent(componentDescriptor, configuration);
-    }
-
-    protected <T extends ConfigurableComponent> T initComponent(ComponentDescriptor componentDefinition, ComponentConfiguration configuration)
-            throws Exception {
-        return initComponent(componentDefinition.getClazz(), componentDefinition.getType(), configuration.getConfiguration());
-    }
-
-    protected <T extends ConfigurableComponent> T initComponent(String clazz, ComponentType type, String configuration)
-            throws Exception {
-        Class<?> componentClazz = Class.forName(clazz);
-        T component = (T) (componentClazz.newInstance());
-        Class<?> configurationClazz;
-        switch (type) {
-            case FILTER:
-                configurationClazz = ((Filter) componentClazz.getAnnotation(Filter.class)).configuration();
-                break;
-            case PROCESSOR:
-                configurationClazz = ((Processor) componentClazz.getAnnotation(Processor.class)).configuration();
-                break;
-            case ACTION:
-                configurationClazz = ((Action) componentClazz.getAnnotation(Action.class)).configuration();
-                break;
-            case PLUGIN:
-                configurationClazz = ((Plugin) componentClazz.getAnnotation(Plugin.class)).configuration();
-                break;
-            default:
-                throw new IllegalStateException("Component with type: " + type + " is not supported!");
-        }
-        component.init(decode(configuration, configurationClazz));
-        return component;
-    }
-
-    public <C> C decode(String configuration, Class<C> configurationClazz) throws IOException, RuntimeException {
-        logger.info("Initializing using configuration: {}", configuration);
-        return mapper.readValue(configuration, configurationClazz);
-    }
-
     @Data
     @AllArgsConstructor
     private static class ComponentConfiguration {
diff --git a/application/src/main/java/org/thingsboard/server/actors/shared/ComponentMsgProcessor.java b/application/src/main/java/org/thingsboard/server/actors/shared/ComponentMsgProcessor.java
index 18d32d9..1cf8339 100644
--- a/application/src/main/java/org/thingsboard/server/actors/shared/ComponentMsgProcessor.java
+++ b/application/src/main/java/org/thingsboard/server/actors/shared/ComponentMsgProcessor.java
@@ -17,39 +17,87 @@ package org.thingsboard.server.actors.shared;
 
 import akka.actor.ActorContext;
 import akka.event.LoggingAdapter;
+import com.google.common.util.concurrent.FutureCallback;
+import com.google.common.util.concurrent.Futures;
 import org.thingsboard.server.actors.ActorSystemContext;
 import org.thingsboard.server.actors.stats.StatsPersistTick;
+import org.thingsboard.server.common.data.id.EntityId;
 import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.plugin.ComponentLifecycleState;
+import org.thingsboard.server.common.msg.TbMsg;
 import org.thingsboard.server.common.msg.cluster.ClusterEventMsg;
+import org.thingsboard.server.service.queue.MsgQueueService;
 
-public abstract class ComponentMsgProcessor<T> extends AbstractContextAwareMsgProcessor {
+import javax.annotation.Nullable;
+import java.util.function.Consumer;
+
+public abstract class ComponentMsgProcessor<T extends EntityId> extends AbstractContextAwareMsgProcessor {
 
     protected final TenantId tenantId;
     protected final T entityId;
+    protected final MsgQueueService queue;
+    protected ComponentLifecycleState state;
 
     protected ComponentMsgProcessor(ActorSystemContext systemContext, LoggingAdapter logger, TenantId tenantId, T id) {
         super(systemContext, logger);
         this.tenantId = tenantId;
         this.entityId = id;
+        this.queue = systemContext.getMsgQueueService();
     }
 
-    public abstract void start() throws Exception;
+    public abstract void start(ActorContext context) throws Exception;
 
-    public abstract void stop() throws Exception;
+    public abstract void stop(ActorContext context) throws Exception;
 
-    public abstract void onCreated(ActorContext context) throws Exception;
+    public abstract void onClusterEventMsg(ClusterEventMsg msg) throws Exception;
 
-    public abstract void onUpdate(ActorContext context) throws Exception;
+    public void onCreated(ActorContext context) throws Exception {
+        start(context);
+    }
 
-    public abstract void onActivate(ActorContext context) throws Exception;
+    public void onUpdate(ActorContext context) throws Exception {
+        restart(context);
+    }
 
-    public abstract void onSuspend(ActorContext context) throws Exception;
+    public void onActivate(ActorContext context) throws Exception {
+        restart(context);
+    }
 
-    public abstract void onStop(ActorContext context) throws Exception;
+    public void onSuspend(ActorContext context) throws Exception {
+        stop(context);
+    }
 
-    public abstract void onClusterEventMsg(ClusterEventMsg msg) throws Exception;
+    public void onStop(ActorContext context) throws Exception {
+        stop(context);
+    }
+
+    private void restart(ActorContext context) throws Exception {
+        stop(context);
+        start(context);
+    }
 
     public void scheduleStatsPersistTick(ActorContext context, long statsPersistFrequency) {
         schedulePeriodicMsgWithDelay(context, new StatsPersistTick(), statsPersistFrequency, statsPersistFrequency);
     }
+
+    protected void checkActive() {
+        if (state != ComponentLifecycleState.ACTIVE) {
+            throw new IllegalStateException("Rule chain is not active!");
+        }
+    }
+
+    protected void putToQueue(final TbMsg tbMsg, final Consumer<TbMsg> onSuccess) {
+        EntityId entityId = tbMsg.getRuleNodeId() != null ? tbMsg.getRuleNodeId() : tbMsg.getRuleChainId();
+        Futures.addCallback(queue.put(this.tenantId, tbMsg, entityId.getId(), tbMsg.getClusterPartition()), new FutureCallback<Void>() {
+            @Override
+            public void onSuccess(@Nullable Void result) {
+                onSuccess.accept(tbMsg);
+            }
+
+            @Override
+            public void onFailure(Throwable t) {
+                logger.debug("Failed to push message [{}] to queue due to [{}]", tbMsg, t);
+            }
+        });
+    }
 }
diff --git a/application/src/main/java/org/thingsboard/server/actors/shared/rulechain/RuleChainManager.java b/application/src/main/java/org/thingsboard/server/actors/shared/rulechain/RuleChainManager.java
new file mode 100644
index 0000000..11ed5a3
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/actors/shared/rulechain/RuleChainManager.java
@@ -0,0 +1,59 @@
+/**
+ * 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.server.actors.shared.rulechain;
+
+import akka.actor.ActorRef;
+import akka.japi.Creator;
+import lombok.Getter;
+import lombok.extern.slf4j.Slf4j;
+import org.thingsboard.server.actors.ActorSystemContext;
+import org.thingsboard.server.actors.ruleChain.RuleChainActor;
+import org.thingsboard.server.actors.shared.EntityActorsManager;
+import org.thingsboard.server.common.data.id.RuleChainId;
+import org.thingsboard.server.common.data.rule.RuleChain;
+import org.thingsboard.server.dao.rule.RuleChainService;
+
+/**
+ * Created by ashvayka on 15.03.18.
+ */
+@Slf4j
+public abstract class RuleChainManager extends EntityActorsManager<RuleChainId, RuleChainActor, RuleChain> {
+
+    protected final RuleChainService service;
+    @Getter
+    protected RuleChain rootChain;
+    @Getter
+    protected ActorRef rootChainActor;
+
+    public RuleChainManager(ActorSystemContext systemContext) {
+        super(systemContext);
+        this.service = systemContext.getRuleChainService();
+    }
+
+    @Override
+    public Creator<RuleChainActor> creator(RuleChainId entityId) {
+        return new RuleChainActor.ActorCreator(systemContext, getTenantId(), entityId);
+    }
+
+    @Override
+    public void visit(RuleChain entity, ActorRef actorRef) {
+        if (entity.isRoot()) {
+            rootChain = entity;
+            rootChainActor = actorRef;
+        }
+    }
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/actors/shared/SessionTimeoutMsg.java b/application/src/main/java/org/thingsboard/server/actors/shared/SessionTimeoutMsg.java
index 7d6dbca..d015fe0 100644
--- a/application/src/main/java/org/thingsboard/server/actors/shared/SessionTimeoutMsg.java
+++ b/application/src/main/java/org/thingsboard/server/actors/shared/SessionTimeoutMsg.java
@@ -17,13 +17,20 @@ package org.thingsboard.server.actors.shared;
 
 import lombok.Data;
 import org.thingsboard.server.common.data.id.SessionId;
+import org.thingsboard.server.common.msg.MsgType;
+import org.thingsboard.server.common.msg.TbActorMsg;
 
 import java.io.Serializable;
 
 @Data
-public class SessionTimeoutMsg implements Serializable {
+public class SessionTimeoutMsg implements Serializable, TbActorMsg {
 
     private static final long serialVersionUID = 1L;
 
     private final SessionId sessionId;
+
+    @Override
+    public MsgType getMsgType() {
+        return MsgType.SESSION_TIMEOUT_MSG;
+    }
 }
diff --git a/application/src/main/java/org/thingsboard/server/actors/stats/StatsActor.java b/application/src/main/java/org/thingsboard/server/actors/stats/StatsActor.java
index ccc31cc..8623370 100644
--- a/application/src/main/java/org/thingsboard/server/actors/stats/StatsActor.java
+++ b/application/src/main/java/org/thingsboard/server/actors/stats/StatsActor.java
@@ -24,6 +24,7 @@ import org.thingsboard.server.actors.service.ContextAwareActor;
 import org.thingsboard.server.actors.service.ContextBasedCreator;
 import org.thingsboard.server.common.data.DataConstants;
 import org.thingsboard.server.common.data.Event;
+import org.thingsboard.server.common.msg.TbActorMsg;
 import org.thingsboard.server.common.msg.cluster.ServerAddress;
 
 public class StatsActor extends ContextAwareActor {
@@ -36,6 +37,12 @@ public class StatsActor extends ContextAwareActor {
     }
 
     @Override
+    protected boolean process(TbActorMsg msg) {
+        //TODO Move everything here, to work with TbActorMsg\
+        return false;
+    }
+
+    @Override
     public void onReceive(Object msg) throws Exception {
         logger.debug("Received message: {}", msg);
         if (msg instanceof StatsPersistMsg) {
diff --git a/application/src/main/java/org/thingsboard/server/actors/tenant/TenantActor.java b/application/src/main/java/org/thingsboard/server/actors/tenant/TenantActor.java
index b923fe1..7a3127d 100644
--- a/application/src/main/java/org/thingsboard/server/actors/tenant/TenantActor.java
+++ b/application/src/main/java/org/thingsboard/server/actors/tenant/TenantActor.java
@@ -15,61 +15,55 @@
  */
 package org.thingsboard.server.actors.tenant;
 
-import java.util.HashMap;
-import java.util.Map;
-import java.util.Optional;
-
+import akka.actor.ActorRef;
+import akka.actor.OneForOneStrategy;
+import akka.actor.Props;
+import akka.actor.SupervisorStrategy;
+import akka.japi.Function;
 import org.thingsboard.server.actors.ActorSystemContext;
 import org.thingsboard.server.actors.device.DeviceActor;
-import org.thingsboard.server.actors.plugin.PluginTerminationMsg;
-import org.thingsboard.server.actors.rule.ComplexRuleActorChain;
-import org.thingsboard.server.actors.rule.RuleActorChain;
-import org.thingsboard.server.actors.service.ContextAwareActor;
+import org.thingsboard.server.actors.device.DeviceActorToRuleEngineMsg;
+import org.thingsboard.server.actors.ruleChain.RuleChainManagerActor;
+import org.thingsboard.server.actors.ruleChain.RuleChainToRuleChainMsg;
 import org.thingsboard.server.actors.service.ContextBasedCreator;
 import org.thingsboard.server.actors.service.DefaultActorService;
-import org.thingsboard.server.actors.shared.plugin.PluginManager;
-import org.thingsboard.server.actors.shared.plugin.TenantPluginManager;
-import org.thingsboard.server.actors.shared.rule.RuleManager;
-import org.thingsboard.server.actors.shared.rule.TenantRuleManager;
+import org.thingsboard.server.actors.shared.rulechain.TenantRuleChainManager;
+import org.thingsboard.server.common.data.EntityType;
 import org.thingsboard.server.common.data.id.DeviceId;
-import org.thingsboard.server.common.data.id.PluginId;
-import org.thingsboard.server.common.data.id.RuleId;
+import org.thingsboard.server.common.data.id.RuleChainId;
 import org.thingsboard.server.common.data.id.TenantId;
-import org.thingsboard.server.common.msg.cluster.ClusterEventMsg;
-import org.thingsboard.server.common.msg.device.ToDeviceActorMsg;
-
-import akka.actor.ActorRef;
-import akka.actor.Props;
-import akka.event.Logging;
-import akka.event.LoggingAdapter;
+import org.thingsboard.server.common.data.rule.RuleChain;
+import org.thingsboard.server.common.msg.TbActorMsg;
+import org.thingsboard.server.common.msg.aware.DeviceAwareMsg;
 import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg;
-import org.thingsboard.server.extensions.api.device.ToDeviceActorNotificationMsg;
-import org.thingsboard.server.extensions.api.plugins.msg.ToPluginActorMsg;
-import org.thingsboard.server.extensions.api.rules.ToRuleActorMsg;
+import org.thingsboard.server.common.msg.system.ServiceToRuleEngineMsg;
+import scala.concurrent.duration.Duration;
 
-public class TenantActor extends ContextAwareActor {
+import java.util.HashMap;
+import java.util.Map;
 
-    private final LoggingAdapter logger = Logging.getLogger(getContext().system(), this);
+public class TenantActor extends RuleChainManagerActor {
 
     private final TenantId tenantId;
-    private final RuleManager ruleManager;
-    private final PluginManager pluginManager;
     private final Map<DeviceId, ActorRef> deviceActors;
 
     private TenantActor(ActorSystemContext systemContext, TenantId tenantId) {
-        super(systemContext);
+        super(systemContext, new TenantRuleChainManager(systemContext, tenantId));
         this.tenantId = tenantId;
-        this.ruleManager = new TenantRuleManager(systemContext, tenantId);
-        this.pluginManager = new TenantPluginManager(systemContext, tenantId);
         this.deviceActors = new HashMap<>();
     }
 
+
+    @Override
+    public SupervisorStrategy supervisorStrategy() {
+        return strategy;
+    }
+
     @Override
     public void preStart() {
         logger.info("[{}] Starting tenant actor.", tenantId);
         try {
-            ruleManager.init(this.context());
-            pluginManager.init(this.context());
+            initRuleChains();
             logger.info("[{}] Tenant actor started.", tenantId);
         } catch (Exception e) {
             logger.error(e, "[{}] Unknown failure", tenantId);
@@ -77,89 +71,74 @@ public class TenantActor extends ContextAwareActor {
     }
 
     @Override
-    public void onReceive(Object msg) throws Exception {
-        logger.debug("[{}] Received message: {}", tenantId, msg);
-        if (msg instanceof RuleChainDeviceMsg) {
-            process((RuleChainDeviceMsg) msg);
-        } else if (msg instanceof ToDeviceActorMsg) {
-            onToDeviceActorMsg((ToDeviceActorMsg) msg);
-        } else if (msg instanceof ToPluginActorMsg) {
-            onToPluginMsg((ToPluginActorMsg) msg);
-        } else if (msg instanceof ToRuleActorMsg) {
-            onToRuleMsg((ToRuleActorMsg) msg);
-        } else if (msg instanceof ToDeviceActorNotificationMsg) {
-            onToDeviceActorMsg((ToDeviceActorNotificationMsg) msg);
-        } else if (msg instanceof ClusterEventMsg) {
-            broadcast(msg);
-        } else if (msg instanceof ComponentLifecycleMsg) {
-            onComponentLifecycleMsg((ComponentLifecycleMsg) msg);
-        } else if (msg instanceof PluginTerminationMsg) {
-            onPluginTerminated((PluginTerminationMsg) msg);
-        } else {
-            logger.warning("[{}] Unknown message: {}!", tenantId, msg);
+    protected boolean process(TbActorMsg msg) {
+        switch (msg.getMsgType()) {
+            case CLUSTER_EVENT_MSG:
+                broadcast(msg);
+                break;
+            case COMPONENT_LIFE_CYCLE_MSG:
+                onComponentLifecycleMsg((ComponentLifecycleMsg) msg);
+                break;
+            case SERVICE_TO_RULE_ENGINE_MSG:
+                onServiceToRuleEngineMsg((ServiceToRuleEngineMsg) msg);
+                break;
+            case DEVICE_ACTOR_TO_RULE_ENGINE_MSG:
+                onDeviceActorToRuleEngineMsg((DeviceActorToRuleEngineMsg) msg);
+                break;
+            case DEVICE_SESSION_TO_DEVICE_ACTOR_MSG:
+            case DEVICE_ATTRIBUTES_UPDATE_TO_DEVICE_ACTOR_MSG:
+            case DEVICE_CREDENTIALS_UPDATE_TO_DEVICE_ACTOR_MSG:
+            case DEVICE_NAME_OR_TYPE_UPDATE_TO_DEVICE_ACTOR_MSG:
+            case DEVICE_RPC_REQUEST_TO_DEVICE_ACTOR_MSG:
+            case SERVER_RPC_RESPONSE_TO_DEVICE_ACTOR_MSG:
+                onToDeviceActorMsg((DeviceAwareMsg) msg);
+                break;
+            case RULE_CHAIN_TO_RULE_CHAIN_MSG:
+                onRuleChainMsg((RuleChainToRuleChainMsg) msg);
+                break;
+            default:
+                return false;
         }
+        return true;
     }
 
-    private void broadcast(Object msg) {
-        pluginManager.broadcast(msg);
+    @Override
+    protected void broadcast(Object msg) {
+        super.broadcast(msg);
         deviceActors.values().forEach(actorRef -> actorRef.tell(msg, ActorRef.noSender()));
     }
 
-    private void onToDeviceActorMsg(ToDeviceActorMsg msg) {
-        getOrCreateDeviceActor(msg.getDeviceId()).tell(msg, ActorRef.noSender());
+    private void onServiceToRuleEngineMsg(ServiceToRuleEngineMsg msg) {
+        ruleChainManager.getRootChainActor().tell(msg, self());
     }
 
-    private void onToDeviceActorMsg(ToDeviceActorNotificationMsg msg) {
-        getOrCreateDeviceActor(msg.getDeviceId()).tell(msg, ActorRef.noSender());
+    private void onDeviceActorToRuleEngineMsg(DeviceActorToRuleEngineMsg msg) {
+        ruleChainManager.getRootChainActor().tell(msg, self());
     }
 
-    private void onToRuleMsg(ToRuleActorMsg msg) {
-        ActorRef target = ruleManager.getOrCreateRuleActor(this.context(), msg.getRuleId());
-        target.tell(msg, ActorRef.noSender());
+    private void onRuleChainMsg(RuleChainToRuleChainMsg msg) {
+        ruleChainManager.getOrCreateActor(context(), msg.getTarget()).tell(msg, self());
     }
 
-    private void onToPluginMsg(ToPluginActorMsg msg) {
-        if (msg.getPluginTenantId().equals(tenantId)) {
-            ActorRef pluginActor = pluginManager.getOrCreatePluginActor(this.context(), msg.getPluginId());
-            pluginActor.tell(msg, ActorRef.noSender());
-        } else {
-            context().parent().tell(msg, ActorRef.noSender());
-        }
+
+    private void onToDeviceActorMsg(DeviceAwareMsg msg) {
+        getOrCreateDeviceActor(msg.getDeviceId()).tell(msg, ActorRef.noSender());
     }
 
     private void onComponentLifecycleMsg(ComponentLifecycleMsg msg) {
-        Optional<PluginId> pluginId = msg.getPluginId();
-        Optional<RuleId> ruleId = msg.getRuleId();
-        if (pluginId.isPresent()) {
-            ActorRef pluginActor = pluginManager.getOrCreatePluginActor(this.context(), pluginId.get());
-            pluginActor.tell(msg, ActorRef.noSender());
-        } else if (ruleId.isPresent()) {
-            ActorRef target;
-            Optional<ActorRef> ref = ruleManager.update(this.context(), ruleId.get(), msg.getEvent());
-            if (ref.isPresent()) {
-                target = ref.get();
-            } else {
-                logger.debug("Failed to find actor for rule: [{}]", ruleId);
-                return;
+        ActorRef target = getEntityActorRef(msg.getEntityId());
+        if (target != null) {
+            if (msg.getEntityId().getEntityType() == EntityType.RULE_CHAIN) {
+                RuleChain ruleChain = systemContext.getRuleChainService().
+                        findRuleChainById(new RuleChainId(msg.getEntityId().getId()));
+                ruleChainManager.visit(ruleChain, target);
             }
             target.tell(msg, ActorRef.noSender());
         } else {
-            logger.debug("[{}] Invalid component lifecycle msg.", tenantId);
+            logger.debug("Invalid component lifecycle msg: {}", msg);
         }
     }
 
-    private void onPluginTerminated(PluginTerminationMsg msg) {
-        pluginManager.remove(msg.getId());
-    }
-
-    private void process(RuleChainDeviceMsg msg) {
-        ToDeviceActorMsg toDeviceActorMsg = msg.getToDeviceActorMsg();
-        ActorRef deviceActor = getOrCreateDeviceActor(toDeviceActorMsg.getDeviceId());
-        RuleActorChain tenantChain = ruleManager.getRuleChain(this.context());
-        RuleActorChain chain = new ComplexRuleActorChain(msg.getRuleChain(), tenantChain);
-        deviceActor.tell(new RuleChainDeviceMsg(toDeviceActorMsg, chain), context().self());
-    }
-
     private ActorRef getOrCreateDeviceActor(DeviceId deviceId) {
         return deviceActors.computeIfAbsent(deviceId, k -> context().actorOf(Props.create(new DeviceActor.ActorCreator(systemContext, tenantId, deviceId))
                 .withDispatcher(DefaultActorService.CORE_DISPATCHER_NAME), deviceId.toString()));
@@ -181,4 +160,12 @@ public class TenantActor extends ContextAwareActor {
         }
     }
 
+    private final SupervisorStrategy strategy = new OneForOneStrategy(3, Duration.create("1 minute"), new Function<Throwable, SupervisorStrategy.Directive>() {
+        @Override
+        public SupervisorStrategy.Directive apply(Throwable t) {
+            logger.error(t, "Unknown failure");
+            return SupervisorStrategy.resume();
+        }
+    });
+
 }
diff --git a/application/src/main/java/org/thingsboard/server/config/AuditLogLevelProperties.java b/application/src/main/java/org/thingsboard/server/config/AuditLogLevelProperties.java
index 34afd35..4c36a15 100644
--- a/application/src/main/java/org/thingsboard/server/config/AuditLogLevelProperties.java
+++ b/application/src/main/java/org/thingsboard/server/config/AuditLogLevelProperties.java
@@ -17,8 +17,6 @@ package org.thingsboard.server.config;
 
 import org.springframework.boot.context.properties.ConfigurationProperties;
 import org.springframework.context.annotation.Configuration;
-import org.thingsboard.server.common.data.EntityType;
-import org.thingsboard.server.common.data.audit.ActionType;
 
 import java.util.HashMap;
 import java.util.Map;
diff --git a/application/src/main/java/org/thingsboard/server/config/SwaggerConfiguration.java b/application/src/main/java/org/thingsboard/server/config/SwaggerConfiguration.java
index 87b187e..2254cf3 100644
--- a/application/src/main/java/org/thingsboard/server/config/SwaggerConfiguration.java
+++ b/application/src/main/java/org/thingsboard/server/config/SwaggerConfiguration.java
@@ -24,7 +24,11 @@ import org.springframework.context.annotation.Configuration;
 import org.thingsboard.server.common.data.security.Authority;
 import springfox.documentation.builders.ApiInfoBuilder;
 import springfox.documentation.schema.AlternateTypeRule;
-import springfox.documentation.service.*;
+import springfox.documentation.service.ApiInfo;
+import springfox.documentation.service.ApiKey;
+import springfox.documentation.service.AuthorizationScope;
+import springfox.documentation.service.Contact;
+import springfox.documentation.service.SecurityReference;
 import springfox.documentation.spi.DocumentationType;
 import springfox.documentation.spi.service.contexts.SecurityContext;
 import springfox.documentation.spring.web.plugins.Docket;
diff --git a/application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java b/application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java
index 2952529..6afa6b2 100644
--- a/application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java
+++ b/application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java
@@ -20,7 +20,6 @@ import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.beans.factory.annotation.Qualifier;
 import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
 import org.springframework.boot.autoconfigure.security.SecurityProperties;
-import org.springframework.boot.context.properties.EnableConfigurationProperties;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
 import org.springframework.core.annotation.Order;
@@ -37,15 +36,18 @@ import org.springframework.security.web.authentication.AuthenticationFailureHand
 import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
 import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
 import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
-import org.springframework.web.cors.CorsUtils;
 import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
 import org.springframework.web.filter.CorsFilter;
 import org.thingsboard.server.dao.audit.AuditLogLevelFilter;
 import org.thingsboard.server.exception.ThingsboardErrorResponseHandler;
+import org.thingsboard.server.service.security.auth.jwt.JwtAuthenticationProvider;
+import org.thingsboard.server.service.security.auth.jwt.JwtTokenAuthenticationProcessingFilter;
+import org.thingsboard.server.service.security.auth.jwt.RefreshTokenAuthenticationProvider;
+import org.thingsboard.server.service.security.auth.jwt.RefreshTokenProcessingFilter;
+import org.thingsboard.server.service.security.auth.jwt.SkipPathRequestMatcher;
+import org.thingsboard.server.service.security.auth.jwt.extractor.TokenExtractor;
 import org.thingsboard.server.service.security.auth.rest.RestAuthenticationProvider;
 import org.thingsboard.server.service.security.auth.rest.RestLoginProcessingFilter;
-import org.thingsboard.server.service.security.auth.jwt.*;
-import org.thingsboard.server.service.security.auth.jwt.extractor.TokenExtractor;
 import org.thingsboard.server.service.security.auth.rest.RestPublicLoginProcessingFilter;
 
 import java.util.ArrayList;
diff --git a/application/src/main/java/org/thingsboard/server/config/WebSocketConfiguration.java b/application/src/main/java/org/thingsboard/server/config/WebSocketConfiguration.java
index a75ecb1..59b7da2 100644
--- a/application/src/main/java/org/thingsboard/server/config/WebSocketConfiguration.java
+++ b/application/src/main/java/org/thingsboard/server/config/WebSocketConfiguration.java
@@ -15,12 +15,6 @@
  */
 package org.thingsboard.server.config;
 
-import java.util.Map;
-
-import org.thingsboard.server.exception.ThingsboardErrorCode;
-import org.thingsboard.server.exception.ThingsboardException;
-import org.thingsboard.server.controller.plugin.PluginWebSocketHandler;
-import org.thingsboard.server.service.security.model.SecurityUser;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
 import org.springframework.http.HttpStatus;
@@ -35,6 +29,12 @@ import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry
 import org.springframework.web.socket.server.HandshakeInterceptor;
 import org.springframework.web.socket.server.standard.ServletServerContainerFactoryBean;
 import org.springframework.web.socket.server.support.HttpSessionHandshakeInterceptor;
+import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
+import org.thingsboard.server.common.data.exception.ThingsboardException;
+import org.thingsboard.server.controller.plugin.TbWebSocketHandler;
+import org.thingsboard.server.service.security.model.SecurityUser;
+
+import java.util.Map;
 
 @Configuration
 @EnableWebSocket
@@ -54,7 +54,7 @@ public class WebSocketConfiguration implements WebSocketConfigurer {
 
     @Override
     public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
-        registry.addHandler(pluginWsHandler(), WS_PLUGIN_MAPPING).setAllowedOrigins("*")
+        registry.addHandler(wsHandler(), WS_PLUGIN_MAPPING).setAllowedOrigins("*")
                 .addInterceptors(new HttpSessionHandshakeInterceptor(), new HandshakeInterceptor() {
 
                     @Override
@@ -82,8 +82,8 @@ public class WebSocketConfiguration implements WebSocketConfigurer {
     }
 
     @Bean
-    public WebSocketHandler pluginWsHandler() {
-        return new PluginWebSocketHandler();
+    public WebSocketHandler wsHandler() {
+        return new TbWebSocketHandler();
     }
 
     protected SecurityUser getCurrentUser() throws ThingsboardException {
diff --git a/application/src/main/java/org/thingsboard/server/controller/AdminController.java b/application/src/main/java/org/thingsboard/server/controller/AdminController.java
index e9a6ba3..0cf1491 100644
--- a/application/src/main/java/org/thingsboard/server/controller/AdminController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/AdminController.java
@@ -17,11 +17,16 @@ package org.thingsboard.server.controller;
 
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.security.access.prepost.PreAuthorize;
-import org.springframework.web.bind.annotation.*;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestMethod;
+import org.springframework.web.bind.annotation.ResponseBody;
+import org.springframework.web.bind.annotation.RestController;
+import org.thingsboard.rule.engine.api.MailService;
 import org.thingsboard.server.common.data.AdminSettings;
+import org.thingsboard.server.common.data.exception.ThingsboardException;
 import org.thingsboard.server.dao.settings.AdminSettingsService;
-import org.thingsboard.server.exception.ThingsboardException;
-import org.thingsboard.server.service.mail.MailService;
 import org.thingsboard.server.service.update.UpdateService;
 import org.thingsboard.server.service.update.model.UpdateMessage;
 
diff --git a/application/src/main/java/org/thingsboard/server/controller/AlarmController.java b/application/src/main/java/org/thingsboard/server/controller/AlarmController.java
index 1959f4e..28cc2fd 100644
--- a/application/src/main/java/org/thingsboard/server/controller/AlarmController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/AlarmController.java
@@ -18,13 +18,27 @@ package org.thingsboard.server.controller;
 import org.apache.commons.lang3.StringUtils;
 import org.springframework.http.HttpStatus;
 import org.springframework.security.access.prepost.PreAuthorize;
-import org.springframework.web.bind.annotation.*;
-import org.thingsboard.server.common.data.alarm.*;
-import org.thingsboard.server.common.data.id.*;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestMethod;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.ResponseBody;
+import org.springframework.web.bind.annotation.ResponseStatus;
+import org.springframework.web.bind.annotation.RestController;
+import org.thingsboard.server.common.data.alarm.Alarm;
+import org.thingsboard.server.common.data.alarm.AlarmId;
+import org.thingsboard.server.common.data.alarm.AlarmInfo;
+import org.thingsboard.server.common.data.alarm.AlarmQuery;
+import org.thingsboard.server.common.data.alarm.AlarmSearchStatus;
+import org.thingsboard.server.common.data.alarm.AlarmSeverity;
+import org.thingsboard.server.common.data.alarm.AlarmStatus;
+import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
+import org.thingsboard.server.common.data.exception.ThingsboardException;
+import org.thingsboard.server.common.data.id.EntityId;
+import org.thingsboard.server.common.data.id.EntityIdFactory;
 import org.thingsboard.server.common.data.page.TimePageData;
 import org.thingsboard.server.common.data.page.TimePageLink;
-import org.thingsboard.server.exception.ThingsboardErrorCode;
-import org.thingsboard.server.exception.ThingsboardException;
 
 @RestController
 @RequestMapping("/api")
@@ -93,7 +107,7 @@ public class AlarmController extends BaseController {
         try {
             AlarmId alarmId = new AlarmId(toUUID(strAlarmId));
             checkAlarmId(alarmId);
-            alarmService.clearAlarm(alarmId, System.currentTimeMillis()).get();
+            alarmService.clearAlarm(alarmId, null, System.currentTimeMillis()).get();
         } catch (Exception e) {
             throw handleException(e);
         }
diff --git a/application/src/main/java/org/thingsboard/server/controller/AssetController.java b/application/src/main/java/org/thingsboard/server/controller/AssetController.java
index 9b43913..35865ab 100644
--- a/application/src/main/java/org/thingsboard/server/controller/AssetController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/AssetController.java
@@ -18,23 +18,30 @@ package org.thingsboard.server.controller;
 import com.google.common.util.concurrent.ListenableFuture;
 import org.springframework.http.HttpStatus;
 import org.springframework.security.access.prepost.PreAuthorize;
-import org.springframework.web.bind.annotation.*;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestMethod;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.ResponseBody;
+import org.springframework.web.bind.annotation.ResponseStatus;
+import org.springframework.web.bind.annotation.RestController;
 import org.thingsboard.server.common.data.Customer;
 import org.thingsboard.server.common.data.EntitySubtype;
 import org.thingsboard.server.common.data.EntityType;
 import org.thingsboard.server.common.data.asset.Asset;
+import org.thingsboard.server.common.data.asset.AssetSearchQuery;
 import org.thingsboard.server.common.data.audit.ActionType;
+import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
+import org.thingsboard.server.common.data.exception.ThingsboardException;
 import org.thingsboard.server.common.data.id.AssetId;
 import org.thingsboard.server.common.data.id.CustomerId;
 import org.thingsboard.server.common.data.id.TenantId;
 import org.thingsboard.server.common.data.page.TextPageData;
 import org.thingsboard.server.common.data.page.TextPageLink;
-import org.thingsboard.server.common.data.asset.AssetSearchQuery;
 import org.thingsboard.server.common.data.security.Authority;
 import org.thingsboard.server.dao.exception.IncorrectParameterException;
 import org.thingsboard.server.dao.model.ModelConstants;
-import org.thingsboard.server.exception.ThingsboardErrorCode;
-import org.thingsboard.server.exception.ThingsboardException;
 import org.thingsboard.server.service.security.model.SecurityUser;
 
 import java.util.ArrayList;
diff --git a/application/src/main/java/org/thingsboard/server/controller/AuditLogController.java b/application/src/main/java/org/thingsboard/server/controller/AuditLogController.java
index 75bcf2a..34bcf84 100644
--- a/application/src/main/java/org/thingsboard/server/controller/AuditLogController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/AuditLogController.java
@@ -16,15 +16,20 @@
 package org.thingsboard.server.controller;
 
 import org.springframework.security.access.prepost.PreAuthorize;
-import org.springframework.web.bind.annotation.*;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestMethod;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.ResponseBody;
+import org.springframework.web.bind.annotation.RestController;
 import org.thingsboard.server.common.data.audit.AuditLog;
+import org.thingsboard.server.common.data.exception.ThingsboardException;
 import org.thingsboard.server.common.data.id.CustomerId;
 import org.thingsboard.server.common.data.id.EntityIdFactory;
 import org.thingsboard.server.common.data.id.TenantId;
 import org.thingsboard.server.common.data.id.UserId;
 import org.thingsboard.server.common.data.page.TimePageData;
 import org.thingsboard.server.common.data.page.TimePageLink;
-import org.thingsboard.server.exception.ThingsboardException;
 
 import java.util.UUID;
 
diff --git a/application/src/main/java/org/thingsboard/server/controller/AuthController.java b/application/src/main/java/org/thingsboard/server/controller/AuthController.java
index ef38d80..af24c9c 100644
--- a/application/src/main/java/org/thingsboard/server/controller/AuthController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/AuthController.java
@@ -25,12 +25,18 @@ import org.springframework.http.HttpStatus;
 import org.springframework.http.ResponseEntity;
 import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
-import org.springframework.web.bind.annotation.*;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestMethod;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.ResponseBody;
+import org.springframework.web.bind.annotation.ResponseStatus;
+import org.springframework.web.bind.annotation.RestController;
+import org.thingsboard.rule.engine.api.MailService;
 import org.thingsboard.server.common.data.User;
+import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
+import org.thingsboard.server.common.data.exception.ThingsboardException;
 import org.thingsboard.server.common.data.security.UserCredentials;
-import org.thingsboard.server.exception.ThingsboardErrorCode;
-import org.thingsboard.server.exception.ThingsboardException;
-import org.thingsboard.server.service.mail.MailService;
 import org.thingsboard.server.service.security.auth.jwt.RefreshTokenRepository;
 import org.thingsboard.server.service.security.model.SecurityUser;
 import org.thingsboard.server.service.security.model.UserPrincipal;
diff --git a/application/src/main/java/org/thingsboard/server/controller/BaseController.java b/application/src/main/java/org/thingsboard/server/controller/BaseController.java
index 3264af4..f044228 100644
--- a/application/src/main/java/org/thingsboard/server/controller/BaseController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/BaseController.java
@@ -15,12 +15,13 @@
  */
 package org.thingsboard.server.controller;
 
-import com.fasterxml.jackson.databind.JsonNode;
+import com.datastax.driver.core.utils.UUIDs;
 import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
 import lombok.extern.slf4j.Slf4j;
 import org.apache.commons.lang3.StringUtils;
 import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.beans.factory.annotation.Value;
 import org.springframework.security.core.Authentication;
 import org.springframework.security.core.context.SecurityContextHolder;
 import org.springframework.web.bind.annotation.ExceptionHandler;
@@ -30,18 +31,24 @@ import org.thingsboard.server.common.data.alarm.Alarm;
 import org.thingsboard.server.common.data.alarm.AlarmId;
 import org.thingsboard.server.common.data.alarm.AlarmInfo;
 import org.thingsboard.server.common.data.asset.Asset;
-import org.thingsboard.server.common.data.audit.ActionStatus;
 import org.thingsboard.server.common.data.audit.ActionType;
+import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
+import org.thingsboard.server.common.data.exception.ThingsboardException;
 import org.thingsboard.server.common.data.id.*;
+import org.thingsboard.server.common.data.kv.AttributeKvEntry;
+import org.thingsboard.server.common.data.kv.DataType;
 import org.thingsboard.server.common.data.page.TextPageLink;
 import org.thingsboard.server.common.data.page.TimePageLink;
 import org.thingsboard.server.common.data.plugin.ComponentDescriptor;
 import org.thingsboard.server.common.data.plugin.ComponentType;
-import org.thingsboard.server.common.data.plugin.PluginMetaData;
-import org.thingsboard.server.common.data.rule.RuleMetaData;
+import org.thingsboard.server.common.data.rule.RuleChain;
 import org.thingsboard.server.common.data.security.Authority;
 import org.thingsboard.server.common.data.widget.WidgetType;
 import org.thingsboard.server.common.data.widget.WidgetsBundle;
+import org.thingsboard.server.common.msg.TbMsg;
+import org.thingsboard.server.common.msg.TbMsgDataType;
+import org.thingsboard.server.common.msg.TbMsgMetaData;
+import org.thingsboard.server.common.msg.system.ServiceToRuleEngineMsg;
 import org.thingsboard.server.dao.alarm.AlarmService;
 import org.thingsboard.server.dao.asset.AssetService;
 import org.thingsboard.server.dao.audit.AuditLogService;
@@ -52,23 +59,23 @@ import org.thingsboard.server.dao.device.DeviceService;
 import org.thingsboard.server.dao.exception.DataValidationException;
 import org.thingsboard.server.dao.exception.IncorrectParameterException;
 import org.thingsboard.server.dao.model.ModelConstants;
-import org.thingsboard.server.dao.plugin.PluginService;
 import org.thingsboard.server.dao.relation.RelationService;
-import org.thingsboard.server.dao.rule.RuleService;
+import org.thingsboard.server.dao.rule.RuleChainService;
+import org.thingsboard.server.dao.tenant.TenantService;
 import org.thingsboard.server.dao.user.UserService;
 import org.thingsboard.server.dao.widget.WidgetTypeService;
 import org.thingsboard.server.dao.widget.WidgetsBundleService;
-import org.thingsboard.server.exception.ThingsboardErrorCode;
 import org.thingsboard.server.exception.ThingsboardErrorResponseHandler;
-import org.thingsboard.server.exception.ThingsboardException;
 import org.thingsboard.server.service.component.ComponentDiscoveryService;
 import org.thingsboard.server.service.security.model.SecurityUser;
+import org.thingsboard.server.service.state.DeviceStateService;
 
 import javax.mail.MessagingException;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 import java.util.List;
 import java.util.Optional;
+import java.util.Set;
 import java.util.UUID;
 
 import static org.thingsboard.server.dao.service.Validator.validateId;
@@ -79,10 +86,15 @@ public abstract class BaseController {
     public static final String INCORRECT_TENANT_ID = "Incorrect tenantId ";
     public static final String YOU_DON_T_HAVE_PERMISSION_TO_PERFORM_THIS_OPERATION = "You don't have permission to perform this operation!";
 
+    private static final ObjectMapper json = new ObjectMapper();
+
     @Autowired
     private ThingsboardErrorResponseHandler errorResponseHandler;
 
     @Autowired
+    protected TenantService tenantService;
+
+    @Autowired
     protected CustomerService customerService;
 
     @Autowired
@@ -113,10 +125,7 @@ public abstract class BaseController {
     protected ComponentDiscoveryService componentDescriptorService;
 
     @Autowired
-    protected RuleService ruleService;
-
-    @Autowired
-    protected PluginService pluginService;
+    protected RuleChainService ruleChainService;
 
     @Autowired
     protected ActorService actorService;
@@ -127,6 +136,9 @@ public abstract class BaseController {
     @Autowired
     protected AuditLogService auditLogService;
 
+    @Autowired
+    protected DeviceStateService deviceStateService;
+
     @ExceptionHandler(ThingsboardException.class)
     public void handleThingsboardException(ThingsboardException ex, HttpServletResponse response) {
         errorResponseHandler.handle(ex, response);
@@ -289,11 +301,8 @@ public abstract class BaseController {
                 case TENANT:
                     checkTenantId(new TenantId(entityId.getId()));
                     return;
-                case PLUGIN:
-                    checkPlugin(new PluginId(entityId.getId()));
-                    return;
-                case RULE:
-                    checkRule(new RuleId(entityId.getId()));
+                case RULE_CHAIN:
+                    checkRuleChain(new RuleChainId(entityId.getId()));
                     return;
                 case ASSET:
                     checkAsset(assetService.findAssetById(new AssetId(entityId.getId())));
@@ -472,60 +481,34 @@ public abstract class BaseController {
         }
     }
 
-    List<ComponentDescriptor> checkPluginActionsByPluginClazz(String pluginClazz) throws ThingsboardException {
+    List<ComponentDescriptor> checkComponentDescriptorsByTypes(Set<ComponentType> types) throws ThingsboardException {
         try {
-            checkComponentDescriptorByClazz(pluginClazz);
-            log.debug("[{}] Lookup plugin actions", pluginClazz);
-            return componentDescriptorService.getPluginActions(pluginClazz);
+            log.debug("[{}] Lookup component descriptors", types);
+            return componentDescriptorService.getComponents(types);
         } catch (Exception e) {
             throw handleException(e, false);
         }
     }
 
-    protected PluginMetaData checkPlugin(PluginMetaData plugin) throws ThingsboardException {
-        checkNotNull(plugin);
-        SecurityUser authUser = getCurrentUser();
-        TenantId tenantId = plugin.getTenantId();
-        validateId(tenantId, INCORRECT_TENANT_ID + tenantId);
-        if (authUser.getAuthority() != Authority.SYS_ADMIN) {
-            if (authUser.getTenantId() == null ||
-                    !tenantId.getId().equals(ModelConstants.NULL_UUID) && !authUser.getTenantId().equals(tenantId)) {
-                throw new ThingsboardException(YOU_DON_T_HAVE_PERMISSION_TO_PERFORM_THIS_OPERATION,
-                        ThingsboardErrorCode.PERMISSION_DENIED);
-
-            } else if (tenantId.getId().equals(ModelConstants.NULL_UUID)) {
-                plugin.setConfiguration(null);
-            }
-        }
-        return plugin;
-    }
-
-    protected PluginMetaData checkPlugin(PluginId pluginId) throws ThingsboardException {
-        checkNotNull(pluginId);
-        return checkPlugin(pluginService.findPluginById(pluginId));
-    }
-
-    protected RuleMetaData checkRule(RuleId ruleId) throws ThingsboardException {
-        checkNotNull(ruleId);
-        return checkRule(ruleService.findRuleById(ruleId));
+    protected RuleChain checkRuleChain(RuleChainId ruleChainId) throws ThingsboardException {
+        checkNotNull(ruleChainId);
+        return checkRuleChain(ruleChainService.findRuleChainById(ruleChainId));
     }
 
-    protected RuleMetaData checkRule(RuleMetaData rule) throws ThingsboardException {
-        checkNotNull(rule);
+    protected RuleChain checkRuleChain(RuleChain ruleChain) throws ThingsboardException {
+        checkNotNull(ruleChain);
         SecurityUser authUser = getCurrentUser();
-        TenantId tenantId = rule.getTenantId();
+        TenantId tenantId = ruleChain.getTenantId();
         validateId(tenantId, INCORRECT_TENANT_ID + tenantId);
-        if (authUser.getAuthority() != Authority.SYS_ADMIN) {
-            if (authUser.getTenantId() == null ||
-                    !tenantId.getId().equals(ModelConstants.NULL_UUID) && !authUser.getTenantId().equals(tenantId)) {
-                throw new ThingsboardException(YOU_DON_T_HAVE_PERMISSION_TO_PERFORM_THIS_OPERATION,
-                        ThingsboardErrorCode.PERMISSION_DENIED);
-
-            }
+        if (authUser.getAuthority() != Authority.TENANT_ADMIN ||
+                !authUser.getTenantId().equals(tenantId)) {
+            throw new ThingsboardException(YOU_DON_T_HAVE_PERMISSION_TO_PERFORM_THIS_OPERATION,
+                    ThingsboardErrorCode.PERMISSION_DENIED);
         }
-        return rule;
+        return ruleChain;
     }
 
+
     protected String constructBaseUrl(HttpServletRequest request) {
         String scheme = request.getScheme();
         if (request.getHeader("x-forwarded-proto") != null) {
@@ -553,12 +536,127 @@ public abstract class BaseController {
     protected <E extends BaseData<I> & HasName,
             I extends UUIDBased & EntityId> void logEntityAction(I entityId, E entity, CustomerId customerId,
                                                                  ActionType actionType, Exception e, Object... additionalInfo) throws ThingsboardException {
-        User user = getCurrentUser();
+        logEntityAction(getCurrentUser(), entityId, entity, customerId, actionType, e, additionalInfo);
+    }
+
+    protected <E extends BaseData<I> & HasName,
+            I extends UUIDBased & EntityId> void logEntityAction(User user, I entityId, E entity, CustomerId customerId,
+                                                                 ActionType actionType, Exception e, Object... additionalInfo) throws ThingsboardException {
         if (customerId == null || customerId.isNullUid()) {
             customerId = user.getCustomerId();
         }
+        if (e == null) {
+            pushEntityActionToRuleEngine(entityId, entity, user, customerId, actionType, additionalInfo);
+        }
         auditLogService.logEntityAction(user.getTenantId(), customerId, user.getId(), user.getName(), entityId, entity, actionType, e, additionalInfo);
     }
 
 
+    public static Exception toException(Throwable error) {
+        return error != null ? (Exception.class.isInstance(error) ? (Exception) error : new Exception(error)) : null;
+    }
+
+    private <E extends BaseData<I> & HasName,
+            I extends UUIDBased & EntityId> void pushEntityActionToRuleEngine(I entityId, E entity, User user, CustomerId customerId,
+                                                                         ActionType actionType, Object... additionalInfo) {
+        String msgType = null;
+        switch (actionType) {
+            case ADDED:
+                msgType = DataConstants.ENTITY_CREATED;
+                break;
+            case DELETED:
+                msgType = DataConstants.ENTITY_DELETED;
+                break;
+            case UPDATED:
+                msgType = DataConstants.ENTITY_UPDATED;
+                break;
+            case ASSIGNED_TO_CUSTOMER:
+                msgType = DataConstants.ENTITY_ASSIGNED;
+                break;
+            case UNASSIGNED_FROM_CUSTOMER:
+                msgType = DataConstants.ENTITY_UNASSIGNED;
+                break;
+            case ATTRIBUTES_UPDATED:
+                msgType = DataConstants.ATTRIBUTES_UPDATED;
+                break;
+            case ATTRIBUTES_DELETED:
+                msgType = DataConstants.ATTRIBUTES_DELETED;
+                break;
+        }
+        if (!StringUtils.isEmpty(msgType)) {
+            try {
+                TbMsgMetaData metaData = new TbMsgMetaData();
+                metaData.putValue("userId", user.getId().toString());
+                metaData.putValue("userName", user.getName());
+                if (customerId != null && !customerId.isNullUid()) {
+                    metaData.putValue("customerId", customerId.toString());
+                }
+                if (actionType == ActionType.ASSIGNED_TO_CUSTOMER) {
+                    String strCustomerId = extractParameter(String.class, 1, additionalInfo);
+                    String strCustomerName = extractParameter(String.class, 2, additionalInfo);
+                    metaData.putValue("assignedCustomerId", strCustomerId);
+                    metaData.putValue("assignedCustomerName", strCustomerName);
+                } else if (actionType == ActionType.UNASSIGNED_FROM_CUSTOMER) {
+                    String strCustomerId = extractParameter(String.class, 1, additionalInfo);
+                    String strCustomerName = extractParameter(String.class, 2, additionalInfo);
+                    metaData.putValue("unassignedCustomerId", strCustomerId);
+                    metaData.putValue("unassignedCustomerName", strCustomerName);
+                }
+                ObjectNode entityNode;
+                if (entity != null) {
+                    entityNode = json.valueToTree(entity);
+                    if (entityId.getEntityType() == EntityType.DASHBOARD) {
+                        entityNode.put("configuration", "");
+                    }
+                } else {
+                    entityNode = json.createObjectNode();
+                    if (actionType == ActionType.ATTRIBUTES_UPDATED) {
+                        String scope = extractParameter(String.class, 0, additionalInfo);
+                        List<AttributeKvEntry> attributes = extractParameter(List.class, 1, additionalInfo);
+                        metaData.putValue("scope", scope);
+                        if (attributes != null) {
+                            for (AttributeKvEntry attr : attributes) {
+                                if (attr.getDataType() == DataType.BOOLEAN) {
+                                    entityNode.put(attr.getKey(), attr.getBooleanValue().get());
+                                } else if (attr.getDataType() == DataType.DOUBLE) {
+                                    entityNode.put(attr.getKey(), attr.getDoubleValue().get());
+                                } else if (attr.getDataType() == DataType.LONG) {
+                                    entityNode.put(attr.getKey(), attr.getLongValue().get());
+                                } else {
+                                    entityNode.put(attr.getKey(), attr.getValueAsString());
+                                }
+                            }
+                        }
+                    } else if (actionType == ActionType.ATTRIBUTES_DELETED) {
+                        String scope = extractParameter(String.class, 0, additionalInfo);
+                        List<String> keys = extractParameter(List.class, 1, additionalInfo);
+                        metaData.putValue("scope", scope);
+                        ArrayNode attrsArrayNode =  entityNode.putArray("attributes");
+                        if (keys != null) {
+                            keys.forEach(attrsArrayNode::add);
+                        }
+                    }
+                }
+                TbMsg tbMsg = new TbMsg(UUIDs.timeBased(), msgType, entityId, metaData, TbMsgDataType.JSON
+                        , json.writeValueAsString(entityNode)
+                        , null, null, 0L);
+                actorService.onMsg(new ServiceToRuleEngineMsg(user.getTenantId(), tbMsg));
+            } catch (Exception e) {
+                log.warn("[{}] Failed to push entity action to rule engine: {}", entityId, actionType, e);
+            }
+        }
+    }
+
+    private <T> T extractParameter(Class<T> clazz, int index, Object... additionalInfo) {
+        T result = null;
+        if (additionalInfo != null && additionalInfo.length > index) {
+            Object paramObject = additionalInfo[index];
+            if (clazz.isInstance(paramObject)) {
+                result = clazz.cast(paramObject);
+            }
+        }
+        return result;
+    }
+
+
 }
diff --git a/application/src/main/java/org/thingsboard/server/controller/ComponentDescriptorController.java b/application/src/main/java/org/thingsboard/server/controller/ComponentDescriptorController.java
index e63a443..fe4829f 100644
--- a/application/src/main/java/org/thingsboard/server/controller/ComponentDescriptorController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/ComponentDescriptorController.java
@@ -16,12 +16,19 @@
 package org.thingsboard.server.controller;
 
 import org.springframework.security.access.prepost.PreAuthorize;
-import org.springframework.web.bind.annotation.*;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestMethod;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.ResponseBody;
+import org.springframework.web.bind.annotation.RestController;
+import org.thingsboard.server.common.data.exception.ThingsboardException;
 import org.thingsboard.server.common.data.plugin.ComponentDescriptor;
 import org.thingsboard.server.common.data.plugin.ComponentType;
-import org.thingsboard.server.exception.ThingsboardException;
 
+import java.util.HashSet;
 import java.util.List;
+import java.util.Set;
 
 @RestController
 @RequestMapping("/api")
@@ -52,12 +59,16 @@ public class ComponentDescriptorController extends BaseController {
     }
 
     @PreAuthorize("hasAnyAuthority('SYS_ADMIN','TENANT_ADMIN')")
-    @RequestMapping(value = "/components/actions/{pluginClazz:.+}", method = RequestMethod.GET)
+    @RequestMapping(value = "/components", params = {"componentTypes"}, method = RequestMethod.GET)
     @ResponseBody
-    public List<ComponentDescriptor> getPluginActionsByPluginClazz(@PathVariable("pluginClazz") String pluginClazz) throws ThingsboardException {
-        checkParameter("pluginClazz", pluginClazz);
+    public List<ComponentDescriptor> getComponentDescriptorsByTypes(@RequestParam("componentTypes") String[] strComponentTypes) throws ThingsboardException {
+        checkArrayParameter("componentTypes", strComponentTypes);
         try {
-            return checkPluginActionsByPluginClazz(pluginClazz);
+            Set<ComponentType> componentTypes = new HashSet<>();
+            for (String strComponentType : strComponentTypes) {
+                componentTypes.add(ComponentType.valueOf(strComponentType));
+            }
+            return checkComponentDescriptorsByTypes(componentTypes);
         } catch (Exception e) {
             throw handleException(e);
         }
diff --git a/application/src/main/java/org/thingsboard/server/controller/CustomerController.java b/application/src/main/java/org/thingsboard/server/controller/CustomerController.java
index b164702..34843ce 100644
--- a/application/src/main/java/org/thingsboard/server/controller/CustomerController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/CustomerController.java
@@ -20,15 +20,22 @@ import com.fasterxml.jackson.databind.ObjectMapper;
 import com.fasterxml.jackson.databind.node.ObjectNode;
 import org.springframework.http.HttpStatus;
 import org.springframework.security.access.prepost.PreAuthorize;
-import org.springframework.web.bind.annotation.*;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestMethod;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.ResponseBody;
+import org.springframework.web.bind.annotation.ResponseStatus;
+import org.springframework.web.bind.annotation.RestController;
 import org.thingsboard.server.common.data.Customer;
 import org.thingsboard.server.common.data.EntityType;
 import org.thingsboard.server.common.data.audit.ActionType;
+import org.thingsboard.server.common.data.exception.ThingsboardException;
 import org.thingsboard.server.common.data.id.CustomerId;
 import org.thingsboard.server.common.data.id.TenantId;
 import org.thingsboard.server.common.data.page.TextPageData;
 import org.thingsboard.server.common.data.page.TextPageLink;
-import org.thingsboard.server.exception.ThingsboardException;
 
 @RestController
 @RequestMapping("/api")
diff --git a/application/src/main/java/org/thingsboard/server/controller/DashboardController.java b/application/src/main/java/org/thingsboard/server/controller/DashboardController.java
index d2952a1..85227e7 100644
--- a/application/src/main/java/org/thingsboard/server/controller/DashboardController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/DashboardController.java
@@ -17,9 +17,21 @@ package org.thingsboard.server.controller;
 
 import org.springframework.http.HttpStatus;
 import org.springframework.security.access.prepost.PreAuthorize;
-import org.springframework.web.bind.annotation.*;
-import org.thingsboard.server.common.data.*;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestMethod;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.ResponseBody;
+import org.springframework.web.bind.annotation.ResponseStatus;
+import org.springframework.web.bind.annotation.RestController;
+import org.thingsboard.server.common.data.Customer;
+import org.thingsboard.server.common.data.Dashboard;
+import org.thingsboard.server.common.data.DashboardInfo;
+import org.thingsboard.server.common.data.EntityType;
+import org.thingsboard.server.common.data.ShortCustomerInfo;
 import org.thingsboard.server.common.data.audit.ActionType;
+import org.thingsboard.server.common.data.exception.ThingsboardException;
 import org.thingsboard.server.common.data.id.CustomerId;
 import org.thingsboard.server.common.data.id.DashboardId;
 import org.thingsboard.server.common.data.id.TenantId;
@@ -27,9 +39,6 @@ import org.thingsboard.server.common.data.page.TextPageData;
 import org.thingsboard.server.common.data.page.TextPageLink;
 import org.thingsboard.server.common.data.page.TimePageData;
 import org.thingsboard.server.common.data.page.TimePageLink;
-import org.thingsboard.server.dao.exception.IncorrectParameterException;
-import org.thingsboard.server.dao.model.ModelConstants;
-import org.thingsboard.server.exception.ThingsboardException;
 
 import java.util.HashSet;
 import java.util.Set;
diff --git a/application/src/main/java/org/thingsboard/server/controller/DeviceController.java b/application/src/main/java/org/thingsboard/server/controller/DeviceController.java
index bceea54..a6daeec 100644
--- a/application/src/main/java/org/thingsboard/server/controller/DeviceController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/DeviceController.java
@@ -18,14 +18,22 @@ package org.thingsboard.server.controller;
 import com.google.common.util.concurrent.ListenableFuture;
 import org.springframework.http.HttpStatus;
 import org.springframework.security.access.prepost.PreAuthorize;
-import org.springframework.web.bind.annotation.*;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestMethod;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.ResponseBody;
+import org.springframework.web.bind.annotation.ResponseStatus;
+import org.springframework.web.bind.annotation.RestController;
 import org.thingsboard.server.common.data.Customer;
 import org.thingsboard.server.common.data.Device;
 import org.thingsboard.server.common.data.EntitySubtype;
 import org.thingsboard.server.common.data.EntityType;
-import org.thingsboard.server.common.data.audit.ActionStatus;
 import org.thingsboard.server.common.data.audit.ActionType;
 import org.thingsboard.server.common.data.device.DeviceSearchQuery;
+import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
+import org.thingsboard.server.common.data.exception.ThingsboardException;
 import org.thingsboard.server.common.data.id.CustomerId;
 import org.thingsboard.server.common.data.id.DeviceId;
 import org.thingsboard.server.common.data.id.TenantId;
@@ -35,8 +43,6 @@ import org.thingsboard.server.common.data.security.Authority;
 import org.thingsboard.server.common.data.security.DeviceCredentials;
 import org.thingsboard.server.dao.exception.IncorrectParameterException;
 import org.thingsboard.server.dao.model.ModelConstants;
-import org.thingsboard.server.exception.ThingsboardErrorCode;
-import org.thingsboard.server.exception.ThingsboardException;
 import org.thingsboard.server.service.security.model.SecurityUser;
 
 import java.util.ArrayList;
@@ -90,6 +96,11 @@ public class DeviceController extends BaseController {
                     savedDevice.getCustomerId(),
                     device.getId() == null ? ActionType.ADDED : ActionType.UPDATED, null);
 
+            if (device.getId() == null) {
+                deviceStateService.onDeviceAdded(savedDevice);
+            } else {
+                deviceStateService.onDeviceUpdated(savedDevice);
+            }
             return savedDevice;
         } catch (Exception e) {
             logEntityAction(emptyId(EntityType.DEVICE), device,
@@ -112,6 +123,7 @@ public class DeviceController extends BaseController {
                     device.getCustomerId(),
                     ActionType.DELETED, null, strDeviceId);
 
+            deviceStateService.onDeviceDeleted(device);
         } catch (Exception e) {
             logEntityAction(emptyId(EntityType.DEVICE),
                     null,
diff --git a/application/src/main/java/org/thingsboard/server/controller/EntityRelationController.java b/application/src/main/java/org/thingsboard/server/controller/EntityRelationController.java
index 03054df..844dbd3 100644
--- a/application/src/main/java/org/thingsboard/server/controller/EntityRelationController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/EntityRelationController.java
@@ -17,15 +17,21 @@ package org.thingsboard.server.controller;
 
 import org.springframework.http.HttpStatus;
 import org.springframework.security.access.prepost.PreAuthorize;
-import org.springframework.web.bind.annotation.*;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestMethod;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.ResponseBody;
+import org.springframework.web.bind.annotation.ResponseStatus;
+import org.springframework.web.bind.annotation.RestController;
+import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
+import org.thingsboard.server.common.data.exception.ThingsboardException;
 import org.thingsboard.server.common.data.id.EntityId;
 import org.thingsboard.server.common.data.id.EntityIdFactory;
 import org.thingsboard.server.common.data.relation.EntityRelation;
 import org.thingsboard.server.common.data.relation.EntityRelationInfo;
-import org.thingsboard.server.common.data.relation.RelationTypeGroup;
 import org.thingsboard.server.common.data.relation.EntityRelationsQuery;
-import org.thingsboard.server.exception.ThingsboardErrorCode;
-import org.thingsboard.server.exception.ThingsboardException;
+import org.thingsboard.server.common.data.relation.RelationTypeGroup;
 
 import java.util.List;
 
diff --git a/application/src/main/java/org/thingsboard/server/controller/EventController.java b/application/src/main/java/org/thingsboard/server/controller/EventController.java
index 331b15e..7c8b2ae 100644
--- a/application/src/main/java/org/thingsboard/server/controller/EventController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/EventController.java
@@ -17,15 +17,21 @@ package org.thingsboard.server.controller;
 
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.security.access.prepost.PreAuthorize;
-import org.springframework.web.bind.annotation.*;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestMethod;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.ResponseBody;
+import org.springframework.web.bind.annotation.RestController;
 import org.thingsboard.server.common.data.Event;
-import org.thingsboard.server.common.data.id.*;
+import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
+import org.thingsboard.server.common.data.exception.ThingsboardException;
+import org.thingsboard.server.common.data.id.EntityIdFactory;
+import org.thingsboard.server.common.data.id.TenantId;
 import org.thingsboard.server.common.data.page.TimePageData;
 import org.thingsboard.server.common.data.page.TimePageLink;
 import org.thingsboard.server.dao.event.EventService;
 import org.thingsboard.server.dao.model.ModelConstants;
-import org.thingsboard.server.exception.ThingsboardErrorCode;
-import org.thingsboard.server.exception.ThingsboardException;
 
 @RestController
 @RequestMapping("/api")
diff --git a/application/src/main/java/org/thingsboard/server/controller/RpcController.java b/application/src/main/java/org/thingsboard/server/controller/RpcController.java
new file mode 100644
index 0000000..e39a3f6
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/controller/RpcController.java
@@ -0,0 +1,217 @@
+/**
+ * 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.server.controller;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.google.common.util.concurrent.FutureCallback;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.util.StringUtils;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestMethod;
+import org.springframework.web.bind.annotation.ResponseBody;
+import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.context.request.async.DeferredResult;
+import org.thingsboard.rule.engine.api.RpcError;
+import org.thingsboard.server.common.data.audit.ActionType;
+import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
+import org.thingsboard.server.common.data.exception.ThingsboardException;
+import org.thingsboard.server.common.data.id.DeviceId;
+import org.thingsboard.server.common.data.id.EntityId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.id.UUIDBased;
+import org.thingsboard.server.common.data.rpc.RpcRequest;
+import org.thingsboard.server.common.data.rpc.ToDeviceRpcRequestBody;
+import org.thingsboard.server.common.msg.rpc.ToDeviceRpcRequest;
+import org.thingsboard.server.service.rpc.DeviceRpcService;
+import org.thingsboard.server.service.rpc.FromDeviceRpcResponse;
+import org.thingsboard.server.service.rpc.LocalRequestMetaData;
+import org.thingsboard.server.service.security.AccessValidator;
+import org.thingsboard.server.service.security.model.SecurityUser;
+import org.thingsboard.server.service.telemetry.exception.ToErrorResponseEntity;
+
+import javax.annotation.Nullable;
+import javax.annotation.PostConstruct;
+import javax.annotation.PreDestroy;
+import java.io.IOException;
+import java.util.Optional;
+import java.util.UUID;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+/**
+ * Created by ashvayka on 22.03.18.
+ */
+@RestController
+@RequestMapping(TbUrlConstants.RPC_URL_PREFIX)
+@Slf4j
+public class RpcController extends BaseController {
+
+    public static final int DEFAULT_TIMEOUT = 10000;
+    protected final ObjectMapper jsonMapper = new ObjectMapper();
+
+    @Autowired
+    private DeviceRpcService deviceRpcService;
+
+    @Autowired
+    private AccessValidator accessValidator;
+
+    private ExecutorService executor;
+
+    @PostConstruct
+    public void initExecutor() {
+        executor = Executors.newSingleThreadExecutor();
+    }
+
+    @PreDestroy
+    public void shutdownExecutor() {
+        if (executor != null) {
+            executor.shutdownNow();
+        }
+    }
+
+    @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
+    @RequestMapping(value = "/oneway/{deviceId}", method = RequestMethod.POST)
+    @ResponseBody
+    public DeferredResult<ResponseEntity> handleOneWayDeviceRPCRequest(@PathVariable("deviceId") String deviceIdStr, @RequestBody String requestBody) throws ThingsboardException {
+        return handleDeviceRPCRequest(true, new DeviceId(UUID.fromString(deviceIdStr)), requestBody);
+    }
+
+    @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
+    @RequestMapping(value = "/twoway/{deviceId}", method = RequestMethod.POST)
+    @ResponseBody
+    public DeferredResult<ResponseEntity> handleTwoWayDeviceRPCRequest(@PathVariable("deviceId") String deviceIdStr, @RequestBody String requestBody) throws ThingsboardException {
+        return handleDeviceRPCRequest(false, new DeviceId(UUID.fromString(deviceIdStr)), requestBody);
+    }
+
+
+    private DeferredResult<ResponseEntity> handleDeviceRPCRequest(boolean oneWay, DeviceId deviceId, String requestBody) throws ThingsboardException {
+        try {
+            JsonNode rpcRequestBody = jsonMapper.readTree(requestBody);
+            RpcRequest cmd = new RpcRequest(rpcRequestBody.get("method").asText(),
+                    jsonMapper.writeValueAsString(rpcRequestBody.get("params")));
+
+            if (rpcRequestBody.has("timeout")) {
+                cmd.setTimeout(rpcRequestBody.get("timeout").asLong());
+            }
+            SecurityUser currentUser = getCurrentUser();
+            TenantId tenantId = currentUser.getTenantId();
+            final DeferredResult<ResponseEntity> response = new DeferredResult<>();
+            long timeout = System.currentTimeMillis() + (cmd.getTimeout() != null ? cmd.getTimeout() : DEFAULT_TIMEOUT);
+            ToDeviceRpcRequestBody body = new ToDeviceRpcRequestBody(cmd.getMethodName(), cmd.getRequestData());
+            accessValidator.validate(currentUser, deviceId, new HttpValidationCallback(response, new FutureCallback<DeferredResult<ResponseEntity>>() {
+                @Override
+                public void onSuccess(@Nullable DeferredResult<ResponseEntity> result) {
+                    ToDeviceRpcRequest rpcRequest = new ToDeviceRpcRequest(UUID.randomUUID(),
+                            tenantId,
+                            deviceId,
+                            oneWay,
+                            timeout,
+                            body
+                    );
+                    deviceRpcService.processRpcRequestToDevice(rpcRequest, fromDeviceRpcResponse -> reply(new LocalRequestMetaData(rpcRequest, currentUser, result), fromDeviceRpcResponse));
+                }
+
+                @Override
+                public void onFailure(Throwable e) {
+                    ResponseEntity entity;
+                    if (e instanceof ToErrorResponseEntity) {
+                        entity = ((ToErrorResponseEntity) e).toErrorResponseEntity();
+                    } else {
+                        entity = new ResponseEntity(HttpStatus.UNAUTHORIZED);
+                    }
+                    logRpcCall(currentUser, deviceId, body, oneWay, Optional.empty(), e);
+                    response.setResult(entity);
+                }
+            }));
+            return response;
+        } catch (IOException ioe) {
+            throw new ThingsboardException("Invalid request body", ioe, ThingsboardErrorCode.BAD_REQUEST_PARAMS);
+        }
+    }
+
+    public void reply(LocalRequestMetaData rpcRequest, FromDeviceRpcResponse response) {
+        Optional<RpcError> rpcError = response.getError();
+        DeferredResult<ResponseEntity> responseWriter = rpcRequest.getResponseWriter();
+        if (rpcError.isPresent()) {
+            logRpcCall(rpcRequest, rpcError, null);
+            RpcError error = rpcError.get();
+            switch (error) {
+                case TIMEOUT:
+                    responseWriter.setResult(new ResponseEntity<>(HttpStatus.REQUEST_TIMEOUT));
+                    break;
+                case NO_ACTIVE_CONNECTION:
+                    responseWriter.setResult(new ResponseEntity<>(HttpStatus.CONFLICT));
+                    break;
+                default:
+                    responseWriter.setResult(new ResponseEntity<>(HttpStatus.REQUEST_TIMEOUT));
+                    break;
+            }
+        } else {
+            Optional<String> responseData = response.getResponse();
+            if (responseData.isPresent() && !StringUtils.isEmpty(responseData.get())) {
+                String data = responseData.get();
+                try {
+                    logRpcCall(rpcRequest, rpcError, null);
+                    responseWriter.setResult(new ResponseEntity<>(jsonMapper.readTree(data), HttpStatus.OK));
+                } catch (IOException e) {
+                    log.debug("Failed to decode device response: {}", data, e);
+                    logRpcCall(rpcRequest, rpcError, e);
+                    responseWriter.setResult(new ResponseEntity<>(HttpStatus.NOT_ACCEPTABLE));
+                }
+            } else {
+                logRpcCall(rpcRequest, rpcError, null);
+                responseWriter.setResult(new ResponseEntity<>(HttpStatus.OK));
+            }
+        }
+    }
+
+    private void logRpcCall(LocalRequestMetaData rpcRequest, Optional<RpcError> rpcError, Throwable e) {
+        logRpcCall(rpcRequest.getUser(), rpcRequest.getRequest().getDeviceId(), rpcRequest.getRequest().getBody(), rpcRequest.getRequest().isOneway(), rpcError, null);
+    }
+
+
+    private void logRpcCall(SecurityUser user, EntityId entityId, ToDeviceRpcRequestBody body, boolean oneWay, Optional<RpcError> rpcError, Throwable e) {
+        String rpcErrorStr = "";
+        if (rpcError.isPresent()) {
+            rpcErrorStr = "RPC Error: " + rpcError.get().name();
+        }
+        String method = body.getMethod();
+        String params = body.getParams();
+
+        auditLogService.logEntityAction(
+                user.getTenantId(),
+                user.getCustomerId(),
+                user.getId(),
+                user.getName(),
+                (UUIDBased & EntityId) entityId,
+                null,
+                ActionType.RPC_CALL,
+                BaseController.toException(e),
+                rpcErrorStr,
+                oneWay,
+                method,
+                params);
+    }
+
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/controller/RuleChainController.java b/application/src/main/java/org/thingsboard/server/controller/RuleChainController.java
new file mode 100644
index 0000000..86b8fda
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/controller/RuleChainController.java
@@ -0,0 +1,334 @@
+/**
+ * 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.server.controller;
+
+import com.datastax.driver.core.utils.UUIDs;
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.HttpStatus;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.util.StringUtils;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestMethod;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.ResponseBody;
+import org.springframework.web.bind.annotation.ResponseStatus;
+import org.springframework.web.bind.annotation.RestController;
+import org.thingsboard.rule.engine.api.ScriptEngine;
+import org.thingsboard.server.common.data.DataConstants;
+import org.thingsboard.server.common.data.EntityType;
+import org.thingsboard.server.common.data.Event;
+import org.thingsboard.server.common.data.audit.ActionType;
+import org.thingsboard.server.common.data.exception.ThingsboardException;
+import org.thingsboard.server.common.data.id.RuleChainId;
+import org.thingsboard.server.common.data.id.RuleNodeId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.page.TextPageData;
+import org.thingsboard.server.common.data.page.TextPageLink;
+import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent;
+import org.thingsboard.server.common.data.rule.RuleChain;
+import org.thingsboard.server.common.data.rule.RuleChainMetaData;
+import org.thingsboard.server.common.msg.TbMsg;
+import org.thingsboard.server.common.msg.TbMsgMetaData;
+import org.thingsboard.server.dao.event.EventService;
+import org.thingsboard.server.service.script.JsSandboxService;
+import org.thingsboard.server.service.script.RuleNodeJsScriptEngine;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+@Slf4j
+@RestController
+@RequestMapping("/api")
+public class RuleChainController extends BaseController {
+
+    public static final String RULE_CHAIN_ID = "ruleChainId";
+    public static final String RULE_NODE_ID = "ruleNodeId";
+
+    private static final ObjectMapper objectMapper = new ObjectMapper();
+
+    @Autowired
+    private EventService eventService;
+
+    @Autowired
+    private JsSandboxService jsSandboxService;
+
+    @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')")
+    @RequestMapping(value = "/ruleChain/{ruleChainId}", method = RequestMethod.GET)
+    @ResponseBody
+    public RuleChain getRuleChainById(@PathVariable(RULE_CHAIN_ID) String strRuleChainId) throws ThingsboardException {
+        checkParameter(RULE_CHAIN_ID, strRuleChainId);
+        try {
+            RuleChainId ruleChainId = new RuleChainId(toUUID(strRuleChainId));
+            return checkRuleChain(ruleChainId);
+        } catch (Exception e) {
+            throw handleException(e);
+        }
+    }
+
+    @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')")
+    @RequestMapping(value = "/ruleChain/{ruleChainId}/metadata", method = RequestMethod.GET)
+    @ResponseBody
+    public RuleChainMetaData getRuleChainMetaData(@PathVariable(RULE_CHAIN_ID) String strRuleChainId) throws ThingsboardException {
+        checkParameter(RULE_CHAIN_ID, strRuleChainId);
+        try {
+            RuleChainId ruleChainId = new RuleChainId(toUUID(strRuleChainId));
+            checkRuleChain(ruleChainId);
+            return ruleChainService.loadRuleChainMetaData(ruleChainId);
+        } catch (Exception e) {
+            throw handleException(e);
+        }
+    }
+
+
+    @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')")
+    @RequestMapping(value = "/ruleChain", method = RequestMethod.POST)
+    @ResponseBody
+    public RuleChain saveRuleChain(@RequestBody RuleChain ruleChain) throws ThingsboardException {
+        try {
+            boolean created = ruleChain.getId() == null;
+            ruleChain.setTenantId(getCurrentUser().getTenantId());
+            RuleChain savedRuleChain = checkNotNull(ruleChainService.saveRuleChain(ruleChain));
+
+            actorService.onEntityStateChange(ruleChain.getTenantId(), savedRuleChain.getId(),
+                    created ? ComponentLifecycleEvent.CREATED : ComponentLifecycleEvent.UPDATED);
+
+            logEntityAction(savedRuleChain.getId(), savedRuleChain,
+                    null,
+                    created ? ActionType.ADDED : ActionType.UPDATED, null);
+
+            return savedRuleChain;
+        } catch (Exception e) {
+
+            logEntityAction(emptyId(EntityType.RULE_CHAIN), ruleChain,
+                    null, ruleChain.getId() == null ? ActionType.ADDED : ActionType.UPDATED, e);
+
+            throw handleException(e);
+        }
+    }
+
+    @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')")
+    @RequestMapping(value = "/ruleChain/{ruleChainId}/root", method = RequestMethod.POST)
+    @ResponseBody
+    public RuleChain setRootRuleChain(@PathVariable(RULE_CHAIN_ID) String strRuleChainId) throws ThingsboardException {
+        checkParameter(RULE_CHAIN_ID, strRuleChainId);
+        try {
+            RuleChainId ruleChainId = new RuleChainId(toUUID(strRuleChainId));
+            RuleChain ruleChain = checkRuleChain(ruleChainId);
+            TenantId tenantId = getCurrentUser().getTenantId();
+            RuleChain previousRootRuleChain = ruleChainService.getRootTenantRuleChain(tenantId);
+            if (ruleChainService.setRootRuleChain(ruleChainId)) {
+
+                previousRootRuleChain = ruleChainService.findRuleChainById(previousRootRuleChain.getId());
+
+                actorService.onEntityStateChange(previousRootRuleChain.getTenantId(), previousRootRuleChain.getId(),
+                        ComponentLifecycleEvent.UPDATED);
+
+                logEntityAction(previousRootRuleChain.getId(), previousRootRuleChain,
+                        null, ActionType.UPDATED, null);
+
+                ruleChain = ruleChainService.findRuleChainById(ruleChainId);
+
+                actorService.onEntityStateChange(ruleChain.getTenantId(), ruleChain.getId(),
+                        ComponentLifecycleEvent.UPDATED);
+
+                logEntityAction(ruleChain.getId(), ruleChain,
+                        null, ActionType.UPDATED, null);
+
+            }
+            return ruleChain;
+        } catch (Exception e) {
+            logEntityAction(emptyId(EntityType.RULE_CHAIN),
+                    null,
+                    null,
+                    ActionType.UPDATED, e, strRuleChainId);
+            throw handleException(e);
+        }
+    }
+
+    @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')")
+    @RequestMapping(value = "/ruleChain/metadata", method = RequestMethod.POST)
+    @ResponseBody
+    public RuleChainMetaData saveRuleChainMetaData(@RequestBody RuleChainMetaData ruleChainMetaData) throws ThingsboardException {
+        try {
+            RuleChain ruleChain = checkRuleChain(ruleChainMetaData.getRuleChainId());
+            RuleChainMetaData savedRuleChainMetaData = checkNotNull(ruleChainService.saveRuleChainMetaData(ruleChainMetaData));
+
+            actorService.onEntityStateChange(ruleChain.getTenantId(), ruleChain.getId(), ComponentLifecycleEvent.UPDATED);
+
+            logEntityAction(ruleChain.getId(), ruleChain,
+                    null,
+                    ActionType.UPDATED, null, ruleChainMetaData);
+
+            return savedRuleChainMetaData;
+        } catch (Exception e) {
+
+            logEntityAction(emptyId(EntityType.RULE_CHAIN), null,
+                    null, ActionType.UPDATED, e, ruleChainMetaData);
+
+            throw handleException(e);
+        }
+    }
+
+    @PreAuthorize("hasAuthority('TENANT_ADMIN')")
+    @RequestMapping(value = "/ruleChains", params = {"limit"}, method = RequestMethod.GET)
+    @ResponseBody
+    public TextPageData<RuleChain> getRuleChains(
+            @RequestParam int limit,
+            @RequestParam(required = false) String textSearch,
+            @RequestParam(required = false) String idOffset,
+            @RequestParam(required = false) String textOffset) throws ThingsboardException {
+        try {
+            TenantId tenantId = getCurrentUser().getTenantId();
+            TextPageLink pageLink = createPageLink(limit, textSearch, idOffset, textOffset);
+            return checkNotNull(ruleChainService.findTenantRuleChains(tenantId, pageLink));
+        } catch (Exception e) {
+            throw handleException(e);
+        }
+    }
+
+    @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')")
+    @RequestMapping(value = "/ruleChain/{ruleChainId}", method = RequestMethod.DELETE)
+    @ResponseStatus(value = HttpStatus.OK)
+    public void deleteRuleChain(@PathVariable(RULE_CHAIN_ID) String strRuleChainId) throws ThingsboardException {
+        checkParameter(RULE_CHAIN_ID, strRuleChainId);
+        try {
+            RuleChainId ruleChainId = new RuleChainId(toUUID(strRuleChainId));
+            RuleChain ruleChain = checkRuleChain(ruleChainId);
+
+            ruleChainService.deleteRuleChainById(ruleChainId);
+
+            actorService.onEntityStateChange(ruleChain.getTenantId(), ruleChain.getId(), ComponentLifecycleEvent.DELETED);
+
+            logEntityAction(ruleChainId, ruleChain,
+                    null,
+                    ActionType.DELETED, null, strRuleChainId);
+
+        } catch (Exception e) {
+            logEntityAction(emptyId(EntityType.RULE_CHAIN),
+                    null,
+                    null,
+                    ActionType.DELETED, e, strRuleChainId);
+            throw handleException(e);
+        }
+    }
+
+    @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')")
+    @RequestMapping(value = "/ruleChain/testScript", method = RequestMethod.POST)
+    @ResponseBody
+    public JsonNode testScript(@RequestBody JsonNode inputParams) throws ThingsboardException {
+        try {
+            String script = inputParams.get("script").asText();
+            String scriptType = inputParams.get("scriptType").asText();
+            JsonNode argNamesJson = inputParams.get("argNames");
+            String[] argNames = objectMapper.treeToValue(argNamesJson, String[].class);
+
+            String data = inputParams.get("msg").asText();
+            JsonNode metadataJson = inputParams.get("metadata");
+            Map<String, String> metadata = objectMapper.convertValue(metadataJson, new TypeReference<Map<String, String>>() {});
+            String msgType = inputParams.get("msgType").asText();
+            String output = "";
+            String errorText = "";
+            ScriptEngine engine = null;
+            try {
+                engine = new RuleNodeJsScriptEngine(jsSandboxService, script, argNames);
+                TbMsg inMsg = new TbMsg(UUIDs.timeBased(), msgType, null, new TbMsgMetaData(metadata), data, null, null, 0L);
+                switch (scriptType) {
+                    case "update":
+                        output = msgToOutput(engine.executeUpdate(inMsg));
+                        break;
+                    case "generate":
+                        output = msgToOutput(engine.executeGenerate(inMsg));
+                        break;
+                    case "filter":
+                        boolean result = engine.executeFilter(inMsg);
+                        output = Boolean.toString(result);
+                        break;
+                    case "switch":
+                        Set<String> states = engine.executeSwitch(inMsg);
+                        output = objectMapper.writeValueAsString(states);
+                        break;
+                    case "json":
+                        JsonNode json = engine.executeJson(inMsg);
+                        output = objectMapper.writeValueAsString(json);
+                        break;
+                    case "string":
+                        output = engine.executeToString(inMsg);
+                        break;
+                    default:
+                        throw new IllegalArgumentException("Unsupported script type: " + scriptType);
+                }
+            } catch (Exception e) {
+                log.error("Error evaluating JS function", e);
+                errorText = e.getMessage();
+            } finally {
+                if (engine != null) {
+                    engine.destroy();
+                }
+            }
+            ObjectNode result = objectMapper.createObjectNode();
+            result.put("output", output);
+            result.put("error", errorText);
+            return result;
+        } catch (Exception e) {
+            throw handleException(e);
+        }
+    }
+
+    private String msgToOutput(TbMsg msg) throws Exception {
+        ObjectNode msgData = objectMapper.createObjectNode();
+        if (!StringUtils.isEmpty(msg.getData())) {
+            msgData.set("msg", objectMapper.readTree(msg.getData()));
+        }
+        Map<String, String> metadata = msg.getMetaData().getData();
+        msgData.set("metadata", objectMapper.valueToTree(metadata));
+        msgData.put("msgType", msg.getType());
+        return objectMapper.writeValueAsString(msgData);
+    }
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/controller/TelemetryController.java b/application/src/main/java/org/thingsboard/server/controller/TelemetryController.java
new file mode 100644
index 0000000..c2839ff
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/controller/TelemetryController.java
@@ -0,0 +1,570 @@
+/**
+ * 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.server.controller;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.google.common.base.Function;
+import com.google.common.util.concurrent.FutureCallback;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonParser;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.util.StringUtils;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestMethod;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.ResponseBody;
+import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.context.request.async.DeferredResult;
+import org.thingsboard.server.common.data.DataConstants;
+import org.thingsboard.server.common.data.EntityType;
+import org.thingsboard.server.common.data.audit.ActionType;
+import org.thingsboard.server.common.data.exception.ThingsboardException;
+import org.thingsboard.server.common.data.id.EntityId;
+import org.thingsboard.server.common.data.id.EntityIdFactory;
+import org.thingsboard.server.common.data.id.UUIDBased;
+import org.thingsboard.server.common.data.kv.Aggregation;
+import org.thingsboard.server.common.data.kv.AttributeKvEntry;
+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.BooleanDataEntry;
+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.StringDataEntry;
+import org.thingsboard.server.common.data.kv.TsKvEntry;
+import org.thingsboard.server.common.data.kv.TsKvQuery;
+import org.thingsboard.server.common.msg.core.TelemetryUploadRequest;
+import org.thingsboard.server.common.transport.adaptor.JsonConverter;
+import org.thingsboard.server.dao.attributes.AttributesService;
+import org.thingsboard.server.dao.timeseries.TimeseriesService;
+import org.thingsboard.server.service.security.AccessValidator;
+import org.thingsboard.server.service.security.model.SecurityUser;
+import org.thingsboard.server.service.telemetry.AttributeData;
+import org.thingsboard.server.service.telemetry.TelemetrySubscriptionService;
+import org.thingsboard.server.service.telemetry.TsData;
+import org.thingsboard.server.service.telemetry.exception.InvalidParametersException;
+import org.thingsboard.server.service.telemetry.exception.UncheckedApiException;
+
+import javax.annotation.Nullable;
+import javax.annotation.PostConstruct;
+import javax.annotation.PreDestroy;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.stream.Collectors;
+
+/**
+ * Created by ashvayka on 22.03.18.
+ */
+@RestController
+@RequestMapping(TbUrlConstants.TELEMETRY_URL_PREFIX)
+@Slf4j
+public class TelemetryController extends BaseController {
+
+    @Autowired
+    private AttributesService attributesService;
+
+    @Autowired
+    private TimeseriesService tsService;
+
+    @Autowired
+    private TelemetrySubscriptionService tsSubService;
+
+    @Autowired
+    private AccessValidator accessValidator;
+
+    private ExecutorService executor;
+
+    @PostConstruct
+    public void initExecutor() {
+        executor = Executors.newSingleThreadExecutor();
+    }
+
+    @PreDestroy
+    public void shutdownExecutor() {
+        if (executor != null) {
+            executor.shutdownNow();
+        }
+    }
+
+    @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
+    @RequestMapping(value = "/{entityType}/{entityId}/keys/attributes", method = RequestMethod.GET)
+    @ResponseBody
+    public DeferredResult<ResponseEntity> getAttributeKeys(
+            @PathVariable("entityType") String entityType, @PathVariable("entityId") String entityIdStr) throws ThingsboardException {
+        return accessValidator.validateEntityAndCallback(getCurrentUser(), entityType, entityIdStr, this::getAttributeKeysCallback);
+    }
+
+    @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
+    @RequestMapping(value = "/{entityType}/{entityId}/keys/attributes/{scope}", method = RequestMethod.GET)
+    @ResponseBody
+    public DeferredResult<ResponseEntity> getAttributeKeysByScope(
+            @PathVariable("entityType") String entityType, @PathVariable("entityId") String entityIdStr
+            , @PathVariable("scope") String scope) throws ThingsboardException {
+        return accessValidator.validateEntityAndCallback(getCurrentUser(), entityType, entityIdStr,
+                (result, entityId) -> getAttributeKeysCallback(result, entityId, scope));
+    }
+
+    @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
+    @RequestMapping(value = "/{entityType}/{entityId}/values/attributes", method = RequestMethod.GET)
+    @ResponseBody
+    public DeferredResult<ResponseEntity> getAttributes(
+            @PathVariable("entityType") String entityType, @PathVariable("entityId") String entityIdStr,
+            @RequestParam(name = "keys", required = false) String keysStr) throws ThingsboardException {
+        SecurityUser user = getCurrentUser();
+        return accessValidator.validateEntityAndCallback(getCurrentUser(), entityType, entityIdStr,
+                (result, entityId) -> getAttributeValuesCallback(result, user, entityId, null, keysStr));
+    }
+
+    @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
+    @RequestMapping(value = "/{entityType}/{entityId}/values/attributes/{scope}", method = RequestMethod.GET)
+    @ResponseBody
+    public DeferredResult<ResponseEntity> getAttributesByScope(
+            @PathVariable("entityType") String entityType, @PathVariable("entityId") String entityIdStr,
+            @PathVariable("scope") String scope,
+            @RequestParam(name = "keys", required = false) String keysStr) throws ThingsboardException {
+        SecurityUser user = getCurrentUser();
+        return accessValidator.validateEntityAndCallback(getCurrentUser(), entityType, entityIdStr,
+                (result, entityId) -> getAttributeValuesCallback(result, user, entityId, scope, keysStr));
+    }
+
+    @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
+    @RequestMapping(value = "/{entityType}/{entityId}/keys/timeseries", method = RequestMethod.GET)
+    @ResponseBody
+    public DeferredResult<ResponseEntity> getTimeseriesKeys(
+            @PathVariable("entityType") String entityType, @PathVariable("entityId") String entityIdStr) throws ThingsboardException {
+        return accessValidator.validateEntityAndCallback(getCurrentUser(), entityType, entityIdStr,
+                (result, entityId) -> {
+                    Futures.addCallback(tsService.findAllLatest(entityId), getTsKeysToResponseCallback(result));
+                });
+    }
+
+    @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
+    @RequestMapping(value = "/{entityType}/{entityId}/values/timeseries", method = RequestMethod.GET)
+    @ResponseBody
+    public DeferredResult<ResponseEntity> getLatestTimeseries(
+            @PathVariable("entityType") String entityType, @PathVariable("entityId") String entityIdStr,
+            @RequestParam(name = "keys", required = false) String keysStr) throws ThingsboardException {
+        SecurityUser user = getCurrentUser();
+
+        return accessValidator.validateEntityAndCallback(getCurrentUser(), entityType, entityIdStr,
+                (result, entityId) -> getLatestTimeseriesValuesCallback(result, user, entityId, keysStr));
+    }
+
+
+    @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
+    @RequestMapping(value = "/{entityType}/{entityId}/values/timeseries", method = RequestMethod.GET, params = {"keys", "startTs", "endTs"})
+    @ResponseBody
+    public DeferredResult<ResponseEntity> getTimeseries(
+            @PathVariable("entityType") String entityType, @PathVariable("entityId") String entityIdStr,
+            @RequestParam(name = "keys") String keys,
+            @RequestParam(name = "startTs") Long startTs,
+            @RequestParam(name = "endTs") Long endTs,
+            @RequestParam(name = "interval", defaultValue = "0") Long interval,
+            @RequestParam(name = "limit", defaultValue = "100") Integer limit,
+            @RequestParam(name = "agg", defaultValue = "NONE") String aggStr
+    ) throws ThingsboardException {
+        return accessValidator.validateEntityAndCallback(getCurrentUser(), entityType, entityIdStr,
+                (result, entityId) -> {
+                    // If interval is 0, convert this to a NONE aggregation, which is probably what the user really wanted
+                    Aggregation agg = interval == 0L ? Aggregation.valueOf(Aggregation.NONE.name()) : Aggregation.valueOf(aggStr);
+                    List<TsKvQuery> queries = toKeysList(keys).stream().map(key -> new BaseTsKvQuery(key, startTs, endTs, interval, limit, agg))
+                            .collect(Collectors.toList());
+
+                    Futures.addCallback(tsService.findAll(entityId, queries), getTsKvListCallback(result));
+                });
+    }
+
+    @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
+    @RequestMapping(value = "/{deviceId}/{scope}", method = RequestMethod.POST)
+    @ResponseBody
+    public DeferredResult<ResponseEntity> saveDeviceAttributes(@PathVariable("deviceId") String deviceIdStr, @PathVariable("scope") String scope,
+                                                               @RequestBody JsonNode request) throws ThingsboardException {
+        EntityId entityId = EntityIdFactory.getByTypeAndUuid(EntityType.DEVICE, deviceIdStr);
+        return saveAttributes(entityId, scope, request);
+    }
+
+    @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
+    @RequestMapping(value = "/{entityType}/{entityId}/{scope}", method = RequestMethod.POST)
+    @ResponseBody
+    public DeferredResult<ResponseEntity> saveEntityAttributesV1(@PathVariable("entityType") String entityType, @PathVariable("entityId") String entityIdStr,
+                                                                 @PathVariable("scope") String scope,
+                                                                 @RequestBody JsonNode request) throws ThingsboardException {
+        EntityId entityId = EntityIdFactory.getByTypeAndId(entityType, entityIdStr);
+        return saveAttributes(entityId, scope, request);
+    }
+
+    @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
+    @RequestMapping(value = "/{entityType}/{entityId}/attributes/{scope}", method = RequestMethod.POST)
+    @ResponseBody
+    public DeferredResult<ResponseEntity> saveEntityAttributesV2(@PathVariable("entityType") String entityType, @PathVariable("entityId") String entityIdStr,
+                                                                 @PathVariable("scope") String scope,
+                                                                 @RequestBody JsonNode request) throws ThingsboardException {
+        EntityId entityId = EntityIdFactory.getByTypeAndId(entityType, entityIdStr);
+        return saveAttributes(entityId, scope, request);
+    }
+
+    @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
+    @RequestMapping(value = "/{entityType}/{entityId}/timeseries/{scope}", method = RequestMethod.POST)
+    @ResponseBody
+    public DeferredResult<ResponseEntity> saveEntityTelemetry(@PathVariable("entityType") String entityType, @PathVariable("entityId") String entityIdStr,
+                                                              @PathVariable("scope") String scope,
+                                                              @RequestBody String requestBody) throws ThingsboardException {
+        EntityId entityId = EntityIdFactory.getByTypeAndId(entityType, entityIdStr);
+        return saveTelemetry(entityId, requestBody, 0L);
+    }
+
+    @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
+    @RequestMapping(value = "/{entityType}/{entityId}/timeseries/{scope}/{ttl}", method = RequestMethod.POST)
+    @ResponseBody
+    public DeferredResult<ResponseEntity> saveEntityTelemetryWithTTL(@PathVariable("entityType") String entityType, @PathVariable("entityId") String entityIdStr,
+                                                                     @PathVariable("scope") String scope, @PathVariable("ttl") Long ttl,
+                                                                     @RequestBody String requestBody) throws ThingsboardException {
+        EntityId entityId = EntityIdFactory.getByTypeAndId(entityType, entityIdStr);
+        return saveTelemetry(entityId, requestBody, ttl);
+    }
+
+    @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
+    @RequestMapping(value = "/{deviceId}/{scope}", method = RequestMethod.DELETE)
+    @ResponseBody
+    public DeferredResult<ResponseEntity> deleteEntityAttributes(@PathVariable("deviceId") String deviceIdStr,
+                                                                 @PathVariable("scope") String scope,
+                                                                 @RequestParam(name = "keys") String keysStr) throws ThingsboardException {
+        EntityId entityId = EntityIdFactory.getByTypeAndUuid(EntityType.DEVICE, deviceIdStr);
+        return deleteAttributes(entityId, scope, keysStr);
+    }
+
+    @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
+    @RequestMapping(value = "/{entityType}/{entityId}/{scope}", method = RequestMethod.DELETE)
+    @ResponseBody
+    public DeferredResult<ResponseEntity> deleteEntityAttributes(@PathVariable("entityType") String entityType, @PathVariable("entityId") String entityIdStr,
+                                                                 @PathVariable("scope") String scope,
+                                                                 @RequestParam(name = "keys") String keysStr) throws ThingsboardException {
+        EntityId entityId = EntityIdFactory.getByTypeAndId(entityType, entityIdStr);
+        return deleteAttributes(entityId, scope, keysStr);
+    }
+
+    private DeferredResult<ResponseEntity> deleteAttributes(EntityId entityIdStr, String scope, String keysStr) throws ThingsboardException {
+        List<String> keys = toKeysList(keysStr);
+        if (keys.isEmpty()) {
+            return getImmediateDeferredResult("Empty keys: " + keysStr, HttpStatus.BAD_REQUEST);
+        }
+        SecurityUser user = getCurrentUser();
+        if (DataConstants.SERVER_SCOPE.equals(scope) ||
+                DataConstants.SHARED_SCOPE.equals(scope) ||
+                DataConstants.CLIENT_SCOPE.equals(scope)) {
+            return accessValidator.validateEntityAndCallback(getCurrentUser(), entityIdStr, (result, entityId) -> {
+                ListenableFuture<List<Void>> future = attributesService.removeAll(entityId, scope, keys);
+                Futures.addCallback(future, new FutureCallback<List<Void>>() {
+                    @Override
+                    public void onSuccess(@Nullable List<Void> tmp) {
+                        logAttributesDeleted(user, entityId, scope, keys, null);
+                        result.setResult(new ResponseEntity<>(HttpStatus.OK));
+                    }
+
+                    @Override
+                    public void onFailure(Throwable t) {
+                        logAttributesDeleted(user, entityId, scope, keys, t);
+                        result.setResult(new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR));
+                    }
+                }, executor);
+            });
+        } else {
+            return getImmediateDeferredResult("Invalid attribute scope: " + scope, HttpStatus.BAD_REQUEST);
+        }
+    }
+
+    private DeferredResult<ResponseEntity> saveAttributes(EntityId entityIdSrc, String scope, JsonNode json) throws ThingsboardException {
+        if (!DataConstants.SERVER_SCOPE.equals(scope) && !DataConstants.SHARED_SCOPE.equals(scope)) {
+            return getImmediateDeferredResult("Invalid scope: " + scope, HttpStatus.BAD_REQUEST);
+        }
+        if (json.isObject()) {
+            List<AttributeKvEntry> attributes = extractRequestAttributes(json);
+            if (attributes.isEmpty()) {
+                return getImmediateDeferredResult("No attributes data found in request body!", HttpStatus.BAD_REQUEST);
+            }
+            SecurityUser user = getCurrentUser();
+            return accessValidator.validateEntityAndCallback(getCurrentUser(), entityIdSrc, (result, entityId) -> {
+                tsSubService.saveAndNotify(entityId, scope, attributes, new FutureCallback<Void>() {
+                    @Override
+                    public void onSuccess(@Nullable Void tmp) {
+                        logAttributesUpdated(user, entityId, scope, attributes, null);
+                        result.setResult(new ResponseEntity(HttpStatus.OK));
+                    }
+
+                    @Override
+                    public void onFailure(Throwable t) {
+                        logAttributesUpdated(user, entityId, scope, attributes, t);
+                        AccessValidator.handleError(t, result, HttpStatus.INTERNAL_SERVER_ERROR);
+                    }
+                });
+            });
+        } else {
+            return getImmediateDeferredResult("Request is not a JSON object", HttpStatus.BAD_REQUEST);
+        }
+    }
+
+    private DeferredResult<ResponseEntity> saveTelemetry(EntityId entityIdSrc, String requestBody, long ttl) throws ThingsboardException {
+        TelemetryUploadRequest telemetryRequest;
+        JsonElement telemetryJson;
+        try {
+            telemetryJson = new JsonParser().parse(requestBody);
+        } catch (Exception e) {
+            return getImmediateDeferredResult("Unable to parse timeseries payload: Invalid JSON body!", HttpStatus.BAD_REQUEST);
+        }
+        try {
+            telemetryRequest = JsonConverter.convertToTelemetry(telemetryJson);
+        } catch (Exception e) {
+            return getImmediateDeferredResult("Unable to parse timeseries payload. Invalid JSON body: " + e.getMessage(), HttpStatus.BAD_REQUEST);
+        }
+        List<TsKvEntry> entries = new ArrayList<>();
+        for (Map.Entry<Long, List<KvEntry>> entry : telemetryRequest.getData().entrySet()) {
+            for (KvEntry kv : entry.getValue()) {
+                entries.add(new BasicTsKvEntry(entry.getKey(), kv));
+            }
+        }
+        if (entries.isEmpty()) {
+            return getImmediateDeferredResult("No timeseries data found in request body!", HttpStatus.BAD_REQUEST);
+        }
+        SecurityUser user = getCurrentUser();
+        return accessValidator.validateEntityAndCallback(getCurrentUser(), entityIdSrc, (result, entityId) -> {
+            tsSubService.saveAndNotify(entityId, entries, ttl, new FutureCallback<Void>() {
+                @Override
+                public void onSuccess(@Nullable Void tmp) {
+                    result.setResult(new ResponseEntity(HttpStatus.OK));
+                }
+
+                @Override
+                public void onFailure(Throwable t) {
+                    AccessValidator.handleError(t, result, HttpStatus.INTERNAL_SERVER_ERROR);
+                }
+            });
+        });
+    }
+
+    private void getLatestTimeseriesValuesCallback(@Nullable DeferredResult<ResponseEntity> result, SecurityUser user, EntityId entityId, String keys) {
+        ListenableFuture<List<TsKvEntry>> future;
+        if (StringUtils.isEmpty(keys)) {
+            future = tsService.findAllLatest(entityId);
+        } else {
+            future = tsService.findLatest(entityId, toKeysList(keys));
+        }
+        Futures.addCallback(future, getTsKvListCallback(result));
+    }
+
+    private void getAttributeValuesCallback(@Nullable DeferredResult<ResponseEntity> result, SecurityUser user, EntityId entityId, String scope, String keys) {
+        List<String> keyList = toKeysList(keys);
+        FutureCallback<List<AttributeKvEntry>> callback = getAttributeValuesToResponseCallback(result, user, scope, entityId, keyList);
+        if (!StringUtils.isEmpty(scope)) {
+            if (keyList != null && !keyList.isEmpty()) {
+                Futures.addCallback(attributesService.find(entityId, scope, keyList), callback);
+            } else {
+                Futures.addCallback(attributesService.findAll(entityId, scope), callback);
+            }
+        } else {
+            List<ListenableFuture<List<AttributeKvEntry>>> futures = new ArrayList<>();
+            for (String tmpScope : DataConstants.allScopes()) {
+                if (keyList != null && !keyList.isEmpty()) {
+                    futures.add(attributesService.find(entityId, tmpScope, keyList));
+                } else {
+                    futures.add(attributesService.findAll(entityId, tmpScope));
+                }
+            }
+
+            ListenableFuture<List<AttributeKvEntry>> future = mergeAllAttributesFutures(futures);
+
+            Futures.addCallback(future, callback);
+        }
+    }
+
+    private void getAttributeKeysCallback(@Nullable DeferredResult<ResponseEntity> result, EntityId entityId, String scope) {
+        Futures.addCallback(attributesService.findAll(entityId, scope), getAttributeKeysToResponseCallback(result));
+    }
+
+    private void getAttributeKeysCallback(@Nullable DeferredResult<ResponseEntity> result, EntityId entityId) {
+        List<ListenableFuture<List<AttributeKvEntry>>> futures = new ArrayList<>();
+        for (String scope : DataConstants.allScopes()) {
+            futures.add(attributesService.findAll(entityId, scope));
+        }
+
+        ListenableFuture<List<AttributeKvEntry>> future = mergeAllAttributesFutures(futures);
+
+        Futures.addCallback(future, getAttributeKeysToResponseCallback(result));
+    }
+
+    private FutureCallback<List<TsKvEntry>> getTsKeysToResponseCallback(final DeferredResult<ResponseEntity> response) {
+        return new FutureCallback<List<TsKvEntry>>() {
+            @Override
+            public void onSuccess(List<TsKvEntry> values) {
+                List<String> keys = values.stream().map(KvEntry::getKey).collect(Collectors.toList());
+                response.setResult(new ResponseEntity<>(keys, HttpStatus.OK));
+            }
+
+            @Override
+            public void onFailure(Throwable e) {
+                log.error("Failed to fetch attributes", e);
+                AccessValidator.handleError(e, response, HttpStatus.INTERNAL_SERVER_ERROR);
+            }
+        };
+    }
+
+    private FutureCallback<List<AttributeKvEntry>> getAttributeKeysToResponseCallback(final DeferredResult<ResponseEntity> response) {
+        return new FutureCallback<List<AttributeKvEntry>>() {
+
+            @Override
+            public void onSuccess(List<AttributeKvEntry> attributes) {
+                List<String> keys = attributes.stream().map(KvEntry::getKey).collect(Collectors.toList());
+                response.setResult(new ResponseEntity<>(keys, HttpStatus.OK));
+            }
+
+            @Override
+            public void onFailure(Throwable e) {
+                log.error("Failed to fetch attributes", e);
+                AccessValidator.handleError(e, response, HttpStatus.INTERNAL_SERVER_ERROR);
+            }
+        };
+    }
+
+    private FutureCallback<List<AttributeKvEntry>> getAttributeValuesToResponseCallback(final DeferredResult<ResponseEntity> response,
+                                                                                        final SecurityUser user, final String scope,
+                                                                                        final EntityId entityId, final List<String> keyList) {
+        return new FutureCallback<List<AttributeKvEntry>>() {
+            @Override
+            public void onSuccess(List<AttributeKvEntry> attributes) {
+                List<AttributeData> values = attributes.stream().map(attribute -> new AttributeData(attribute.getLastUpdateTs(),
+                        attribute.getKey(), attribute.getValue())).collect(Collectors.toList());
+                logAttributesRead(user, entityId, scope, keyList, null);
+                response.setResult(new ResponseEntity<>(values, HttpStatus.OK));
+            }
+
+            @Override
+            public void onFailure(Throwable e) {
+                log.error("Failed to fetch attributes", e);
+                logAttributesRead(user, entityId, scope, keyList, e);
+                AccessValidator.handleError(e, response, HttpStatus.INTERNAL_SERVER_ERROR);
+            }
+        };
+    }
+
+    private FutureCallback<List<TsKvEntry>> getTsKvListCallback(final DeferredResult<ResponseEntity> response) {
+        return new FutureCallback<List<TsKvEntry>>() {
+            @Override
+            public void onSuccess(List<TsKvEntry> data) {
+                Map<String, List<TsData>> result = new LinkedHashMap<>();
+                for (TsKvEntry entry : data) {
+                    result.computeIfAbsent(entry.getKey(), k -> new ArrayList<>())
+                            .add(new TsData(entry.getTs(), entry.getValueAsString()));
+                }
+                response.setResult(new ResponseEntity<>(result, HttpStatus.OK));
+            }
+
+            @Override
+            public void onFailure(Throwable e) {
+                log.error("Failed to fetch historical data", e);
+                AccessValidator.handleError(e, response, HttpStatus.INTERNAL_SERVER_ERROR);
+            }
+        };
+    }
+
+    private void logAttributesDeleted(SecurityUser user, EntityId entityId, String scope, List<String> keys, Throwable e) {
+        try {
+            logEntityAction(user, (UUIDBased & EntityId)entityId, null, null, ActionType.ATTRIBUTES_DELETED, toException(e),
+                    scope, keys);
+        } catch (ThingsboardException te) {
+            log.warn("Failed to log attributes delete", te);
+        }
+    }
+
+    private void logAttributesUpdated(SecurityUser user, EntityId entityId, String scope, List<AttributeKvEntry> attributes, Throwable e) {
+        try {
+            logEntityAction(user, (UUIDBased & EntityId)entityId, null, null, ActionType.ATTRIBUTES_UPDATED, toException(e),
+                    scope, attributes);
+        } catch (ThingsboardException te) {
+            log.warn("Failed to log attributes update", te);
+        }
+    }
+
+
+    private void logAttributesRead(SecurityUser user, EntityId entityId, String scope, List<String> keys, Throwable e) {
+        try {
+            logEntityAction(user, (UUIDBased & EntityId)entityId, null, null, ActionType.ATTRIBUTES_READ, toException(e),
+                    scope, keys);
+        } catch (ThingsboardException te) {
+            log.warn("Failed to log attributes read", te);
+        }
+    }
+
+    private ListenableFuture<List<AttributeKvEntry>> mergeAllAttributesFutures(List<ListenableFuture<List<AttributeKvEntry>>> futures) {
+        return Futures.transform(Futures.successfulAsList(futures),
+                (Function<? super List<List<AttributeKvEntry>>, ? extends List<AttributeKvEntry>>) input -> {
+                    List<AttributeKvEntry> tmp = new ArrayList<>();
+                    if (input != null) {
+                        input.forEach(tmp::addAll);
+                    }
+                    return tmp;
+                }, executor);
+    }
+
+    private List<String> toKeysList(String keys) {
+        List<String> keyList = null;
+        if (!StringUtils.isEmpty(keys)) {
+            keyList = Arrays.asList(keys.split(","));
+        }
+        return keyList;
+    }
+
+    private DeferredResult<ResponseEntity> getImmediateDeferredResult(String message, HttpStatus status) {
+        DeferredResult<ResponseEntity> result = new DeferredResult<>();
+        result.setResult(new ResponseEntity<>(message, status));
+        return result;
+    }
+
+    private List<AttributeKvEntry> extractRequestAttributes(JsonNode jsonNode) {
+        long ts = System.currentTimeMillis();
+        List<AttributeKvEntry> attributes = new ArrayList<>();
+        jsonNode.fields().forEachRemaining(entry -> {
+            String key = entry.getKey();
+            JsonNode value = entry.getValue();
+            if (entry.getValue().isTextual()) {
+                attributes.add(new BaseAttributeKvEntry(new StringDataEntry(key, value.textValue()), ts));
+            } else if (entry.getValue().isBoolean()) {
+                attributes.add(new BaseAttributeKvEntry(new BooleanDataEntry(key, value.booleanValue()), ts));
+            } else if (entry.getValue().isDouble()) {
+                attributes.add(new BaseAttributeKvEntry(new DoubleDataEntry(key, value.doubleValue()), ts));
+            } else if (entry.getValue().isNumber()) {
+                if (entry.getValue().isBigInteger()) {
+                    throw new UncheckedApiException(new InvalidParametersException("Big integer values are not supported!"));
+                } else {
+                    attributes.add(new BaseAttributeKvEntry(new LongDataEntry(key, value.longValue()), ts));
+                }
+            }
+        });
+        return attributes;
+    }
+}
diff --git a/application/src/main/java/org/thingsboard/server/controller/TenantController.java b/application/src/main/java/org/thingsboard/server/controller/TenantController.java
index 5acb4eb..1a7c116 100644
--- a/application/src/main/java/org/thingsboard/server/controller/TenantController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/TenantController.java
@@ -15,21 +15,34 @@
  */
 package org.thingsboard.server.controller;
 
+import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.http.HttpStatus;
 import org.springframework.security.access.prepost.PreAuthorize;
-import org.springframework.web.bind.annotation.*;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestMethod;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.ResponseBody;
+import org.springframework.web.bind.annotation.ResponseStatus;
+import org.springframework.web.bind.annotation.RestController;
 import org.thingsboard.server.common.data.Tenant;
+import org.thingsboard.server.common.data.exception.ThingsboardException;
 import org.thingsboard.server.common.data.id.TenantId;
 import org.thingsboard.server.common.data.page.TextPageData;
 import org.thingsboard.server.common.data.page.TextPageLink;
 import org.thingsboard.server.dao.tenant.TenantService;
-import org.thingsboard.server.exception.ThingsboardException;
+import org.thingsboard.server.service.install.InstallScripts;
 
 @RestController
 @RequestMapping("/api")
+@Slf4j
 public class TenantController extends BaseController {
-    
+
+    @Autowired
+    private InstallScripts installScripts;
+
     @Autowired
     private TenantService tenantService;
 
@@ -49,10 +62,15 @@ public class TenantController extends BaseController {
 
     @PreAuthorize("hasAuthority('SYS_ADMIN')")
     @RequestMapping(value = "/tenant", method = RequestMethod.POST)
-    @ResponseBody 
+    @ResponseBody
     public Tenant saveTenant(@RequestBody Tenant tenant) throws ThingsboardException {
         try {
-            return checkNotNull(tenantService.saveTenant(tenant));
+            boolean newTenant = tenant.getId() == null;
+            tenant = checkNotNull(tenantService.saveTenant(tenant));
+            if (newTenant) {
+                installScripts.createDefaultRuleChains(tenant.getId());
+            }
+            return tenant;
         } catch (Exception e) {
             throw handleException(e);
         }
@@ -72,7 +90,7 @@ public class TenantController extends BaseController {
     }
 
     @PreAuthorize("hasAuthority('SYS_ADMIN')")
-    @RequestMapping(value = "/tenants", params = { "limit" }, method = RequestMethod.GET)
+    @RequestMapping(value = "/tenants", params = {"limit"}, method = RequestMethod.GET)
     @ResponseBody
     public TextPageData<Tenant> getTenants(@RequestParam int limit,
                                            @RequestParam(required = false) String textSearch,
@@ -85,5 +103,5 @@ public class TenantController extends BaseController {
             throw handleException(e);
         }
     }
-    
+
 }
diff --git a/application/src/main/java/org/thingsboard/server/controller/UserController.java b/application/src/main/java/org/thingsboard/server/controller/UserController.java
index 2a1531a..5f6c1ce 100644
--- a/application/src/main/java/org/thingsboard/server/controller/UserController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/UserController.java
@@ -18,10 +18,20 @@ package org.thingsboard.server.controller;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.http.HttpStatus;
 import org.springframework.security.access.prepost.PreAuthorize;
-import org.springframework.web.bind.annotation.*;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestMethod;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.ResponseBody;
+import org.springframework.web.bind.annotation.ResponseStatus;
+import org.springframework.web.bind.annotation.RestController;
+import org.thingsboard.rule.engine.api.MailService;
 import org.thingsboard.server.common.data.EntityType;
 import org.thingsboard.server.common.data.User;
 import org.thingsboard.server.common.data.audit.ActionType;
+import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
+import org.thingsboard.server.common.data.exception.ThingsboardException;
 import org.thingsboard.server.common.data.id.CustomerId;
 import org.thingsboard.server.common.data.id.TenantId;
 import org.thingsboard.server.common.data.id.UserId;
@@ -29,9 +39,6 @@ import org.thingsboard.server.common.data.page.TextPageData;
 import org.thingsboard.server.common.data.page.TextPageLink;
 import org.thingsboard.server.common.data.security.Authority;
 import org.thingsboard.server.common.data.security.UserCredentials;
-import org.thingsboard.server.exception.ThingsboardErrorCode;
-import org.thingsboard.server.exception.ThingsboardException;
-import org.thingsboard.server.service.mail.MailService;
 import org.thingsboard.server.service.security.model.SecurityUser;
 
 import javax.servlet.http.HttpServletRequest;
diff --git a/application/src/main/java/org/thingsboard/server/controller/WidgetsBundleController.java b/application/src/main/java/org/thingsboard/server/controller/WidgetsBundleController.java
index 757f765..eb229fe 100644
--- a/application/src/main/java/org/thingsboard/server/controller/WidgetsBundleController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/WidgetsBundleController.java
@@ -17,7 +17,15 @@ package org.thingsboard.server.controller;
 
 import org.springframework.http.HttpStatus;
 import org.springframework.security.access.prepost.PreAuthorize;
-import org.springframework.web.bind.annotation.*;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestMethod;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.ResponseBody;
+import org.springframework.web.bind.annotation.ResponseStatus;
+import org.springframework.web.bind.annotation.RestController;
+import org.thingsboard.server.common.data.exception.ThingsboardException;
 import org.thingsboard.server.common.data.id.TenantId;
 import org.thingsboard.server.common.data.id.WidgetsBundleId;
 import org.thingsboard.server.common.data.page.TextPageData;
@@ -25,7 +33,6 @@ import org.thingsboard.server.common.data.page.TextPageLink;
 import org.thingsboard.server.common.data.security.Authority;
 import org.thingsboard.server.common.data.widget.WidgetsBundle;
 import org.thingsboard.server.dao.model.ModelConstants;
-import org.thingsboard.server.exception.ThingsboardException;
 
 import java.util.List;
 
diff --git a/application/src/main/java/org/thingsboard/server/controller/WidgetTypeController.java b/application/src/main/java/org/thingsboard/server/controller/WidgetTypeController.java
index 44c7d94..60f40a8 100644
--- a/application/src/main/java/org/thingsboard/server/controller/WidgetTypeController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/WidgetTypeController.java
@@ -17,13 +17,20 @@ package org.thingsboard.server.controller;
 
 import org.springframework.http.HttpStatus;
 import org.springframework.security.access.prepost.PreAuthorize;
-import org.springframework.web.bind.annotation.*;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestMethod;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.ResponseBody;
+import org.springframework.web.bind.annotation.ResponseStatus;
+import org.springframework.web.bind.annotation.RestController;
+import org.thingsboard.server.common.data.exception.ThingsboardException;
 import org.thingsboard.server.common.data.id.TenantId;
 import org.thingsboard.server.common.data.id.WidgetTypeId;
 import org.thingsboard.server.common.data.security.Authority;
 import org.thingsboard.server.common.data.widget.WidgetType;
 import org.thingsboard.server.dao.model.ModelConstants;
-import org.thingsboard.server.exception.ThingsboardException;
 
 import java.util.List;
 
diff --git a/application/src/main/java/org/thingsboard/server/exception/ThingsboardErrorResponse.java b/application/src/main/java/org/thingsboard/server/exception/ThingsboardErrorResponse.java
index 3b897d6..031073f 100644
--- a/application/src/main/java/org/thingsboard/server/exception/ThingsboardErrorResponse.java
+++ b/application/src/main/java/org/thingsboard/server/exception/ThingsboardErrorResponse.java
@@ -16,6 +16,7 @@
 package org.thingsboard.server.exception;
 
 import org.springframework.http.HttpStatus;
+import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
 
 import java.util.Date;
 
diff --git a/application/src/main/java/org/thingsboard/server/exception/ThingsboardErrorResponseHandler.java b/application/src/main/java/org/thingsboard/server/exception/ThingsboardErrorResponseHandler.java
index c70c561..63fe17a 100644
--- a/application/src/main/java/org/thingsboard/server/exception/ThingsboardErrorResponseHandler.java
+++ b/application/src/main/java/org/thingsboard/server/exception/ThingsboardErrorResponseHandler.java
@@ -17,8 +17,6 @@ package org.thingsboard.server.exception;
 
 import com.fasterxml.jackson.databind.ObjectMapper;
 import lombok.extern.slf4j.Slf4j;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.http.HttpStatus;
 import org.springframework.http.MediaType;
@@ -27,6 +25,8 @@ import org.springframework.security.authentication.BadCredentialsException;
 import org.springframework.security.core.AuthenticationException;
 import org.springframework.security.web.access.AccessDeniedHandler;
 import org.springframework.stereotype.Component;
+import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
+import org.thingsboard.server.common.data.exception.ThingsboardException;
 import org.thingsboard.server.service.security.exception.AuthMethodNotSupportedException;
 import org.thingsboard.server.service.security.exception.JwtExpiredTokenException;
 
diff --git a/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java b/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java
index e765c40..f863d0b 100644
--- a/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java
+++ b/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java
@@ -23,12 +23,11 @@ import org.springframework.context.ApplicationContext;
 import org.springframework.context.annotation.Profile;
 import org.springframework.stereotype.Service;
 import org.thingsboard.server.service.component.ComponentDiscoveryService;
+import org.thingsboard.server.service.install.DataUpdateService;
 import org.thingsboard.server.service.install.DatabaseSchemaService;
 import org.thingsboard.server.service.install.DatabaseUpgradeService;
 import org.thingsboard.server.service.install.SystemDataLoaderService;
 
-import java.nio.file.Paths;
-
 @Service
 @Profile("install")
 @Slf4j
@@ -40,9 +39,6 @@ public class ThingsboardInstallService {
     @Value("${install.upgrade.from_version:1.2.3}")
     private String upgradeFromVersion;
 
-    @Value("${install.data_dir}")
-    private String dataDir;
-
     @Value("${install.load_demo:false}")
     private Boolean loadDemo;
 
@@ -61,6 +57,9 @@ public class ThingsboardInstallService {
     @Autowired
     private SystemDataLoaderService systemDataLoaderService;
 
+    @Autowired
+    private DataUpdateService dataUpdateService;
+
     public void performInstall() {
         try {
             if (isUpgrade) {
@@ -77,11 +76,18 @@ public class ThingsboardInstallService {
 
                         databaseUpgradeService.upgradeDatabase("1.3.0");
 
-                    case "1.3.1":
+                    case "1.3.1": //NOSONAR, Need to execute gradual upgrade starting from upgradeFromVersion
                         log.info("Upgrading ThingsBoard from version 1.3.1 to 1.4.0 ...");
 
                         databaseUpgradeService.upgradeDatabase("1.3.1");
 
+                    case "1.4.0":
+                        log.info("Upgrading ThingsBoard from version 1.4.0 to 2.0.0 ...");
+
+                        databaseUpgradeService.upgradeDatabase("1.4.0");
+
+                        dataUpdateService.updateData("1.4.0");
+
                         log.info("Updating system data...");
 
                         systemDataLoaderService.deleteSystemWidgetBundle("charts");
@@ -108,13 +114,6 @@ public class ThingsboardInstallService {
 
                 log.info("Starting ThingsBoard Installation...");
 
-                if (this.dataDir == null) {
-                    throw new RuntimeException("'install.data_dir' property should specified!");
-                }
-                if (!Paths.get(this.dataDir).toFile().isDirectory()) {
-                    throw new RuntimeException("'install.data_dir' property value is not a valid directory!");
-                }
-
                 log.info("Installing DataBase schema...");
 
                 databaseSchemaService.createDatabaseSchema();
@@ -126,8 +125,8 @@ public class ThingsboardInstallService {
                 systemDataLoaderService.createSysAdmin();
                 systemDataLoaderService.createAdminSettings();
                 systemDataLoaderService.loadSystemWidgets();
-                systemDataLoaderService.loadSystemPlugins();
-                systemDataLoaderService.loadSystemRules();
+//                systemDataLoaderService.loadSystemPlugins();
+//                systemDataLoaderService.loadSystemRules();
 
                 if (loadDemo) {
                     log.info("Loading demo data...");
diff --git a/application/src/main/java/org/thingsboard/server/service/cluster/discovery/DummyDiscoveryService.java b/application/src/main/java/org/thingsboard/server/service/cluster/discovery/DummyDiscoveryService.java
index 865325f..c21f1aa 100644
--- a/application/src/main/java/org/thingsboard/server/service/cluster/discovery/DummyDiscoveryService.java
+++ b/application/src/main/java/org/thingsboard/server/service/cluster/discovery/DummyDiscoveryService.java
@@ -20,7 +20,6 @@ import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
 import org.springframework.context.annotation.DependsOn;
 import org.springframework.stereotype.Service;
-import org.thingsboard.server.service.environment.EnvironmentLogService;
 
 import javax.annotation.PostConstruct;
 import java.util.Collections;
diff --git a/application/src/main/java/org/thingsboard/server/service/cluster/discovery/ServerInstance.java b/application/src/main/java/org/thingsboard/server/service/cluster/discovery/ServerInstance.java
index 03c9694..6eee5f3 100644
--- a/application/src/main/java/org/thingsboard/server/service/cluster/discovery/ServerInstance.java
+++ b/application/src/main/java/org/thingsboard/server/service/cluster/discovery/ServerInstance.java
@@ -15,12 +15,11 @@
  */
 package org.thingsboard.server.service.cluster.discovery;
 
-import lombok.AccessLevel;
 import lombok.EqualsAndHashCode;
 import lombok.Getter;
 import lombok.ToString;
 import org.thingsboard.server.common.msg.cluster.ServerAddress;
-import org.thingsboard.server.gen.discovery.ServerInstanceProtos.ServerInfo;
+import org.thingsboard.server.gen.discovery.ServerInstanceProtos;
 
 /**
  * @author Andrew Shvayka
@@ -29,8 +28,6 @@ import org.thingsboard.server.gen.discovery.ServerInstanceProtos.ServerInfo;
 @EqualsAndHashCode(exclude = {"serverInfo", "serverAddress"})
 public final class ServerInstance implements Comparable<ServerInstance> {
 
-    @Getter(AccessLevel.PACKAGE)
-    private final ServerInfo serverInfo;
     @Getter
     private final String host;
     @Getter
@@ -38,8 +35,13 @@ public final class ServerInstance implements Comparable<ServerInstance> {
     @Getter
     private final ServerAddress serverAddress;
 
-    public ServerInstance(ServerInfo serverInfo) {
-        this.serverInfo = serverInfo;
+    public ServerInstance(ServerAddress serverAddress) {
+        this.serverAddress = serverAddress;
+        this.host = serverAddress.getHost();
+        this.port = serverAddress.getPort();
+    }
+
+    public ServerInstance(ServerInstanceProtos.ServerInfo serverInfo) {
         this.host = serverInfo.getHost();
         this.port = serverInfo.getPort();
         this.serverAddress = new ServerAddress(host, port);
diff --git a/application/src/main/java/org/thingsboard/server/service/cluster/discovery/ZkDiscoveryService.java b/application/src/main/java/org/thingsboard/server/service/cluster/discovery/ZkDiscoveryService.java
index 818d2b1..6002b0e 100644
--- a/application/src/main/java/org/thingsboard/server/service/cluster/discovery/ZkDiscoveryService.java
+++ b/application/src/main/java/org/thingsboard/server/service/cluster/discovery/ZkDiscoveryService.java
@@ -15,8 +15,9 @@
  */
 package org.thingsboard.server.service.cluster.discovery;
 
-import com.google.protobuf.InvalidProtocolBufferException;
 import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.SerializationException;
+import org.apache.commons.lang3.SerializationUtils;
 import org.apache.curator.framework.CuratorFramework;
 import org.apache.curator.framework.CuratorFrameworkFactory;
 import org.apache.curator.framework.recipes.cache.ChildData;
@@ -31,15 +32,17 @@ import org.springframework.beans.factory.annotation.Value;
 import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
 import org.springframework.boot.context.event.ApplicationReadyEvent;
 import org.springframework.context.ApplicationListener;
+import org.springframework.context.annotation.Lazy;
 import org.springframework.stereotype.Service;
 import org.springframework.util.Assert;
-import org.thingsboard.server.gen.discovery.ServerInstanceProtos.ServerInfo;
+import org.thingsboard.server.common.msg.cluster.ServerAddress;
+import org.thingsboard.server.service.telemetry.TelemetrySubscriptionService;
 import org.thingsboard.server.utils.MiscUtils;
 
 import javax.annotation.PostConstruct;
 import javax.annotation.PreDestroy;
-import java.io.IOException;
 import java.util.List;
+import java.util.NoSuchElementException;
 import java.util.concurrent.CopyOnWriteArrayList;
 import java.util.stream.Collectors;
 
@@ -67,6 +70,10 @@ public class ZkDiscoveryService implements DiscoveryService, PathChildrenCacheLi
     @Autowired
     private ServerInstanceService serverInstance;
 
+    @Autowired
+    @Lazy
+    private TelemetrySubscriptionService tsSubService;
+
     private final List<DiscoveryServiceListener> listeners = new CopyOnWriteArrayList<>();
 
     private CuratorFramework client;
@@ -113,7 +120,7 @@ public class ZkDiscoveryService implements DiscoveryService, PathChildrenCacheLi
             log.info("[{}:{}] Creating ZK node for current instance", self.getHost(), self.getPort());
             nodePath = client.create()
                     .creatingParentsIfNeeded()
-                    .withMode(CreateMode.EPHEMERAL_SEQUENTIAL).forPath(zkNodesDir + "/", self.getServerInfo().toByteArray());
+                    .withMode(CreateMode.EPHEMERAL_SEQUENTIAL).forPath(zkNodesDir + "/", SerializationUtils.serialize(self.getServerAddress()));
             log.info("[{}:{}] Created ZK node for current instance: {}", self.getHost(), self.getPort(), nodePath);
         } catch (Exception e) {
             log.error("Failed to create ZK node", e);
@@ -144,8 +151,8 @@ public class ZkDiscoveryService implements DiscoveryService, PathChildrenCacheLi
                 .filter(cd -> !cd.getPath().equals(nodePath))
                 .map(cd -> {
                     try {
-                        return new ServerInstance(ServerInfo.parseFrom(cd.getData()));
-                    } catch (InvalidProtocolBufferException e) {
+                        return new ServerInstance( (ServerAddress) SerializationUtils.deserialize(cd.getData()));
+                    } catch (NoSuchElementException e) {
                         log.error("Failed to decode ZK node", e);
                         throw new RuntimeException(e);
                     }
@@ -186,20 +193,23 @@ public class ZkDiscoveryService implements DiscoveryService, PathChildrenCacheLi
         }
         ServerInstance instance;
         try {
-            instance = new ServerInstance(ServerInfo.parseFrom(data.getData()));
-        } catch (IOException e) {
+            ServerAddress serverAddress  = SerializationUtils.deserialize(data.getData());
+            instance = new ServerInstance(serverAddress);
+        } catch (SerializationException e) {
             log.error("Failed to decode server instance for node {}", data.getPath(), e);
             throw e;
         }
         log.info("Processing [{}] event for [{}:{}]", pathChildrenCacheEvent.getType(), instance.getHost(), instance.getPort());
         switch (pathChildrenCacheEvent.getType()) {
             case CHILD_ADDED:
+                tsSubService.onClusterUpdate();
                 listeners.forEach(listener -> listener.onServerAdded(instance));
                 break;
             case CHILD_UPDATED:
                 listeners.forEach(listener -> listener.onServerUpdated(instance));
                 break;
             case CHILD_REMOVED:
+                tsSubService.onClusterUpdate();
                 listeners.forEach(listener -> listener.onServerRemoved(instance));
                 break;
             default:
diff --git a/application/src/main/java/org/thingsboard/server/service/cluster/routing/ClusterRoutingService.java b/application/src/main/java/org/thingsboard/server/service/cluster/routing/ClusterRoutingService.java
index b29668c..272073d 100644
--- a/application/src/main/java/org/thingsboard/server/service/cluster/routing/ClusterRoutingService.java
+++ b/application/src/main/java/org/thingsboard/server/service/cluster/routing/ClusterRoutingService.java
@@ -16,9 +16,7 @@
 package org.thingsboard.server.service.cluster.routing;
 
 import org.thingsboard.server.common.data.id.EntityId;
-import org.thingsboard.server.common.data.id.UUIDBased;
 import org.thingsboard.server.common.msg.cluster.ServerAddress;
-import org.thingsboard.server.service.cluster.discovery.ServerInstance;
 
 import java.util.Optional;
 import java.util.UUID;
diff --git a/application/src/main/java/org/thingsboard/server/service/cluster/routing/ConsistentClusterRoutingService.java b/application/src/main/java/org/thingsboard/server/service/cluster/routing/ConsistentClusterRoutingService.java
index 4067797..5087b5c 100644
--- a/application/src/main/java/org/thingsboard/server/service/cluster/routing/ConsistentClusterRoutingService.java
+++ b/application/src/main/java/org/thingsboard/server/service/cluster/routing/ConsistentClusterRoutingService.java
@@ -23,7 +23,6 @@ import org.springframework.beans.factory.annotation.Value;
 import org.springframework.stereotype.Service;
 import org.springframework.util.Assert;
 import org.thingsboard.server.common.data.id.EntityId;
-import org.thingsboard.server.common.data.id.UUIDBased;
 import org.thingsboard.server.common.msg.cluster.ServerAddress;
 import org.thingsboard.server.service.cluster.discovery.DiscoveryService;
 import org.thingsboard.server.service.cluster.discovery.DiscoveryServiceListener;
@@ -107,7 +106,7 @@ public class ConsistentClusterRoutingService implements ClusterRoutingService, D
 
     @Override
     public void onServerAdded(ServerInstance server) {
-        log.debug("On server added event: {}", server);
+        log.info("On server added event: {}", server);
         addNode(server);
         logCircle();
     }
@@ -119,7 +118,7 @@ public class ConsistentClusterRoutingService implements ClusterRoutingService, D
 
     @Override
     public void onServerRemoved(ServerInstance server) {
-        log.debug("On server removed event: {}", server);
+        log.info("On server removed event: {}", server);
         removeNode(server);
         logCircle();
     }
diff --git a/application/src/main/java/org/thingsboard/server/service/cluster/rpc/ClusterGrpcService.java b/application/src/main/java/org/thingsboard/server/service/cluster/rpc/ClusterGrpcService.java
index 5a6b307..19715b8 100644
--- a/application/src/main/java/org/thingsboard/server/service/cluster/rpc/ClusterGrpcService.java
+++ b/application/src/main/java/org/thingsboard/server/service/cluster/rpc/ClusterGrpcService.java
@@ -19,38 +19,24 @@ import com.google.protobuf.ByteString;
 import io.grpc.Server;
 import io.grpc.ServerBuilder;
 import io.grpc.stub.StreamObserver;
-import lombok.Setter;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
-import org.springframework.util.SerializationUtils;
 import org.thingsboard.server.actors.rpc.RpcBroadcastMsg;
 import org.thingsboard.server.actors.rpc.RpcSessionCreateRequestMsg;
-import org.thingsboard.server.actors.rpc.RpcSessionTellMsg;
-import org.thingsboard.server.actors.service.ActorService;
-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.cluster.ToAllNodesMsg;
-import org.thingsboard.server.common.msg.core.ToDeviceSessionActorMsg;
-import org.thingsboard.server.common.msg.device.ToDeviceActorMsg;
-import org.thingsboard.server.extensions.api.device.ToDeviceActorNotificationMsg;
-import org.thingsboard.server.extensions.api.plugins.msg.FromDeviceRpcResponse;
-import org.thingsboard.server.extensions.api.plugins.msg.ToDeviceRpcRequest;
-import org.thingsboard.server.extensions.api.plugins.msg.ToDeviceRpcRequestPluginMsg;
-import org.thingsboard.server.extensions.api.plugins.msg.ToPluginRpcResponseDeviceMsg;
-import org.thingsboard.server.extensions.api.plugins.rpc.PluginRpcMsg;
 import org.thingsboard.server.gen.cluster.ClusterAPIProtos;
 import org.thingsboard.server.gen.cluster.ClusterRpcServiceGrpc;
-import org.thingsboard.server.service.cluster.discovery.DiscoveryService;
 import org.thingsboard.server.service.cluster.discovery.ServerInstance;
 import org.thingsboard.server.service.cluster.discovery.ServerInstanceService;
-import org.thingsboard.server.service.cluster.routing.ClusterRoutingService;
+import org.thingsboard.server.service.encoding.DataDecodingEncodingService;
 
-import javax.annotation.PostConstruct;
 import javax.annotation.PreDestroy;
 import java.io.IOException;
-import java.util.Set;
 import java.util.UUID;
+import java.util.concurrent.ArrayBlockingQueue;
+import java.util.concurrent.BlockingQueue;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.ConcurrentMap;
 
@@ -64,13 +50,17 @@ public class ClusterGrpcService extends ClusterRpcServiceGrpc.ClusterRpcServiceI
     @Autowired
     private ServerInstanceService instanceService;
 
+    @Autowired
+    private DataDecodingEncodingService encodingService;
+
     private RpcMsgListener listener;
 
     private Server server;
 
     private ServerInstance instance;
 
-    private ConcurrentMap<UUID, RpcSessionCreationFuture> pendingSessionMap = new ConcurrentHashMap<>();
+    private ConcurrentMap<UUID, BlockingQueue<StreamObserver<ClusterAPIProtos.ClusterMessage>>> pendingSessionMap =
+            new ConcurrentHashMap<>();
 
     public void init(RpcMsgListener listener) {
         this.listener = listener;
@@ -88,11 +78,11 @@ public class ClusterGrpcService extends ClusterRpcServiceGrpc.ClusterRpcServiceI
     }
 
     @Override
-    public void onSessionCreated(UUID msgUid, StreamObserver<ClusterAPIProtos.ToRpcServerMessage> msg) {
-        RpcSessionCreationFuture future = pendingSessionMap.remove(msgUid);
-        if (future != null) {
+    public void onSessionCreated(UUID msgUid, StreamObserver<ClusterAPIProtos.ClusterMessage> inputStream) {
+        BlockingQueue<StreamObserver<ClusterAPIProtos.ClusterMessage>> queue = pendingSessionMap.remove(msgUid);
+        if (queue != null) {
             try {
-                future.onMsg(msg);
+                queue.put(inputStream);
             } catch (InterruptedException e) {
                 log.warn("Failed to report created session!");
                 Thread.currentThread().interrupt();
@@ -103,11 +93,13 @@ public class ClusterGrpcService extends ClusterRpcServiceGrpc.ClusterRpcServiceI
     }
 
     @Override
-    public StreamObserver<ClusterAPIProtos.ToRpcServerMessage> handlePluginMsgs(StreamObserver<ClusterAPIProtos.ToRpcServerMessage> responseObserver) {
+    public StreamObserver<ClusterAPIProtos.ClusterMessage> handleMsgs(
+            StreamObserver<ClusterAPIProtos.ClusterMessage> responseObserver) {
         log.info("Processing new session.");
         return createSession(new RpcSessionCreateRequestMsg(UUID.randomUUID(), null, responseObserver));
     }
 
+
     @PreDestroy
     public void stop() {
         if (server != null) {
@@ -123,65 +115,18 @@ public class ClusterGrpcService extends ClusterRpcServiceGrpc.ClusterRpcServiceI
         }
     }
 
-    @Override
-    public void tell(ServerAddress serverAddress, ToDeviceActorMsg toForward) {
-        ClusterAPIProtos.ToRpcServerMessage msg = ClusterAPIProtos.ToRpcServerMessage.newBuilder()
-                .setToDeviceActorRpcMsg(toProtoMsg(toForward)).build();
-        tell(serverAddress, msg);
-    }
-
-    @Override
-    public void tell(ServerAddress serverAddress, ToDeviceActorNotificationMsg toForward) {
-        ClusterAPIProtos.ToRpcServerMessage msg = ClusterAPIProtos.ToRpcServerMessage.newBuilder()
-                .setToDeviceActorNotificationRpcMsg(toProtoMsg(toForward)).build();
-        tell(serverAddress, msg);
-    }
-
-    @Override
-    public void tell(ServerAddress serverAddress, ToDeviceRpcRequestPluginMsg toForward) {
-        ClusterAPIProtos.ToRpcServerMessage msg = ClusterAPIProtos.ToRpcServerMessage.newBuilder()
-                .setToDeviceRpcRequestRpcMsg(toProtoMsg(toForward)).build();
-        tell(serverAddress, msg);
-    }
 
     @Override
-    public void tell(ServerAddress serverAddress, ToPluginRpcResponseDeviceMsg toForward) {
-        ClusterAPIProtos.ToRpcServerMessage msg = ClusterAPIProtos.ToRpcServerMessage.newBuilder()
-                .setToPluginRpcResponseRpcMsg(toProtoMsg(toForward)).build();
-        tell(serverAddress, msg);
+    public void broadcast(RpcBroadcastMsg msg) {
+        listener.onBroadcastMsg(msg);
     }
 
-    @Override
-    public void tell(ServerAddress serverAddress, ToDeviceSessionActorMsg toForward) {
-        ClusterAPIProtos.ToRpcServerMessage msg = ClusterAPIProtos.ToRpcServerMessage.newBuilder()
-                .setToDeviceSessionActorRpcMsg(toProtoMsg(toForward)).build();
-        tell(serverAddress, msg);
-    }
-
-    @Override
-    public void tell(PluginRpcMsg toForward) {
-        ClusterAPIProtos.ToRpcServerMessage msg = ClusterAPIProtos.ToRpcServerMessage.newBuilder()
-                .setToPluginRpcMsg(toProtoMsg(toForward)).build();
-        tell(toForward.getRpcMsg().getServerAddress(), msg);
-    }
-
-    @Override
-    public void broadcast(ToAllNodesMsg toForward) {
-        ClusterAPIProtos.ToRpcServerMessage msg = ClusterAPIProtos.ToRpcServerMessage.newBuilder()
-                .setToAllNodesRpcMsg(toProtoMsg(toForward)).build();
-        listener.onMsg(new RpcBroadcastMsg(msg));
-    }
-
-    private void tell(ServerAddress serverAddress, ClusterAPIProtos.ToRpcServerMessage msg) {
-        listener.onMsg(new RpcSessionTellMsg(serverAddress, msg));
-    }
-
-    private StreamObserver<ClusterAPIProtos.ToRpcServerMessage> createSession(RpcSessionCreateRequestMsg msg) {
-        RpcSessionCreationFuture future = new RpcSessionCreationFuture();
-        pendingSessionMap.put(msg.getMsgUid(), future);
-        listener.onMsg(msg);
+    private StreamObserver<ClusterAPIProtos.ClusterMessage> createSession(RpcSessionCreateRequestMsg msg) {
+        BlockingQueue<StreamObserver<ClusterAPIProtos.ClusterMessage>> queue = new ArrayBlockingQueue<>(1);
+        pendingSessionMap.put(msg.getMsgUid(), queue);
+        listener.onRpcSessionCreateRequestMsg(msg);
         try {
-            StreamObserver<ClusterAPIProtos.ToRpcServerMessage> observer = future.get();
+            StreamObserver<ClusterAPIProtos.ClusterMessage> observer = queue.take();
             log.info("Processed new session.");
             return observer;
         } catch (Exception e) {
@@ -190,86 +135,27 @@ public class ClusterGrpcService extends ClusterRpcServiceGrpc.ClusterRpcServiceI
         }
     }
 
-    private static ClusterAPIProtos.ToDeviceActorRpcMessage toProtoMsg(ToDeviceActorMsg msg) {
-        return ClusterAPIProtos.ToDeviceActorRpcMessage.newBuilder().setData(
-                ByteString.copyFrom(SerializationUtils.serialize(msg))
-        ).build();
-    }
-
-    private static ClusterAPIProtos.ToDeviceActorNotificationRpcMessage toProtoMsg(ToDeviceActorNotificationMsg msg) {
-        return ClusterAPIProtos.ToDeviceActorNotificationRpcMessage.newBuilder().setData(
-                ByteString.copyFrom(SerializationUtils.serialize(msg))
-        ).build();
-    }
-
-    private static ClusterAPIProtos.ToDeviceRpcRequestRpcMessage toProtoMsg(ToDeviceRpcRequestPluginMsg msg) {
-        ClusterAPIProtos.ToDeviceRpcRequestRpcMessage.Builder builder = ClusterAPIProtos.ToDeviceRpcRequestRpcMessage.newBuilder();
-        ToDeviceRpcRequest request = msg.getMsg();
-
-        builder.setAddress(ClusterAPIProtos.PluginAddress.newBuilder()
-                .setTenantId(toUid(msg.getPluginTenantId().getId()))
-                .setPluginId(toUid(msg.getPluginId().getId()))
-                .build());
-
-        builder.setDeviceTenantId(toUid(msg.getTenantId()));
-        builder.setDeviceId(toUid(msg.getDeviceId()));
-
-        builder.setMsgId(toUid(request.getId()));
-        builder.setOneway(request.isOneway());
-        builder.setExpTime(request.getExpirationTime());
-        builder.setMethod(request.getBody().getMethod());
-        builder.setParams(request.getBody().getParams());
-
-        return builder.build();
-    }
-
-    private static ClusterAPIProtos.ToPluginRpcResponseRpcMessage toProtoMsg(ToPluginRpcResponseDeviceMsg msg) {
-        ClusterAPIProtos.ToPluginRpcResponseRpcMessage.Builder builder = ClusterAPIProtos.ToPluginRpcResponseRpcMessage.newBuilder();
-        FromDeviceRpcResponse request = msg.getResponse();
-
-        builder.setAddress(ClusterAPIProtos.PluginAddress.newBuilder()
-                .setTenantId(toUid(msg.getPluginTenantId().getId()))
-                .setPluginId(toUid(msg.getPluginId().getId()))
-                .build());
-
-        builder.setMsgId(toUid(request.getId()));
-        request.getResponse().ifPresent(builder::setResponse);
-        request.getError().ifPresent(e -> builder.setError(e.name()));
-
-        return builder.build();
-    }
-
-    private ClusterAPIProtos.ToAllNodesRpcMessage toProtoMsg(ToAllNodesMsg msg) {
-        return ClusterAPIProtos.ToAllNodesRpcMessage.newBuilder().setData(
-                ByteString.copyFrom(SerializationUtils.serialize(msg))
-        ).build();
-    }
-
-
-    private ClusterAPIProtos.ToPluginRpcMessage toProtoMsg(PluginRpcMsg msg) {
-        return ClusterAPIProtos.ToPluginRpcMessage.newBuilder()
-                .setClazz(msg.getRpcMsg().getMsgClazz())
-                .setData(ByteString.copyFrom(msg.getRpcMsg().getMsgData()))
-                .setAddress(ClusterAPIProtos.PluginAddress.newBuilder()
-                        .setTenantId(toUid(msg.getPluginTenantId().getId()))
-                        .setPluginId(toUid(msg.getPluginId().getId()))
-                        .build()
-                ).build();
-    }
-
-    private static ClusterAPIProtos.Uid toUid(EntityId uuid) {
-        return toUid(uuid.getId());
+    @Override
+    public void tell(ClusterAPIProtos.ClusterMessage message) {
+        listener.onSendMsg(message);
     }
 
-    private static ClusterAPIProtos.Uid toUid(UUID uuid) {
-        return ClusterAPIProtos.Uid.newBuilder().setPluginUuidMsb(uuid.getMostSignificantBits()).setPluginUuidLsb(
-                uuid.getLeastSignificantBits()).build();
+    @Override
+    public void tell(ServerAddress serverAddress, TbActorMsg actorMsg) {
+        listener.onSendMsg(encodingService.convertToProtoDataMessage(serverAddress, actorMsg));
     }
 
-    private static ClusterAPIProtos.ToDeviceSessionActorRpcMessage toProtoMsg(ToDeviceSessionActorMsg msg) {
-        return ClusterAPIProtos.ToDeviceSessionActorRpcMessage.newBuilder().setData(
-                ByteString.copyFrom(SerializationUtils.serialize(msg))
-        ).build();
+    @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);
     }
-
 }
diff --git a/application/src/main/java/org/thingsboard/server/service/cluster/rpc/ClusterRpcService.java b/application/src/main/java/org/thingsboard/server/service/cluster/rpc/ClusterRpcService.java
index 8c50bb7..de29b89 100644
--- a/application/src/main/java/org/thingsboard/server/service/cluster/rpc/ClusterRpcService.java
+++ b/application/src/main/java/org/thingsboard/server/service/cluster/rpc/ClusterRpcService.java
@@ -16,14 +16,9 @@
 package org.thingsboard.server.service.cluster.rpc;
 
 import io.grpc.stub.StreamObserver;
+import org.thingsboard.server.actors.rpc.RpcBroadcastMsg;
+import org.thingsboard.server.common.msg.TbActorMsg;
 import org.thingsboard.server.common.msg.cluster.ServerAddress;
-import org.thingsboard.server.common.msg.cluster.ToAllNodesMsg;
-import org.thingsboard.server.common.msg.core.ToDeviceSessionActorMsg;
-import org.thingsboard.server.common.msg.device.ToDeviceActorMsg;
-import org.thingsboard.server.extensions.api.device.ToDeviceActorNotificationMsg;
-import org.thingsboard.server.extensions.api.plugins.msg.ToDeviceRpcRequestPluginMsg;
-import org.thingsboard.server.extensions.api.plugins.msg.ToPluginRpcResponseDeviceMsg;
-import org.thingsboard.server.extensions.api.plugins.rpc.PluginRpcMsg;
 import org.thingsboard.server.gen.cluster.ClusterAPIProtos;
 
 import java.util.UUID;
@@ -35,19 +30,13 @@ public interface ClusterRpcService {
 
     void init(RpcMsgListener listener);
 
-    void tell(ServerAddress serverAddress, ToDeviceActorMsg toForward);
+    void broadcast(RpcBroadcastMsg msg);
 
-    void tell(ServerAddress serverAddress, ToDeviceSessionActorMsg toForward);
+    void onSessionCreated(UUID msgUid, StreamObserver<ClusterAPIProtos.ClusterMessage> inputStream);
 
-    void tell(ServerAddress serverAddress, ToDeviceActorNotificationMsg toForward);
+    void tell(ClusterAPIProtos.ClusterMessage message);
 
-    void tell(ServerAddress serverAddress, ToDeviceRpcRequestPluginMsg toForward);
+    void tell(ServerAddress serverAddress, TbActorMsg actorMsg);
 
-    void tell(ServerAddress serverAddress, ToPluginRpcResponseDeviceMsg toForward);
-
-    void tell(PluginRpcMsg toForward);
-
-    void broadcast(ToAllNodesMsg msg);
-
-    void onSessionCreated(UUID msgUid, StreamObserver<ClusterAPIProtos.ToRpcServerMessage> inputStream);
+    void tell(ServerAddress serverAddress, ClusterAPIProtos.MessageType msgType, byte[] data);
 }
diff --git a/application/src/main/java/org/thingsboard/server/service/cluster/rpc/GrpcSession.java b/application/src/main/java/org/thingsboard/server/service/cluster/rpc/GrpcSession.java
index c403895..7216c43 100644
--- a/application/src/main/java/org/thingsboard/server/service/cluster/rpc/GrpcSession.java
+++ b/application/src/main/java/org/thingsboard/server/service/cluster/rpc/GrpcSession.java
@@ -33,8 +33,8 @@ public final class GrpcSession implements Closeable {
     private final UUID sessionId;
     private final boolean client;
     private final GrpcSessionListener listener;
-    private StreamObserver<ClusterAPIProtos.ToRpcServerMessage> inputStream;
-    private StreamObserver<ClusterAPIProtos.ToRpcServerMessage> outputStream;
+    private StreamObserver<ClusterAPIProtos.ClusterMessage> inputStream;
+    private StreamObserver<ClusterAPIProtos.ClusterMessage> outputStream;
 
     private boolean connected;
     private ServerAddress remoteServer;
@@ -56,17 +56,17 @@ public final class GrpcSession implements Closeable {
     }
 
     public void initInputStream() {
-        this.inputStream = new StreamObserver<ClusterAPIProtos.ToRpcServerMessage>() {
+        this.inputStream = new StreamObserver<ClusterAPIProtos.ClusterMessage>() {
             @Override
-            public void onNext(ClusterAPIProtos.ToRpcServerMessage msg) {
-                if (!connected && msg.hasConnectMsg()) {
+            public void onNext(ClusterAPIProtos.ClusterMessage clusterMessage) {
+                if (!connected && clusterMessage.getMessageType() == ClusterAPIProtos.MessageType.CONNECT_RPC_MESSAGE) {
                     connected = true;
-                    ClusterAPIProtos.ServerAddress rpcAddress = msg.getConnectMsg().getServerAddress();
+                    ServerAddress rpcAddress = new ServerAddress(clusterMessage.getServerAddress().getHost(), clusterMessage.getServerAddress().getPort());
                     remoteServer = new ServerAddress(rpcAddress.getHost(), rpcAddress.getPort());
                     listener.onConnected(GrpcSession.this);
                 }
                 if (connected) {
-                    handleToRpcServerMessage(msg);
+                    listener.onReceiveClusterGrpcMsg(GrpcSession.this, clusterMessage);
                 }
             }
 
@@ -83,37 +83,13 @@ public final class GrpcSession implements Closeable {
         };
     }
 
-    private void handleToRpcServerMessage(ClusterAPIProtos.ToRpcServerMessage msg) {
-        if (msg.hasToPluginRpcMsg()) {
-            listener.onToPluginRpcMsg(GrpcSession.this, msg.getToPluginRpcMsg());
-        }
-        if (msg.hasToDeviceActorRpcMsg()) {
-            listener.onToDeviceActorRpcMsg(GrpcSession.this, msg.getToDeviceActorRpcMsg());
-        }
-        if (msg.hasToDeviceSessionActorRpcMsg()) {
-            listener.onToDeviceSessionActorRpcMsg(GrpcSession.this, msg.getToDeviceSessionActorRpcMsg());
-        }
-        if (msg.hasToDeviceActorNotificationRpcMsg()) {
-            listener.onToDeviceActorNotificationRpcMsg(GrpcSession.this, msg.getToDeviceActorNotificationRpcMsg());
-        }
-        if (msg.hasToDeviceRpcRequestRpcMsg()) {
-            listener.onToDeviceRpcRequestRpcMsg(GrpcSession.this, msg.getToDeviceRpcRequestRpcMsg());
-        }
-        if (msg.hasToPluginRpcResponseRpcMsg()) {
-            listener.onFromDeviceRpcResponseRpcMsg(GrpcSession.this, msg.getToPluginRpcResponseRpcMsg());
-        }
-        if (msg.hasToAllNodesRpcMsg()) {
-            listener.onToAllNodesRpcMessage(GrpcSession.this, msg.getToAllNodesRpcMsg());
-        }
-    }
-
     public void initOutputStream() {
         if (client) {
             listener.onConnected(GrpcSession.this);
         }
     }
 
-    public void sendMsg(ClusterAPIProtos.ToRpcServerMessage msg) {
+    public void sendMsg(ClusterAPIProtos.ClusterMessage msg) {
         outputStream.onNext(msg);
     }
 
diff --git a/application/src/main/java/org/thingsboard/server/service/cluster/rpc/GrpcSessionListener.java b/application/src/main/java/org/thingsboard/server/service/cluster/rpc/GrpcSessionListener.java
index 44e0693..266b1f5 100644
--- a/application/src/main/java/org/thingsboard/server/service/cluster/rpc/GrpcSessionListener.java
+++ b/application/src/main/java/org/thingsboard/server/service/cluster/rpc/GrpcSessionListener.java
@@ -26,20 +26,7 @@ public interface GrpcSessionListener {
 
     void onDisconnected(GrpcSession session);
 
-    void onToPluginRpcMsg(GrpcSession session, ClusterAPIProtos.ToPluginRpcMessage msg);
-
-    void onToDeviceActorRpcMsg(GrpcSession session, ClusterAPIProtos.ToDeviceActorRpcMessage msg);
-
-    void onToDeviceActorNotificationRpcMsg(GrpcSession grpcSession, ClusterAPIProtos.ToDeviceActorNotificationRpcMessage msg);
-
-    void onToDeviceSessionActorRpcMsg(GrpcSession session, ClusterAPIProtos.ToDeviceSessionActorRpcMessage msg);
-
-    void onToAllNodesRpcMessage(GrpcSession grpcSession, ClusterAPIProtos.ToAllNodesRpcMessage toAllNodesRpcMessage);
-
-    void onToDeviceRpcRequestRpcMsg(GrpcSession grpcSession, ClusterAPIProtos.ToDeviceRpcRequestRpcMessage toDeviceRpcRequestRpcMsg);
-
-    void onFromDeviceRpcResponseRpcMsg(GrpcSession grpcSession, ClusterAPIProtos.ToPluginRpcResponseRpcMessage toPluginRpcResponseRpcMsg);
+    void onReceiveClusterGrpcMsg(GrpcSession session, ClusterAPIProtos.ClusterMessage clusterMessage);
 
     void onError(GrpcSession session, Throwable t);
-
 }
diff --git a/application/src/main/java/org/thingsboard/server/service/cluster/rpc/RpcMsgListener.java b/application/src/main/java/org/thingsboard/server/service/cluster/rpc/RpcMsgListener.java
index a5c3151..33f3847 100644
--- a/application/src/main/java/org/thingsboard/server/service/cluster/rpc/RpcMsgListener.java
+++ b/application/src/main/java/org/thingsboard/server/service/cluster/rpc/RpcMsgListener.java
@@ -17,34 +17,16 @@ package org.thingsboard.server.service.cluster.rpc;
 
 import org.thingsboard.server.actors.rpc.RpcBroadcastMsg;
 import org.thingsboard.server.actors.rpc.RpcSessionCreateRequestMsg;
-import org.thingsboard.server.actors.rpc.RpcSessionTellMsg;
-import org.thingsboard.server.common.msg.cluster.ToAllNodesMsg;
-import org.thingsboard.server.common.msg.core.ToDeviceSessionActorMsg;
-import org.thingsboard.server.common.msg.device.ToDeviceActorMsg;
-import org.thingsboard.server.extensions.api.device.ToDeviceActorNotificationMsg;
-import org.thingsboard.server.extensions.api.plugins.msg.ToPluginActorMsg;
-import org.thingsboard.server.extensions.api.plugins.rpc.PluginRpcMsg;
+import org.thingsboard.server.common.msg.cluster.ServerAddress;
 import org.thingsboard.server.gen.cluster.ClusterAPIProtos;
 
 /**
  * @author Andrew Shvayka
  */
-public interface RpcMsgListener {
-
-    void onMsg(ToDeviceActorMsg msg);
-
-    void onMsg(ToDeviceActorNotificationMsg msg);
-
-    void onMsg(ToDeviceSessionActorMsg msg);
-
-    void onMsg(ToAllNodesMsg nodeMsg);
-
-    void onMsg(ToPluginActorMsg msg);
-
-    void onMsg(RpcSessionCreateRequestMsg msg);
-
-    void onMsg(RpcSessionTellMsg rpcSessionTellMsg);
-
-    void onMsg(RpcBroadcastMsg rpcBroadcastMsg);
 
+public interface RpcMsgListener {
+    void onReceivedMsg(ServerAddress remoteServer, ClusterAPIProtos.ClusterMessage msg);
+    void onSendMsg(ClusterAPIProtos.ClusterMessage msg);
+    void onRpcSessionCreateRequestMsg(RpcSessionCreateRequestMsg msg);
+    void onBroadcastMsg(RpcBroadcastMsg msg);
 }
diff --git a/application/src/main/java/org/thingsboard/server/service/component/AnnotationComponentDiscoveryService.java b/application/src/main/java/org/thingsboard/server/service/component/AnnotationComponentDiscoveryService.java
index 0a6081d..f3ceed2 100644
--- a/application/src/main/java/org/thingsboard/server/service/component/AnnotationComponentDiscoveryService.java
+++ b/application/src/main/java/org/thingsboard/server/service/component/AnnotationComponentDiscoveryService.java
@@ -15,9 +15,9 @@
  */
 package org.thingsboard.server.service.component;
 
+import com.fasterxml.jackson.databind.JsonNode;
 import com.fasterxml.jackson.databind.ObjectMapper;
-import com.google.common.base.Charsets;
-import com.google.common.io.Resources;
+import com.fasterxml.jackson.databind.node.ObjectNode;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.beans.factory.annotation.Value;
@@ -26,15 +26,27 @@ import org.springframework.context.annotation.ClassPathScanningCandidateComponen
 import org.springframework.core.env.Environment;
 import org.springframework.core.type.filter.AnnotationTypeFilter;
 import org.springframework.stereotype.Service;
+import org.thingsboard.rule.engine.api.NodeConfiguration;
+import org.thingsboard.rule.engine.api.NodeDefinition;
+import org.thingsboard.rule.engine.api.RuleNode;
+import org.thingsboard.rule.engine.api.TbRelationTypes;
+import org.thingsboard.server.common.data.DataConstants;
 import org.thingsboard.server.common.data.plugin.ComponentDescriptor;
 import org.thingsboard.server.common.data.plugin.ComponentType;
 import org.thingsboard.server.dao.component.ComponentDescriptorService;
-import org.thingsboard.server.extensions.api.component.*;
 
 import javax.annotation.PostConstruct;
 import java.lang.annotation.Annotation;
-import java.util.*;
-import java.util.stream.Collectors;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
 
 @Service
 @Slf4j
@@ -66,6 +78,24 @@ public class AnnotationComponentDiscoveryService implements ComponentDiscoverySe
         }
     }
 
+    private void registerRuleNodeComponents() {
+        Set<BeanDefinition> ruleNodeBeanDefinitions = getBeanDefinitions(RuleNode.class);
+        for (BeanDefinition def : ruleNodeBeanDefinitions) {
+            try {
+                String clazzName = def.getBeanClassName();
+                Class<?> clazz = Class.forName(clazzName);
+                RuleNode ruleNodeAnnotation = clazz.getAnnotation(RuleNode.class);
+                ComponentType type = ruleNodeAnnotation.type();
+                ComponentDescriptor component = scanAndPersistComponent(def, type);
+                components.put(component.getClazz(), component);
+                componentsMap.computeIfAbsent(type, k -> new ArrayList<>()).add(component);
+            } catch (Exception e) {
+                log.error("Can't initialize component {}, due to {}", def.getBeanClassName(), e.getMessage(), e);
+                throw new RuntimeException(e);
+            }
+        }
+    }
+
     private void registerComponents(ComponentType type, Class<? extends Annotation> annotation) {
         List<ComponentDescriptor> components = persist(getBeanDefinitions(annotation), type);
         componentsMap.put(type, components);
@@ -79,8 +109,7 @@ public class AnnotationComponentDiscoveryService implements ComponentDiscoverySe
     private List<ComponentDescriptor> persist(Set<BeanDefinition> filterDefs, ComponentType type) {
         List<ComponentDescriptor> result = new ArrayList<>();
         for (BeanDefinition def : filterDefs) {
-            ComponentDescriptor scannedComponent = scanAndPersistComponent(def, type);
-            result.add(scannedComponent);
+            result.add(scanAndPersistComponent(def, type));
         }
         return result;
     }
@@ -91,49 +120,24 @@ public class AnnotationComponentDiscoveryService implements ComponentDiscoverySe
         try {
             scannedComponent.setType(type);
             Class<?> clazz = Class.forName(clazzName);
-            String descriptorResourceName;
             switch (type) {
+                case ENRICHMENT:
                 case FILTER:
-                    Filter filterAnnotation = clazz.getAnnotation(Filter.class);
-                    scannedComponent.setName(filterAnnotation.name());
-                    scannedComponent.setScope(filterAnnotation.scope());
-                    descriptorResourceName = filterAnnotation.descriptor();
-                    break;
-                case PROCESSOR:
-                    Processor processorAnnotation = clazz.getAnnotation(Processor.class);
-                    scannedComponent.setName(processorAnnotation.name());
-                    scannedComponent.setScope(processorAnnotation.scope());
-                    descriptorResourceName = processorAnnotation.descriptor();
-                    break;
+                case TRANSFORMATION:
                 case ACTION:
-                    Action actionAnnotation = clazz.getAnnotation(Action.class);
-                    scannedComponent.setName(actionAnnotation.name());
-                    scannedComponent.setScope(actionAnnotation.scope());
-                    descriptorResourceName = actionAnnotation.descriptor();
-                    break;
-                case PLUGIN:
-                    Plugin pluginAnnotation = clazz.getAnnotation(Plugin.class);
-                    scannedComponent.setName(pluginAnnotation.name());
-                    scannedComponent.setScope(pluginAnnotation.scope());
-                    descriptorResourceName = pluginAnnotation.descriptor();
-                    for (Class<?> actionClazz : pluginAnnotation.actions()) {
-                        ComponentDescriptor actionComponent = getComponent(actionClazz.getName())
-                                .orElseThrow(() -> {
-                                    log.error("Can't initialize plugin {}, due to missing action {}!", def.getBeanClassName(), actionClazz.getName());
-                                    return new ClassNotFoundException("Action: " + actionClazz.getName() + "is missing!");
-                                });
-                        if (actionComponent.getType() != ComponentType.ACTION) {
-                            log.error("Plugin {} action {} has wrong component type!", def.getBeanClassName(), actionClazz.getName(), actionComponent.getType());
-                            throw new RuntimeException("Plugin " + def.getBeanClassName() + "action " + actionClazz.getName() + " has wrong component type!");
-                        }
-                    }
-                    scannedComponent.setActions(Arrays.stream(pluginAnnotation.actions()).map(action -> action.getName()).collect(Collectors.joining(",")));
+                case EXTERNAL:
+                    RuleNode ruleNodeAnnotation = clazz.getAnnotation(RuleNode.class);
+                    scannedComponent.setName(ruleNodeAnnotation.name());
+                    scannedComponent.setScope(ruleNodeAnnotation.scope());
+                    NodeDefinition nodeDefinition = prepareNodeDefinition(ruleNodeAnnotation);
+                    ObjectNode configurationDescriptor = mapper.createObjectNode();
+                    JsonNode node = mapper.valueToTree(nodeDefinition);
+                    configurationDescriptor.set("nodeDefinition", node);
+                    scannedComponent.setConfigurationDescriptor(configurationDescriptor);
                     break;
                 default:
                     throw new RuntimeException(type + " is not supported yet!");
             }
-            scannedComponent.setConfigurationDescriptor(mapper.readTree(
-                    Resources.toString(Resources.getResource(descriptorResourceName), Charsets.UTF_8)));
             scannedComponent.setClazz(clazzName);
             log.info("Processing scanned component: {}", scannedComponent);
         } catch (Exception e) {
@@ -156,6 +160,34 @@ public class AnnotationComponentDiscoveryService implements ComponentDiscoverySe
         return scannedComponent;
     }
 
+    private NodeDefinition prepareNodeDefinition(RuleNode nodeAnnotation) throws Exception {
+        NodeDefinition nodeDefinition = new NodeDefinition();
+        nodeDefinition.setDetails(nodeAnnotation.nodeDetails());
+        nodeDefinition.setDescription(nodeAnnotation.nodeDescription());
+        nodeDefinition.setInEnabled(nodeAnnotation.inEnabled());
+        nodeDefinition.setOutEnabled(nodeAnnotation.outEnabled());
+        nodeDefinition.setRelationTypes(getRelationTypesWithFailureRelation(nodeAnnotation));
+        nodeDefinition.setCustomRelations(nodeAnnotation.customRelations());
+        Class<? extends NodeConfiguration> configClazz = nodeAnnotation.configClazz();
+        NodeConfiguration config = configClazz.newInstance();
+        NodeConfiguration defaultConfiguration = config.defaultConfiguration();
+        nodeDefinition.setDefaultConfiguration(mapper.valueToTree(defaultConfiguration));
+        nodeDefinition.setUiResources(nodeAnnotation.uiResources());
+        nodeDefinition.setConfigDirective(nodeAnnotation.configDirective());
+        nodeDefinition.setIcon(nodeAnnotation.icon());
+        nodeDefinition.setIconUrl(nodeAnnotation.iconUrl());
+        nodeDefinition.setDocUrl(nodeAnnotation.docUrl());
+        return nodeDefinition;
+    }
+
+    private String[] getRelationTypesWithFailureRelation(RuleNode nodeAnnotation) {
+        List<String> relationTypes = new ArrayList<>(Arrays.asList(nodeAnnotation.relationTypes()));
+        if (!relationTypes.contains(TbRelationTypes.FAILURE)) {
+            relationTypes.add(TbRelationTypes.FAILURE);
+        }
+        return relationTypes.toArray(new String[relationTypes.size()]);
+    }
+
     private Set<BeanDefinition> getBeanDefinitions(Class<? extends Annotation> componentType) {
         ClassPathScanningCandidateComponentProvider scanner = new ClassPathScanningCandidateComponentProvider(false);
         scanner.addIncludeFilter(new AnnotationTypeFilter(componentType));
@@ -168,42 +200,30 @@ public class AnnotationComponentDiscoveryService implements ComponentDiscoverySe
 
     @Override
     public void discoverComponents() {
-        registerComponents(ComponentType.FILTER, Filter.class);
-
-        registerComponents(ComponentType.PROCESSOR, Processor.class);
-
-        registerComponents(ComponentType.ACTION, Action.class);
-
-        registerComponents(ComponentType.PLUGIN, Plugin.class);
-
+        registerRuleNodeComponents();
         log.info("Found following definitions: {}", components.values());
     }
 
     @Override
     public List<ComponentDescriptor> getComponents(ComponentType type) {
-        return Collections.unmodifiableList(componentsMap.get(type));
+        if (componentsMap.containsKey(type)) {
+            return Collections.unmodifiableList(componentsMap.get(type));
+        } else {
+            return Collections.emptyList();
+        }
     }
 
     @Override
-    public Optional<ComponentDescriptor> getComponent(String clazz) {
-        return Optional.ofNullable(components.get(clazz));
+    public List<ComponentDescriptor> getComponents(Set<ComponentType> types) {
+        List<ComponentDescriptor> result = new ArrayList<>();
+        types.stream().filter(type -> componentsMap.containsKey(type)).forEach(type -> {
+            result.addAll(componentsMap.get(type));
+        });
+        return Collections.unmodifiableList(result);
     }
 
     @Override
-    public List<ComponentDescriptor> getPluginActions(String pluginClazz) {
-        Optional<ComponentDescriptor> pluginOpt = getComponent(pluginClazz);
-        if (pluginOpt.isPresent()) {
-            ComponentDescriptor plugin = pluginOpt.get();
-            if (ComponentType.PLUGIN != plugin.getType()) {
-                throw new IllegalArgumentException(pluginClazz + " is not a plugin!");
-            }
-            List<ComponentDescriptor> result = new ArrayList<>();
-            for (String action : plugin.getActions().split(",")) {
-                getComponent(action).ifPresent(v -> result.add(v));
-            }
-            return result;
-        } else {
-            throw new IllegalArgumentException(pluginClazz + " is not a component!");
-        }
+    public Optional<ComponentDescriptor> getComponent(String clazz) {
+        return Optional.ofNullable(components.get(clazz));
     }
 }
diff --git a/application/src/main/java/org/thingsboard/server/service/component/ComponentDiscoveryService.java b/application/src/main/java/org/thingsboard/server/service/component/ComponentDiscoveryService.java
index ea27e60..3ee3251 100644
--- a/application/src/main/java/org/thingsboard/server/service/component/ComponentDiscoveryService.java
+++ b/application/src/main/java/org/thingsboard/server/service/component/ComponentDiscoveryService.java
@@ -20,6 +20,7 @@ import org.thingsboard.server.common.data.plugin.ComponentType;
 
 import java.util.List;
 import java.util.Optional;
+import java.util.Set;
 
 /**
  * @author Andrew Shvayka
@@ -30,8 +31,8 @@ public interface ComponentDiscoveryService {
 
     List<ComponentDescriptor> getComponents(ComponentType type);
 
-    Optional<ComponentDescriptor> getComponent(String clazz);
+    List<ComponentDescriptor> getComponents(Set<ComponentType> types);
 
-    List<ComponentDescriptor> getPluginActions(String pluginClazz);
+    Optional<ComponentDescriptor> getComponent(String clazz);
 
 }
diff --git a/application/src/main/java/org/thingsboard/server/service/encoding/ProtoWithJavaSerializationDecodingEncodingService.java b/application/src/main/java/org/thingsboard/server/service/encoding/ProtoWithJavaSerializationDecodingEncodingService.java
new file mode 100644
index 0000000..2cf9299
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/encoding/ProtoWithJavaSerializationDecodingEncodingService.java
@@ -0,0 +1,67 @@
+/**
+ * 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.server.service.encoding;
+
+import com.google.protobuf.ByteString;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+import org.springframework.util.SerializationUtils;
+import org.thingsboard.server.common.msg.TbActorMsg;
+import org.thingsboard.server.common.msg.cluster.ServerAddress;
+import org.thingsboard.server.gen.cluster.ClusterAPIProtos;
+
+import java.util.Optional;
+
+import static org.thingsboard.server.gen.cluster.ClusterAPIProtos.MessageType.CLUSTER_ACTOR_MESSAGE;
+
+
+@Slf4j
+@Service
+public class ProtoWithJavaSerializationDecodingEncodingService implements DataDecodingEncodingService {
+
+
+    @Override
+    public Optional<TbActorMsg> decode(byte[] byteArray) {
+        try {
+            TbActorMsg msg = (TbActorMsg) SerializationUtils.deserialize(byteArray);
+            return Optional.of(msg);
+
+        } catch (IllegalArgumentException e) {
+            log.error("Error during deserialization message, [{}]", e.getMessage());
+           return Optional.empty();
+        }
+    }
+
+    @Override
+    public byte[] encode(TbActorMsg msq) {
+        return SerializationUtils.serialize(msq);
+    }
+
+    @Override
+    public ClusterAPIProtos.ClusterMessage convertToProtoDataMessage(ServerAddress serverAddress,
+                                                                     TbActorMsg msg) {
+        return ClusterAPIProtos.ClusterMessage
+                .newBuilder()
+                .setServerAddress(ClusterAPIProtos.ServerAddress
+                        .newBuilder()
+                        .setHost(serverAddress.getHost())
+                        .setPort(serverAddress.getPort())
+                        .build())
+                .setMessageType(CLUSTER_ACTOR_MESSAGE)
+                .setPayload(ByteString.copyFrom(encode(msg))).build();
+
+    }
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/executors/AbstractListeningExecutor.java b/application/src/main/java/org/thingsboard/server/service/executors/AbstractListeningExecutor.java
new file mode 100644
index 0000000..210d9da
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/executors/AbstractListeningExecutor.java
@@ -0,0 +1,63 @@
+/**
+ * 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.server.service.executors;
+
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.ListeningExecutorService;
+import com.google.common.util.concurrent.MoreExecutors;
+import org.thingsboard.rule.engine.api.ListeningExecutor;
+
+import javax.annotation.PostConstruct;
+import javax.annotation.PreDestroy;
+import java.util.concurrent.Callable;
+import java.util.concurrent.Executors;
+
+/**
+ * Created by igor on 4/13/18.
+ */
+public abstract class AbstractListeningExecutor implements ListeningExecutor {
+
+    private ListeningExecutorService service;
+
+    @PostConstruct
+    public void init() {
+        this.service = MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(getThreadPollSize()));
+    }
+
+    @PreDestroy
+    public void destroy() {
+        if (this.service != null) {
+            this.service.shutdown();
+        }
+    }
+
+    @Override
+    public <T> ListenableFuture<T> executeAsync(Callable<T> task) {
+        return service.submit(task);
+    }
+
+    @Override
+    public void execute(Runnable command) {
+        service.execute(command);
+    }
+
+    public ListeningExecutorService executor() {
+        return service;
+    }
+
+    protected abstract int getThreadPollSize();
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/install/CassandraDatabaseSchemaService.java b/application/src/main/java/org/thingsboard/server/service/install/CassandraDatabaseSchemaService.java
index 5bdc0a7..dd76b21 100644
--- a/application/src/main/java/org/thingsboard/server/service/install/CassandraDatabaseSchemaService.java
+++ b/application/src/main/java/org/thingsboard/server/service/install/CassandraDatabaseSchemaService.java
@@ -17,7 +17,6 @@ package org.thingsboard.server.service.install;
 
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.beans.factory.annotation.Value;
 import org.springframework.context.annotation.Profile;
 import org.springframework.stereotype.Service;
 import org.thingsboard.server.dao.cassandra.CassandraInstallCluster;
@@ -37,17 +36,16 @@ public class CassandraDatabaseSchemaService implements DatabaseSchemaService {
     private static final String CASSANDRA_DIR = "cassandra";
     private static final String SCHEMA_CQL = "schema.cql";
 
-    @Value("${install.data_dir}")
-    private String dataDir;
-
     @Autowired
     private CassandraInstallCluster cluster;
 
+    @Autowired
+    private InstallScripts installScripts;
+
     @Override
     public void createDatabaseSchema() throws Exception {
         log.info("Installing Cassandra DataBase schema...");
-
-        Path schemaFile = Paths.get(this.dataDir, CASSANDRA_DIR, SCHEMA_CQL);
+        Path schemaFile = Paths.get(installScripts.getDataDir(), CASSANDRA_DIR, SCHEMA_CQL);
         loadCql(schemaFile);
 
     }
diff --git a/application/src/main/java/org/thingsboard/server/service/install/CassandraDatabaseUpgradeService.java b/application/src/main/java/org/thingsboard/server/service/install/CassandraDatabaseUpgradeService.java
index e6826ec..4d2adea 100644
--- a/application/src/main/java/org/thingsboard/server/service/install/CassandraDatabaseUpgradeService.java
+++ b/application/src/main/java/org/thingsboard/server/service/install/CassandraDatabaseUpgradeService.java
@@ -18,7 +18,6 @@ package org.thingsboard.server.service.install;
 import com.datastax.driver.core.KeyspaceMetadata;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.beans.factory.annotation.Value;
 import org.springframework.context.annotation.Profile;
 import org.springframework.stereotype.Service;
 import org.thingsboard.server.dao.cassandra.CassandraCluster;
@@ -33,7 +32,17 @@ import java.nio.file.Path;
 import java.nio.file.Paths;
 import java.util.List;
 
-import static org.thingsboard.server.service.install.DatabaseHelper.*;
+import static org.thingsboard.server.service.install.DatabaseHelper.ADDITIONAL_INFO;
+import static org.thingsboard.server.service.install.DatabaseHelper.ASSET;
+import static org.thingsboard.server.service.install.DatabaseHelper.ASSIGNED_CUSTOMERS;
+import static org.thingsboard.server.service.install.DatabaseHelper.CONFIGURATION;
+import static org.thingsboard.server.service.install.DatabaseHelper.CUSTOMER_ID;
+import static org.thingsboard.server.service.install.DatabaseHelper.DASHBOARD;
+import static org.thingsboard.server.service.install.DatabaseHelper.DEVICE;
+import static org.thingsboard.server.service.install.DatabaseHelper.ID;
+import static org.thingsboard.server.service.install.DatabaseHelper.SEARCH_TEXT;
+import static org.thingsboard.server.service.install.DatabaseHelper.TENANT_ID;
+import static org.thingsboard.server.service.install.DatabaseHelper.TITLE;
 
 @Service
 @NoSqlDao
@@ -43,9 +52,6 @@ public class CassandraDatabaseUpgradeService implements DatabaseUpgradeService {
 
     private static final String SCHEMA_UPDATE_CQL = "schema_update.cql";
 
-    @Value("${install.data_dir}")
-    private String dataDir;
-
     @Autowired
     private CassandraCluster cluster;
 
@@ -55,6 +61,9 @@ public class CassandraDatabaseUpgradeService implements DatabaseUpgradeService {
     @Autowired
     private DashboardService dashboardService;
 
+    @Autowired
+    private InstallScripts installScripts;
+
     @Override
     public void upgradeDatabase(String fromVersion) throws Exception {
 
@@ -91,7 +100,7 @@ public class CassandraDatabaseUpgradeService implements DatabaseUpgradeService {
                 log.info("Relations dumped.");
 
                 log.info("Updating schema ...");
-                Path schemaUpdateFile = Paths.get(this.dataDir, "upgrade", "1.3.0", SCHEMA_UPDATE_CQL);
+                Path schemaUpdateFile = Paths.get(installScripts.getDataDir(), "upgrade", "1.3.0", SCHEMA_UPDATE_CQL);
                 loadCql(schemaUpdateFile);
                 log.info("Schema updated.");
 
@@ -173,7 +182,7 @@ public class CassandraDatabaseUpgradeService implements DatabaseUpgradeService {
 
 
                 log.info("Updating schema ...");
-                schemaUpdateFile = Paths.get(this.dataDir, "upgrade", "1.4.0", SCHEMA_UPDATE_CQL);
+                schemaUpdateFile = Paths.get(installScripts.getDataDir(), "upgrade", "1.4.0", SCHEMA_UPDATE_CQL);
                 loadCql(schemaUpdateFile);
                 log.info("Schema updated.");
 
@@ -186,6 +195,14 @@ public class CassandraDatabaseUpgradeService implements DatabaseUpgradeService {
                 }
                 log.info("Dashboards restored.");
                 break;
+            case "1.4.0":
+
+                log.info("Updating schema ...");
+                schemaUpdateFile = Paths.get(installScripts.getDataDir(), "upgrade", "2.0.0", SCHEMA_UPDATE_CQL);
+                loadCql(schemaUpdateFile);
+                log.info("Schema updated.");
+
+                break;
             default:
                 throw new RuntimeException("Unable to upgrade Cassandra database, unsupported fromVersion: " + fromVersion);
         }
diff --git a/application/src/main/java/org/thingsboard/server/service/install/cql/CassandraDbHelper.java b/application/src/main/java/org/thingsboard/server/service/install/cql/CassandraDbHelper.java
index c13d70e..bdf980f 100644
--- a/application/src/main/java/org/thingsboard/server/service/install/cql/CassandraDbHelper.java
+++ b/application/src/main/java/org/thingsboard/server/service/install/cql/CassandraDbHelper.java
@@ -15,7 +15,16 @@
  */
 package org.thingsboard.server.service.install.cql;
 
-import com.datastax.driver.core.*;
+import com.datastax.driver.core.BoundStatement;
+import com.datastax.driver.core.DataType;
+import com.datastax.driver.core.KeyspaceMetadata;
+import com.datastax.driver.core.PreparedStatement;
+import com.datastax.driver.core.ResultSet;
+import com.datastax.driver.core.Row;
+import com.datastax.driver.core.Session;
+import com.datastax.driver.core.SimpleStatement;
+import com.datastax.driver.core.Statement;
+import com.datastax.driver.core.TableMetadata;
 import org.apache.commons.csv.CSVFormat;
 import org.apache.commons.csv.CSVParser;
 import org.apache.commons.csv.CSVPrinter;
@@ -25,7 +34,11 @@ import java.io.IOException;
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.nio.file.StandardCopyOption;
-import java.util.*;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.Iterator;
+import java.util.List;
+import java.util.UUID;
 
 import static org.thingsboard.server.service.install.DatabaseHelper.CSV_DUMP_FORMAT;
 
diff --git a/application/src/main/java/org/thingsboard/server/service/install/DatabaseHelper.java b/application/src/main/java/org/thingsboard/server/service/install/DatabaseHelper.java
index 4a21412..2f9b4a5 100644
--- a/application/src/main/java/org/thingsboard/server/service/install/DatabaseHelper.java
+++ b/application/src/main/java/org/thingsboard/server/service/install/DatabaseHelper.java
@@ -30,7 +30,11 @@ import org.thingsboard.server.dao.dashboard.DashboardService;
 import java.io.IOException;
 import java.nio.file.Files;
 import java.nio.file.Path;
-import java.util.*;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.UUID;
 
 /**
  * Created by igor on 2/27/18.
diff --git a/application/src/main/java/org/thingsboard/server/service/install/DefaultDataUpdateService.java b/application/src/main/java/org/thingsboard/server/service/install/DefaultDataUpdateService.java
new file mode 100644
index 0000000..b372368
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/install/DefaultDataUpdateService.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.server.service.install;
+
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Profile;
+import org.springframework.stereotype.Service;
+import org.thingsboard.server.common.data.Tenant;
+import org.thingsboard.server.common.data.id.IdBased;
+import org.thingsboard.server.common.data.page.TextPageLink;
+import org.thingsboard.server.common.data.rule.RuleChain;
+import org.thingsboard.server.dao.rule.RuleChainService;
+import org.thingsboard.server.dao.tenant.TenantService;
+
+import java.util.List;
+import java.util.UUID;
+
+@Service
+@Profile("install")
+@Slf4j
+public class DefaultDataUpdateService implements DataUpdateService {
+
+    @Autowired
+    private TenantService tenantService;
+
+    @Autowired
+    private RuleChainService ruleChainService;
+
+    @Autowired
+    private InstallScripts installScripts;
+
+    @Override
+    public void updateData(String fromVersion) throws Exception {
+        switch (fromVersion) {
+            case "1.4.0":
+                log.info("Updating data from version 1.4.0 to 2.0.0 ...");
+                tenantsDefaultRuleChainUpdater.updateEntities(null);
+                break;
+            default:
+                throw new RuntimeException("Unable to update data, unsupported fromVersion: " + fromVersion);
+        }
+    }
+
+    private PaginatedUpdater<String, Tenant> tenantsDefaultRuleChainUpdater =
+            new PaginatedUpdater<String, Tenant>() {
+
+                @Override
+                protected List<Tenant> findEntities(String region, TextPageLink pageLink) {
+                    return tenantService.findTenants(pageLink).getData();
+                }
+
+                @Override
+                protected void updateEntity(Tenant tenant) {
+                    try {
+                        RuleChain ruleChain = ruleChainService.getRootTenantRuleChain(tenant.getId());
+                        if (ruleChain == null) {
+                            installScripts.createDefaultRuleChains(tenant.getId());
+                        }
+                    } catch (Exception e) {
+                        log.error("Unable to update Tenant", e);
+                    }
+                }
+            };
+
+    public abstract class PaginatedUpdater<I, D extends IdBased<?>> {
+
+        private static final int DEFAULT_LIMIT = 100;
+
+        public void updateEntities(I id) {
+            TextPageLink pageLink = new TextPageLink(DEFAULT_LIMIT);
+            boolean hasNext = true;
+            while (hasNext) {
+                List<D> entities = findEntities(id, pageLink);
+                for (D entity : entities) {
+                    updateEntity(entity);
+                }
+                hasNext = entities.size() == pageLink.getLimit();
+                if (hasNext) {
+                    int index = entities.size() - 1;
+                    UUID idOffset = entities.get(index).getUuidId();
+                    pageLink.setIdOffset(idOffset);
+                }
+            }
+        }
+
+        protected abstract List<D> findEntities(I id, TextPageLink pageLink);
+
+        protected abstract void updateEntity(D entity);
+
+    }
+
+}
\ No newline at end of file
diff --git a/application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java b/application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java
index 1ef805f..aa8c60b 100644
--- a/application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java
+++ b/application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java
@@ -15,66 +15,45 @@
  */
 package org.thingsboard.server.service.install;
 
-import com.fasterxml.jackson.databind.JsonNode;
 import com.fasterxml.jackson.databind.ObjectMapper;
 import com.fasterxml.jackson.databind.node.ObjectNode;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.beans.factory.annotation.Value;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Profile;
 import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
 import org.springframework.stereotype.Service;
-import org.thingsboard.server.common.data.*;
+import org.thingsboard.server.common.data.AdminSettings;
+import org.thingsboard.server.common.data.Customer;
+import org.thingsboard.server.common.data.Device;
+import org.thingsboard.server.common.data.Tenant;
+import org.thingsboard.server.common.data.User;
 import org.thingsboard.server.common.data.id.CustomerId;
 import org.thingsboard.server.common.data.id.TenantId;
-import org.thingsboard.server.common.data.plugin.ComponentLifecycleState;
-import org.thingsboard.server.common.data.plugin.PluginMetaData;
-import org.thingsboard.server.common.data.rule.RuleMetaData;
 import org.thingsboard.server.common.data.security.Authority;
 import org.thingsboard.server.common.data.security.DeviceCredentials;
 import org.thingsboard.server.common.data.security.UserCredentials;
-import org.thingsboard.server.common.data.widget.WidgetType;
 import org.thingsboard.server.common.data.widget.WidgetsBundle;
 import org.thingsboard.server.dao.customer.CustomerService;
-import org.thingsboard.server.dao.dashboard.DashboardService;
 import org.thingsboard.server.dao.device.DeviceCredentialsService;
 import org.thingsboard.server.dao.device.DeviceService;
 import org.thingsboard.server.dao.model.ModelConstants;
-import org.thingsboard.server.dao.plugin.PluginService;
-import org.thingsboard.server.dao.rule.RuleService;
 import org.thingsboard.server.dao.settings.AdminSettingsService;
 import org.thingsboard.server.dao.tenant.TenantService;
 import org.thingsboard.server.dao.user.UserService;
-import org.thingsboard.server.dao.widget.WidgetTypeService;
 import org.thingsboard.server.dao.widget.WidgetsBundleService;
 
-import java.io.IOException;
-import java.nio.file.DirectoryStream;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.nio.file.Paths;
-
 @Service
 @Profile("install")
 @Slf4j
 public class DefaultSystemDataLoaderService implements SystemDataLoaderService {
 
-    private static final String JSON_DIR = "json";
-    private static final String SYSTEM_DIR = "system";
-    private static final String DEMO_DIR = "demo";
-    private static final String WIDGET_BUNDLES_DIR = "widget_bundles";
-    private static final String PLUGINS_DIR = "plugins";
-    private static final String RULES_DIR = "rules";
-    private static final String DASHBOARDS_DIR = "dashboards";
-
     private static final ObjectMapper objectMapper = new ObjectMapper();
-    public static final String JSON_EXT = ".json";
     public static final String CUSTOMER_CRED = "customer";
     public static final String DEFAULT_DEVICE_TYPE = "default";
 
-    @Value("${install.data_dir}")
-    private String dataDir;
+    @Autowired
+    private InstallScripts installScripts;
 
     @Autowired
     private BCryptPasswordEncoder passwordEncoder;
@@ -89,15 +68,6 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService {
     private WidgetsBundleService widgetsBundleService;
 
     @Autowired
-    private WidgetTypeService widgetTypeService;
-
-    @Autowired
-    private PluginService pluginService;
-
-    @Autowired
-    private RuleService ruleService;
-
-    @Autowired
     private TenantService tenantService;
 
     @Autowired
@@ -109,9 +79,6 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService {
     @Autowired
     private DeviceCredentialsService deviceCredentialsService;
 
-    @Autowired
-    private DashboardService dashboardService;
-
     @Bean
     protected BCryptPasswordEncoder passwordEncoder() {
         return new BCryptPasswordEncoder();
@@ -147,55 +114,12 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService {
     }
 
     @Override
-    public void loadSystemWidgets() throws Exception {
-        Path widgetBundlesDir = Paths.get(dataDir, JSON_DIR, SYSTEM_DIR, WIDGET_BUNDLES_DIR);
-        try (DirectoryStream<Path> dirStream = Files.newDirectoryStream(widgetBundlesDir, path -> path.toString().endsWith(JSON_EXT))) {
-            dirStream.forEach(
-                    path -> {
-                        try {
-                            JsonNode widgetsBundleDescriptorJson = objectMapper.readTree(path.toFile());
-                            JsonNode widgetsBundleJson = widgetsBundleDescriptorJson.get("widgetsBundle");
-                            WidgetsBundle widgetsBundle = objectMapper.treeToValue(widgetsBundleJson, WidgetsBundle.class);
-                            WidgetsBundle savedWidgetsBundle = widgetsBundleService.saveWidgetsBundle(widgetsBundle);
-                            JsonNode widgetTypesArrayJson = widgetsBundleDescriptorJson.get("widgetTypes");
-                            widgetTypesArrayJson.forEach(
-                                    widgetTypeJson -> {
-                                        try {
-                                            WidgetType widgetType = objectMapper.treeToValue(widgetTypeJson, WidgetType.class);
-                                            widgetType.setBundleAlias(savedWidgetsBundle.getAlias());
-                                            widgetTypeService.saveWidgetType(widgetType);
-                                        } catch (Exception e) {
-                                            log.error("Unable to load widget type from json: [{}]", path.toString());
-                                            throw new RuntimeException("Unable to load widget type from json", e);
-                                        }
-                                    }
-                            );
-                        } catch (Exception e) {
-                            log.error("Unable to load widgets bundle from json: [{}]", path.toString());
-                            throw new RuntimeException("Unable to load widgets bundle from json", e);
-                        }
-                    }
-            );
-        }
-    }
-
-    @Override
-    public void loadSystemPlugins() throws Exception {
-        loadPlugins(Paths.get(dataDir, JSON_DIR, SYSTEM_DIR, PLUGINS_DIR), null);
-    }
-
-
-    @Override
-    public void loadSystemRules() throws Exception {
-        loadRules(Paths.get(dataDir, JSON_DIR, SYSTEM_DIR, RULES_DIR), null);
-    }
-
-    @Override
     public void loadDemoData() throws Exception {
         Tenant demoTenant = new Tenant();
         demoTenant.setRegion("Global");
         demoTenant.setTitle("Tenant");
         demoTenant = tenantService.saveTenant(demoTenant);
+        installScripts.createDefaultRuleChains(demoTenant.getId());
         createUser(Authority.TENANT_ADMIN, demoTenant.getId(), null, "tenant@thingsboard.org", "tenant");
 
         Customer customerA = new Customer();
@@ -227,9 +151,7 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService {
         createDevice(demoTenant.getId(), null, DEFAULT_DEVICE_TYPE, "Raspberry Pi Demo Device", "RASPBERRY_PI_DEMO_TOKEN", "Demo device that is used in " +
                 "Raspberry Pi GPIO control sample application");
 
-        loadPlugins(Paths.get(dataDir, JSON_DIR, DEMO_DIR, PLUGINS_DIR), demoTenant.getId());
-        loadRules(Paths.get(dataDir, JSON_DIR, DEMO_DIR, RULES_DIR), demoTenant.getId());
-        loadDashboards(Paths.get(dataDir, JSON_DIR, DEMO_DIR, DASHBOARDS_DIR), demoTenant.getId(), null);
+        installScripts.loadDashboards(demoTenant.getId(), null);
     }
 
     @Override
@@ -240,6 +162,11 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService {
         }
     }
 
+    @Override
+    public void loadSystemWidgets() throws Exception {
+        installScripts.loadSystemWidgets();
+    }
+
     private User createUser(Authority authority,
                             TenantId tenantId,
                             CustomerId customerId,
@@ -282,72 +209,4 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService {
         return device;
     }
 
-    private void loadPlugins(Path pluginsDir, TenantId tenantId) throws Exception{
-        try (DirectoryStream<Path> dirStream = Files.newDirectoryStream(pluginsDir, path -> path.toString().endsWith(JSON_EXT))) {
-            dirStream.forEach(
-                    path -> {
-                        try {
-                            JsonNode pluginJson = objectMapper.readTree(path.toFile());
-                            PluginMetaData plugin = objectMapper.treeToValue(pluginJson, PluginMetaData.class);
-                            plugin.setTenantId(tenantId);
-                            if (plugin.getState() == ComponentLifecycleState.ACTIVE) {
-                                plugin.setState(ComponentLifecycleState.SUSPENDED);
-                                PluginMetaData savedPlugin = pluginService.savePlugin(plugin);
-                                pluginService.activatePluginById(savedPlugin.getId());
-                            } else {
-                                pluginService.savePlugin(plugin);
-                            }
-                        } catch (Exception e) {
-                            log.error("Unable to load plugin from json: [{}]", path.toString());
-                            throw new RuntimeException("Unable to load plugin from json", e);
-                        }
-                    }
-            );
-        }
-    }
-
-    private void loadRules(Path rulesDir, TenantId tenantId) throws Exception {
-        try (DirectoryStream<Path> dirStream = Files.newDirectoryStream(rulesDir, path -> path.toString().endsWith(JSON_EXT))) {
-            dirStream.forEach(
-                    path -> {
-                        try {
-                            JsonNode ruleJson = objectMapper.readTree(path.toFile());
-                            RuleMetaData rule = objectMapper.treeToValue(ruleJson, RuleMetaData.class);
-                            rule.setTenantId(tenantId);
-                            if (rule.getState() == ComponentLifecycleState.ACTIVE) {
-                                rule.setState(ComponentLifecycleState.SUSPENDED);
-                                RuleMetaData savedRule = ruleService.saveRule(rule);
-                                ruleService.activateRuleById(savedRule.getId());
-                            } else {
-                                ruleService.saveRule(rule);
-                            }
-                        } catch (Exception e) {
-                            log.error("Unable to load rule from json: [{}]", path.toString());
-                            throw new RuntimeException("Unable to load rule from json", e);
-                        }
-                    }
-            );
-        }
-    }
-
-    private void loadDashboards(Path dashboardsDir, TenantId tenantId, CustomerId customerId) throws Exception {
-        try (DirectoryStream<Path> dirStream = Files.newDirectoryStream(dashboardsDir, path -> path.toString().endsWith(JSON_EXT))) {
-            dirStream.forEach(
-                    path -> {
-                        try {
-                            JsonNode dashboardJson = objectMapper.readTree(path.toFile());
-                            Dashboard dashboard = objectMapper.treeToValue(dashboardJson, Dashboard.class);
-                            dashboard.setTenantId(tenantId);
-                            Dashboard savedDashboard = dashboardService.saveDashboard(dashboard);
-                            if (customerId != null && !customerId.isNullUid()) {
-                                dashboardService.assignDashboardToCustomer(savedDashboard.getId(), customerId);
-                            }
-                        } catch (Exception e) {
-                            log.error("Unable to load dashboard from json: [{}]", path.toString());
-                            throw new RuntimeException("Unable to load dashboard from json", e);
-                        }
-                    }
-            );
-        }
-    }
 }
diff --git a/application/src/main/java/org/thingsboard/server/service/install/InstallScripts.java b/application/src/main/java/org/thingsboard/server/service/install/InstallScripts.java
new file mode 100644
index 0000000..0ad0ede
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/install/InstallScripts.java
@@ -0,0 +1,184 @@
+/**
+ * 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.server.service.install;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Component;
+import org.springframework.util.StringUtils;
+import org.thingsboard.server.common.data.Dashboard;
+import org.thingsboard.server.common.data.id.CustomerId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.rule.RuleChain;
+import org.thingsboard.server.common.data.rule.RuleChainMetaData;
+import org.thingsboard.server.common.data.widget.WidgetType;
+import org.thingsboard.server.common.data.widget.WidgetsBundle;
+import org.thingsboard.server.dao.dashboard.DashboardService;
+import org.thingsboard.server.dao.rule.RuleChainService;
+import org.thingsboard.server.dao.widget.WidgetTypeService;
+import org.thingsboard.server.dao.widget.WidgetsBundleService;
+
+import java.io.IOException;
+import java.nio.file.DirectoryStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+
+import static org.thingsboard.server.service.install.DatabaseHelper.objectMapper;
+
+/**
+ * Created by ashvayka on 18.04.18.
+ */
+@Component
+@Slf4j
+public class InstallScripts {
+
+    public static final String APP_DIR = "application";
+    public static final String SRC_DIR = "src";
+    public static final String MAIN_DIR = "main";
+    public static final String DATA_DIR = "data";
+    public static final String JSON_DIR = "json";
+    public static final String SYSTEM_DIR = "system";
+    public static final String TENANT_DIR = "tenant";
+    public static final String DEMO_DIR = "demo";
+    public static final String RULE_CHAINS_DIR = "rule_chains";
+    public static final String WIDGET_BUNDLES_DIR = "widget_bundles";
+    public static final String DASHBOARDS_DIR = "dashboards";
+
+    public static final String JSON_EXT = ".json";
+
+    @Value("${install.data_dir:}")
+    private String dataDir;
+
+    @Autowired
+    private RuleChainService ruleChainService;
+
+    @Autowired
+    private DashboardService dashboardService;
+
+    @Autowired
+    private WidgetTypeService widgetTypeService;
+
+    @Autowired
+    private WidgetsBundleService widgetsBundleService;
+
+    public Path getTenantRuleChainsDir() {
+        return Paths.get(getDataDir(), JSON_DIR, TENANT_DIR, RULE_CHAINS_DIR);
+    }
+
+    public String getDataDir() {
+        if (!StringUtils.isEmpty(dataDir)) {
+            if (!Paths.get(this.dataDir).toFile().isDirectory()) {
+                throw new RuntimeException("'install.data_dir' property value is not a valid directory!");
+            }
+            return dataDir;
+        } else {
+            String workDir = System.getProperty("user.dir");
+            if (workDir.endsWith("application")) {
+                return Paths.get(workDir, SRC_DIR, MAIN_DIR, DATA_DIR).toString();
+            } else {
+                Path dataDirPath = Paths.get(workDir, APP_DIR, SRC_DIR, MAIN_DIR, DATA_DIR);
+                if (Files.exists(dataDirPath)) {
+                    return dataDirPath.toString();
+                } else {
+                    throw new RuntimeException("Not valid working directory: " + workDir + ". Please use either root project directory, application module directory or specify valid \"install.data_dir\" ENV variable to avoid automatic data directory lookup!");
+                }
+            }
+        }
+    }
+
+    public void createDefaultRuleChains(TenantId tenantId) throws IOException {
+        Path tenantChainsDir = getTenantRuleChainsDir();
+        try (DirectoryStream<Path> dirStream = Files.newDirectoryStream(tenantChainsDir, path -> path.toString().endsWith(InstallScripts.JSON_EXT))) {
+            dirStream.forEach(
+                    path -> {
+                        try {
+                            JsonNode ruleChainJson = objectMapper.readTree(path.toFile());
+                            RuleChain ruleChain = objectMapper.treeToValue(ruleChainJson.get("ruleChain"), RuleChain.class);
+                            RuleChainMetaData ruleChainMetaData = objectMapper.treeToValue(ruleChainJson.get("metadata"), RuleChainMetaData.class);
+
+                            ruleChain.setTenantId(tenantId);
+                            ruleChain = ruleChainService.saveRuleChain(ruleChain);
+
+                            ruleChainMetaData.setRuleChainId(ruleChain.getId());
+                            ruleChainService.saveRuleChainMetaData(ruleChainMetaData);
+                        } catch (Exception e) {
+                            log.error("Unable to load rule chain from json: [{}]", path.toString());
+                            throw new RuntimeException("Unable to load rule chain from json", e);
+                        }
+                    }
+            );
+        }
+    }
+
+    public void loadSystemWidgets() throws Exception {
+        Path widgetBundlesDir = Paths.get(getDataDir(), JSON_DIR, SYSTEM_DIR, WIDGET_BUNDLES_DIR);
+        try (DirectoryStream<Path> dirStream = Files.newDirectoryStream(widgetBundlesDir, path -> path.toString().endsWith(JSON_EXT))) {
+            dirStream.forEach(
+                    path -> {
+                        try {
+                            JsonNode widgetsBundleDescriptorJson = objectMapper.readTree(path.toFile());
+                            JsonNode widgetsBundleJson = widgetsBundleDescriptorJson.get("widgetsBundle");
+                            WidgetsBundle widgetsBundle = objectMapper.treeToValue(widgetsBundleJson, WidgetsBundle.class);
+                            WidgetsBundle savedWidgetsBundle = widgetsBundleService.saveWidgetsBundle(widgetsBundle);
+                            JsonNode widgetTypesArrayJson = widgetsBundleDescriptorJson.get("widgetTypes");
+                            widgetTypesArrayJson.forEach(
+                                    widgetTypeJson -> {
+                                        try {
+                                            WidgetType widgetType = objectMapper.treeToValue(widgetTypeJson, WidgetType.class);
+                                            widgetType.setBundleAlias(savedWidgetsBundle.getAlias());
+                                            widgetTypeService.saveWidgetType(widgetType);
+                                        } catch (Exception e) {
+                                            log.error("Unable to load widget type from json: [{}]", path.toString());
+                                            throw new RuntimeException("Unable to load widget type from json", e);
+                                        }
+                                    }
+                            );
+                        } catch (Exception e) {
+                            log.error("Unable to load widgets bundle from json: [{}]", path.toString());
+                            throw new RuntimeException("Unable to load widgets bundle from json", e);
+                        }
+                    }
+            );
+        }
+    }
+
+    public void loadDashboards(TenantId tenantId, CustomerId customerId) throws Exception {
+        Path dashboardsDir = Paths.get(getDataDir(), JSON_DIR, DEMO_DIR, DASHBOARDS_DIR);
+        try (DirectoryStream<Path> dirStream = Files.newDirectoryStream(dashboardsDir, path -> path.toString().endsWith(JSON_EXT))) {
+            dirStream.forEach(
+                    path -> {
+                        try {
+                            JsonNode dashboardJson = objectMapper.readTree(path.toFile());
+                            Dashboard dashboard = objectMapper.treeToValue(dashboardJson, Dashboard.class);
+                            dashboard.setTenantId(tenantId);
+                            Dashboard savedDashboard = dashboardService.saveDashboard(dashboard);
+                            if (customerId != null && !customerId.isNullUid()) {
+                                dashboardService.assignDashboardToCustomer(savedDashboard.getId(), customerId);
+                            }
+                        } catch (Exception e) {
+                            log.error("Unable to load dashboard from json: [{}]", path.toString());
+                            throw new RuntimeException("Unable to load dashboard from json", e);
+                        }
+                    }
+            );
+        }
+    }
+
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/install/sql/SqlDbHelper.java b/application/src/main/java/org/thingsboard/server/service/install/sql/SqlDbHelper.java
index f6c4749..6e3d354 100644
--- a/application/src/main/java/org/thingsboard/server/service/install/sql/SqlDbHelper.java
+++ b/application/src/main/java/org/thingsboard/server/service/install/sql/SqlDbHelper.java
@@ -23,7 +23,12 @@ import org.apache.commons.csv.CSVRecord;
 
 import java.nio.file.Files;
 import java.nio.file.Path;
-import java.sql.*;
+import java.sql.Connection;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.ResultSetMetaData;
+import java.sql.SQLException;
+import java.sql.Statement;
 import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.List;
diff --git a/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseSchemaService.java b/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseSchemaService.java
index 443ec0c..1daf660 100644
--- a/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseSchemaService.java
+++ b/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseSchemaService.java
@@ -16,6 +16,7 @@
 package org.thingsboard.server.service.install;
 
 import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.beans.factory.annotation.Value;
 import org.springframework.context.annotation.Profile;
 import org.springframework.stereotype.Service;
@@ -27,7 +28,6 @@ import java.nio.file.Path;
 import java.nio.file.Paths;
 import java.sql.Connection;
 import java.sql.DriverManager;
-import java.sql.PreparedStatement;
 
 @Service
 @Profile("install")
@@ -38,9 +38,6 @@ public class SqlDatabaseSchemaService implements DatabaseSchemaService {
     private static final String SQL_DIR = "sql";
     private static final String SCHEMA_SQL = "schema.sql";
 
-    @Value("${install.data_dir}")
-    private String dataDir;
-
     @Value("${spring.datasource.url}")
     private String dbUrl;
 
@@ -50,12 +47,15 @@ public class SqlDatabaseSchemaService implements DatabaseSchemaService {
     @Value("${spring.datasource.password}")
     private String dbPassword;
 
+    @Autowired
+    private InstallScripts installScripts;
+
     @Override
     public void createDatabaseSchema() throws Exception {
 
         log.info("Installing SQL DataBase schema...");
 
-        Path schemaFile = Paths.get(this.dataDir, SQL_DIR, SCHEMA_SQL);
+        Path schemaFile = Paths.get(installScripts.getDataDir(), SQL_DIR, SCHEMA_SQL);
         try (Connection conn = DriverManager.getConnection(dbUrl, dbUserName, dbPassword)) {
             String sql = new String(Files.readAllBytes(schemaFile), Charset.forName("UTF-8"));
             conn.createStatement().execute(sql); //NOSONAR, ignoring because method used to load initial thingsboard database schema
diff --git a/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java b/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java
index cdd3103..29d5c65 100644
--- a/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java
+++ b/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java
@@ -22,7 +22,6 @@ import org.springframework.context.annotation.Profile;
 import org.springframework.stereotype.Service;
 import org.thingsboard.server.dao.dashboard.DashboardService;
 import org.thingsboard.server.dao.util.SqlDao;
-import org.thingsboard.server.service.install.cql.CassandraDbHelper;
 import org.thingsboard.server.service.install.sql.SqlDbHelper;
 
 import java.nio.charset.Charset;
@@ -30,11 +29,16 @@ import java.nio.file.Files;
 import java.nio.file.Path;
 import java.nio.file.Paths;
 import java.sql.Connection;
-import java.sql.DatabaseMetaData;
 import java.sql.DriverManager;
 
-import static org.thingsboard.server.service.install.DatabaseHelper.*;
+import static org.thingsboard.server.service.install.DatabaseHelper.ASSIGNED_CUSTOMERS;
 import static org.thingsboard.server.service.install.DatabaseHelper.CONFIGURATION;
+import static org.thingsboard.server.service.install.DatabaseHelper.CUSTOMER_ID;
+import static org.thingsboard.server.service.install.DatabaseHelper.DASHBOARD;
+import static org.thingsboard.server.service.install.DatabaseHelper.ID;
+import static org.thingsboard.server.service.install.DatabaseHelper.SEARCH_TEXT;
+import static org.thingsboard.server.service.install.DatabaseHelper.TENANT_ID;
+import static org.thingsboard.server.service.install.DatabaseHelper.TITLE;
 
 @Service
 @Profile("install")
@@ -44,9 +48,6 @@ public class SqlDatabaseUpgradeService implements DatabaseUpgradeService {
 
     private static final String SCHEMA_UPDATE_SQL = "schema_update.sql";
 
-    @Value("${install.data_dir}")
-    private String dataDir;
-
     @Value("${spring.datasource.url}")
     private String dbUrl;
 
@@ -59,12 +60,15 @@ public class SqlDatabaseUpgradeService implements DatabaseUpgradeService {
     @Autowired
     private DashboardService dashboardService;
 
+    @Autowired
+    private InstallScripts installScripts;
+
     @Override
     public void upgradeDatabase(String fromVersion) throws Exception {
         switch (fromVersion) {
             case "1.3.0":
                 log.info("Updating schema ...");
-                Path schemaUpdateFile = Paths.get(this.dataDir, "upgrade", "1.3.1", SCHEMA_UPDATE_SQL);
+                Path schemaUpdateFile = Paths.get(installScripts.getDataDir(), "upgrade", "1.3.1", SCHEMA_UPDATE_SQL);
                 try (Connection conn = DriverManager.getConnection(dbUrl, dbUserName, dbPassword)) {
                     String sql = new String(Files.readAllBytes(schemaUpdateFile), Charset.forName("UTF-8"));
                     conn.createStatement().execute(sql); //NOSONAR, ignoring because method used to execute thingsboard database upgrade script
@@ -82,7 +86,7 @@ public class SqlDatabaseUpgradeService implements DatabaseUpgradeService {
                     log.info("Dashboards dumped.");
 
                     log.info("Updating schema ...");
-                    schemaUpdateFile = Paths.get(this.dataDir, "upgrade", "1.4.0", SCHEMA_UPDATE_SQL);
+                    schemaUpdateFile = Paths.get(installScripts.getDataDir(), "upgrade", "1.4.0", SCHEMA_UPDATE_SQL);
                     String sql = new String(Files.readAllBytes(schemaUpdateFile), Charset.forName("UTF-8"));
                     conn.createStatement().execute(sql); //NOSONAR, ignoring because method used to execute thingsboard database upgrade script
                     log.info("Schema updated.");
@@ -97,6 +101,15 @@ public class SqlDatabaseUpgradeService implements DatabaseUpgradeService {
                     log.info("Dashboards restored.");
                 }
                 break;
+            case "1.4.0":
+                try (Connection conn = DriverManager.getConnection(dbUrl, dbUserName, dbPassword)) {
+                    log.info("Updating schema ...");
+                    schemaUpdateFile = Paths.get(installScripts.getDataDir(), "upgrade", "2.0.0", SCHEMA_UPDATE_SQL);
+                    String sql = new String(Files.readAllBytes(schemaUpdateFile), Charset.forName("UTF-8"));
+                    conn.createStatement().execute(sql); //NOSONAR, ignoring because method used to execute thingsboard database upgrade script
+                    log.info("Schema updated.");
+                }
+                break;
             default:
                 throw new RuntimeException("Unable to upgrade SQL database, unsupported fromVersion: " + fromVersion);
         }
diff --git a/application/src/main/java/org/thingsboard/server/service/install/SystemDataLoaderService.java b/application/src/main/java/org/thingsboard/server/service/install/SystemDataLoaderService.java
index a3dcb68..f3a6af4 100644
--- a/application/src/main/java/org/thingsboard/server/service/install/SystemDataLoaderService.java
+++ b/application/src/main/java/org/thingsboard/server/service/install/SystemDataLoaderService.java
@@ -23,10 +23,6 @@ public interface SystemDataLoaderService {
 
     void loadSystemWidgets() throws Exception;
 
-    void loadSystemPlugins() throws Exception;
-
-    void loadSystemRules() throws Exception;
-
     void loadDemoData() throws Exception;
 
     void deleteSystemWidgetBundle(String bundleAlias) throws Exception;
diff --git a/application/src/main/java/org/thingsboard/server/service/mail/DefaultMailService.java b/application/src/main/java/org/thingsboard/server/service/mail/DefaultMailService.java
index 25b911c..818c935 100644
--- a/application/src/main/java/org/thingsboard/server/service/mail/DefaultMailService.java
+++ b/application/src/main/java/org/thingsboard/server/service/mail/DefaultMailService.java
@@ -27,13 +27,15 @@ import org.springframework.mail.javamail.JavaMailSenderImpl;
 import org.springframework.mail.javamail.MimeMessageHelper;
 import org.springframework.stereotype.Service;
 import org.springframework.ui.velocity.VelocityEngineUtils;
+import org.thingsboard.rule.engine.api.MailService;
 import org.thingsboard.server.common.data.AdminSettings;
+import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
+import org.thingsboard.server.common.data.exception.ThingsboardException;
 import org.thingsboard.server.dao.exception.IncorrectParameterException;
 import org.thingsboard.server.dao.settings.AdminSettingsService;
-import org.thingsboard.server.exception.ThingsboardErrorCode;
-import org.thingsboard.server.exception.ThingsboardException;
 
 import javax.annotation.PostConstruct;
+import javax.mail.MessagingException;
 import javax.mail.internet.MimeMessage;
 import java.util.HashMap;
 import java.util.Locale;
@@ -49,18 +51,18 @@ public class DefaultMailService implements MailService {
     public static final String UTF_8 = "UTF-8";
     @Autowired
     private MessageSource messages;
-    
+
     @Autowired
     @Qualifier("velocityEngine")
     private VelocityEngine engine;
-    
+
     private JavaMailSenderImpl mailSender;
-    
+
     private String mailFrom;
-    
+
     @Autowired
-    private AdminSettingsService adminSettingsService; 
-    
+    private AdminSettingsService adminSettingsService;
+
     @PostConstruct
     private void init() {
         updateMailConfiguration();
@@ -77,7 +79,7 @@ public class DefaultMailService implements MailService {
             throw new IncorrectParameterException("Failed to date mail configuration. Settings not found!");
         }
     }
-    
+
     private JavaMailSenderImpl createMailSender(JsonNode jsonConfig) {
         JavaMailSenderImpl mailSender = new JavaMailSenderImpl();
         mailSender.setHost(jsonConfig.get("smtpHost").asText());
@@ -99,7 +101,7 @@ public class DefaultMailService implements MailService {
         javaMailProperties.put(MAIL_PROP + protocol + ".starttls.enable", jsonConfig.has("enableTls") ? jsonConfig.get("enableTls").asText() : "false");
         return javaMailProperties;
     }
-    
+
     private int parsePort(String strPort) {
         try {
             return Integer.valueOf(strPort);
@@ -112,86 +114,102 @@ public class DefaultMailService implements MailService {
     public void sendEmail(String email, String subject, String message) throws ThingsboardException {
         sendMail(mailSender, mailFrom, email, subject, message);
     }
-    
+
     @Override
     public void sendTestMail(JsonNode jsonConfig, String email) throws ThingsboardException {
         JavaMailSenderImpl testMailSender = createMailSender(jsonConfig);
         String mailFrom = jsonConfig.get("mailFrom").asText();
         String subject = messages.getMessage("test.message.subject", null, Locale.US);
-        
+
         Map<String, Object> model = new HashMap<String, Object>();
         model.put(TARGET_EMAIL, email);
-        
+
         String message = VelocityEngineUtils.mergeTemplateIntoString(this.engine,
                 "test.vm", UTF_8, model);
-        
-        sendMail(testMailSender, mailFrom, email, subject, message); 
+
+        sendMail(testMailSender, mailFrom, email, subject, message);
     }
 
     @Override
     public void sendActivationEmail(String activationLink, String email) throws ThingsboardException {
-        
+
         String subject = messages.getMessage("activation.subject", null, Locale.US);
-        
+
         Map<String, Object> model = new HashMap<String, Object>();
         model.put("activationLink", activationLink);
         model.put(TARGET_EMAIL, email);
-        
+
         String message = VelocityEngineUtils.mergeTemplateIntoString(this.engine,
                 "activation.vm", UTF_8, model);
-        
-        sendMail(mailSender, mailFrom, email, subject, message); 
+
+        sendMail(mailSender, mailFrom, email, subject, message);
     }
-    
+
     @Override
     public void sendAccountActivatedEmail(String loginLink, String email) throws ThingsboardException {
-        
+
         String subject = messages.getMessage("account.activated.subject", null, Locale.US);
-        
+
         Map<String, Object> model = new HashMap<String, Object>();
         model.put("loginLink", loginLink);
         model.put(TARGET_EMAIL, email);
-        
+
         String message = VelocityEngineUtils.mergeTemplateIntoString(this.engine,
                 "account.activated.vm", UTF_8, model);
-        
-        sendMail(mailSender, mailFrom, email, subject, message); 
+
+        sendMail(mailSender, mailFrom, email, subject, message);
     }
 
     @Override
     public void sendResetPasswordEmail(String passwordResetLink, String email) throws ThingsboardException {
-        
+
         String subject = messages.getMessage("reset.password.subject", null, Locale.US);
-        
+
         Map<String, Object> model = new HashMap<String, Object>();
         model.put("passwordResetLink", passwordResetLink);
         model.put(TARGET_EMAIL, email);
-        
+
         String message = VelocityEngineUtils.mergeTemplateIntoString(this.engine,
                 "reset.password.vm", UTF_8, model);
-        
-        sendMail(mailSender, mailFrom, email, subject, message); 
+
+        sendMail(mailSender, mailFrom, email, subject, message);
     }
-    
+
     @Override
     public void sendPasswordWasResetEmail(String loginLink, String email) throws ThingsboardException {
-        
+
         String subject = messages.getMessage("password.was.reset.subject", null, Locale.US);
-        
+
         Map<String, Object> model = new HashMap<String, Object>();
         model.put("loginLink", loginLink);
         model.put(TARGET_EMAIL, email);
-        
+
         String message = VelocityEngineUtils.mergeTemplateIntoString(this.engine,
                 "password.was.reset.vm", UTF_8, model);
-        
-        sendMail(mailSender, mailFrom, email, subject, message); 
+
+        sendMail(mailSender, mailFrom, email, subject, message);
     }
 
+    @Override
+    public void send(String from, String to, String cc, String bcc, String subject, String body) throws MessagingException {
+        MimeMessage mailMsg = mailSender.createMimeMessage();
+        MimeMessageHelper helper = new MimeMessageHelper(mailMsg, "UTF-8");
+        helper.setFrom(StringUtils.isBlank(from) ? mailFrom : from);
+        helper.setTo(to.split("\\s*,\\s*"));
+        if (!StringUtils.isBlank(cc)) {
+            helper.setCc(cc.split("\\s*,\\s*"));
+        }
+        if (!StringUtils.isBlank(bcc)) {
+            helper.setBcc(bcc.split("\\s*,\\s*"));
+        }
+        helper.setSubject(subject);
+        helper.setText(body);
+        mailSender.send(helper.getMimeMessage());
+    }
 
-    private void sendMail(JavaMailSenderImpl mailSender, 
-            String mailFrom, String email, 
-            String subject, String message) throws ThingsboardException {
+    private void sendMail(JavaMailSenderImpl mailSender,
+                          String mailFrom, String email,
+                          String subject, String message) throws ThingsboardException {
         try {
             MimeMessage mimeMsg = mailSender.createMimeMessage();
             MimeMessageHelper helper = new MimeMessageHelper(mimeMsg, UTF_8);
@@ -208,7 +226,7 @@ public class DefaultMailService implements MailService {
     protected ThingsboardException handleException(Exception exception) {
         String message;
         if (exception instanceof NestedRuntimeException) {
-            message = ((NestedRuntimeException)exception).getMostSpecificCause().getMessage();
+            message = ((NestedRuntimeException) exception).getMostSpecificCause().getMessage();
         } else {
             message = exception.getMessage();
         }
diff --git a/application/src/main/java/org/thingsboard/server/service/queue/DefaultMsgQueueService.java b/application/src/main/java/org/thingsboard/server/service/queue/DefaultMsgQueueService.java
new file mode 100644
index 0000000..9275847
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/queue/DefaultMsgQueueService.java
@@ -0,0 +1,111 @@
+/**
+ * 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.server.service.queue;
+
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Service;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.msg.TbMsg;
+import org.thingsboard.server.common.transport.quota.tenant.TenantQuotaService;
+import org.thingsboard.server.dao.queue.MsgQueue;
+
+import javax.annotation.PostConstruct;
+import javax.annotation.PreDestroy;
+import java.util.Map;
+import java.util.UUID;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicLong;
+
+@Service
+@Slf4j
+public class DefaultMsgQueueService implements MsgQueueService {
+
+    @Value("${actors.rule.queue.max_size}")
+    private long queueMaxSize;
+
+    @Value("${actors.rule.queue.cleanup_period}")
+    private long queueCleanUpPeriod;
+
+    @Autowired
+    private MsgQueue msgQueue;
+
+    @Autowired
+    private TenantQuotaService quotaService;
+
+    private ScheduledExecutorService cleanupExecutor;
+
+    private Map<TenantId, AtomicLong> pendingCountPerTenant = new ConcurrentHashMap<>();
+
+    @PostConstruct
+    public void init() {
+        if (queueCleanUpPeriod > 0) {
+            cleanupExecutor = Executors.newSingleThreadScheduledExecutor();
+            cleanupExecutor.scheduleAtFixedRate(() -> cleanup(),
+                    queueCleanUpPeriod, queueCleanUpPeriod, TimeUnit.SECONDS);
+        }
+    }
+
+    @PreDestroy
+    public void stop() {
+        if (cleanupExecutor != null) {
+            cleanupExecutor.shutdownNow();
+        }
+    }
+
+    @Override
+    public ListenableFuture<Void> put(TenantId tenantId, TbMsg msg, UUID nodeId, long clusterPartition) {
+        if(quotaService.isQuotaExceeded(tenantId.getId().toString())) {
+            log.warn("Tenant TbMsg Quota exceeded for [{}:{}] . Reject", tenantId.getId());
+            return Futures.immediateFailedFuture(new RuntimeException("Tenant TbMsg Quota exceeded"));
+        }
+
+        AtomicLong pendingMsgCount = pendingCountPerTenant.computeIfAbsent(tenantId, key -> new AtomicLong());
+        if (pendingMsgCount.incrementAndGet() < queueMaxSize) {
+            return msgQueue.put(tenantId, msg, nodeId, clusterPartition);
+        } else {
+            pendingMsgCount.decrementAndGet();
+            return Futures.immediateFailedFuture(new RuntimeException("Message queue is full!"));
+        }
+    }
+
+    @Override
+    public ListenableFuture<Void> ack(TenantId tenantId, TbMsg msg, UUID nodeId, long clusterPartition) {
+        ListenableFuture<Void> result = msgQueue.ack(tenantId, msg, nodeId, clusterPartition);
+        AtomicLong pendingMsgCount = pendingCountPerTenant.computeIfAbsent(tenantId, key -> new AtomicLong());
+        pendingMsgCount.decrementAndGet();
+        return result;
+    }
+
+    @Override
+    public Iterable<TbMsg> findUnprocessed(TenantId tenantId, UUID nodeId, long clusterPartition) {
+        return msgQueue.findUnprocessed(tenantId, nodeId, clusterPartition);
+    }
+
+    private void cleanup() {
+        pendingCountPerTenant.forEach((tenantId, pendingMsgCount) -> {
+            pendingMsgCount.set(0);
+            msgQueue.cleanUp(tenantId);
+        });
+    }
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/rpc/DefaultDeviceRpcService.java b/application/src/main/java/org/thingsboard/server/service/rpc/DefaultDeviceRpcService.java
new file mode 100644
index 0000000..678495b
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/rpc/DefaultDeviceRpcService.java
@@ -0,0 +1,152 @@
+/**
+ * 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.server.service.rpc;
+
+import com.google.protobuf.InvalidProtocolBufferException;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.thingsboard.rule.engine.api.RpcError;
+import org.thingsboard.rule.engine.api.msg.ToDeviceActorNotificationMsg;
+import org.thingsboard.server.actors.service.ActorService;
+import org.thingsboard.server.common.data.id.DeviceId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.msg.cluster.SendToClusterMsg;
+import org.thingsboard.server.common.msg.cluster.ServerAddress;
+import org.thingsboard.server.common.msg.core.ToServerRpcResponseMsg;
+import org.thingsboard.server.common.msg.rpc.ToDeviceRpcRequest;
+import org.thingsboard.server.dao.audit.AuditLogService;
+import org.thingsboard.server.gen.cluster.ClusterAPIProtos;
+import org.thingsboard.server.service.cluster.routing.ClusterRoutingService;
+import org.thingsboard.server.service.cluster.rpc.ClusterRpcService;
+import org.thingsboard.server.service.encoding.DataDecodingEncodingService;
+import org.thingsboard.server.service.telemetry.sub.Subscription;
+
+import javax.annotation.PostConstruct;
+import javax.annotation.PreDestroy;
+import java.util.Optional;
+import java.util.UUID;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+import java.util.function.Consumer;
+
+/**
+ * Created by ashvayka on 27.03.18.
+ */
+@Service
+@Slf4j
+public class DefaultDeviceRpcService implements DeviceRpcService {
+
+    @Autowired
+    private ClusterRoutingService routingService;
+
+    @Autowired
+    private ClusterRpcService rpcService;
+
+    @Autowired
+    private ActorService actorService;
+
+    private ScheduledExecutorService rpcCallBackExecutor;
+
+    private final ConcurrentMap<UUID, Consumer<FromDeviceRpcResponse>> localRpcRequests = new ConcurrentHashMap<>();
+
+    @PostConstruct
+    public void initExecutor() {
+        rpcCallBackExecutor = Executors.newSingleThreadScheduledExecutor();
+    }
+
+    @PreDestroy
+    public void shutdownExecutor() {
+        if (rpcCallBackExecutor != null) {
+            rpcCallBackExecutor.shutdownNow();
+        }
+    }
+
+    @Override
+    public void processRpcRequestToDevice(ToDeviceRpcRequest request, Consumer<FromDeviceRpcResponse> responseConsumer) {
+        log.trace("[{}] Processing local rpc call for device [{}]", request.getTenantId(), request.getDeviceId());
+        sendRpcRequest(request);
+        UUID requestId = request.getId();
+        localRpcRequests.put(requestId, responseConsumer);
+        long timeout = Math.max(0, request.getExpirationTime() - System.currentTimeMillis());
+        log.error("[{}] processing the request: [{}]", this.hashCode(), requestId);
+        rpcCallBackExecutor.schedule(() -> {
+            log.error("[{}] timeout the request: [{}]", this.hashCode(), requestId);
+            Consumer<FromDeviceRpcResponse> consumer = localRpcRequests.remove(requestId);
+            if (consumer != null) {
+                consumer.accept(new FromDeviceRpcResponse(requestId, null, null, RpcError.TIMEOUT));
+            }
+        }, timeout, TimeUnit.MILLISECONDS);
+    }
+
+    @Override
+    public void processRpcResponseFromDevice(FromDeviceRpcResponse response) {
+        log.error("[{}] response to request: [{}]", this.hashCode(), response.getId());
+        if (routingService.getCurrentServer().equals(response.getServerAddress())) {
+            UUID requestId = response.getId();
+            Consumer<FromDeviceRpcResponse> consumer = localRpcRequests.remove(requestId);
+            if (consumer != null) {
+                consumer.accept(response);
+            } else {
+                log.trace("[{}] Unknown or stale rpc response received [{}]", requestId, response);
+            }
+        } else {
+            ClusterAPIProtos.FromDeviceRPCResponseProto.Builder builder = ClusterAPIProtos.FromDeviceRPCResponseProto.newBuilder();
+            builder.setRequestIdMSB(response.getId().getMostSignificantBits());
+            builder.setRequestIdLSB(response.getId().getLeastSignificantBits());
+            response.getResponse().ifPresent(builder::setResponse);
+            if (response.getError().isPresent()) {
+                builder.setError(response.getError().get().ordinal());
+            } else {
+                builder.setError(-1);
+            }
+            rpcService.tell(response.getServerAddress(), ClusterAPIProtos.MessageType.CLUSTER_RPC_FROM_DEVICE_RESPONSE_MESSAGE, builder.build().toByteArray());
+        }
+    }
+
+    @Override
+    public void processRemoteResponseFromDevice(ServerAddress serverAddress, byte[] data) {
+        ClusterAPIProtos.FromDeviceRPCResponseProto proto;
+        try {
+            proto = ClusterAPIProtos.FromDeviceRPCResponseProto.parseFrom(data);
+        } catch (InvalidProtocolBufferException e) {
+            throw new RuntimeException(e);
+        }
+        RpcError error = proto.getError() > 0 ? RpcError.values()[proto.getError()] : null;
+        FromDeviceRpcResponse response = new FromDeviceRpcResponse(new UUID(proto.getRequestIdMSB(), proto.getRequestIdLSB()), routingService.getCurrentServer(),
+                proto.getResponse(), error);
+        processRpcResponseFromDevice(response);
+    }
+
+    @Override
+    public void sendRpcReplyToDevice(TenantId tenantId, DeviceId deviceId, int requestId, String body) {
+        ToServerRpcResponseActorMsg rpcMsg = new ToServerRpcResponseActorMsg(tenantId, deviceId, new ToServerRpcResponseMsg(requestId, body));
+        forward(deviceId, rpcMsg);
+    }
+
+    private void sendRpcRequest(ToDeviceRpcRequest msg) {
+        ToDeviceRpcRequestActorMsg rpcMsg = new ToDeviceRpcRequestActorMsg(routingService.getCurrentServer(), msg);
+        log.trace("[{}] Forwarding msg {} to device actor!", msg.getDeviceId(), msg);
+        forward(msg.getDeviceId(), rpcMsg);
+    }
+
+    private <T extends ToDeviceActorNotificationMsg> void forward(DeviceId deviceId, T msg) {
+        actorService.onMsg(new SendToClusterMsg(deviceId, msg));
+    }
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/script/AbstractNashornJsSandboxService.java b/application/src/main/java/org/thingsboard/server/service/script/AbstractNashornJsSandboxService.java
new file mode 100644
index 0000000..7f274ec
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/script/AbstractNashornJsSandboxService.java
@@ -0,0 +1,159 @@
+/**
+ * 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.server.service.script;
+
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import delight.nashornsandbox.NashornSandbox;
+import delight.nashornsandbox.NashornSandboxes;
+import jdk.nashorn.api.scripting.NashornScriptEngineFactory;
+import lombok.extern.slf4j.Slf4j;
+
+import javax.annotation.PostConstruct;
+import javax.annotation.PreDestroy;
+import javax.script.Invocable;
+import javax.script.ScriptEngine;
+import javax.script.ScriptException;
+import java.util.Map;
+import java.util.UUID;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.atomic.AtomicInteger;
+
+@Slf4j
+public abstract class AbstractNashornJsSandboxService implements JsSandboxService {
+
+    private NashornSandbox sandbox;
+    private ScriptEngine engine;
+    private ExecutorService monitorExecutorService;
+
+    private Map<UUID, String> functionsMap = new ConcurrentHashMap<>();
+
+    private Map<UUID,AtomicInteger> blackListedFunctions = new ConcurrentHashMap<>();
+
+    @PostConstruct
+    public void init() {
+        if (useJsSandbox()) {
+            sandbox = NashornSandboxes.create();
+            monitorExecutorService = Executors.newFixedThreadPool(getMonitorThreadPoolSize());
+            sandbox.setExecutor(monitorExecutorService);
+            sandbox.setMaxCPUTime(getMaxCpuTime());
+            sandbox.allowNoBraces(false);
+            sandbox.setMaxPreparedStatements(30);
+        } else {
+            NashornScriptEngineFactory factory = new NashornScriptEngineFactory();
+            engine = factory.getScriptEngine(new String[]{"--no-java"});
+        }
+    }
+
+    @PreDestroy
+    public void stop() {
+        if  (monitorExecutorService != null) {
+            monitorExecutorService.shutdownNow();
+        }
+    }
+
+    protected abstract boolean useJsSandbox();
+
+    protected abstract int getMonitorThreadPoolSize();
+
+    protected abstract long getMaxCpuTime();
+
+    protected abstract int getMaxErrors();
+
+    @Override
+    public ListenableFuture<UUID> eval(JsScriptType scriptType, String scriptBody, String... argNames) {
+        UUID scriptId = UUID.randomUUID();
+        String functionName = "invokeInternal_" + scriptId.toString().replace('-','_');
+        String jsScript = generateJsScript(scriptType, functionName, scriptBody, argNames);
+        try {
+            if (useJsSandbox()) {
+                sandbox.eval(jsScript);
+            } else {
+                engine.eval(jsScript);
+            }
+            functionsMap.put(scriptId, functionName);
+        } catch (Exception e) {
+            log.warn("Failed to compile JS script: {}", e.getMessage(), e);
+            return Futures.immediateFailedFuture(e);
+        }
+        return Futures.immediateFuture(scriptId);
+    }
+
+    @Override
+    public ListenableFuture<Object> invokeFunction(UUID scriptId, Object... args) {
+        String functionName = functionsMap.get(scriptId);
+        if (functionName == null) {
+            return Futures.immediateFailedFuture(new RuntimeException("No compiled script found for scriptId: [" + scriptId + "]!"));
+        }
+        if (!isBlackListed(scriptId)) {
+            try {
+                Object result;
+                if (useJsSandbox()) {
+                    result = sandbox.getSandboxedInvocable().invokeFunction(functionName, args);
+                } else {
+                    result = ((Invocable)engine).invokeFunction(functionName, args);
+                }
+                return Futures.immediateFuture(result);
+            } catch (Exception e) {
+                blackListedFunctions.computeIfAbsent(scriptId, key -> new AtomicInteger(0)).incrementAndGet();
+                return Futures.immediateFailedFuture(e);
+            }
+        } else {
+            return Futures.immediateFailedFuture(
+                    new RuntimeException("Script is blacklisted due to maximum error count " + getMaxErrors() + "!"));
+        }
+    }
+
+    @Override
+    public ListenableFuture<Void> release(UUID scriptId) {
+        String functionName = functionsMap.get(scriptId);
+        if (functionName != null) {
+            try {
+                if (useJsSandbox()) {
+                    sandbox.eval(functionName + " = undefined;");
+                } else {
+                    engine.eval(functionName + " = undefined;");
+                }
+                functionsMap.remove(scriptId);
+                blackListedFunctions.remove(scriptId);
+            } catch (ScriptException e) {
+                return Futures.immediateFailedFuture(e);
+            }
+        }
+        return Futures.immediateFuture(null);
+    }
+
+    private boolean isBlackListed(UUID scriptId) {
+        if (blackListedFunctions.containsKey(scriptId)) {
+            AtomicInteger errorCount = blackListedFunctions.get(scriptId);
+            return errorCount.get() >= getMaxErrors();
+        } else {
+            return false;
+        }
+    }
+
+    private String generateJsScript(JsScriptType scriptType, String functionName, String scriptBody, String... argNames) {
+        switch (scriptType) {
+            case RULE_NODE_SCRIPT:
+                return RuleNodeScriptFactory.generateRuleNodeScript(functionName, scriptBody, argNames);
+            default:
+                throw new RuntimeException("No script factory implemented for scriptType: " + scriptType);
+        }
+    }
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/script/NashornJsSandboxService.java b/application/src/main/java/org/thingsboard/server/service/script/NashornJsSandboxService.java
new file mode 100644
index 0000000..3e8b4e9
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/script/NashornJsSandboxService.java
@@ -0,0 +1,58 @@
+/**
+ * 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.server.service.script;
+
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Service;
+
+@Slf4j
+@Service
+public class NashornJsSandboxService extends AbstractNashornJsSandboxService {
+
+    @Value("${actors.rule.js_sandbox.use_js_sandbox}")
+    private boolean useJsSandbox;
+
+    @Value("${actors.rule.js_sandbox.monitor_thread_pool_size}")
+    private int monitorThreadPoolSize;
+
+    @Value("${actors.rule.js_sandbox.max_cpu_time}")
+    private long maxCpuTime;
+
+    @Value("${actors.rule.js_sandbox.max_errors}")
+    private int maxErrors;
+
+    @Override
+    protected boolean useJsSandbox() {
+        return useJsSandbox;
+    }
+
+    @Override
+    protected int getMonitorThreadPoolSize() {
+        return monitorThreadPoolSize;
+    }
+
+    @Override
+    protected long getMaxCpuTime() {
+        return maxCpuTime;
+    }
+
+    @Override
+    protected int getMaxErrors() {
+        return maxErrors;
+    }
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/script/RuleNodeJsScriptEngine.java b/application/src/main/java/org/thingsboard/server/service/script/RuleNodeJsScriptEngine.java
new file mode 100644
index 0000000..98b0e77
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/script/RuleNodeJsScriptEngine.java
@@ -0,0 +1,181 @@
+/**
+ * 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.server.service.script;
+
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.google.common.collect.Sets;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
+import org.thingsboard.server.common.msg.TbMsg;
+import org.thingsboard.server.common.msg.TbMsgMetaData;
+
+import javax.script.ScriptException;
+import java.util.Collections;
+import java.util.Map;
+import java.util.Set;
+import java.util.UUID;
+import java.util.concurrent.ExecutionException;
+
+
+@Slf4j
+public class RuleNodeJsScriptEngine implements org.thingsboard.rule.engine.api.ScriptEngine {
+
+    private static final ObjectMapper mapper = new ObjectMapper();
+    private final JsSandboxService sandboxService;
+
+    private final UUID scriptId;
+
+    public RuleNodeJsScriptEngine(JsSandboxService sandboxService, String script, String... argNames) {
+        this.sandboxService = sandboxService;
+        try {
+            this.scriptId = this.sandboxService.eval(JsScriptType.RULE_NODE_SCRIPT, script, argNames).get();
+        } catch (Exception e) {
+            throw new IllegalArgumentException("Can't compile script: " + e.getMessage());
+        }
+    }
+
+    private static String[] prepareArgs(TbMsg msg) {
+        try {
+            String[] args = new String[3];
+            if (msg.getData() != null) {
+                args[0] = msg.getData();
+            } else {
+                args[0] = "";
+            }
+            args[1] = mapper.writeValueAsString(msg.getMetaData().getData());
+            args[2] = msg.getType();
+            return args;
+        } catch (Throwable th) {
+            throw new IllegalArgumentException("Cannot bind js args", th);
+        }
+    }
+
+    private static TbMsg unbindMsg(JsonNode msgData, TbMsg msg) {
+        try {
+            String data = null;
+            Map<String, String> metadata = null;
+            String messageType = null;
+            if (msgData.has(RuleNodeScriptFactory.MSG)) {
+                JsonNode msgPayload = msgData.get(RuleNodeScriptFactory.MSG);
+                data = mapper.writeValueAsString(msgPayload);
+            }
+            if (msgData.has(RuleNodeScriptFactory.METADATA)) {
+                JsonNode msgMetadata = msgData.get(RuleNodeScriptFactory.METADATA);
+                metadata = mapper.convertValue(msgMetadata, new TypeReference<Map<String, String>>() {
+                });
+            }
+            if (msgData.has(RuleNodeScriptFactory.MSG_TYPE)) {
+                messageType = msgData.get(RuleNodeScriptFactory.MSG_TYPE).asText();
+            }
+            String newData = data != null ? data : msg.getData();
+            TbMsgMetaData newMetadata = metadata != null ? new TbMsgMetaData(metadata) : msg.getMetaData().copy();
+            String newMessageType = !StringUtils.isEmpty(messageType) ? messageType : msg.getType();
+            return new TbMsg(msg.getId(), newMessageType, msg.getOriginator(), newMetadata, newData, msg.getRuleChainId(), msg.getRuleNodeId(), msg.getClusterPartition());
+        } catch (Throwable th) {
+            th.printStackTrace();
+            throw new RuntimeException("Failed to unbind message data from javascript result", th);
+        }
+    }
+
+    @Override
+    public TbMsg executeUpdate(TbMsg msg) throws ScriptException {
+        JsonNode result = executeScript(msg);
+        if (!result.isObject()) {
+            log.warn("Wrong result type: {}", result.getNodeType());
+            throw new ScriptException("Wrong result type: " + result.getNodeType());
+        }
+        return unbindMsg(result, msg);
+    }
+
+    @Override
+    public TbMsg executeGenerate(TbMsg prevMsg) throws ScriptException {
+        JsonNode result = executeScript(prevMsg);
+        if (!result.isObject()) {
+            log.warn("Wrong result type: {}", result.getNodeType());
+            throw new ScriptException("Wrong result type: " + result.getNodeType());
+        }
+        return unbindMsg(result, prevMsg);
+    }
+
+    @Override
+    public JsonNode executeJson(TbMsg msg) throws ScriptException {
+        return executeScript(msg);
+    }
+
+    @Override
+    public String executeToString(TbMsg msg) throws ScriptException {
+        JsonNode result = executeScript(msg);
+        if (!result.isTextual()) {
+            log.warn("Wrong result type: {}", result.getNodeType());
+            throw new ScriptException("Wrong result type: " + result.getNodeType());
+        }
+        return result.asText();
+    }
+
+    @Override
+    public boolean executeFilter(TbMsg msg) throws ScriptException {
+        JsonNode result = executeScript(msg);
+        if (!result.isBoolean()) {
+            log.warn("Wrong result type: {}", result.getNodeType());
+            throw new ScriptException("Wrong result type: " + result.getNodeType());
+        }
+        return result.asBoolean();
+    }
+
+    @Override
+    public Set<String> executeSwitch(TbMsg msg) throws ScriptException {
+        JsonNode result = executeScript(msg);
+        if (result.isTextual()) {
+            return Collections.singleton(result.asText());
+        } else if (result.isArray()) {
+            Set<String> nextStates = Sets.newHashSet();
+            for (JsonNode val : result) {
+                if (!val.isTextual()) {
+                    log.warn("Wrong result type: {}", val.getNodeType());
+                    throw new ScriptException("Wrong result type: " + val.getNodeType());
+                } else {
+                    nextStates.add(val.asText());
+                }
+            }
+            return nextStates;
+        } else {
+            log.warn("Wrong result type: {}", result.getNodeType());
+            throw new ScriptException("Wrong result type: " + result.getNodeType());
+        }
+    }
+
+    private JsonNode executeScript(TbMsg msg) throws ScriptException {
+        try {
+            String[] inArgs = prepareArgs(msg);
+            String eval = sandboxService.invokeFunction(this.scriptId, inArgs[0], inArgs[1], inArgs[2]).get().toString();
+            return mapper.readTree(eval);
+        } catch (ExecutionException e) {
+            if (e.getCause() instanceof ScriptException) {
+                throw (ScriptException)e.getCause();
+            } else {
+                throw new ScriptException("Failed to execute js script: " + e.getMessage());
+            }
+        } catch (Exception e) {
+            throw new ScriptException("Failed to execute js script: " + e.getMessage());
+        }
+    }
+
+    public void destroy() {
+        sandboxService.release(this.scriptId);
+    }
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/script/RuleNodeScriptFactory.java b/application/src/main/java/org/thingsboard/server/service/script/RuleNodeScriptFactory.java
new file mode 100644
index 0000000..5cc9c55
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/script/RuleNodeScriptFactory.java
@@ -0,0 +1,53 @@
+/**
+ * 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.server.service.script;
+
+public class RuleNodeScriptFactory {
+
+    public static final String MSG = "msg";
+    public static final String METADATA = "metadata";
+    public static final String MSG_TYPE = "msgType";
+    public static final String RULE_NODE_FUNCTION_NAME = "ruleNodeFunc";
+
+    private static final String JS_WRAPPER_PREFIX_TEMPLATE = "function %s(msgStr, metadataStr, msgType) { " +
+            "    var msg = JSON.parse(msgStr); " +
+            "    var metadata = JSON.parse(metadataStr); " +
+            "    return JSON.stringify(%s(msg, metadata, msgType));" +
+            "    function %s(%s, %s, %s) {";
+    private static final String JS_WRAPPER_SUFFIX = "}" +
+            "\n}";
+
+
+    public static String generateRuleNodeScript(String functionName, String scriptBody, String... argNames) {
+        String msgArg;
+        String metadataArg;
+        String msgTypeArg;
+        if (argNames != null && argNames.length == 3) {
+            msgArg = argNames[0];
+            metadataArg = argNames[1];
+            msgTypeArg = argNames[2];
+        } else {
+            msgArg = MSG;
+            metadataArg = METADATA;
+            msgTypeArg = MSG_TYPE;
+        }
+        String jsWrapperPrefix = String.format(JS_WRAPPER_PREFIX_TEMPLATE, functionName,
+                RULE_NODE_FUNCTION_NAME, RULE_NODE_FUNCTION_NAME, msgArg, metadataArg, msgTypeArg);
+        return jsWrapperPrefix + scriptBody + JS_WRAPPER_SUFFIX;
+    }
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/security/AccessValidator.java b/application/src/main/java/org/thingsboard/server/service/security/AccessValidator.java
new file mode 100644
index 0000000..600820e
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/security/AccessValidator.java
@@ -0,0 +1,321 @@
+/**
+ * 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.server.service.security;
+
+import com.google.common.base.Function;
+import com.google.common.util.concurrent.FutureCallback;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.stereotype.Component;
+import org.springframework.web.context.request.async.DeferredResult;
+import org.thingsboard.server.common.data.Customer;
+import org.thingsboard.server.common.data.Device;
+import org.thingsboard.server.common.data.Tenant;
+import org.thingsboard.server.common.data.asset.Asset;
+import org.thingsboard.server.common.data.exception.ThingsboardException;
+import org.thingsboard.server.common.data.id.AssetId;
+import org.thingsboard.server.common.data.id.CustomerId;
+import org.thingsboard.server.common.data.id.DeviceId;
+import org.thingsboard.server.common.data.id.EntityId;
+import org.thingsboard.server.common.data.id.EntityIdFactory;
+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.rule.RuleChain;
+import org.thingsboard.server.common.data.rule.RuleNode;
+import org.thingsboard.server.controller.HttpValidationCallback;
+import org.thingsboard.server.dao.alarm.AlarmService;
+import org.thingsboard.server.dao.asset.AssetService;
+import org.thingsboard.server.dao.customer.CustomerService;
+import org.thingsboard.server.dao.device.DeviceService;
+import org.thingsboard.server.dao.rule.RuleChainService;
+import org.thingsboard.server.dao.tenant.TenantService;
+import org.thingsboard.server.dao.user.UserService;
+import org.thingsboard.server.service.security.model.SecurityUser;
+import org.thingsboard.server.service.telemetry.exception.ToErrorResponseEntity;
+
+import javax.annotation.Nullable;
+import javax.annotation.PostConstruct;
+import javax.annotation.PreDestroy;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.function.BiConsumer;
+
+/**
+ * Created by ashvayka on 27.03.18.
+ */
+@Component
+public class AccessValidator {
+
+    public static final String CUSTOMER_USER_IS_NOT_ALLOWED_TO_PERFORM_THIS_OPERATION = "Customer user is not allowed to perform this operation!";
+    public static final String SYSTEM_ADMINISTRATOR_IS_NOT_ALLOWED_TO_PERFORM_THIS_OPERATION = "System administrator is not allowed to perform this operation!";
+    public static final String DEVICE_WITH_REQUESTED_ID_NOT_FOUND = "Device with requested id wasn't found!";
+
+    @Autowired
+    protected TenantService tenantService;
+
+    @Autowired
+    protected CustomerService customerService;
+
+    @Autowired
+    protected UserService userService;
+
+    @Autowired
+    protected DeviceService deviceService;
+
+    @Autowired
+    protected AssetService assetService;
+
+    @Autowired
+    protected AlarmService alarmService;
+
+    @Autowired
+    protected RuleChainService ruleChainService;
+
+    private ExecutorService executor;
+
+    @PostConstruct
+    public void initExecutor() {
+        executor = Executors.newSingleThreadExecutor();
+    }
+
+    @PreDestroy
+    public void shutdownExecutor() {
+        if (executor != null) {
+            executor.shutdownNow();
+        }
+    }
+
+    public DeferredResult<ResponseEntity> validateEntityAndCallback(SecurityUser currentUser, String entityType, String entityIdStr,
+                                                                    BiConsumer<DeferredResult<ResponseEntity>, EntityId> onSuccess) throws ThingsboardException {
+        return validateEntityAndCallback(currentUser, entityType, entityIdStr, onSuccess, (result, t) -> handleError(t, result, HttpStatus.INTERNAL_SERVER_ERROR));
+    }
+
+    public DeferredResult<ResponseEntity> validateEntityAndCallback(SecurityUser currentUser, String entityType, String entityIdStr,
+                                                                    BiConsumer<DeferredResult<ResponseEntity>, EntityId> onSuccess,
+                                                                    BiConsumer<DeferredResult<ResponseEntity>, Throwable> onFailure) throws ThingsboardException {
+        return validateEntityAndCallback(currentUser, EntityIdFactory.getByTypeAndId(entityType, entityIdStr),
+                onSuccess, onFailure);
+    }
+
+    public DeferredResult<ResponseEntity> validateEntityAndCallback(SecurityUser currentUser, EntityId entityId,
+                                                                    BiConsumer<DeferredResult<ResponseEntity>, EntityId> onSuccess) throws ThingsboardException {
+        return validateEntityAndCallback(currentUser, entityId, onSuccess, (result, t) -> handleError(t, result, HttpStatus.INTERNAL_SERVER_ERROR));
+    }
+
+    public DeferredResult<ResponseEntity> validateEntityAndCallback(SecurityUser currentUser, EntityId entityId,
+                                                                    BiConsumer<DeferredResult<ResponseEntity>, EntityId> onSuccess,
+                                                                    BiConsumer<DeferredResult<ResponseEntity>, Throwable> onFailure) throws ThingsboardException {
+
+        final DeferredResult<ResponseEntity> response = new DeferredResult<>();
+
+        validate(currentUser, entityId, new HttpValidationCallback(response,
+                new FutureCallback<DeferredResult<ResponseEntity>>() {
+                    @Override
+                    public void onSuccess(@Nullable DeferredResult<ResponseEntity> result) {
+                        onSuccess.accept(response, entityId);
+                    }
+
+                    @Override
+                    public void onFailure(Throwable t) {
+                        onFailure.accept(response, t);
+                    }
+                }));
+
+        return response;
+    }
+
+    public void validate(SecurityUser currentUser, EntityId entityId, FutureCallback<ValidationResult> callback) {
+        switch (entityId.getEntityType()) {
+            case DEVICE:
+                validateDevice(currentUser, entityId, callback);
+                return;
+            case ASSET:
+                validateAsset(currentUser, entityId, callback);
+                return;
+            case RULE_CHAIN:
+                validateRuleChain(currentUser, entityId, callback);
+                return;
+            case CUSTOMER:
+                validateCustomer(currentUser, entityId, callback);
+                return;
+            case TENANT:
+                validateTenant(currentUser, entityId, callback);
+                return;
+            default:
+                //TODO: add support of other entities
+                throw new IllegalStateException("Not Implemented!");
+        }
+    }
+
+    private void validateDevice(final SecurityUser currentUser, EntityId entityId, FutureCallback<ValidationResult> callback) {
+        if (currentUser.isSystemAdmin()) {
+            callback.onSuccess(ValidationResult.accessDenied(SYSTEM_ADMINISTRATOR_IS_NOT_ALLOWED_TO_PERFORM_THIS_OPERATION));
+        } else {
+            ListenableFuture<Device> deviceFuture = deviceService.findDeviceByIdAsync(new DeviceId(entityId.getId()));
+            Futures.addCallback(deviceFuture, getCallback(callback, device -> {
+                if (device == null) {
+                    return ValidationResult.entityNotFound(DEVICE_WITH_REQUESTED_ID_NOT_FOUND);
+                } else {
+                    if (!device.getTenantId().equals(currentUser.getTenantId())) {
+                        return ValidationResult.accessDenied("Device doesn't belong to the current Tenant!");
+                    } else if (currentUser.isCustomerUser() && !device.getCustomerId().equals(currentUser.getCustomerId())) {
+                        return ValidationResult.accessDenied("Device doesn't belong to the current Customer!");
+                    } else {
+                        return ValidationResult.ok(device);
+                    }
+                }
+            }), executor);
+        }
+    }
+
+    private void validateAsset(final SecurityUser currentUser, EntityId entityId, FutureCallback<ValidationResult> callback) {
+        if (currentUser.isSystemAdmin()) {
+            callback.onSuccess(ValidationResult.accessDenied(SYSTEM_ADMINISTRATOR_IS_NOT_ALLOWED_TO_PERFORM_THIS_OPERATION));
+        } else {
+            ListenableFuture<Asset> assetFuture = assetService.findAssetByIdAsync(new AssetId(entityId.getId()));
+            Futures.addCallback(assetFuture, getCallback(callback, asset -> {
+                if (asset == null) {
+                    return ValidationResult.entityNotFound("Asset with requested id wasn't found!");
+                } else {
+                    if (!asset.getTenantId().equals(currentUser.getTenantId())) {
+                        return ValidationResult.accessDenied("Asset doesn't belong to the current Tenant!");
+                    } else if (currentUser.isCustomerUser() && !asset.getCustomerId().equals(currentUser.getCustomerId())) {
+                        return ValidationResult.accessDenied("Asset doesn't belong to the current Customer!");
+                    } else {
+                        return ValidationResult.ok(asset);
+                    }
+                }
+            }), executor);
+        }
+    }
+
+    private void validateRuleChain(final SecurityUser currentUser, EntityId entityId, FutureCallback<ValidationResult> callback) {
+        if (currentUser.isCustomerUser()) {
+            callback.onSuccess(ValidationResult.accessDenied(CUSTOMER_USER_IS_NOT_ALLOWED_TO_PERFORM_THIS_OPERATION));
+        } else {
+            ListenableFuture<RuleChain> ruleChainFuture = ruleChainService.findRuleChainByIdAsync(new RuleChainId(entityId.getId()));
+            Futures.addCallback(ruleChainFuture, getCallback(callback, ruleChain -> {
+                if (ruleChain == null) {
+                    return ValidationResult.entityNotFound("Rule chain with requested id wasn't found!");
+                } else {
+                    if (currentUser.isTenantAdmin() && !ruleChain.getTenantId().equals(currentUser.getTenantId())) {
+                        return ValidationResult.accessDenied("Rule chain doesn't belong to the current Tenant!");
+                    } else if (currentUser.isSystemAdmin() && !ruleChain.getTenantId().isNullUid()) {
+                        return ValidationResult.accessDenied("Rule chain is not in system scope!");
+                    } else {
+                        return ValidationResult.ok(ruleChain);
+                    }
+                }
+            }), executor);
+        }
+    }
+
+    private void validateRule(final SecurityUser currentUser, EntityId entityId, FutureCallback<ValidationResult> callback) {
+        if (currentUser.isCustomerUser()) {
+            callback.onSuccess(ValidationResult.accessDenied(CUSTOMER_USER_IS_NOT_ALLOWED_TO_PERFORM_THIS_OPERATION));
+        } else {
+            ListenableFuture<RuleNode> ruleNodeFuture = ruleChainService.findRuleNodeByIdAsync(new RuleNodeId(entityId.getId()));
+            Futures.addCallback(ruleNodeFuture, getCallback(callback, ruleNodeTmp -> {
+                RuleNode ruleNode = ruleNodeTmp;
+                if (ruleNode == null) {
+                    return ValidationResult.entityNotFound("Rule node with requested id wasn't found!");
+                } else if (ruleNode.getRuleChainId() == null) {
+                    return ValidationResult.entityNotFound("Rule chain with requested node id wasn't found!");
+                } else {
+                    //TODO: make async
+                    RuleChain ruleChain = ruleChainService.findRuleChainById(ruleNode.getRuleChainId());
+                    if (currentUser.isTenantAdmin() && !ruleChain.getTenantId().equals(currentUser.getTenantId())) {
+                        return ValidationResult.accessDenied("Rule chain doesn't belong to the current Tenant!");
+                    } else if (currentUser.isSystemAdmin() && !ruleChain.getTenantId().isNullUid()) {
+                        return ValidationResult.accessDenied("Rule chain is not in system scope!");
+                    } else {
+                        return ValidationResult.ok(ruleNode);
+                    }
+                }
+            }), executor);
+        }
+    }
+
+    private void validateCustomer(final SecurityUser currentUser, EntityId entityId, FutureCallback<ValidationResult> callback) {
+        if (currentUser.isSystemAdmin()) {
+            callback.onSuccess(ValidationResult.accessDenied(SYSTEM_ADMINISTRATOR_IS_NOT_ALLOWED_TO_PERFORM_THIS_OPERATION));
+        } else {
+            ListenableFuture<Customer> customerFuture = customerService.findCustomerByIdAsync(new CustomerId(entityId.getId()));
+            Futures.addCallback(customerFuture, getCallback(callback, customer -> {
+                if (customer == null) {
+                    return ValidationResult.entityNotFound("Customer with requested id wasn't found!");
+                } else {
+                    if (!customer.getTenantId().equals(currentUser.getTenantId())) {
+                        return ValidationResult.accessDenied("Customer doesn't belong to the current Tenant!");
+                    } else if (currentUser.isCustomerUser() && !customer.getId().equals(currentUser.getCustomerId())) {
+                        return ValidationResult.accessDenied("Customer doesn't relate to the currently authorized customer user!");
+                    } else {
+                        return ValidationResult.ok(customer);
+                    }
+                }
+            }), executor);
+        }
+    }
+
+    private void validateTenant(final SecurityUser currentUser, EntityId entityId, FutureCallback<ValidationResult> callback) {
+        if (currentUser.isCustomerUser()) {
+            callback.onSuccess(ValidationResult.accessDenied(CUSTOMER_USER_IS_NOT_ALLOWED_TO_PERFORM_THIS_OPERATION));
+        } else if (currentUser.isSystemAdmin()) {
+            callback.onSuccess(ValidationResult.ok(null));
+        } else {
+            ListenableFuture<Tenant> tenantFuture = tenantService.findTenantByIdAsync(new TenantId(entityId.getId()));
+            Futures.addCallback(tenantFuture, getCallback(callback, tenant -> {
+                if (tenant == null) {
+                    return ValidationResult.entityNotFound("Tenant with requested id wasn't found!");
+                } else if (!tenant.getId().equals(currentUser.getTenantId())) {
+                    return ValidationResult.accessDenied("Tenant doesn't relate to the currently authorized user!");
+                } else {
+                    return ValidationResult.ok(tenant);
+                }
+            }), executor);
+        }
+    }
+
+    private <T, V> FutureCallback<T> getCallback(FutureCallback<ValidationResult> callback, Function<T, ValidationResult<V>> transformer) {
+        return new FutureCallback<T>() {
+            @Override
+            public void onSuccess(@Nullable T result) {
+                callback.onSuccess(transformer.apply(result));
+            }
+
+            @Override
+            public void onFailure(Throwable t) {
+                callback.onFailure(t);
+            }
+        };
+    }
+
+    public static void handleError(Throwable e, final DeferredResult<ResponseEntity> response, HttpStatus defaultErrorStatus) {
+        ResponseEntity responseEntity;
+        if (e != null && e instanceof ToErrorResponseEntity) {
+            responseEntity = ((ToErrorResponseEntity) e).toErrorResponseEntity();
+        } else if (e != null && e instanceof IllegalArgumentException) {
+            responseEntity = new ResponseEntity<>(e.getMessage(), HttpStatus.BAD_REQUEST);
+        } else {
+            responseEntity = new ResponseEntity<>(defaultErrorStatus);
+        }
+        response.setResult(responseEntity);
+    }
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/JwtAuthenticationProvider.java b/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/JwtAuthenticationProvider.java
index a46fb48..527e2be 100644
--- a/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/JwtAuthenticationProvider.java
+++ b/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/JwtAuthenticationProvider.java
@@ -15,24 +15,16 @@
  */
 package org.thingsboard.server.service.security.auth.jwt;
 
-import io.jsonwebtoken.Claims;
-import io.jsonwebtoken.Jws;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.security.authentication.AuthenticationProvider;
 import org.springframework.security.core.Authentication;
 import org.springframework.security.core.AuthenticationException;
-import org.springframework.security.core.GrantedAuthority;
-import org.springframework.security.core.authority.SimpleGrantedAuthority;
 import org.springframework.stereotype.Component;
-import org.thingsboard.server.config.JwtSettings;
 import org.thingsboard.server.service.security.auth.JwtAuthenticationToken;
 import org.thingsboard.server.service.security.model.SecurityUser;
 import org.thingsboard.server.service.security.model.token.JwtTokenFactory;
 import org.thingsboard.server.service.security.model.token.RawAccessJwtToken;
 
-import java.util.List;
-import java.util.stream.Collectors;
-
 @Component
 @SuppressWarnings("unchecked")
 public class JwtAuthenticationProvider implements AuthenticationProvider {
diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/JwtTokenAuthenticationProcessingFilter.java b/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/JwtTokenAuthenticationProcessingFilter.java
index f4da3a5..7008662 100644
--- a/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/JwtTokenAuthenticationProcessingFilter.java
+++ b/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/JwtTokenAuthenticationProcessingFilter.java
@@ -23,7 +23,6 @@ import org.springframework.security.core.context.SecurityContextHolder;
 import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
 import org.springframework.security.web.authentication.AuthenticationFailureHandler;
 import org.springframework.security.web.util.matcher.RequestMatcher;
-import org.thingsboard.server.config.ThingsboardSecurityConfiguration;
 import org.thingsboard.server.service.security.auth.JwtAuthenticationToken;
 import org.thingsboard.server.service.security.auth.jwt.extractor.TokenExtractor;
 import org.thingsboard.server.service.security.model.token.RawAccessJwtToken;
diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/RefreshTokenAuthenticationProvider.java b/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/RefreshTokenAuthenticationProvider.java
index 08de9ef..2e68b35 100644
--- a/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/RefreshTokenAuthenticationProvider.java
+++ b/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/RefreshTokenAuthenticationProvider.java
@@ -16,7 +16,10 @@
 package org.thingsboard.server.service.security.auth.jwt;
 
 import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.security.authentication.*;
+import org.springframework.security.authentication.AuthenticationProvider;
+import org.springframework.security.authentication.BadCredentialsException;
+import org.springframework.security.authentication.DisabledException;
+import org.springframework.security.authentication.InsufficientAuthenticationException;
 import org.springframework.security.core.Authentication;
 import org.springframework.security.core.AuthenticationException;
 import org.springframework.security.core.userdetails.UsernameNotFoundException;
@@ -26,7 +29,6 @@ import org.thingsboard.server.common.data.Customer;
 import org.thingsboard.server.common.data.User;
 import org.thingsboard.server.common.data.id.CustomerId;
 import org.thingsboard.server.common.data.id.EntityId;
-import org.thingsboard.server.common.data.id.UUIDBased;
 import org.thingsboard.server.common.data.id.UserId;
 import org.thingsboard.server.common.data.security.Authority;
 import org.thingsboard.server.common.data.security.UserCredentials;
diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/RefreshTokenProcessingFilter.java b/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/RefreshTokenProcessingFilter.java
index a016f3a..fff15f2 100644
--- a/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/RefreshTokenProcessingFilter.java
+++ b/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/RefreshTokenProcessingFilter.java
@@ -18,8 +18,6 @@ package org.thingsboard.server.service.security.auth.jwt;
 import com.fasterxml.jackson.databind.ObjectMapper;
 import lombok.extern.slf4j.Slf4j;
 import org.apache.commons.lang3.StringUtils;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 import org.springframework.http.HttpMethod;
 import org.springframework.security.authentication.AuthenticationServiceException;
 import org.springframework.security.core.Authentication;
diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestAuthenticationProvider.java b/application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestAuthenticationProvider.java
index 36f2199..195208a 100644
--- a/application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestAuthenticationProvider.java
+++ b/application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestAuthenticationProvider.java
@@ -16,7 +16,11 @@
 package org.thingsboard.server.service.security.auth.rest;
 
 import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.security.authentication.*;
+import org.springframework.security.authentication.AuthenticationProvider;
+import org.springframework.security.authentication.BadCredentialsException;
+import org.springframework.security.authentication.DisabledException;
+import org.springframework.security.authentication.InsufficientAuthenticationException;
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
 import org.springframework.security.core.Authentication;
 import org.springframework.security.core.AuthenticationException;
 import org.springframework.security.core.userdetails.UsernameNotFoundException;
@@ -27,7 +31,6 @@ import org.thingsboard.server.common.data.Customer;
 import org.thingsboard.server.common.data.User;
 import org.thingsboard.server.common.data.id.CustomerId;
 import org.thingsboard.server.common.data.id.EntityId;
-import org.thingsboard.server.common.data.id.UUIDBased;
 import org.thingsboard.server.common.data.id.UserId;
 import org.thingsboard.server.common.data.security.Authority;
 import org.thingsboard.server.common.data.security.UserCredentials;
diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestLoginProcessingFilter.java b/application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestLoginProcessingFilter.java
index 4f6a87c..03c1b34 100644
--- a/application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestLoginProcessingFilter.java
+++ b/application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestLoginProcessingFilter.java
@@ -18,8 +18,6 @@ package org.thingsboard.server.service.security.auth.rest;
 import com.fasterxml.jackson.databind.ObjectMapper;
 import lombok.extern.slf4j.Slf4j;
 import org.apache.commons.lang3.StringUtils;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 import org.springframework.http.HttpMethod;
 import org.springframework.security.authentication.AuthenticationServiceException;
 import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestPublicLoginProcessingFilter.java b/application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestPublicLoginProcessingFilter.java
index 5f8488e..2bbeddf 100644
--- a/application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestPublicLoginProcessingFilter.java
+++ b/application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestPublicLoginProcessingFilter.java
@@ -18,8 +18,6 @@ package org.thingsboard.server.service.security.auth.rest;
 import com.fasterxml.jackson.databind.ObjectMapper;
 import lombok.extern.slf4j.Slf4j;
 import org.apache.commons.lang3.StringUtils;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 import org.springframework.http.HttpMethod;
 import org.springframework.security.authentication.AuthenticationServiceException;
 import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
diff --git a/application/src/main/java/org/thingsboard/server/service/security/model/token/RawAccessJwtToken.java b/application/src/main/java/org/thingsboard/server/service/security/model/token/RawAccessJwtToken.java
index db6a336..544b6fd 100644
--- a/application/src/main/java/org/thingsboard/server/service/security/model/token/RawAccessJwtToken.java
+++ b/application/src/main/java/org/thingsboard/server/service/security/model/token/RawAccessJwtToken.java
@@ -15,7 +15,13 @@
  */
 package org.thingsboard.server.service.security.model.token;
 
-import io.jsonwebtoken.*;
+import io.jsonwebtoken.Claims;
+import io.jsonwebtoken.ExpiredJwtException;
+import io.jsonwebtoken.Jws;
+import io.jsonwebtoken.Jwts;
+import io.jsonwebtoken.MalformedJwtException;
+import io.jsonwebtoken.SignatureException;
+import io.jsonwebtoken.UnsupportedJwtException;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.security.authentication.BadCredentialsException;
diff --git a/application/src/main/java/org/thingsboard/server/service/state/DefaultDeviceStateService.java b/application/src/main/java/org/thingsboard/server/service/state/DefaultDeviceStateService.java
new file mode 100644
index 0000000..45b9696
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/state/DefaultDeviceStateService.java
@@ -0,0 +1,386 @@
+/**
+ * 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.server.service.state;
+
+import com.datastax.driver.core.utils.UUIDs;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.google.common.base.Function;
+import com.google.common.util.concurrent.FutureCallback;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.ListeningScheduledExecutorService;
+import com.google.common.util.concurrent.MoreExecutors;
+import lombok.Getter;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Service;
+import org.thingsboard.server.actors.service.ActorService;
+import org.thingsboard.server.common.data.DataConstants;
+import org.thingsboard.server.common.data.Device;
+import org.thingsboard.server.common.data.Tenant;
+import org.thingsboard.server.common.data.id.DeviceId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.kv.AttributeKvEntry;
+import org.thingsboard.server.common.data.page.TextPageLink;
+import org.thingsboard.server.common.msg.TbMsg;
+import org.thingsboard.server.common.msg.TbMsgDataType;
+import org.thingsboard.server.common.msg.TbMsgMetaData;
+import org.thingsboard.server.common.msg.system.ServiceToRuleEngineMsg;
+import org.thingsboard.server.dao.attributes.AttributesService;
+import org.thingsboard.server.dao.device.DeviceService;
+import org.thingsboard.server.dao.tenant.TenantService;
+import org.thingsboard.server.service.telemetry.TelemetrySubscriptionService;
+
+import javax.annotation.Nullable;
+import javax.annotation.PostConstruct;
+import javax.annotation.PreDestroy;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Optional;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+
+import static org.thingsboard.server.common.data.DataConstants.ACTIVITY_EVENT;
+import static org.thingsboard.server.common.data.DataConstants.CONNECT_EVENT;
+import static org.thingsboard.server.common.data.DataConstants.DISCONNECT_EVENT;
+import static org.thingsboard.server.common.data.DataConstants.INACTIVITY_EVENT;
+
+/**
+ * Created by ashvayka on 01.05.18.
+ */
+@Service
+@Slf4j
+//TODO: refactor to use page links as cursor and not fetch all
+public class DefaultDeviceStateService implements DeviceStateService {
+
+    private static final ObjectMapper json = new ObjectMapper();
+    public static final String ACTIVITY_STATE = "active";
+    public static final String LAST_CONNECT_TIME = "lastConnectTime";
+    public static final String LAST_DISCONNECT_TIME = "lastDisconnectTime";
+    public static final String LAST_ACTIVITY_TIME = "lastActivityTime";
+    public static final String INACTIVITY_ALARM_TIME = "inactivityAlarmTime";
+    public static final String INACTIVITY_TIMEOUT = "inactivityTimeout";
+
+    public static final List<String> PERSISTENT_ATTRIBUTES = Arrays.asList(ACTIVITY_STATE, LAST_CONNECT_TIME, LAST_DISCONNECT_TIME, LAST_ACTIVITY_TIME, INACTIVITY_ALARM_TIME, INACTIVITY_TIMEOUT);
+
+    @Autowired
+    private TenantService tenantService;
+
+    @Autowired
+    private DeviceService deviceService;
+
+    @Autowired
+    private AttributesService attributesService;
+
+    @Autowired
+    private ActorService actorService;
+
+    @Autowired
+    private TelemetrySubscriptionService tsSubService;
+
+    @Value("${state.defaultInactivityTimeoutInSec}")
+    @Getter
+    private long defaultInactivityTimeoutInSec;
+
+    @Value("${state.defaultStateCheckIntervalInSec}")
+    @Getter
+    private long defaultStateCheckIntervalInSec;
+
+// TODO in v2.1
+//    @Value("${state.defaultStatePersistenceIntervalInSec}")
+//    @Getter
+//    private long defaultStatePersistenceIntervalInSec;
+//
+//    @Value("${state.defaultStatePersistencePack}")
+//    @Getter
+//    private long defaultStatePersistencePack;
+
+    private ListeningScheduledExecutorService queueExecutor;
+
+    private ConcurrentMap<TenantId, Set<DeviceId>> tenantDevices = new ConcurrentHashMap<>();
+    private ConcurrentMap<DeviceId, DeviceStateData> deviceStates = new ConcurrentHashMap<>();
+
+    @PostConstruct
+    public void init() {
+        // Should be always single threaded due to absence of locks.
+        queueExecutor = MoreExecutors.listeningDecorator(Executors.newSingleThreadScheduledExecutor());
+        queueExecutor.submit(this::initStateFromDB);
+        queueExecutor.scheduleAtFixedRate(this::updateState, defaultStateCheckIntervalInSec, defaultStateCheckIntervalInSec, TimeUnit.SECONDS);
+        //TODO: schedule persistence in v2.1;
+    }
+
+    @PreDestroy
+    public void stop() {
+        if (queueExecutor != null) {
+            queueExecutor.shutdownNow();
+        }
+    }
+
+    @Override
+    public void onDeviceAdded(Device device) {
+        queueExecutor.submit(() -> onDeviceAddedSync(device));
+    }
+
+    @Override
+    public void onDeviceUpdated(Device device) {
+        queueExecutor.submit(() -> onDeviceUpdatedSync(device));
+    }
+
+    @Override
+    public void onDeviceConnect(DeviceId deviceId) {
+        queueExecutor.submit(() -> onDeviceConnectSync(deviceId));
+    }
+
+    @Override
+    public void onDeviceActivity(DeviceId deviceId) {
+        queueExecutor.submit(() -> onDeviceActivitySync(deviceId));
+    }
+
+    @Override
+    public void onDeviceDisconnect(DeviceId deviceId) {
+        queueExecutor.submit(() -> onDeviceDisconnectSync(deviceId));
+    }
+
+    @Override
+    public void onDeviceDeleted(Device device) {
+        queueExecutor.submit(() -> onDeviceDeleted(device.getTenantId(), device.getId()));
+    }
+
+    @Override
+    public void onDeviceInactivityTimeoutUpdate(DeviceId deviceId, long inactivityTimeout) {
+        queueExecutor.submit(() -> onInactivityTimeoutUpdate(deviceId, inactivityTimeout));
+    }
+
+    @Override
+    public Optional<DeviceState> getDeviceState(DeviceId deviceId) {
+        DeviceStateData state = deviceStates.get(deviceId);
+        if (state != null) {
+            return Optional.of(state.getState());
+        } else {
+            return Optional.empty();
+        }
+    }
+
+    private void initStateFromDB() {
+        List<Tenant> tenants = tenantService.findTenants(new TextPageLink(Integer.MAX_VALUE)).getData();
+        for (Tenant tenant : tenants) {
+            List<ListenableFuture<DeviceStateData>> fetchFutures = new ArrayList<>();
+            List<Device> devices = deviceService.findDevicesByTenantId(tenant.getId(), new TextPageLink(Integer.MAX_VALUE)).getData();
+            for (Device device : devices) {
+                fetchFutures.add(fetchDeviceState(device));
+            }
+            try {
+                Futures.successfulAsList(fetchFutures).get().forEach(this::addDeviceUsingState);
+            } catch (InterruptedException | ExecutionException e) {
+                log.warn("Failed to init device state service from DB", e);
+            }
+        }
+    }
+
+    private void addDeviceUsingState(DeviceStateData state) {
+        tenantDevices.computeIfAbsent(state.getTenantId(), id -> ConcurrentHashMap.newKeySet()).add(state.getDeviceId());
+        deviceStates.put(state.getDeviceId(), state);
+    }
+
+    private void updateState() {
+        long ts = System.currentTimeMillis();
+        Set<DeviceId> deviceIds = new HashSet<>(deviceStates.keySet());
+        for (DeviceId deviceId : deviceIds) {
+            DeviceStateData stateData = deviceStates.get(deviceId);
+            DeviceState state = stateData.getState();
+            state.setActive(ts < state.getLastActivityTime() + state.getInactivityTimeout());
+            if (!state.isActive() && state.getLastInactivityAlarmTime() < state.getLastActivityTime()) {
+                state.setLastInactivityAlarmTime(ts);
+                pushRuleEngineMessage(stateData, INACTIVITY_EVENT);
+                saveAttribute(deviceId, INACTIVITY_ALARM_TIME, ts);
+                saveAttribute(deviceId, ACTIVITY_STATE, state.isActive());
+            }
+        }
+    }
+
+    private void onDeviceConnectSync(DeviceId deviceId) {
+        DeviceStateData stateData = deviceStates.get(deviceId);
+        if (stateData != null) {
+            long ts = System.currentTimeMillis();
+            stateData.getState().setLastConnectTime(ts);
+            pushRuleEngineMessage(stateData, CONNECT_EVENT);
+            saveAttribute(deviceId, LAST_CONNECT_TIME, ts);
+        }
+    }
+
+    private void onDeviceDisconnectSync(DeviceId deviceId) {
+        DeviceStateData stateData = deviceStates.get(deviceId);
+        if (stateData != null) {
+            long ts = System.currentTimeMillis();
+            stateData.getState().setLastDisconnectTime(ts);
+            pushRuleEngineMessage(stateData, DISCONNECT_EVENT);
+            saveAttribute(deviceId, LAST_DISCONNECT_TIME, ts);
+        }
+    }
+
+    private void onDeviceActivitySync(DeviceId deviceId) {
+        DeviceStateData stateData = deviceStates.get(deviceId);
+        if (stateData != null) {
+            DeviceState state = stateData.getState();
+            long ts = System.currentTimeMillis();
+            state.setActive(true);
+            stateData.getState().setLastActivityTime(ts);
+            pushRuleEngineMessage(stateData, ACTIVITY_EVENT);
+            saveAttribute(deviceId, LAST_ACTIVITY_TIME, ts);
+            saveAttribute(deviceId, ACTIVITY_STATE, state.isActive());
+        }
+    }
+
+    private void onInactivityTimeoutUpdate(DeviceId deviceId, long inactivityTimeout) {
+        if (inactivityTimeout == 0L) {
+            return;
+        }
+        DeviceStateData stateData = deviceStates.get(deviceId);
+        if (stateData != null) {
+            long ts = System.currentTimeMillis();
+            DeviceState state = stateData.getState();
+            state.setInactivityTimeout(inactivityTimeout);
+            boolean oldActive = state.isActive();
+            state.setActive(ts < state.getLastActivityTime() + state.getInactivityTimeout());
+            if (!oldActive && state.isActive() || oldActive && !state.isActive()) {
+                saveAttribute(deviceId, ACTIVITY_STATE, state.isActive());
+            }
+        }
+    }
+
+    private void onDeviceAddedSync(Device device) {
+        Futures.addCallback(fetchDeviceState(device), new FutureCallback<DeviceStateData>() {
+            @Override
+            public void onSuccess(@Nullable DeviceStateData state) {
+                addDeviceUsingState(state);
+            }
+
+            @Override
+            public void onFailure(Throwable t) {
+                log.warn("Failed to register device to the state service", t);
+            }
+        });
+    }
+
+    private void onDeviceUpdatedSync(Device device) {
+        DeviceStateData stateData = deviceStates.get(device.getId());
+        if (stateData != null) {
+            TbMsgMetaData md = new TbMsgMetaData();
+            md.putValue("deviceName", device.getName());
+            md.putValue("deviceType", device.getType());
+            stateData.setMetaData(md);
+        }
+    }
+
+    private void onDeviceDeleted(TenantId tenantId, DeviceId deviceId) {
+        deviceStates.remove(deviceId);
+        Set<DeviceId> deviceIds = tenantDevices.get(tenantId);
+        if (deviceIds != null) {
+            deviceIds.remove(deviceId);
+            if (deviceIds.isEmpty()) {
+                tenantDevices.remove(tenantId);
+            }
+        }
+    }
+
+    private ListenableFuture<DeviceStateData> fetchDeviceState(Device device) {
+        ListenableFuture<List<AttributeKvEntry>> attributes = attributesService.find(device.getId(), DataConstants.SERVER_SCOPE, PERSISTENT_ATTRIBUTES);
+        return Futures.transform(attributes, new Function<List<AttributeKvEntry>, DeviceStateData>() {
+            @Nullable
+            @Override
+            public DeviceStateData apply(@Nullable List<AttributeKvEntry> attributes) {
+                long lastActivityTime = getAttributeValue(attributes, LAST_ACTIVITY_TIME, 0L);
+                long inactivityAlarmTime = getAttributeValue(attributes, INACTIVITY_ALARM_TIME, 0L);
+                long inactivityTimeout = getAttributeValue(attributes, INACTIVITY_TIMEOUT, TimeUnit.SECONDS.toMillis(defaultInactivityTimeoutInSec));
+                boolean active = System.currentTimeMillis() < lastActivityTime + inactivityTimeout;
+                DeviceState deviceState = DeviceState.builder()
+                        .active(active)
+                        .lastConnectTime(getAttributeValue(attributes, LAST_CONNECT_TIME, 0L))
+                        .lastDisconnectTime(getAttributeValue(attributes, LAST_DISCONNECT_TIME, 0L))
+                        .lastActivityTime(lastActivityTime)
+                        .lastInactivityAlarmTime(inactivityAlarmTime)
+                        .inactivityTimeout(inactivityTimeout)
+                        .build();
+                TbMsgMetaData md = new TbMsgMetaData();
+                md.putValue("deviceName", device.getName());
+                md.putValue("deviceType", device.getType());
+                return DeviceStateData.builder()
+                        .tenantId(device.getTenantId())
+                        .deviceId(device.getId())
+                        .metaData(md)
+                        .state(deviceState).build();
+            }
+        });
+    }
+
+    private long getAttributeValue(List<AttributeKvEntry> attributes, String attributeName, long defaultValue) {
+        for (AttributeKvEntry attribute : attributes) {
+            if (attribute.getKey().equals(attributeName)) {
+                return attribute.getLongValue().orElse(defaultValue);
+            }
+        }
+        return defaultValue;
+    }
+
+    private void pushRuleEngineMessage(DeviceStateData stateData, String msgType) {
+        DeviceState state = stateData.getState();
+        try {
+            TbMsg tbMsg = new TbMsg(UUIDs.timeBased(), msgType, stateData.getDeviceId(), stateData.getMetaData().copy(), TbMsgDataType.JSON
+                    , json.writeValueAsString(state)
+                    , null, null, 0L);
+            actorService.onMsg(new ServiceToRuleEngineMsg(stateData.getTenantId(), tbMsg));
+        } catch (Exception e) {
+            log.warn("[{}] Failed to push inactivity alarm: {}", stateData.getDeviceId(), state, e);
+        }
+    }
+
+    private void saveAttribute(DeviceId deviceId, String key, long value) {
+        tsSubService.saveAttrAndNotify(deviceId, DataConstants.SERVER_SCOPE, key, value, new AttributeSaveCallback(deviceId, key, value));
+    }
+
+    private void saveAttribute(DeviceId deviceId, String key, boolean value) {
+        tsSubService.saveAttrAndNotify(deviceId, DataConstants.SERVER_SCOPE, key, value, new AttributeSaveCallback(deviceId, key, value));
+    }
+
+    private class AttributeSaveCallback implements FutureCallback<Void> {
+        private final DeviceId deviceId;
+        private final String key;
+        private final Object value;
+
+        AttributeSaveCallback(DeviceId deviceId, String key, Object value) {
+            this.deviceId = deviceId;
+            this.key = key;
+            this.value = value;
+        }
+
+        @Override
+        public void onSuccess(@Nullable Void result) {
+            log.trace("[{}] Successfully updated attribute [{}] with value [{}]", deviceId, key, value);
+        }
+
+        @Override
+        public void onFailure(Throwable t) {
+            log.warn("[{}] Failed to update attribute [{}] with value [{}]", deviceId, key, value, t);
+        }
+    }
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/GetHistoryCmd.java b/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/GetHistoryCmd.java
new file mode 100644
index 0000000..9fc28f6
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/GetHistoryCmd.java
@@ -0,0 +1,40 @@
+/**
+ * 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.server.service.telemetry.cmd;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+/**
+ * @author Andrew Shvayka
+ */
+@NoArgsConstructor
+@AllArgsConstructor
+@Data
+public class GetHistoryCmd implements TelemetryPluginCmd {
+
+    private int cmdId;
+    private String entityType;
+    private String entityId;
+    private String keys;
+    private long startTs;
+    private long endTs;
+    private long interval;
+    private int limit;
+    private String agg;
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/SubscriptionCmd.java b/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/SubscriptionCmd.java
new file mode 100644
index 0000000..15701ca
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/SubscriptionCmd.java
@@ -0,0 +1,42 @@
+/**
+ * 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.server.service.telemetry.cmd;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import org.thingsboard.server.service.telemetry.TelemetryFeature;
+
+@NoArgsConstructor
+@AllArgsConstructor
+@Data
+public abstract class SubscriptionCmd implements TelemetryPluginCmd {
+
+    private int cmdId;
+    private String entityType;
+    private String entityId;
+    private String keys;
+    private String scope;
+    private boolean unsubscribe;
+
+    public abstract TelemetryFeature getType();
+
+    @Override
+    public String toString() {
+        return "SubscriptionCmd [entityType=" + entityType  + ", entityId=" + entityId + ", tags=" + keys + ", unsubscribe=" + unsubscribe + "]";
+    }
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/TelemetryPluginCmdsWrapper.java b/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/TelemetryPluginCmdsWrapper.java
new file mode 100644
index 0000000..2f5c037
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/TelemetryPluginCmdsWrapper.java
@@ -0,0 +1,58 @@
+/**
+ * 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.server.service.telemetry.cmd;
+
+import java.util.List;
+
+/**
+ * @author Andrew Shvayka
+ */
+public class TelemetryPluginCmdsWrapper {
+
+    private List<AttributesSubscriptionCmd> attrSubCmds;
+
+    private List<TimeseriesSubscriptionCmd> tsSubCmds;
+
+    private List<GetHistoryCmd> historyCmds;
+
+    public TelemetryPluginCmdsWrapper() {
+        super();
+    }
+
+    public List<AttributesSubscriptionCmd> getAttrSubCmds() {
+        return attrSubCmds;
+    }
+
+    public void setAttrSubCmds(List<AttributesSubscriptionCmd> attrSubCmds) {
+        this.attrSubCmds = attrSubCmds;
+    }
+
+    public List<TimeseriesSubscriptionCmd> getTsSubCmds() {
+        return tsSubCmds;
+    }
+
+    public void setTsSubCmds(List<TimeseriesSubscriptionCmd> tsSubCmds) {
+        this.tsSubCmds = tsSubCmds;
+    }
+
+    public List<GetHistoryCmd> getHistoryCmds() {
+        return historyCmds;
+    }
+
+    public void setHistoryCmds(List<GetHistoryCmd> historyCmds) {
+        this.historyCmds = historyCmds;
+    }
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionService.java b/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionService.java
new file mode 100644
index 0000000..0942b40
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionService.java
@@ -0,0 +1,710 @@
+/**
+ * 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.server.service.telemetry;
+
+import com.google.common.util.concurrent.FutureCallback;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.protobuf.InvalidProtocolBufferException;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Lazy;
+import org.springframework.stereotype.Service;
+import org.springframework.util.StringUtils;
+import org.thingsboard.rule.engine.api.util.DonAsynchron;
+import org.thingsboard.server.common.data.DataConstants;
+import org.thingsboard.server.common.data.EntityType;
+import org.thingsboard.server.common.data.id.DeviceId;
+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.BaseAttributeKvEntry;
+import org.thingsboard.server.common.data.kv.BaseTsKvQuery;
+import org.thingsboard.server.common.data.kv.BasicTsKvEntry;
+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.KvEntry;
+import org.thingsboard.server.common.data.kv.LongDataEntry;
+import org.thingsboard.server.common.data.kv.StringDataEntry;
+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.dao.attributes.AttributesService;
+import org.thingsboard.server.dao.timeseries.TimeseriesService;
+import org.thingsboard.server.gen.cluster.ClusterAPIProtos;
+import org.thingsboard.server.service.cluster.routing.ClusterRoutingService;
+import org.thingsboard.server.service.cluster.rpc.ClusterRpcService;
+import org.thingsboard.server.service.state.DefaultDeviceStateService;
+import org.thingsboard.server.service.state.DeviceStateService;
+import org.thingsboard.server.service.telemetry.sub.Subscription;
+import org.thingsboard.server.service.telemetry.sub.SubscriptionErrorCode;
+import org.thingsboard.server.service.telemetry.sub.SubscriptionState;
+import org.thingsboard.server.service.telemetry.sub.SubscriptionUpdate;
+
+import javax.annotation.Nullable;
+import javax.annotation.PostConstruct;
+import javax.annotation.PreDestroy;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.TreeMap;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.function.Consumer;
+import java.util.function.Function;
+import java.util.function.Predicate;
+import java.util.stream.Collectors;
+
+/**
+ * Created by ashvayka on 27.03.18.
+ */
+@Service
+@Slf4j
+public class DefaultTelemetrySubscriptionService implements TelemetrySubscriptionService {
+
+    @Autowired
+    private TelemetryWebSocketService wsService;
+
+    @Autowired
+    private AttributesService attrService;
+
+    @Autowired
+    private TimeseriesService tsService;
+
+    @Autowired
+    private ClusterRoutingService routingService;
+
+    @Autowired
+    private ClusterRpcService rpcService;
+
+    @Autowired
+    @Lazy
+    private DeviceStateService stateService;
+
+    private ExecutorService tsCallBackExecutor;
+    private ExecutorService wsCallBackExecutor;
+
+    @PostConstruct
+    public void initExecutor() {
+        tsCallBackExecutor = Executors.newSingleThreadExecutor();
+        wsCallBackExecutor = Executors.newSingleThreadExecutor();
+    }
+
+    @PreDestroy
+    public void shutdownExecutor() {
+        if (tsCallBackExecutor != null) {
+            tsCallBackExecutor.shutdownNow();
+        }
+        if (wsCallBackExecutor != null) {
+            wsCallBackExecutor.shutdownNow();
+        }
+    }
+
+    private final Map<EntityId, Set<Subscription>> subscriptionsByEntityId = new HashMap<>();
+    private final Map<String, Map<Integer, Subscription>> subscriptionsByWsSessionId = new HashMap<>();
+
+    @Override
+    public void addLocalWsSubscription(String sessionId, EntityId entityId, SubscriptionState sub) {
+        Optional<ServerAddress> server = routingService.resolveById(entityId);
+        Subscription subscription;
+        if (server.isPresent()) {
+            ServerAddress address = server.get();
+            log.trace("[{}] Forwarding subscription [{}] for device [{}] to [{}]", sessionId, sub.getSubscriptionId(), entityId, address);
+            subscription = new Subscription(sub, true, address);
+            tellNewSubscription(address, sessionId, subscription);
+        } else {
+            log.trace("[{}] Registering local subscription [{}] for device [{}]", sessionId, sub.getSubscriptionId(), entityId);
+            subscription = new Subscription(sub, true);
+        }
+        registerSubscription(sessionId, entityId, subscription);
+    }
+
+    @Override
+    public void cleanupLocalWsSessionSubscriptions(TelemetryWebSocketSessionRef sessionRef, String sessionId) {
+        cleanupLocalWsSessionSubscriptions(sessionId);
+    }
+
+    @Override
+    public void removeSubscription(String sessionId, int subscriptionId) {
+        log.debug("[{}][{}] Going to remove subscription.", sessionId, subscriptionId);
+        Map<Integer, Subscription> sessionSubscriptions = subscriptionsByWsSessionId.get(sessionId);
+        if (sessionSubscriptions != null) {
+            Subscription subscription = sessionSubscriptions.remove(subscriptionId);
+            if (subscription != null) {
+                processSubscriptionRemoval(sessionId, sessionSubscriptions, subscription);
+            } else {
+                log.debug("[{}][{}] Subscription not found!", sessionId, subscriptionId);
+            }
+        } else {
+            log.debug("[{}] No session subscriptions found!", sessionId);
+        }
+    }
+
+    @Override
+    public void saveAndNotify(EntityId entityId, List<TsKvEntry> ts, FutureCallback<Void> callback) {
+        saveAndNotify(entityId, ts, 0L, callback);
+    }
+
+    @Override
+    public void saveAndNotify(EntityId entityId, List<TsKvEntry> ts, long ttl, FutureCallback<Void> callback) {
+        ListenableFuture<List<Void>> saveFuture = tsService.save(entityId, ts, ttl);
+        addMainCallback(saveFuture, callback);
+        addWsCallback(saveFuture, success -> onTimeseriesUpdate(entityId, ts));
+    }
+
+    @Override
+    public void saveAndNotify(EntityId entityId, String scope, List<AttributeKvEntry> attributes, FutureCallback<Void> callback) {
+        ListenableFuture<List<Void>> saveFuture = attrService.save(entityId, scope, attributes);
+        addMainCallback(saveFuture, callback);
+        addWsCallback(saveFuture, success -> onAttributesUpdate(entityId, scope, attributes));
+    }
+
+    @Override
+    public void saveAttrAndNotify(EntityId entityId, String scope, String key, long value, FutureCallback<Void> callback) {
+        saveAndNotify(entityId, scope, Collections.singletonList(new BaseAttributeKvEntry(new LongDataEntry(key, value)
+                , System.currentTimeMillis())), callback);
+    }
+
+    @Override
+    public void saveAttrAndNotify(EntityId entityId, String scope, String key, String value, FutureCallback<Void> callback) {
+        saveAndNotify(entityId, scope, Collections.singletonList(new BaseAttributeKvEntry(new StringDataEntry(key, value)
+                , System.currentTimeMillis())), callback);
+    }
+
+    @Override
+    public void saveAttrAndNotify(EntityId entityId, String scope, String key, double value, FutureCallback<Void> callback) {
+        saveAndNotify(entityId, scope, Collections.singletonList(new BaseAttributeKvEntry(new DoubleDataEntry(key, value)
+                , System.currentTimeMillis())), callback);
+    }
+
+    @Override
+    public void saveAttrAndNotify(EntityId entityId, String scope, String key, boolean value, FutureCallback<Void> callback) {
+        saveAndNotify(entityId, scope, Collections.singletonList(new BaseAttributeKvEntry(new BooleanDataEntry(key, value)
+                , 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) {
+        Optional<ServerAddress> serverAddress = routingService.resolveById(entityId);
+        if (!serverAddress.isPresent()) {
+            onLocalAttributesUpdate(entityId, scope, attributes);
+            if (entityId.getEntityType() == EntityType.DEVICE && DataConstants.SERVER_SCOPE.equalsIgnoreCase(scope)) {
+                for (AttributeKvEntry attribute : attributes) {
+                    if (attribute.getKey().equals(DefaultDeviceStateService.INACTIVITY_TIMEOUT)) {
+                        stateService.onDeviceInactivityTimeoutUpdate(new DeviceId(entityId.getId()), attribute.getLongValue().orElse(0L));
+                    }
+                }
+            }
+        } else {
+            tellRemoteAttributesUpdate(serverAddress.get(), entityId, scope, attributes);
+        }
+    }
+
+    private void onTimeseriesUpdate(EntityId entityId, List<TsKvEntry> ts) {
+        Optional<ServerAddress> serverAddress = routingService.resolveById(entityId);
+        if (!serverAddress.isPresent()) {
+            onLocalTimeseriesUpdate(entityId, ts);
+        } else {
+            tellRemoteTimeseriesUpdate(serverAddress.get(), entityId, ts);
+        }
+    }
+
+    private void onLocalAttributesUpdate(EntityId entityId, String scope, List<AttributeKvEntry> attributes) {
+        onLocalSubUpdate(entityId, s -> TelemetryFeature.ATTRIBUTES == s.getType() && (StringUtils.isEmpty(s.getScope()) || scope.equals(s.getScope())), s -> {
+            List<TsKvEntry> subscriptionUpdate = null;
+            for (AttributeKvEntry kv : attributes) {
+                if (s.isAllKeys() || s.getKeyStates().containsKey(kv.getKey())) {
+                    if (subscriptionUpdate == null) {
+                        subscriptionUpdate = new ArrayList<>();
+                    }
+                    subscriptionUpdate.add(new BasicTsKvEntry(kv.getLastUpdateTs(), kv));
+                }
+            }
+            return subscriptionUpdate;
+        });
+    }
+
+    private void onLocalTimeseriesUpdate(EntityId entityId, List<TsKvEntry> ts) {
+        onLocalSubUpdate(entityId, s -> TelemetryFeature.TIMESERIES == s.getType(), s -> {
+            List<TsKvEntry> subscriptionUpdate = null;
+            for (TsKvEntry kv : ts) {
+                if (s.isAllKeys() || s.getKeyStates().containsKey((kv.getKey()))) {
+                    if (subscriptionUpdate == null) {
+                        subscriptionUpdate = new ArrayList<>();
+                    }
+                    subscriptionUpdate.add(kv);
+                }
+            }
+            return subscriptionUpdate;
+        });
+    }
+
+    private void onLocalSubUpdate(EntityId entityId, Predicate<Subscription> filter, Function<Subscription, List<TsKvEntry>> f) {
+        Set<Subscription> deviceSubscriptions = subscriptionsByEntityId.get(entityId);
+        if (deviceSubscriptions != null) {
+            deviceSubscriptions.stream().filter(filter).forEach(s -> {
+                String sessionId = s.getWsSessionId();
+                List<TsKvEntry> subscriptionUpdate = f.apply(s);
+                if (subscriptionUpdate == null || !subscriptionUpdate.isEmpty()) {
+                    SubscriptionUpdate update = new SubscriptionUpdate(s.getSubscriptionId(), subscriptionUpdate);
+                    if (s.isLocal()) {
+                        updateSubscriptionState(sessionId, s, update);
+                        wsService.sendWsMsg(sessionId, update);
+                    } else {
+                        tellRemoteSubUpdate(s.getServer(), sessionId, update);
+                    }
+                }
+            });
+        } else {
+            log.debug("[{}] No device subscriptions to process!", entityId);
+        }
+    }
+
+    private void updateSubscriptionState(String sessionId, Subscription subState, SubscriptionUpdate update) {
+        log.trace("[{}] updating subscription state {} using onUpdate {}", sessionId, subState, update);
+        update.getLatestValues().entrySet().forEach(e -> subState.setKeyState(e.getKey(), e.getValue()));
+    }
+
+    private void registerSubscription(String sessionId, EntityId entityId, Subscription subscription) {
+        Set<Subscription> deviceSubscriptions = subscriptionsByEntityId.computeIfAbsent(entityId, k -> new HashSet<>());
+        deviceSubscriptions.add(subscription);
+        Map<Integer, Subscription> sessionSubscriptions = subscriptionsByWsSessionId.computeIfAbsent(sessionId, k -> new HashMap<>());
+        sessionSubscriptions.put(subscription.getSubscriptionId(), subscription);
+    }
+
+    private void cleanupLocalWsSessionSubscriptions(String sessionId) {
+        cleanupWsSessionSubscriptions(sessionId, true);
+    }
+
+    private void cleanupRemoteWsSessionSubscriptions(String sessionId) {
+        cleanupWsSessionSubscriptions(sessionId, false);
+    }
+
+    private void cleanupWsSessionSubscriptions(String sessionId, boolean localSession) {
+        log.debug("[{}] Removing all subscriptions for particular session.", sessionId);
+        Map<Integer, Subscription> sessionSubscriptions = subscriptionsByWsSessionId.get(sessionId);
+        if (sessionSubscriptions != null) {
+            int sessionSubscriptionSize = sessionSubscriptions.size();
+
+            for (Subscription subscription : sessionSubscriptions.values()) {
+                EntityId entityId = subscription.getEntityId();
+                Set<Subscription> deviceSubscriptions = subscriptionsByEntityId.get(entityId);
+                deviceSubscriptions.remove(subscription);
+                if (deviceSubscriptions.isEmpty()) {
+                    subscriptionsByEntityId.remove(entityId);
+                }
+            }
+            subscriptionsByWsSessionId.remove(sessionId);
+            log.debug("[{}] Removed {} subscriptions for particular session.", sessionId, sessionSubscriptionSize);
+
+            if (localSession) {
+                notifyWsSubscriptionClosed(sessionId, sessionSubscriptions);
+            }
+        } else {
+            log.debug("[{}] No subscriptions found!", sessionId);
+        }
+    }
+
+    private void notifyWsSubscriptionClosed(String sessionId, Map<Integer, Subscription> sessionSubscriptions) {
+        Set<ServerAddress> affectedServers = new HashSet<>();
+        for (Subscription subscription : sessionSubscriptions.values()) {
+            if (subscription.getServer() != null) {
+                affectedServers.add(subscription.getServer());
+            }
+        }
+        for (ServerAddress address : affectedServers) {
+            log.debug("[{}] Going to onSubscriptionUpdate [{}] server about session close event", sessionId, address);
+            tellRemoteSessionClose(address, sessionId);
+        }
+    }
+
+    private void processSubscriptionRemoval(String sessionId, Map<Integer, Subscription> sessionSubscriptions, Subscription subscription) {
+        EntityId entityId = subscription.getEntityId();
+        if (subscription.isLocal() && subscription.getServer() != null) {
+            tellRemoteSubClose(subscription.getServer(), sessionId, subscription.getSubscriptionId());
+        }
+        if (sessionSubscriptions.isEmpty()) {
+            log.debug("[{}] Removed last subscription for particular session.", sessionId);
+            subscriptionsByWsSessionId.remove(sessionId);
+        } else {
+            log.debug("[{}] Removed session subscription.", sessionId);
+        }
+        Set<Subscription> deviceSubscriptions = subscriptionsByEntityId.get(entityId);
+        if (deviceSubscriptions != null) {
+            boolean result = deviceSubscriptions.remove(subscription);
+            if (result) {
+                if (deviceSubscriptions.size() == 0) {
+                    log.debug("[{}] Removed last subscription for particular device.", sessionId);
+                    subscriptionsByEntityId.remove(entityId);
+                } else {
+                    log.debug("[{}] Removed device subscription.", sessionId);
+                }
+            } else {
+                log.debug("[{}] Subscription not found!", sessionId);
+            }
+        } else {
+            log.debug("[{}] No device subscriptions found!", sessionId);
+        }
+    }
+
+    private void addMainCallback(ListenableFuture<List<Void>> saveFuture, final FutureCallback<Void> callback) {
+        Futures.addCallback(saveFuture, new FutureCallback<List<Void>>() {
+            @Override
+            public void onSuccess(@Nullable List<Void> result) {
+                callback.onSuccess(null);
+            }
+
+            @Override
+            public void onFailure(Throwable t) {
+                callback.onFailure(t);
+            }
+        }, tsCallBackExecutor);
+    }
+
+    private void addWsCallback(ListenableFuture<List<Void>> saveFuture, Consumer<Void> callback) {
+        Futures.addCallback(saveFuture, new FutureCallback<List<Void>>() {
+            @Override
+            public void onSuccess(@Nullable List<Void> result) {
+                callback.accept(null);
+            }
+
+            @Override
+            public void onFailure(Throwable t) {
+            }
+        }, 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);
+    }
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetryWebSocketService.java b/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetryWebSocketService.java
new file mode 100644
index 0000000..3b2d690
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetryWebSocketService.java
@@ -0,0 +1,560 @@
+/**
+ * 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.server.service.telemetry;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.google.common.base.Function;
+import com.google.common.util.concurrent.FutureCallback;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.util.StringUtils;
+import org.thingsboard.server.common.data.DataConstants;
+import org.thingsboard.server.common.data.id.EntityId;
+import org.thingsboard.server.common.data.id.EntityIdFactory;
+import org.thingsboard.server.common.data.kv.Aggregation;
+import org.thingsboard.server.common.data.kv.AttributeKvEntry;
+import org.thingsboard.server.common.data.kv.BaseTsKvQuery;
+import org.thingsboard.server.common.data.kv.BasicTsKvEntry;
+import org.thingsboard.server.common.data.kv.TsKvEntry;
+import org.thingsboard.server.common.data.kv.TsKvQuery;
+import org.thingsboard.server.dao.attributes.AttributesService;
+import org.thingsboard.server.dao.timeseries.TimeseriesService;
+import org.thingsboard.server.service.security.AccessValidator;
+import org.thingsboard.server.service.security.ValidationResult;
+import org.thingsboard.server.service.telemetry.cmd.AttributesSubscriptionCmd;
+import org.thingsboard.server.service.telemetry.cmd.GetHistoryCmd;
+import org.thingsboard.server.service.telemetry.cmd.SubscriptionCmd;
+import org.thingsboard.server.service.telemetry.cmd.TelemetryPluginCmd;
+import org.thingsboard.server.service.telemetry.cmd.TelemetryPluginCmdsWrapper;
+import org.thingsboard.server.service.telemetry.cmd.TimeseriesSubscriptionCmd;
+import org.thingsboard.server.service.telemetry.exception.UnauthorizedException;
+import org.thingsboard.server.service.telemetry.sub.SubscriptionErrorCode;
+import org.thingsboard.server.service.telemetry.sub.SubscriptionState;
+import org.thingsboard.server.service.telemetry.sub.SubscriptionUpdate;
+
+import javax.annotation.Nullable;
+import javax.annotation.PostConstruct;
+import javax.annotation.PreDestroy;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.function.Consumer;
+import java.util.stream.Collectors;
+
+/**
+ * Created by ashvayka on 27.03.18.
+ */
+@Service
+@Slf4j
+public class DefaultTelemetryWebSocketService implements TelemetryWebSocketService {
+
+    public static final int DEFAULT_LIMIT = 100;
+    public static final Aggregation DEFAULT_AGGREGATION = Aggregation.NONE;
+    private static final int UNKNOWN_SUBSCRIPTION_ID = 0;
+    private static final String PROCESSING_MSG = "[{}] Processing: {}";
+    private static final ObjectMapper jsonMapper = new ObjectMapper();
+    private static final String FAILED_TO_FETCH_DATA = "Failed to fetch data!";
+    private static final String FAILED_TO_FETCH_ATTRIBUTES = "Failed to fetch attributes!";
+    private static final String SESSION_META_DATA_NOT_FOUND = "Session meta-data not found!";
+
+    private final ConcurrentMap<String, WsSessionMetaData> wsSessionsMap = new ConcurrentHashMap<>();
+
+    @Autowired
+    private TelemetrySubscriptionService subscriptionManager;
+
+    @Autowired
+    private TelemetryWebSocketMsgEndpoint msgEndpoint;
+
+    @Autowired
+    private AccessValidator accessValidator;
+
+    @Autowired
+    private AttributesService attributesService;
+
+    @Autowired
+    private TimeseriesService tsService;
+
+    private ExecutorService executor;
+
+    @PostConstruct
+    public void initExecutor() {
+        executor = Executors.newSingleThreadExecutor();
+    }
+
+    @PreDestroy
+    public void shutdownExecutor() {
+        if (executor != null) {
+            executor.shutdownNow();
+        }
+    }
+
+    @Override
+    public void handleWebSocketSessionEvent(TelemetryWebSocketSessionRef sessionRef, SessionEvent event) {
+        String sessionId = sessionRef.getSessionId();
+        log.debug(PROCESSING_MSG, sessionId, event);
+        switch (event.getEventType()) {
+            case ESTABLISHED:
+                wsSessionsMap.put(sessionId, new WsSessionMetaData(sessionRef));
+                break;
+            case ERROR:
+                log.debug("[{}] Unknown websocket session error: {}. ", sessionId, event.getError().orElse(null));
+                break;
+            case CLOSED:
+                wsSessionsMap.remove(sessionId);
+                subscriptionManager.cleanupLocalWsSessionSubscriptions(sessionRef, sessionId);
+                break;
+        }
+    }
+
+    @Override
+    public void handleWebSocketMsg(TelemetryWebSocketSessionRef sessionRef, String msg) {
+        if (log.isTraceEnabled()) {
+            log.trace("[{}] Processing: {}", sessionRef.getSessionId(), msg);
+        }
+
+        try {
+            TelemetryPluginCmdsWrapper cmdsWrapper = jsonMapper.readValue(msg, TelemetryPluginCmdsWrapper.class);
+            if (cmdsWrapper != null) {
+                if (cmdsWrapper.getAttrSubCmds() != null) {
+                    cmdsWrapper.getAttrSubCmds().forEach(cmd -> handleWsAttributesSubscriptionCmd(sessionRef, cmd));
+                }
+                if (cmdsWrapper.getTsSubCmds() != null) {
+                    cmdsWrapper.getTsSubCmds().forEach(cmd -> handleWsTimeseriesSubscriptionCmd(sessionRef, cmd));
+                }
+                if (cmdsWrapper.getHistoryCmds() != null) {
+                    cmdsWrapper.getHistoryCmds().forEach(cmd -> handleWsHistoryCmd(sessionRef, cmd));
+                }
+            }
+        } catch (IOException e) {
+            log.warn("Failed to decode subscription cmd: {}", e.getMessage(), e);
+            SubscriptionUpdate update = new SubscriptionUpdate(UNKNOWN_SUBSCRIPTION_ID, SubscriptionErrorCode.INTERNAL_ERROR, SESSION_META_DATA_NOT_FOUND);
+            sendWsMsg(sessionRef, update);
+        }
+    }
+
+    @Override
+    public void sendWsMsg(String sessionId, SubscriptionUpdate update) {
+        WsSessionMetaData md = wsSessionsMap.get(sessionId);
+        if (md != null) {
+            sendWsMsg(md.getSessionRef(), update);
+        }
+    }
+
+    private void handleWsAttributesSubscriptionCmd(TelemetryWebSocketSessionRef sessionRef, AttributesSubscriptionCmd cmd) {
+        String sessionId = sessionRef.getSessionId();
+        log.debug("[{}] Processing: {}", sessionId, cmd);
+
+        if (validateSessionMetadata(sessionRef, cmd, sessionId)) {
+            if (cmd.isUnsubscribe()) {
+                unsubscribe(sessionRef, cmd, sessionId);
+            } else if (validateSubscriptionCmd(sessionRef, cmd)) {
+                EntityId entityId = EntityIdFactory.getByTypeAndId(cmd.getEntityType(), cmd.getEntityId());
+                log.debug("[{}] fetching latest attributes ({}) values for device: {}", sessionId, cmd.getKeys(), entityId);
+                Optional<Set<String>> keysOptional = getKeys(cmd);
+                if (keysOptional.isPresent()) {
+                    List<String> keys = new ArrayList<>(keysOptional.get());
+                    handleWsAttributesSubscriptionByKeys(sessionRef, cmd, sessionId, entityId, keys);
+                } else {
+                    handleWsAttributesSubscription(sessionRef, cmd, sessionId, entityId);
+                }
+            }
+        }
+    }
+
+    private void handleWsAttributesSubscriptionByKeys(TelemetryWebSocketSessionRef sessionRef,
+                                                      AttributesSubscriptionCmd cmd, String sessionId, EntityId entityId,
+                                                      List<String> keys) {
+        FutureCallback<List<AttributeKvEntry>> callback = new FutureCallback<List<AttributeKvEntry>>() {
+            @Override
+            public void onSuccess(List<AttributeKvEntry> data) {
+                List<TsKvEntry> attributesData = data.stream().map(d -> new BasicTsKvEntry(d.getLastUpdateTs(), d)).collect(Collectors.toList());
+                sendWsMsg(sessionRef, new SubscriptionUpdate(cmd.getCmdId(), attributesData));
+
+                Map<String, Long> subState = new HashMap<>(keys.size());
+                keys.forEach(key -> subState.put(key, 0L));
+                attributesData.forEach(v -> subState.put(v.getKey(), v.getTs()));
+
+                SubscriptionState sub = new SubscriptionState(sessionId, cmd.getCmdId(), entityId, TelemetryFeature.ATTRIBUTES, false, subState, cmd.getScope());
+                subscriptionManager.addLocalWsSubscription(sessionId, entityId, sub);
+            }
+
+            @Override
+            public void onFailure(Throwable e) {
+                log.error(FAILED_TO_FETCH_ATTRIBUTES, e);
+                SubscriptionUpdate update;
+                if (UnauthorizedException.class.isInstance(e)) {
+                    update = new SubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.UNAUTHORIZED,
+                            SubscriptionErrorCode.UNAUTHORIZED.getDefaultMsg());
+                } else {
+                    update = new SubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.INTERNAL_ERROR,
+                            FAILED_TO_FETCH_ATTRIBUTES);
+                }
+                sendWsMsg(sessionRef, update);
+            }
+        };
+
+        if (StringUtils.isEmpty(cmd.getScope())) {
+            accessValidator.validate(sessionRef.getSecurityCtx(), entityId, getAttributesFetchCallback(entityId, keys, callback));
+        } else {
+            accessValidator.validate(sessionRef.getSecurityCtx(), entityId, getAttributesFetchCallback(entityId, cmd.getScope(), keys, callback));
+        }
+    }
+
+    private void handleWsHistoryCmd(TelemetryWebSocketSessionRef sessionRef, GetHistoryCmd cmd) {
+        String sessionId = sessionRef.getSessionId();
+        WsSessionMetaData sessionMD = wsSessionsMap.get(sessionId);
+        if (sessionMD == null) {
+            log.warn("[{}] Session meta data not found. ", sessionId);
+            SubscriptionUpdate update = new SubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.INTERNAL_ERROR,
+                    SESSION_META_DATA_NOT_FOUND);
+            sendWsMsg(sessionRef, update);
+            return;
+        }
+        if (cmd.getEntityId() == null || cmd.getEntityId().isEmpty() || cmd.getEntityType() == null || cmd.getEntityType().isEmpty()) {
+            SubscriptionUpdate update = new SubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.BAD_REQUEST,
+                    "Device id is empty!");
+            sendWsMsg(sessionRef, update);
+            return;
+        }
+        if (cmd.getKeys() == null || cmd.getKeys().isEmpty()) {
+            SubscriptionUpdate update = new SubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.BAD_REQUEST,
+                    "Keys are empty!");
+            sendWsMsg(sessionRef, update);
+            return;
+        }
+        EntityId entityId = EntityIdFactory.getByTypeAndId(cmd.getEntityType(), cmd.getEntityId());
+        List<String> keys = new ArrayList<>(getKeys(cmd).orElse(Collections.emptySet()));
+        List<TsKvQuery> queries = keys.stream().map(key -> new BaseTsKvQuery(key, cmd.getStartTs(), cmd.getEndTs(), cmd.getInterval(), getLimit(cmd.getLimit()), getAggregation(cmd.getAgg())))
+                .collect(Collectors.toList());
+
+        FutureCallback<List<TsKvEntry>> callback = new FutureCallback<List<TsKvEntry>>() {
+            @Override
+            public void onSuccess(List<TsKvEntry> data) {
+                sendWsMsg(sessionRef, new SubscriptionUpdate(cmd.getCmdId(), data));
+            }
+
+            @Override
+            public void onFailure(Throwable e) {
+                SubscriptionUpdate update;
+                if (UnauthorizedException.class.isInstance(e)) {
+                    update = new SubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.UNAUTHORIZED,
+                            SubscriptionErrorCode.UNAUTHORIZED.getDefaultMsg());
+                } else {
+                    update = new SubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.INTERNAL_ERROR,
+                            FAILED_TO_FETCH_DATA);
+                }
+                sendWsMsg(sessionRef, update);
+            }
+        };
+        accessValidator.validate(sessionRef.getSecurityCtx(), entityId,
+                on(r -> Futures.addCallback(tsService.findAll(entityId, queries), callback, executor), callback::onFailure));
+    }
+
+    private void handleWsAttributesSubscription(TelemetryWebSocketSessionRef sessionRef,
+                                                AttributesSubscriptionCmd cmd, String sessionId, EntityId entityId) {
+        FutureCallback<List<AttributeKvEntry>> callback = new FutureCallback<List<AttributeKvEntry>>() {
+            @Override
+            public void onSuccess(List<AttributeKvEntry> data) {
+                List<TsKvEntry> attributesData = data.stream().map(d -> new BasicTsKvEntry(d.getLastUpdateTs(), d)).collect(Collectors.toList());
+                sendWsMsg(sessionRef, new SubscriptionUpdate(cmd.getCmdId(), attributesData));
+
+                Map<String, Long> subState = new HashMap<>(attributesData.size());
+                attributesData.forEach(v -> subState.put(v.getKey(), v.getTs()));
+
+                SubscriptionState sub = new SubscriptionState(sessionId, cmd.getCmdId(), entityId, TelemetryFeature.ATTRIBUTES, true, subState, cmd.getScope());
+                subscriptionManager.addLocalWsSubscription(sessionId, entityId, sub);
+            }
+
+            @Override
+            public void onFailure(Throwable e) {
+                log.error(FAILED_TO_FETCH_ATTRIBUTES, e);
+                SubscriptionUpdate update = new SubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.INTERNAL_ERROR,
+                        FAILED_TO_FETCH_ATTRIBUTES);
+                sendWsMsg(sessionRef, update);
+            }
+        };
+
+
+        if (StringUtils.isEmpty(cmd.getScope())) {
+            accessValidator.validate(sessionRef.getSecurityCtx(), entityId, getAttributesFetchCallback(entityId, callback));
+        } else {
+            accessValidator.validate(sessionRef.getSecurityCtx(), entityId, getAttributesFetchCallback(entityId, cmd.getScope(), callback));
+        }
+    }
+
+    private void handleWsTimeseriesSubscriptionCmd(TelemetryWebSocketSessionRef sessionRef, TimeseriesSubscriptionCmd cmd) {
+        String sessionId = sessionRef.getSessionId();
+        log.debug("[{}] Processing: {}", sessionId, cmd);
+
+        if (validateSessionMetadata(sessionRef, cmd, sessionId)) {
+            if (cmd.isUnsubscribe()) {
+                unsubscribe(sessionRef, cmd, sessionId);
+            } else if (validateSubscriptionCmd(sessionRef, cmd)) {
+                EntityId entityId = EntityIdFactory.getByTypeAndId(cmd.getEntityType(), cmd.getEntityId());
+                Optional<Set<String>> keysOptional = getKeys(cmd);
+
+                if (keysOptional.isPresent()) {
+                    handleWsTimeseriesSubscriptionByKeys(sessionRef, cmd, sessionId, entityId);
+                } else {
+                    handleWsTimeseriesSubscription(sessionRef, cmd, sessionId, entityId);
+                }
+            }
+        }
+    }
+
+    private void handleWsTimeseriesSubscriptionByKeys(TelemetryWebSocketSessionRef sessionRef,
+                                                      TimeseriesSubscriptionCmd cmd, String sessionId, EntityId entityId) {
+        long startTs;
+        if (cmd.getTimeWindow() > 0) {
+            List<String> keys = new ArrayList<>(getKeys(cmd).orElse(Collections.emptySet()));
+            log.debug("[{}] fetching timeseries data for last {} ms for keys: ({}) for device : {}", sessionId, cmd.getTimeWindow(), cmd.getKeys(), entityId);
+            startTs = cmd.getStartTs();
+            long endTs = cmd.getStartTs() + cmd.getTimeWindow();
+            List<TsKvQuery> queries = keys.stream().map(key -> new BaseTsKvQuery(key, startTs, endTs, cmd.getInterval(),
+                    getLimit(cmd.getLimit()), getAggregation(cmd.getAgg()))).collect(Collectors.toList());
+
+            final FutureCallback<List<TsKvEntry>> callback = getSubscriptionCallback(sessionRef, cmd, sessionId, entityId, startTs, keys);
+            accessValidator.validate(sessionRef.getSecurityCtx(), entityId,
+                    on(r -> Futures.addCallback(tsService.findAll(entityId, queries), callback, executor), callback::onFailure));
+        } else {
+            List<String> keys = new ArrayList<>(getKeys(cmd).orElse(Collections.emptySet()));
+            startTs = System.currentTimeMillis();
+            log.debug("[{}] fetching latest timeseries data for keys: ({}) for device : {}", sessionId, cmd.getKeys(), entityId);
+            final FutureCallback<List<TsKvEntry>> callback = getSubscriptionCallback(sessionRef, cmd, sessionId, entityId, startTs, keys);
+            accessValidator.validate(sessionRef.getSecurityCtx(), entityId,
+                    on(r -> Futures.addCallback(tsService.findLatest(entityId, keys), callback, executor), callback::onFailure));
+        }
+    }
+
+    private void handleWsTimeseriesSubscription(TelemetryWebSocketSessionRef sessionRef,
+                                                TimeseriesSubscriptionCmd cmd, String sessionId, EntityId entityId) {
+        FutureCallback<List<TsKvEntry>> callback = new FutureCallback<List<TsKvEntry>>() {
+            @Override
+            public void onSuccess(List<TsKvEntry> data) {
+                sendWsMsg(sessionRef, new SubscriptionUpdate(cmd.getCmdId(), data));
+                Map<String, Long> subState = new HashMap<>(data.size());
+                data.forEach(v -> subState.put(v.getKey(), v.getTs()));
+                SubscriptionState sub = new SubscriptionState(sessionId, cmd.getCmdId(), entityId, TelemetryFeature.TIMESERIES, true, subState, cmd.getScope());
+                subscriptionManager.addLocalWsSubscription(sessionId, entityId, sub);
+            }
+
+            @Override
+            public void onFailure(Throwable e) {
+                SubscriptionUpdate update;
+                if (UnauthorizedException.class.isInstance(e)) {
+                    update = new SubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.UNAUTHORIZED,
+                            SubscriptionErrorCode.UNAUTHORIZED.getDefaultMsg());
+                } else {
+                    update = new SubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.INTERNAL_ERROR,
+                            FAILED_TO_FETCH_DATA);
+                }
+                sendWsMsg(sessionRef, update);
+            }
+        };
+        accessValidator.validate(sessionRef.getSecurityCtx(), entityId,
+                on(r -> Futures.addCallback(tsService.findAllLatest(entityId), callback, executor), callback::onFailure));
+    }
+
+    private FutureCallback<List<TsKvEntry>> getSubscriptionCallback(final TelemetryWebSocketSessionRef sessionRef, final TimeseriesSubscriptionCmd cmd, final String sessionId, final EntityId entityId, final long startTs, final List<String> keys) {
+        return new FutureCallback<List<TsKvEntry>>() {
+            @Override
+            public void onSuccess(List<TsKvEntry> data) {
+                sendWsMsg(sessionRef, new SubscriptionUpdate(cmd.getCmdId(), data));
+
+                Map<String, Long> subState = new HashMap<>(keys.size());
+                keys.forEach(key -> subState.put(key, startTs));
+                data.forEach(v -> subState.put(v.getKey(), v.getTs()));
+                SubscriptionState sub = new SubscriptionState(sessionId, cmd.getCmdId(), entityId, TelemetryFeature.TIMESERIES, false, subState, cmd.getScope());
+                subscriptionManager.addLocalWsSubscription(sessionId, entityId, sub);
+            }
+
+            @Override
+            public void onFailure(Throwable e) {
+                log.error(FAILED_TO_FETCH_DATA, e);
+                SubscriptionUpdate update = new SubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.INTERNAL_ERROR,
+                        FAILED_TO_FETCH_DATA);
+                sendWsMsg(sessionRef, update);
+            }
+        };
+    }
+
+    private void unsubscribe(TelemetryWebSocketSessionRef sessionRef, SubscriptionCmd cmd, String sessionId) {
+        if (cmd.getEntityId() == null || cmd.getEntityId().isEmpty()) {
+            subscriptionManager.cleanupLocalWsSessionSubscriptions(sessionRef, sessionId);
+        } else {
+            subscriptionManager.removeSubscription(sessionId, cmd.getCmdId());
+        }
+    }
+
+    private boolean validateSubscriptionCmd(TelemetryWebSocketSessionRef sessionRef, SubscriptionCmd cmd) {
+        if (cmd.getEntityId() == null || cmd.getEntityId().isEmpty()) {
+            SubscriptionUpdate update = new SubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.BAD_REQUEST,
+                    "Device id is empty!");
+            sendWsMsg(sessionRef, update);
+            return false;
+        }
+        return true;
+    }
+
+    private boolean validateSessionMetadata(TelemetryWebSocketSessionRef sessionRef, SubscriptionCmd cmd, String sessionId) {
+        WsSessionMetaData sessionMD = wsSessionsMap.get(sessionId);
+        if (sessionMD == null) {
+            log.warn("[{}] Session meta data not found. ", sessionId);
+            SubscriptionUpdate update = new SubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.INTERNAL_ERROR,
+                    SESSION_META_DATA_NOT_FOUND);
+            sendWsMsg(sessionRef, update);
+            return false;
+        } else {
+            return true;
+        }
+    }
+
+    private void sendWsMsg(TelemetryWebSocketSessionRef sessionRef, SubscriptionUpdate update) {
+        try {
+            msgEndpoint.send(sessionRef, jsonMapper.writeValueAsString(update));
+        } catch (JsonProcessingException e) {
+            log.warn("[{}] Failed to encode reply: {}", sessionRef.getSessionId(), update, e);
+        } catch (IOException e) {
+            log.warn("[{}] Failed to send reply: {}", sessionRef.getSessionId(), update, e);
+        }
+    }
+
+    private static Optional<Set<String>> getKeys(TelemetryPluginCmd cmd) {
+        if (!StringUtils.isEmpty(cmd.getKeys())) {
+            Set<String> keys = new HashSet<>();
+            Collections.addAll(keys, cmd.getKeys().split(","));
+            return Optional.of(keys);
+        } else {
+            return Optional.empty();
+        }
+    }
+
+    private ListenableFuture<List<AttributeKvEntry>> mergeAllAttributesFutures(List<ListenableFuture<List<AttributeKvEntry>>> futures) {
+        return Futures.transform(Futures.successfulAsList(futures),
+                (Function<? super List<List<AttributeKvEntry>>, ? extends List<AttributeKvEntry>>) input -> {
+                    List<AttributeKvEntry> tmp = new ArrayList<>();
+                    if (input != null) {
+                        input.forEach(tmp::addAll);
+                    }
+                    return tmp;
+                }, executor);
+    }
+
+    private <T> FutureCallback<ValidationResult> getAttributesFetchCallback(final EntityId entityId, final List<String> keys, final FutureCallback<List<AttributeKvEntry>> callback) {
+        return new FutureCallback<ValidationResult>() {
+            @Override
+            public void onSuccess(@Nullable ValidationResult result) {
+                List<ListenableFuture<List<AttributeKvEntry>>> futures = new ArrayList<>();
+                for (String scope : DataConstants.allScopes()) {
+                    futures.add(attributesService.find(entityId, scope, keys));
+                }
+
+                ListenableFuture<List<AttributeKvEntry>> future = mergeAllAttributesFutures(futures);
+                Futures.addCallback(future, callback);
+            }
+
+            @Override
+            public void onFailure(Throwable t) {
+                callback.onFailure(t);
+            }
+        };
+    }
+
+    private <T> FutureCallback<ValidationResult> getAttributesFetchCallback(final EntityId entityId, final String scope, final List<String> keys, final FutureCallback<List<AttributeKvEntry>> callback) {
+        return new FutureCallback<ValidationResult>() {
+            @Override
+            public void onSuccess(@Nullable ValidationResult result) {
+                Futures.addCallback(attributesService.find(entityId, scope, keys), callback);
+            }
+
+            @Override
+            public void onFailure(Throwable t) {
+                callback.onFailure(t);
+            }
+        };
+    }
+
+    private <T> FutureCallback<ValidationResult> getAttributesFetchCallback(final EntityId entityId, final FutureCallback<List<AttributeKvEntry>> callback) {
+        return new FutureCallback<ValidationResult>() {
+            @Override
+            public void onSuccess(@Nullable ValidationResult result) {
+                List<ListenableFuture<List<AttributeKvEntry>>> futures = new ArrayList<>();
+                for (String scope : DataConstants.allScopes()) {
+                    futures.add(attributesService.findAll(entityId, scope));
+                }
+
+                ListenableFuture<List<AttributeKvEntry>> future = mergeAllAttributesFutures(futures);
+                Futures.addCallback(future, callback);
+            }
+
+            @Override
+            public void onFailure(Throwable t) {
+                callback.onFailure(t);
+            }
+        };
+    }
+
+    private <T> FutureCallback<ValidationResult> getAttributesFetchCallback(final EntityId entityId, final String scope, final FutureCallback<List<AttributeKvEntry>> callback) {
+        return new FutureCallback<ValidationResult>() {
+            @Override
+            public void onSuccess(@Nullable ValidationResult result) {
+                Futures.addCallback(attributesService.findAll(entityId, scope), callback);
+            }
+
+            @Override
+            public void onFailure(Throwable t) {
+                callback.onFailure(t);
+            }
+        };
+    }
+
+    private FutureCallback<ValidationResult> on(Consumer<ValidationResult> success, Consumer<Throwable> failure) {
+        return new FutureCallback<ValidationResult>() {
+            @Override
+            public void onSuccess(@Nullable ValidationResult result) {
+                success.accept(result);
+            }
+
+            @Override
+            public void onFailure(Throwable t) {
+                failure.accept(t);
+            }
+        };
+    }
+
+
+    private static Aggregation getAggregation(String agg) {
+        return StringUtils.isEmpty(agg) ? DEFAULT_AGGREGATION : Aggregation.valueOf(agg);
+    }
+
+    private int getLimit(int limit) {
+        return limit == 0 ? DEFAULT_LIMIT : limit;
+    }
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/sub/Subscription.java b/application/src/main/java/org/thingsboard/server/service/telemetry/sub/Subscription.java
new file mode 100644
index 0000000..811c055
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/telemetry/sub/Subscription.java
@@ -0,0 +1,78 @@
+/**
+ * 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.server.service.telemetry.sub;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import org.thingsboard.server.common.data.id.EntityId;
+import org.thingsboard.server.common.msg.cluster.ServerAddress;
+import org.thingsboard.server.service.telemetry.TelemetryFeature;
+
+import java.util.Map;
+
+@Data
+@AllArgsConstructor
+public class Subscription {
+
+    private final SubscriptionState sub;
+    private final boolean local;
+    private ServerAddress server;
+
+    public Subscription(SubscriptionState sub, boolean local) {
+        this(sub, local, null);
+    }
+
+    public String getWsSessionId() {
+        return getSub().getWsSessionId();
+    }
+
+    public int getSubscriptionId() {
+        return getSub().getSubscriptionId();
+    }
+
+    public EntityId getEntityId() {
+        return getSub().getEntityId();
+    }
+
+    public TelemetryFeature getType() {
+        return getSub().getType();
+    }
+
+    public String getScope() {
+        return getSub().getScope();
+    }
+
+    public boolean isAllKeys() {
+        return getSub().isAllKeys();
+    }
+
+    public Map<String, Long> getKeyStates() {
+        return getSub().getKeyStates();
+    }
+
+    public void setKeyState(String key, long ts) {
+        getSub().getKeyStates().put(key, ts);
+    }
+
+    @Override
+    public String toString() {
+        return "Subscription{" +
+                "sub=" + sub +
+                ", local=" + local +
+                ", server=" + server +
+                '}';
+    }
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/sub/SubscriptionErrorCode.java b/application/src/main/java/org/thingsboard/server/service/telemetry/sub/SubscriptionErrorCode.java
new file mode 100644
index 0000000..9da74d9
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/telemetry/sub/SubscriptionErrorCode.java
@@ -0,0 +1,50 @@
+/**
+ * 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.server.service.telemetry.sub;
+
+public enum SubscriptionErrorCode {
+
+    NO_ERROR(0), INTERNAL_ERROR(1, "Internal Server error!"), BAD_REQUEST(2, "Bad request"), UNAUTHORIZED(3, "Unauthorized");
+
+    private final int code;
+    private final String defaultMsg;
+
+    private SubscriptionErrorCode(int code) {
+        this(code, null);
+    }
+
+    private SubscriptionErrorCode(int code, String defaultMsg) {
+        this.code = code;
+        this.defaultMsg = defaultMsg;
+    }
+
+    public static SubscriptionErrorCode forCode(int code) {
+        for (SubscriptionErrorCode errorCode : SubscriptionErrorCode.values()) {
+            if (errorCode.getCode() == code) {
+                return errorCode;
+            }
+        }
+        throw new IllegalArgumentException("Invalid error code: " + code);
+    }
+
+    public int getCode() {
+        return code;
+    }
+
+    public String getDefaultMsg() {
+        return defaultMsg;
+    }
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/sub/SubscriptionState.java b/application/src/main/java/org/thingsboard/server/service/telemetry/sub/SubscriptionState.java
new file mode 100644
index 0000000..a088fa9
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/telemetry/sub/SubscriptionState.java
@@ -0,0 +1,70 @@
+/**
+ * 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.server.service.telemetry.sub;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import org.thingsboard.server.common.data.id.EntityId;
+import org.thingsboard.server.service.telemetry.TelemetryFeature;
+
+import java.util.Map;
+
+/**
+ * @author Andrew Shvayka
+ */
+@AllArgsConstructor
+public class SubscriptionState {
+
+    @Getter private final String wsSessionId;
+    @Getter private final int subscriptionId;
+    @Getter private final EntityId entityId;
+    @Getter private final TelemetryFeature type;
+    @Getter private final boolean allKeys;
+    @Getter private final Map<String, Long> keyStates;
+    @Getter private final String scope;
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+
+        SubscriptionState that = (SubscriptionState) o;
+
+        if (subscriptionId != that.subscriptionId) return false;
+        if (wsSessionId != null ? !wsSessionId.equals(that.wsSessionId) : that.wsSessionId != null) return false;
+        if (entityId != null ? !entityId.equals(that.entityId) : that.entityId != null) return false;
+        return type == that.type;
+    }
+
+    @Override
+    public int hashCode() {
+        int result = wsSessionId != null ? wsSessionId.hashCode() : 0;
+        result = 31 * result + subscriptionId;
+        result = 31 * result + (entityId != null ? entityId.hashCode() : 0);
+        result = 31 * result + (type != null ? type.hashCode() : 0);
+        return result;
+    }
+
+    @Override
+    public String toString() {
+        return "SubscriptionState{" +
+                "type=" + type +
+                ", entityId=" + entityId +
+                ", subscriptionId=" + subscriptionId +
+                ", wsSessionId='" + wsSessionId + '\'' +
+                '}';
+    }
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/sub/SubscriptionUpdate.java b/application/src/main/java/org/thingsboard/server/service/telemetry/sub/SubscriptionUpdate.java
new file mode 100644
index 0000000..bbabe7a
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/telemetry/sub/SubscriptionUpdate.java
@@ -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.
+ */
+package org.thingsboard.server.service.telemetry.sub;
+
+import org.thingsboard.server.common.data.kv.TsKvEntry;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.TreeMap;
+import java.util.stream.Collectors;
+
+public class SubscriptionUpdate {
+
+    private int subscriptionId;
+    private int errorCode;
+    private String errorMsg;
+    private Map<String, List<Object>> data;
+
+    public SubscriptionUpdate(int subscriptionId, List<TsKvEntry> data) {
+        super();
+        this.subscriptionId = subscriptionId;
+        this.data = new TreeMap<>();
+        if (data != null) {
+            for (TsKvEntry tsEntry : data) {
+                List<Object> values = this.data.computeIfAbsent(tsEntry.getKey(), k -> new ArrayList<>());
+                Object[] value = new Object[2];
+                value[0] = tsEntry.getTs();
+                value[1] = tsEntry.getValueAsString();
+                values.add(value);
+            }
+        }
+    }
+
+    public SubscriptionUpdate(int subscriptionId, Map<String, List<Object>> data) {
+        super();
+        this.subscriptionId = subscriptionId;
+        this.data = data;
+    }
+
+    public SubscriptionUpdate(int subscriptionId, SubscriptionErrorCode errorCode) {
+        this(subscriptionId, errorCode, null);
+    }
+
+    public SubscriptionUpdate(int subscriptionId, SubscriptionErrorCode errorCode, String errorMsg) {
+        super();
+        this.subscriptionId = subscriptionId;
+        this.errorCode = errorCode.getCode();
+        this.errorMsg = errorMsg != null ? errorMsg : errorCode.getDefaultMsg();
+    }
+
+    public int getSubscriptionId() {
+        return subscriptionId;
+    }
+
+    public Map<String, List<Object>> getData() {
+        return data;
+    }
+
+    public Map<String, Long> getLatestValues() {
+        if (data == null) {
+            return Collections.emptyMap();
+        } else {
+            return data.entrySet().stream().collect(Collectors.toMap(e -> e.getKey(), e -> {
+                List<Object> data = e.getValue();
+                Object[] latest = (Object[]) data.get(data.size() - 1);
+                return (long) latest[0];
+            }));
+        }
+    }
+
+    public int getErrorCode() {
+        return errorCode;
+    }
+
+    public String getErrorMsg() {
+        return errorMsg;
+    }
+
+    @Override
+    public String toString() {
+        return "SubscriptionUpdate [subscriptionId=" + subscriptionId + ", errorCode=" + errorCode + ", errorMsg=" + errorMsg + ", data="
+                + data + "]";
+    }
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/TelemetrySubscriptionService.java b/application/src/main/java/org/thingsboard/server/service/telemetry/TelemetrySubscriptionService.java
new file mode 100644
index 0000000..f1802ac
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/telemetry/TelemetrySubscriptionService.java
@@ -0,0 +1,47 @@
+/**
+ * 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.server.service.telemetry;
+
+import org.thingsboard.rule.engine.api.RuleEngineTelemetryService;
+import org.thingsboard.server.common.data.id.EntityId;
+import org.thingsboard.server.common.msg.cluster.ServerAddress;
+import org.thingsboard.server.service.telemetry.sub.SubscriptionState;
+
+/**
+ * Created by ashvayka on 27.03.18.
+ */
+public interface TelemetrySubscriptionService extends RuleEngineTelemetryService {
+
+    void addLocalWsSubscription(String sessionId, EntityId entityId, SubscriptionState sub);
+
+    void cleanupLocalWsSessionSubscriptions(TelemetryWebSocketSessionRef sessionRef, String sessionId);
+
+    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();
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/TelemetryWebSocketSessionRef.java b/application/src/main/java/org/thingsboard/server/service/telemetry/TelemetryWebSocketSessionRef.java
new file mode 100644
index 0000000..53438c5
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/telemetry/TelemetryWebSocketSessionRef.java
@@ -0,0 +1,68 @@
+/**
+ * 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.server.service.telemetry;
+
+import lombok.Getter;
+import org.thingsboard.server.service.security.model.SecurityUser;
+
+import java.net.InetSocketAddress;
+import java.util.Objects;
+
+/**
+ * Created by ashvayka on 27.03.18.
+ */
+public class TelemetryWebSocketSessionRef {
+
+    private static final long serialVersionUID = 1L;
+
+    @Getter
+    private final String sessionId;
+    @Getter
+    private final SecurityUser securityCtx;
+    @Getter
+    private final InetSocketAddress localAddress;
+    @Getter
+    private final InetSocketAddress remoteAddress;
+
+    public TelemetryWebSocketSessionRef(String sessionId, SecurityUser securityCtx, InetSocketAddress localAddress, InetSocketAddress remoteAddress) {
+        this.sessionId = sessionId;
+        this.securityCtx = securityCtx;
+        this.localAddress = localAddress;
+        this.remoteAddress = remoteAddress;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+        TelemetryWebSocketSessionRef that = (TelemetryWebSocketSessionRef) o;
+        return Objects.equals(sessionId, that.sessionId);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(sessionId);
+    }
+
+    @Override
+    public String toString() {
+        return "TelemetryWebSocketSessionRef{" +
+                "sessionId='" + sessionId + '\'' +
+                ", localAddress=" + localAddress +
+                ", remoteAddress=" + remoteAddress +
+                '}';
+    }
+}
diff --git a/application/src/main/proto/cluster.proto b/application/src/main/proto/cluster.proto
index e106d1b..ac96010 100644
--- a/application/src/main/proto/cluster.proto
+++ b/application/src/main/proto/cluster.proto
@@ -19,79 +19,112 @@ package cluster;
 option java_package = "org.thingsboard.server.gen.cluster";
 option java_outer_classname = "ClusterAPIProtos";
 
+service ClusterRpcService {
+  rpc handleMsgs(stream ClusterMessage) returns (stream ClusterMessage) {}
+}
+message ClusterMessage {
+  MessageType messageType = 1;
+  MessageMataInfo messageMetaInfo = 2;
+  ServerAddress serverAddress = 3;
+  bytes payload = 4;
+}
+
 message ServerAddress {
   string host = 1;
   int32 port = 2;
 }
 
-message Uid {
-  sint64 pluginUuidMsb = 1;
-  sint64 pluginUuidLsb = 2;
+message MessageMataInfo {
+  string payloadMetaInfo = 1;
+  repeated string tags = 2;
 }
 
-message PluginAddress {
-  Uid pluginId = 1;
-  Uid tenantId = 2;
+enum MessageType {
+
+  //Cluster control messages
+  RPC_SESSION_CREATE_REQUEST_MSG = 0;
+  TO_ALL_NODES_MSG = 1;
+  RPC_SESSION_TELL_MSG = 2;
+  RPC_BROADCAST_MSG = 3;
+  CONNECT_RPC_MESSAGE =4;
+
+  CLUSTER_ACTOR_MESSAGE = 5;
+  // Messages related to TelemetrySubscriptionService
+  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;
+  CLUSTER_RPC_FROM_DEVICE_RESPONSE_MESSAGE = 12;
 }
 
-message ToPluginRpcMessage {
-  PluginAddress address = 1;
-  int32 clazz = 2;
-  bytes data = 3;
+// 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 ToDeviceActorRpcMessage {
-  bytes data = 1;
+message SubscriptionUpdateProto {
+    string sessionId = 1;
+    int32 subscriptionId = 2;
+    int32 errorCode = 3;
+    string errorMsg = 4;
+    repeated SubscriptionUpdateValueListProto data = 5;
 }
 
-message ToDeviceSessionActorRpcMessage {
-  bytes data = 1;
+message AttributeUpdateProto {
+    string entityType = 1;
+    string entityId = 2;
+    string scope = 3;
+    repeated KeyValueProto data = 4;
 }
 
-message ToDeviceActorNotificationRpcMessage {
-  bytes data = 1;
+message TimeseriesUpdateProto {
+    string entityType = 1;
+    string entityId = 2;
+    repeated KeyValueProto data = 4;
 }
 
-message ToAllNodesRpcMessage {
-  bytes data = 1;
+message SessionCloseProto {
+    string sessionId = 1;
 }
 
-message ConnectRpcMessage {
-  ServerAddress serverAddress = 1;
+message SubscriptionCloseProto {
+    string sessionId = 1;
+    int32 subscriptionId = 2;
 }
 
-message ToDeviceRpcRequestRpcMessage {
-  PluginAddress address = 1;
-  Uid deviceTenantId = 2;
-  Uid deviceId = 3;
-
-  Uid msgId = 4;
-  bool oneway = 5;
-  int64 expTime = 6;
-  string method = 7;
-  string params = 8;
+message SubscriptionKetStateProto {
+    string key = 1;
+    int64 ts = 2;
 }
 
-message ToPluginRpcResponseRpcMessage {
-  PluginAddress address = 1;
-
-  Uid msgId = 2;
-  string response = 3;
-  string error = 4;
+message SubscriptionUpdateValueListProto {
+    string key = 1;
+    repeated int64 ts = 2;
+    repeated string value = 3;
 }
 
-message ToRpcServerMessage {
-  ConnectRpcMessage connectMsg = 1;
-  ToPluginRpcMessage toPluginRpcMsg = 2;
-  ToDeviceActorRpcMessage toDeviceActorRpcMsg = 3;
-  ToDeviceSessionActorRpcMessage toDeviceSessionActorRpcMsg = 4;
-  ToDeviceActorNotificationRpcMessage toDeviceActorNotificationRpcMsg = 5;
-  ToAllNodesRpcMessage toAllNodesRpcMsg = 6;
-  ToDeviceRpcRequestRpcMessage toDeviceRpcRequestRpcMsg = 7;
-  ToPluginRpcResponseRpcMessage toPluginRpcResponseRpcMsg = 8;
+message KeyValueProto {
+    string key = 1;
+    int64 ts = 2;
+    int32 valueType = 3;
+    string strValue = 4;
+    int64 longValue = 5;
+    double doubleValue = 6;
+    bool boolValue = 7;
 }
 
-service ClusterRpcService {
-  rpc handlePluginMsgs(stream ToRpcServerMessage) returns (stream ToRpcServerMessage) {}
+message FromDeviceRPCResponseProto {
+    int64 requestIdMSB = 1;
+    int64 requestIdLSB = 2;
+    string response = 3;
+    int32 error = 4;
 }
-
diff --git a/application/src/main/resources/logback.xml b/application/src/main/resources/logback.xml
index 1779912..978a570 100644
--- a/application/src/main/resources/logback.xml
+++ b/application/src/main/resources/logback.xml
@@ -25,7 +25,7 @@
         </encoder>
     </appender>
 
-    <logger name="org.thingsboard.server" level="TRACE" />
+    <logger name="org.thingsboard.server" level="INFO" />
     <logger name="akka" level="INFO" />
 
     <root level="INFO">
diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml
index 6a45f42..5653193 100644
--- a/application/src/main/resources/thingsboard.yml
+++ b/application/src/main/resources/thingsboard.yml
@@ -58,11 +58,13 @@ cluster:
   hash_function_name: "${CLUSTER_HASH_FUNCTION_NAME:murmur3_128}"
   # Amount of virtual nodes in consistent hash ring.
   vitrual_nodes_size: "${CLUSTER_VIRTUAL_NODES_SIZE:16}"
+  # Queue partition id for current node
+  partition_id: "${QUEUE_PARTITION_ID:0}"
 
 # Plugins configuration parameters
 plugins:
   # Comma seperated package list used during classpath scanning for plugins
-  scan_packages: "${PLUGINS_SCAN_PACKAGES:org.thingsboard.server.extensions}"
+  scan_packages: "${PLUGINS_SCAN_PACKAGES:org.thingsboard.server.extensions,org.thingsboard.rule.engine}"
 
 # JWT Token parameters
 security.jwt:
@@ -106,7 +108,7 @@ mqtt:
 # CoAP server parameters
 coap:
   # Enable/disable coap transport protocol.
-  enabled: "${COAP_ENABLED:true}"
+  enabled: "${COAP_ENABLED:false}"
   bind_address: "${COAP_BIND_ADDRESS:0.0.0.0}"
   bind_port: "${COAP_BIND_PORT:5683}"
   adaptor:  "${COAP_ADAPTOR_NAME:JsonCoapAdaptor}"
@@ -129,9 +131,28 @@ quota:
     whitelist: "${QUOTA_HOST_WHITELIST:localhost,127.0.0.1}"
     # Array of blacklist hosts
     blacklist: "${QUOTA_HOST_BLACKLIST:}"
-  log:
-    topSize: 10
-    intervalMin: 2
+    log:
+      topSize: 10
+      intervalMin: 2
+  rule:
+    tenant:
+      # Max allowed number of API requests in interval for single tenant
+      limit: "${QUOTA_TENANT_LIMIT:100000}"
+      # Interval duration
+      intervalMs: "${QUOTA_TENANT_INTERVAL_MS:60000}"
+      # Maximum silence duration for tenant after which Tenant removed from QuotaService. Must be bigger than intervalMs
+      ttlMs: "${QUOTA_TENANT_TTL_MS:60000}"
+      # Interval for scheduled task that cleans expired records. TTL is used for expiring
+      cleanPeriodMs: "${QUOTA_TENANT_CLEAN_PERIOD_MS:300000}"
+      # Enable Host API Limits
+      enabled: "${QUOTA_TENANT_ENABLED:false}"
+      # Array of whitelist tenants
+      whitelist: "${QUOTA_TENANT_WHITELIST:}"
+      # Array of blacklist tenants
+      blacklist: "${QUOTA_HOST_BLACKLIST:}"
+      log:
+        topSize: 10
+        intervalMin: 2
 
 database:
   type: "${DATABASE_TYPE:sql}" # cassandra OR sql
@@ -182,11 +203,18 @@ cassandra:
     default_fetch_size: "${CASSANDRA_DEFAULT_FETCH_SIZE:2000}"
     # Specify partitioning size for timestamp key-value storage. Example MINUTES, HOURS, DAYS, MONTHS
     ts_key_value_partitioning: "${TS_KV_PARTITIONING:MONTHS}"
+    ts_key_value_ttl: "${TS_KV_TTL:0}"
     buffer_size: "${CASSANDRA_QUERY_BUFFER_SIZE:200000}"
     concurrent_limit: "${CASSANDRA_QUERY_CONCURRENT_LIMIT:1000}"
     permit_max_wait_time: "${PERMIT_MAX_WAIT_TIME:120000}"
     rate_limit_print_interval_ms: "${CASSANDRA_QUERY_RATE_LIMIT_PRINT_MS:30000}"
 
+  queue:
+    msg.ttl: 604800 # 7 days
+    ack.ttl: 604800 # 7 days
+    partitions.ttl: 604800 # 7 days
+    partitioning: "HOURS"
+
 # SQL configuration parameters
 sql:
     # Specify executor service type used to perform timeseries insert tasks: SINGLE FIXED CACHED
@@ -199,25 +227,54 @@ actors:
   tenant:
     create_components_on_init: true
   session:
+    max_concurrent_sessions_per_device: "${ACTORS_MAX_CONCURRENT_SESSION_PER_DEVICE:1}"
     sync:
       # Default timeout for processing request using synchronous session (HTTP, CoAP) in milliseconds
       timeout: "${ACTORS_SESSION_SYNC_TIMEOUT:10000}"
-  plugin:
-    # Default timeout for termination of the plugin actor after it is stopped
-    termination.delay: "${ACTORS_PLUGIN_TERMINATION_DELAY:60000}"
-    # Default timeout for processing of particular message by particular plugin
-    processing.timeout: "${ACTORS_PLUGIN_TIMEOUT:60000}"
-    # Errors for particular actor are persisted once per specified amount of milliseconds
-    error_persist_frequency: "${ACTORS_PLUGIN_ERROR_FREQUENCY:3000}"
   rule:
-    # Default timeout for termination of the rule actor after it is stopped
-    termination.delay: "${ACTORS_RULE_TERMINATION_DELAY:30000}"
-    # Errors for particular actor are persisted once per specified amount of milliseconds
-    error_persist_frequency: "${ACTORS_RULE_ERROR_FREQUENCY:3000}"
+    # Specify thread pool size for database request callbacks executor service
+    db_callback_thread_pool_size: "${ACTORS_RULE_DB_CALLBACK_THREAD_POOL_SIZE:1}"
+    # Specify thread pool size for javascript executor service
+    js_thread_pool_size: "${ACTORS_RULE_JS_THREAD_POOL_SIZE:10}"
+    # Specify thread pool size for mail sender executor service
+    mail_thread_pool_size: "${ACTORS_RULE_MAIL_THREAD_POOL_SIZE:10}"
+    # Whether to allow usage of system mail service for rules
+    allow_system_mail_service: "${ACTORS_RULE_ALLOW_SYSTEM_MAIL_SERVICE:true}"
+    # Specify thread pool size for external call service
+    external_call_thread_pool_size: "${ACTORS_RULE_EXTERNAL_CALL_THREAD_POOL_SIZE:10}"
+    js_sandbox:
+      # Use Sandboxed (secured) JavaScript environment
+      use_js_sandbox: "${ACTORS_RULE_JS_SANDBOX_USE_JS_SANDBOX:true}"
+      # Specify thread pool size for JavaScript sandbox resource monitor
+      monitor_thread_pool_size: "${ACTORS_RULE_JS_SANDBOX_MONITOR_THREAD_POOL_SIZE:4}"
+      # Maximum CPU time in milliseconds allowed for script execution
+      max_cpu_time: "${ACTORS_RULE_JS_SANDBOX_MAX_CPU_TIME:100}"
+      # Maximum allowed JavaScript execution errors before JavaScript will be blacklisted
+      max_errors: "${ACTORS_RULE_JS_SANDBOX_MAX_ERRORS:3}"
+    chain:
+      # Errors for particular actor are persisted once per specified amount of milliseconds
+      error_persist_frequency: "${ACTORS_RULE_CHAIN_ERROR_FREQUENCY:3000}"
+    node:
+      # Errors for particular actor are persisted once per specified amount of milliseconds
+      error_persist_frequency: "${ACTORS_RULE_NODE_ERROR_FREQUENCY:3000}"
+    queue:
+      # Message queue type
+      type: "${ACTORS_RULE_QUEUE_TYPE:memory}"
+      # Message queue maximum size (per tenant)
+      max_size: "${ACTORS_RULE_QUEUE_MAX_SIZE:100}"
+      # Message queue cleanup period in seconds
+      cleanup_period: "${ACTORS_RULE_QUEUE_CLEANUP_PERIOD:3600}"
   statistics:
     # Enable/disable actor statistics
     enabled: "${ACTORS_STATISTICS_ENABLED:true}"
     persist_frequency: "${ACTORS_STATISTICS_PERSIST_FREQUENCY:3600000}"
+  queue:
+    # Enable/disable persistence of un-processed messages to the queue
+    enabled: "${ACTORS_QUEUE_ENABLED:true}"
+    # Maximum allowed timeout for persistence into the queue
+    timeout: "${ACTORS_QUEUE_PERSISTENCE_TIMEOUT:30000}"
+  client_side_rpc:
+    timeout:  "${CLIENT_SIDE_RPC_TIMEOUT:60000}"
 
 cache:
   # caffeine or redis
@@ -283,17 +340,17 @@ spring:
     database-platform: "${SPRING_JPA_DATABASE_PLATFORM:org.hibernate.dialect.HSQLDialect}"
   datasource:
     driverClassName: "${SPRING_DRIVER_CLASS_NAME:org.hsqldb.jdbc.JDBCDriver}"
-    url: "${SPRING_DATASOURCE_URL:jdbc:hsqldb:file:${SQL_DATA_FOLDER:/tmp}/thingsboardDb;sql.enforce_size=false}"
+    url: "${SPRING_DATASOURCE_URL:jdbc:hsqldb:file:${SQL_DATA_FOLDER:/tmp}/thingsboardDb;sql.enforce_size=false;hsqldb.log_size=5}"
     username: "${SPRING_DATASOURCE_USERNAME:sa}"
     password: "${SPRING_DATASOURCE_PASSWORD:}"
 
 # PostgreSQL DAO Configuration
 #spring:
 #  data:
-#    jpa:
+#    sql:
 #      repositories:
 #        enabled: "true"
-#  jpa:
+#  sql:
 #    hibernate:
 #      ddl-auto: "validate"
 #    database-platform: "${SPRING_JPA_DATABASE_PLATFORM:org.hibernate.dialect.PostgreSQLDialect}"
@@ -320,8 +377,7 @@ audit_log:
       "dashboard": "${AUDIT_LOG_MASK_DASHBOARD:W}"
       "customer": "${AUDIT_LOG_MASK_CUSTOMER:W}"
       "user": "${AUDIT_LOG_MASK_USER:W}"
-      "rule": "${AUDIT_LOG_MASK_RULE:W}"
-      "plugin": "${AUDIT_LOG_MASK_PLUGIN:W}"
+      "rule_chain": "${AUDIT_LOG_MASK_RULE_CHAIN:W}"
   sink:
     # Type of external sink. possible options: none, elasticsearch
     type: "${AUDIT_LOG_SINK_TYPE:none}"
@@ -337,4 +393,11 @@ audit_log:
     host: "${AUDIT_LOG_SINK_HOST:localhost}"
     port: "${AUDIT_LOG_SINK_POST:9200}"
     user_name: "${AUDIT_LOG_SINK_USER_NAME:}"
-    password: "${AUDIT_LOG_SINK_PASSWORD:}"
\ No newline at end of file
+    password: "${AUDIT_LOG_SINK_PASSWORD:}"
+
+state:
+  defaultInactivityTimeoutInSec: 10
+  defaultStateCheckIntervalInSec: 10
+# TODO in v2.1
+#  defaultStatePersistenceIntervalInSec: 60
+#  defaultStatePersistencePack: 100
\ No newline at end of file
diff --git a/application/src/test/java/org/thingsboard/server/controller/AbstractControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/AbstractControllerTest.java
index b92e464..3ec4dc8 100644
--- a/application/src/test/java/org/thingsboard/server/controller/AbstractControllerTest.java
+++ b/application/src/test/java/org/thingsboard/server/controller/AbstractControllerTest.java
@@ -96,6 +96,8 @@ import static org.springframework.test.web.servlet.setup.MockMvcBuilders.webAppC
 @Slf4j
 public abstract class AbstractControllerTest {
 
+    protected ObjectMapper mapper = new ObjectMapper();
+
     protected static final String TEST_TENANT_NAME = "TEST TENANT";
 
     protected static final String SYS_ADMIN_EMAIL = "sysadmin@thingsboard.org";
diff --git a/application/src/test/java/org/thingsboard/server/controller/AbstractRuleEngineControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/AbstractRuleEngineControllerTest.java
new file mode 100644
index 0000000..1b042a8
--- /dev/null
+++ b/application/src/test/java/org/thingsboard/server/controller/AbstractRuleEngineControllerTest.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.server.controller;
+
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.JsonNode;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.thingsboard.server.common.data.DataConstants;
+import org.thingsboard.server.common.data.Event;
+import org.thingsboard.server.common.data.id.EntityId;
+import org.thingsboard.server.common.data.id.RuleChainId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.page.TimePageData;
+import org.thingsboard.server.common.data.page.TimePageLink;
+import org.thingsboard.server.common.data.rule.RuleChain;
+import org.thingsboard.server.common.data.rule.RuleChainMetaData;
+import org.thingsboard.server.dao.queue.MsgQueue;
+import org.thingsboard.server.dao.rule.RuleChainService;
+import org.thingsboard.server.service.queue.MsgQueueService;
+
+import java.io.IOException;
+import java.util.function.Predicate;
+
+/**
+ * Created by ashvayka on 20.03.18.
+ */
+public class AbstractRuleEngineControllerTest extends AbstractControllerTest {
+
+    @Autowired
+    protected RuleChainService ruleChainService;
+
+    @Autowired
+    protected MsgQueueService msgQueueService;
+
+    protected RuleChain saveRuleChain(RuleChain ruleChain) throws Exception {
+        return doPost("/api/ruleChain", ruleChain, RuleChain.class);
+    }
+
+    protected RuleChain getRuleChain(RuleChainId ruleChainId) throws Exception {
+        return doGet("/api/ruleChain/" + ruleChainId.getId().toString(), RuleChain.class);
+    }
+
+    protected RuleChainMetaData saveRuleChainMetaData(RuleChainMetaData ruleChainMD) throws Exception {
+        return doPost("/api/ruleChain/metadata", ruleChainMD, RuleChainMetaData.class);
+    }
+
+    protected RuleChainMetaData getRuleChainMetaData(RuleChainId ruleChainId) throws Exception {
+        return doGet("/api/ruleChain/metadata/" + ruleChainId.getId().toString(), RuleChainMetaData.class);
+    }
+
+    protected TimePageData<Event> getDebugEvents(TenantId tenantId, EntityId entityId, int limit) throws Exception {
+        TimePageLink pageLink = new TimePageLink(limit);
+        return doGetTypedWithTimePageLink("/api/events/{entityType}/{entityId}/{eventType}?tenantId={tenantId}&",
+                new TypeReference<TimePageData<Event>>() {
+                }, pageLink, entityId.getEntityType(), entityId.getId(), DataConstants.DEBUG_RULE_NODE, tenantId.getId());
+    }
+
+    protected JsonNode getMetadata(Event outEvent) {
+        String metaDataStr = outEvent.getBody().get("metadata").asText();
+        try {
+            return mapper.readTree(metaDataStr);
+        } catch (IOException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    protected Predicate<Event> filterByCustomEvent() {
+        return event -> event.getBody().get("msgType").textValue().equals("CUSTOM");
+    }
+
+}
diff --git a/application/src/test/java/org/thingsboard/server/controller/BaseComponentDescriptorControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/BaseComponentDescriptorControllerTest.java
index 4346538..62f7562 100644
--- a/application/src/test/java/org/thingsboard/server/controller/BaseComponentDescriptorControllerTest.java
+++ b/application/src/test/java/org/thingsboard/server/controller/BaseComponentDescriptorControllerTest.java
@@ -20,14 +20,13 @@ import org.junit.After;
 import org.junit.Assert;
 import org.junit.Before;
 import org.junit.Test;
+import org.thingsboard.rule.engine.filter.TbJsFilterNode;
 import org.thingsboard.server.common.data.Tenant;
 import org.thingsboard.server.common.data.User;
 import org.thingsboard.server.common.data.plugin.ComponentDescriptor;
 import org.thingsboard.server.common.data.plugin.ComponentScope;
 import org.thingsboard.server.common.data.plugin.ComponentType;
 import org.thingsboard.server.common.data.security.Authority;
-import org.thingsboard.server.extensions.core.action.telemetry.TelemetryPluginAction;
-import org.thingsboard.server.extensions.core.plugin.telemetry.TelemetryStoragePlugin;
 
 import java.util.List;
 
@@ -35,7 +34,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
 
 public abstract class BaseComponentDescriptorControllerTest extends AbstractControllerTest {
 
-    private static final int AMOUNT_OF_DEFAULT_PLUGINS_DESCRIPTORS = 5;
+    private static final int AMOUNT_OF_DEFAULT_FILTER_NODES = 4;
     private Tenant savedTenant;
     private User tenantAdmin;
 
@@ -69,38 +68,28 @@ public abstract class BaseComponentDescriptorControllerTest extends AbstractCont
     @Test
     public void testGetByClazz() throws Exception {
         ComponentDescriptor descriptor =
-                doGet("/api/component/" + TelemetryStoragePlugin.class.getName(), ComponentDescriptor.class);
+                doGet("/api/component/" + TbJsFilterNode.class.getName(), ComponentDescriptor.class);
 
         Assert.assertNotNull(descriptor);
         Assert.assertNotNull(descriptor.getId());
         Assert.assertNotNull(descriptor.getName());
         Assert.assertEquals(ComponentScope.TENANT, descriptor.getScope());
-        Assert.assertEquals(ComponentType.PLUGIN, descriptor.getType());
+        Assert.assertEquals(ComponentType.FILTER, descriptor.getType());
         Assert.assertEquals(descriptor.getClazz(), descriptor.getClazz());
     }
 
     @Test
     public void testGetByType() throws Exception {
         List<ComponentDescriptor> descriptors = readResponse(
-                doGet("/api/components/" + ComponentType.PLUGIN).andExpect(status().isOk()), new TypeReference<List<ComponentDescriptor>>() {
+                doGet("/api/components/" + ComponentType.FILTER).andExpect(status().isOk()), new TypeReference<List<ComponentDescriptor>>() {
                 });
 
         Assert.assertNotNull(descriptors);
-        Assert.assertEquals(AMOUNT_OF_DEFAULT_PLUGINS_DESCRIPTORS, descriptors.size());
+        Assert.assertTrue(descriptors.size() >= AMOUNT_OF_DEFAULT_FILTER_NODES);
 
         for (ComponentType type : ComponentType.values()) {
             doGet("/api/components/" + type).andExpect(status().isOk());
         }
     }
 
-    @Test
-    public void testGetActionsByType() throws Exception {
-        List<ComponentDescriptor> descriptors = readResponse(
-                doGet("/api/components/actions/" + TelemetryStoragePlugin.class.getName()).andExpect(status().isOk()), new TypeReference<List<ComponentDescriptor>>() {
-                });
-
-        Assert.assertNotNull(descriptors);
-        Assert.assertEquals(1, descriptors.size());
-        Assert.assertEquals(TelemetryPluginAction.class.getName(), descriptors.get(0).getClazz());
-    }
 }
diff --git a/application/src/test/java/org/thingsboard/server/mqtt/rpc/AbstractMqttServerSideRpcIntegrationTest.java b/application/src/test/java/org/thingsboard/server/mqtt/rpc/AbstractMqttServerSideRpcIntegrationTest.java
index b732e66..f3e29dc 100644
--- a/application/src/test/java/org/thingsboard/server/mqtt/rpc/AbstractMqttServerSideRpcIntegrationTest.java
+++ b/application/src/test/java/org/thingsboard/server/mqtt/rpc/AbstractMqttServerSideRpcIntegrationTest.java
@@ -15,23 +15,27 @@
  */
 package org.thingsboard.server.mqtt.rpc;
 
-import java.util.Arrays;
-
 import com.datastax.driver.core.utils.UUIDs;
-import com.fasterxml.jackson.core.type.TypeReference;
 import lombok.extern.slf4j.Slf4j;
 import org.apache.commons.lang3.StringUtils;
-import org.eclipse.paho.client.mqttv3.*;
-import org.junit.*;
-import org.thingsboard.server.actors.plugin.PluginProcessingContext;
+import org.eclipse.paho.client.mqttv3.IMqttDeliveryToken;
+import org.eclipse.paho.client.mqttv3.MqttAsyncClient;
+import org.eclipse.paho.client.mqttv3.MqttCallback;
+import org.eclipse.paho.client.mqttv3.MqttConnectOptions;
+import org.eclipse.paho.client.mqttv3.MqttMessage;
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
 import org.thingsboard.server.common.data.Device;
 import org.thingsboard.server.common.data.Tenant;
 import org.thingsboard.server.common.data.User;
-import org.thingsboard.server.common.data.page.TextPageData;
-import org.thingsboard.server.common.data.plugin.PluginMetaData;
 import org.thingsboard.server.common.data.security.Authority;
 import org.thingsboard.server.common.data.security.DeviceCredentials;
 import org.thingsboard.server.controller.AbstractControllerTest;
+import org.thingsboard.server.service.security.AccessValidator;
+
+import java.util.Arrays;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
@@ -55,7 +59,7 @@ public abstract class AbstractMqttServerSideRpcIntegrationTest extends AbstractC
     public void beforeTest() throws Exception {
         loginSysAdmin();
 
-        asyncContextTimeoutToUseRpcPlugin = getAsyncContextTimeoutToUseRpcPlugin();
+        asyncContextTimeoutToUseRpcPlugin = 10000L;
 
         Tenant tenant = new Tenant();
         tenant.setTitle("My tenant");
@@ -131,7 +135,7 @@ public abstract class AbstractMqttServerSideRpcIntegrationTest extends AbstractC
 
         String result = doPostAsync("/api/plugins/rpc/oneway/" + nonExistentDeviceId, setGpioRequest, String.class,
                 status().isNotFound());
-        Assert.assertEquals(PluginProcessingContext.DEVICE_WITH_REQUESTED_ID_NOT_FOUND, result);
+        Assert.assertEquals(AccessValidator.DEVICE_WITH_REQUESTED_ID_NOT_FOUND, result);
     }
 
     @Test
@@ -186,7 +190,7 @@ public abstract class AbstractMqttServerSideRpcIntegrationTest extends AbstractC
 
         String result = doPostAsync("/api/plugins/rpc/twoway/" + nonExistentDeviceId, setGpioRequest, String.class,
                 status().isNotFound());
-        Assert.assertEquals(PluginProcessingContext.DEVICE_WITH_REQUESTED_ID_NOT_FOUND, result);
+        Assert.assertEquals(AccessValidator.DEVICE_WITH_REQUESTED_ID_NOT_FOUND, result);
     }
 
     private Device getSavedDevice(Device device) throws Exception {
@@ -197,13 +201,6 @@ public abstract class AbstractMqttServerSideRpcIntegrationTest extends AbstractC
         return doGet("/api/device/" + savedDevice.getId().getId().toString() + "/credentials", DeviceCredentials.class);
     }
 
-    private Long getAsyncContextTimeoutToUseRpcPlugin() throws Exception {
-        TextPageData<PluginMetaData> plugins = doGetTyped("/api/plugin/system?limit=1&textSearch=system rpc plugin",
-                new TypeReference<TextPageData<PluginMetaData>>(){});
-        Long systemRpcPluginTimeout = plugins.getData().iterator().next().getConfiguration().get("defaultTimeout").asLong();
-        return systemRpcPluginTimeout + TIME_TO_HANDLE_REQUEST;
-    }
-
     private static class TestMqttCallback implements MqttCallback {
 
         private final MqttAsyncClient client;
diff --git a/application/src/test/java/org/thingsboard/server/mqtt/rpc/sql/MqttServerSideRpcSqlIntegrationTest.java b/application/src/test/java/org/thingsboard/server/mqtt/rpc/sql/MqttServerSideRpcSqlIntegrationTest.java
index 7c9c058..f3d69ba 100644
--- a/application/src/test/java/org/thingsboard/server/mqtt/rpc/sql/MqttServerSideRpcSqlIntegrationTest.java
+++ b/application/src/test/java/org/thingsboard/server/mqtt/rpc/sql/MqttServerSideRpcSqlIntegrationTest.java
@@ -15,7 +15,6 @@
  */
 package org.thingsboard.server.mqtt.rpc.sql;
 
-import org.thingsboard.server.dao.service.DaoNoSqlTest;
 import org.thingsboard.server.dao.service.DaoSqlTest;
 import org.thingsboard.server.mqtt.rpc.AbstractMqttServerSideRpcIntegrationTest;
 
diff --git a/application/src/test/java/org/thingsboard/server/rules/flow/AbstractRuleEngineFlowIntegrationTest.java b/application/src/test/java/org/thingsboard/server/rules/flow/AbstractRuleEngineFlowIntegrationTest.java
new file mode 100644
index 0000000..c86d496
--- /dev/null
+++ b/application/src/test/java/org/thingsboard/server/rules/flow/AbstractRuleEngineFlowIntegrationTest.java
@@ -0,0 +1,322 @@
+/**
+ * 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.server.rules.flow;
+
+import com.datastax.driver.core.utils.UUIDs;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.google.common.collect.Lists;
+import lombok.Data;
+import lombok.extern.slf4j.Slf4j;
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.thingsboard.rule.engine.metadata.TbGetAttributesNodeConfiguration;
+import org.thingsboard.server.actors.service.ActorService;
+import org.thingsboard.server.common.data.*;
+import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry;
+import org.thingsboard.server.common.data.kv.StringDataEntry;
+import org.thingsboard.server.common.data.page.TextPageLink;
+import org.thingsboard.server.common.data.page.TimePageData;
+import org.thingsboard.server.common.data.rule.RuleChain;
+import org.thingsboard.server.common.data.rule.RuleChainMetaData;
+import org.thingsboard.server.common.data.rule.RuleNode;
+import org.thingsboard.server.common.data.security.Authority;
+import org.thingsboard.server.common.msg.TbMsg;
+import org.thingsboard.server.common.msg.TbMsgMetaData;
+import org.thingsboard.server.common.msg.system.ServiceToRuleEngineMsg;
+import org.thingsboard.server.controller.AbstractRuleEngineControllerTest;
+import org.thingsboard.server.dao.attributes.AttributesService;
+import org.thingsboard.server.dao.rule.RuleChainService;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.function.Predicate;
+import java.util.stream.Collectors;
+
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+/**
+ * @author Valerii Sosliuk
+ */
+@Slf4j
+public abstract class AbstractRuleEngineFlowIntegrationTest extends AbstractRuleEngineControllerTest {
+
+    protected Tenant savedTenant;
+    protected User tenantAdmin;
+
+    @Autowired
+    protected ActorService actorService;
+
+    @Autowired
+    protected AttributesService attributesService;
+
+    @Before
+    public void beforeTest() throws Exception {
+        loginSysAdmin();
+
+        Tenant tenant = new Tenant();
+        tenant.setTitle("My tenant");
+        savedTenant = doPost("/api/tenant", tenant, Tenant.class);
+        Assert.assertNotNull(savedTenant);
+        ruleChainService.deleteRuleChainsByTenantId(savedTenant.getId());
+
+        tenantAdmin = new User();
+        tenantAdmin.setAuthority(Authority.TENANT_ADMIN);
+        tenantAdmin.setTenantId(savedTenant.getId());
+        tenantAdmin.setEmail("tenant2@thingsboard.org");
+        tenantAdmin.setFirstName("Joe");
+        tenantAdmin.setLastName("Downs");
+
+        createUserAndLogin(tenantAdmin, "testPassword1");
+    }
+
+    @After
+    public void afterTest() throws Exception {
+        loginSysAdmin();
+        if (savedTenant != null) {
+            doDelete("/api/tenant/" + savedTenant.getId().getId().toString()).andExpect(status().isOk());
+        }
+    }
+
+    @Test
+    public void testRuleChainWithTwoRules() throws Exception {
+        // Creating Rule Chain
+        RuleChain ruleChain = new RuleChain();
+        ruleChain.setName("Simple Rule Chain");
+        ruleChain.setTenantId(savedTenant.getId());
+        ruleChain.setRoot(true);
+        ruleChain.setDebugMode(true);
+        ruleChain = saveRuleChain(ruleChain);
+        Assert.assertNull(ruleChain.getFirstRuleNodeId());
+
+        RuleChainMetaData metaData = new RuleChainMetaData();
+        metaData.setRuleChainId(ruleChain.getId());
+
+        RuleNode ruleNode1 = new RuleNode();
+        ruleNode1.setName("Simple Rule Node 1");
+        ruleNode1.setType(org.thingsboard.rule.engine.metadata.TbGetAttributesNode.class.getName());
+        ruleNode1.setDebugMode(true);
+        TbGetAttributesNodeConfiguration configuration1 = new TbGetAttributesNodeConfiguration();
+        configuration1.setServerAttributeNames(Collections.singletonList("serverAttributeKey1"));
+        ruleNode1.setConfiguration(mapper.valueToTree(configuration1));
+
+        RuleNode ruleNode2 = new RuleNode();
+        ruleNode2.setName("Simple Rule Node 2");
+        ruleNode2.setType(org.thingsboard.rule.engine.metadata.TbGetAttributesNode.class.getName());
+        ruleNode2.setDebugMode(true);
+        TbGetAttributesNodeConfiguration configuration2 = new TbGetAttributesNodeConfiguration();
+        configuration2.setServerAttributeNames(Collections.singletonList("serverAttributeKey2"));
+        ruleNode2.setConfiguration(mapper.valueToTree(configuration2));
+
+
+        metaData.setNodes(Arrays.asList(ruleNode1, ruleNode2));
+        metaData.setFirstNodeIndex(0);
+        metaData.addConnectionInfo(0, 1, "Success");
+        metaData = saveRuleChainMetaData(metaData);
+        Assert.assertNotNull(metaData);
+
+        ruleChain = getRuleChain(ruleChain.getId());
+        Assert.assertNotNull(ruleChain.getFirstRuleNodeId());
+
+        // Saving the device
+        Device device = new Device();
+        device.setName("My device");
+        device.setType("default");
+        device = doPost("/api/device", device, Device.class);
+
+        attributesService.save(device.getId(), DataConstants.SERVER_SCOPE,
+                Collections.singletonList(new BaseAttributeKvEntry(new StringDataEntry("serverAttributeKey1", "serverAttributeValue1"), System.currentTimeMillis())));
+        attributesService.save(device.getId(), DataConstants.SERVER_SCOPE,
+                Collections.singletonList(new BaseAttributeKvEntry(new StringDataEntry("serverAttributeKey2", "serverAttributeValue2"), System.currentTimeMillis())));
+
+
+        Thread.sleep(1000);
+
+        // Pushing Message to the system
+        TbMsg tbMsg = new TbMsg(UUIDs.timeBased(),
+                "CUSTOM",
+                device.getId(),
+                new TbMsgMetaData(),
+                "{}", null, null, 0L);
+        actorService.onMsg(new ServiceToRuleEngineMsg(savedTenant.getId(), tbMsg));
+
+        Thread.sleep(3000);
+
+        TimePageData<Event> eventsPage = getDebugEvents(savedTenant.getId(), ruleChain.getFirstRuleNodeId(), 1000);
+        List<Event> events = eventsPage.getData().stream().filter(filterByCustomEvent()).collect(Collectors.toList());
+        Assert.assertEquals(2, events.size());
+
+        Event inEvent = events.stream().filter(e -> e.getBody().get("type").asText().equals(DataConstants.IN)).findFirst().get();
+        Assert.assertEquals(ruleChain.getFirstRuleNodeId(), inEvent.getEntityId());
+        Assert.assertEquals(device.getId().getId().toString(), inEvent.getBody().get("entityId").asText());
+
+        Event outEvent = events.stream().filter(e -> e.getBody().get("type").asText().equals(DataConstants.OUT)).findFirst().get();
+        Assert.assertEquals(ruleChain.getFirstRuleNodeId(), outEvent.getEntityId());
+        Assert.assertEquals(device.getId().getId().toString(), outEvent.getBody().get("entityId").asText());
+
+        Assert.assertEquals("serverAttributeValue1", getMetadata(outEvent).get("ss_serverAttributeKey1").asText());
+
+        RuleChain finalRuleChain = ruleChain;
+        RuleNode lastRuleNode = metaData.getNodes().stream().filter(node -> !node.getId().equals(finalRuleChain.getFirstRuleNodeId())).findFirst().get();
+
+        eventsPage = getDebugEvents(savedTenant.getId(), lastRuleNode.getId(), 1000);
+        events = eventsPage.getData().stream().filter(filterByCustomEvent()).collect(Collectors.toList());
+
+        Assert.assertEquals(2, events.size());
+
+        inEvent = events.stream().filter(e -> e.getBody().get("type").asText().equals(DataConstants.IN)).findFirst().get();
+        Assert.assertEquals(lastRuleNode.getId(), inEvent.getEntityId());
+        Assert.assertEquals(device.getId().getId().toString(), inEvent.getBody().get("entityId").asText());
+
+        outEvent = events.stream().filter(e -> e.getBody().get("type").asText().equals(DataConstants.OUT)).findFirst().get();
+        Assert.assertEquals(lastRuleNode.getId(), outEvent.getEntityId());
+        Assert.assertEquals(device.getId().getId().toString(), outEvent.getBody().get("entityId").asText());
+
+        Assert.assertEquals("serverAttributeValue1", getMetadata(outEvent).get("ss_serverAttributeKey1").asText());
+        Assert.assertEquals("serverAttributeValue2", getMetadata(outEvent).get("ss_serverAttributeKey2").asText());
+
+        List<TbMsg> unAckMsgList = Lists.newArrayList(msgQueueService.findUnprocessed(savedTenant.getId(), ruleChain.getId().getId(), 0L));
+        Assert.assertEquals(0, unAckMsgList.size());
+    }
+
+    @Test
+    public void testTwoRuleChainsWithTwoRules() throws Exception {
+        // Creating Rule Chain
+        RuleChain rootRuleChain = new RuleChain();
+        rootRuleChain.setName("Root Rule Chain");
+        rootRuleChain.setTenantId(savedTenant.getId());
+        rootRuleChain.setRoot(true);
+        rootRuleChain.setDebugMode(true);
+        rootRuleChain = saveRuleChain(rootRuleChain);
+        Assert.assertNull(rootRuleChain.getFirstRuleNodeId());
+
+        // Creating Rule Chain
+        RuleChain secondaryRuleChain = new RuleChain();
+        secondaryRuleChain.setName("Secondary Rule Chain");
+        secondaryRuleChain.setTenantId(savedTenant.getId());
+        secondaryRuleChain.setRoot(false);
+        secondaryRuleChain.setDebugMode(true);
+        secondaryRuleChain = saveRuleChain(secondaryRuleChain);
+        Assert.assertNull(secondaryRuleChain.getFirstRuleNodeId());
+
+        RuleChainMetaData rootMetaData = new RuleChainMetaData();
+        rootMetaData.setRuleChainId(rootRuleChain.getId());
+
+        RuleNode ruleNode1 = new RuleNode();
+        ruleNode1.setName("Simple Rule Node 1");
+        ruleNode1.setType(org.thingsboard.rule.engine.metadata.TbGetAttributesNode.class.getName());
+        ruleNode1.setDebugMode(true);
+        TbGetAttributesNodeConfiguration configuration1 = new TbGetAttributesNodeConfiguration();
+        configuration1.setServerAttributeNames(Collections.singletonList("serverAttributeKey1"));
+        ruleNode1.setConfiguration(mapper.valueToTree(configuration1));
+
+        rootMetaData.setNodes(Collections.singletonList(ruleNode1));
+        rootMetaData.setFirstNodeIndex(0);
+        rootMetaData.addRuleChainConnectionInfo(0, secondaryRuleChain.getId(), "Success", mapper.createObjectNode());
+        rootMetaData = saveRuleChainMetaData(rootMetaData);
+        Assert.assertNotNull(rootMetaData);
+
+        rootRuleChain = getRuleChain(rootRuleChain.getId());
+        Assert.assertNotNull(rootRuleChain.getFirstRuleNodeId());
+
+
+        RuleChainMetaData secondaryMetaData = new RuleChainMetaData();
+        secondaryMetaData.setRuleChainId(secondaryRuleChain.getId());
+
+        RuleNode ruleNode2 = new RuleNode();
+        ruleNode2.setName("Simple Rule Node 2");
+        ruleNode2.setType(org.thingsboard.rule.engine.metadata.TbGetAttributesNode.class.getName());
+        ruleNode2.setDebugMode(true);
+        TbGetAttributesNodeConfiguration configuration2 = new TbGetAttributesNodeConfiguration();
+        configuration2.setServerAttributeNames(Collections.singletonList("serverAttributeKey2"));
+        ruleNode2.setConfiguration(mapper.valueToTree(configuration2));
+
+        secondaryMetaData.setNodes(Collections.singletonList(ruleNode2));
+        secondaryMetaData.setFirstNodeIndex(0);
+        secondaryMetaData = saveRuleChainMetaData(secondaryMetaData);
+        Assert.assertNotNull(secondaryMetaData);
+
+        // Saving the device
+        Device device = new Device();
+        device.setName("My device");
+        device.setType("default");
+        device = doPost("/api/device", device, Device.class);
+
+        attributesService.save(device.getId(), DataConstants.SERVER_SCOPE,
+                Collections.singletonList(new BaseAttributeKvEntry(new StringDataEntry("serverAttributeKey1", "serverAttributeValue1"), System.currentTimeMillis())));
+        attributesService.save(device.getId(), DataConstants.SERVER_SCOPE,
+                Collections.singletonList(new BaseAttributeKvEntry(new StringDataEntry("serverAttributeKey2", "serverAttributeValue2"), System.currentTimeMillis())));
+
+
+        Thread.sleep(1000);
+
+        // Pushing Message to the system
+        TbMsg tbMsg = new TbMsg(UUIDs.timeBased(),
+                "CUSTOM",
+                device.getId(),
+                new TbMsgMetaData(),
+                "{}", null, null, 0L);
+        actorService.onMsg(new ServiceToRuleEngineMsg(savedTenant.getId(), tbMsg));
+
+        Thread.sleep(3000);
+
+        TimePageData<Event> eventsPage = getDebugEvents(savedTenant.getId(), rootRuleChain.getFirstRuleNodeId(), 1000);
+        List<Event> events = eventsPage.getData().stream().filter(filterByCustomEvent()).collect(Collectors.toList());
+
+        Assert.assertEquals(2, events.size());
+
+        Event inEvent = events.stream().filter(e -> e.getBody().get("type").asText().equals(DataConstants.IN)).findFirst().get();
+        Assert.assertEquals(rootRuleChain.getFirstRuleNodeId(), inEvent.getEntityId());
+        Assert.assertEquals(device.getId().getId().toString(), inEvent.getBody().get("entityId").asText());
+
+        Event outEvent = events.stream().filter(e -> e.getBody().get("type").asText().equals(DataConstants.OUT)).findFirst().get();
+        Assert.assertEquals(rootRuleChain.getFirstRuleNodeId(), outEvent.getEntityId());
+        Assert.assertEquals(device.getId().getId().toString(), outEvent.getBody().get("entityId").asText());
+
+        Assert.assertEquals("serverAttributeValue1", getMetadata(outEvent).get("ss_serverAttributeKey1").asText());
+
+        RuleChain finalRuleChain = rootRuleChain;
+        RuleNode lastRuleNode = secondaryMetaData.getNodes().stream().filter(node -> !node.getId().equals(finalRuleChain.getFirstRuleNodeId())).findFirst().get();
+
+        eventsPage = getDebugEvents(savedTenant.getId(), lastRuleNode.getId(), 1000);
+        events = eventsPage.getData().stream().filter(filterByCustomEvent()).collect(Collectors.toList());
+
+
+        Assert.assertEquals(2, events.size());
+
+        inEvent = events.stream().filter(e -> e.getBody().get("type").asText().equals(DataConstants.IN)).findFirst().get();
+        Assert.assertEquals(lastRuleNode.getId(), inEvent.getEntityId());
+        Assert.assertEquals(device.getId().getId().toString(), inEvent.getBody().get("entityId").asText());
+
+        outEvent = events.stream().filter(e -> e.getBody().get("type").asText().equals(DataConstants.OUT)).findFirst().get();
+        Assert.assertEquals(lastRuleNode.getId(), outEvent.getEntityId());
+        Assert.assertEquals(device.getId().getId().toString(), outEvent.getBody().get("entityId").asText());
+
+        Assert.assertEquals("serverAttributeValue1", getMetadata(outEvent).get("ss_serverAttributeKey1").asText());
+        Assert.assertEquals("serverAttributeValue2", getMetadata(outEvent).get("ss_serverAttributeKey2").asText());
+
+        List<TbMsg> unAckMsgList = Lists.newArrayList(msgQueueService.findUnprocessed(savedTenant.getId(), rootRuleChain.getId().getId(), 0L));
+        Assert.assertEquals(0, unAckMsgList.size());
+
+        unAckMsgList = Lists.newArrayList(msgQueueService.findUnprocessed(savedTenant.getId(), secondaryRuleChain.getId().getId(), 0L));
+        Assert.assertEquals(0, unAckMsgList.size());
+    }
+
+}
diff --git a/application/src/test/java/org/thingsboard/server/rules/lifecycle/AbstractRuleEngineLifecycleIntegrationTest.java b/application/src/test/java/org/thingsboard/server/rules/lifecycle/AbstractRuleEngineLifecycleIntegrationTest.java
new file mode 100644
index 0000000..7ac0789
--- /dev/null
+++ b/application/src/test/java/org/thingsboard/server/rules/lifecycle/AbstractRuleEngineLifecycleIntegrationTest.java
@@ -0,0 +1,234 @@
+/**
+ * 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.server.rules.lifecycle;
+
+import com.datastax.driver.core.utils.UUIDs;
+import com.fasterxml.jackson.databind.JsonNode;
+import lombok.extern.slf4j.Slf4j;
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.thingsboard.rule.engine.metadata.TbGetAttributesNodeConfiguration;
+import org.thingsboard.server.actors.service.ActorService;
+import org.thingsboard.server.common.data.DataConstants;
+import org.thingsboard.server.common.data.Device;
+import org.thingsboard.server.common.data.Event;
+import org.thingsboard.server.common.data.Tenant;
+import org.thingsboard.server.common.data.User;
+import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry;
+import org.thingsboard.server.common.data.kv.StringDataEntry;
+import org.thingsboard.server.common.data.page.TimePageData;
+import org.thingsboard.server.common.data.rule.RuleChain;
+import org.thingsboard.server.common.data.rule.RuleChainMetaData;
+import org.thingsboard.server.common.data.rule.RuleNode;
+import org.thingsboard.server.common.data.security.Authority;
+import org.thingsboard.server.common.msg.TbMsg;
+import org.thingsboard.server.common.msg.TbMsgMetaData;
+import org.thingsboard.server.common.msg.system.ServiceToRuleEngineMsg;
+import org.thingsboard.server.controller.AbstractRuleEngineControllerTest;
+import org.thingsboard.server.dao.attributes.AttributesService;
+
+import java.io.IOException;
+import java.util.Collections;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+/**
+ * @author Valerii Sosliuk
+ */
+@Slf4j
+public abstract class AbstractRuleEngineLifecycleIntegrationTest extends AbstractRuleEngineControllerTest {
+
+    protected Tenant savedTenant;
+    protected User tenantAdmin;
+
+    @Autowired
+    protected ActorService actorService;
+
+    @Autowired
+    protected AttributesService attributesService;
+
+    @Before
+    public void beforeTest() throws Exception {
+        loginSysAdmin();
+
+        Tenant tenant = new Tenant();
+        tenant.setTitle("My tenant");
+        savedTenant = doPost("/api/tenant", tenant, Tenant.class);
+        Assert.assertNotNull(savedTenant);
+        ruleChainService.deleteRuleChainsByTenantId(savedTenant.getId());
+
+        tenantAdmin = new User();
+        tenantAdmin.setAuthority(Authority.TENANT_ADMIN);
+        tenantAdmin.setTenantId(savedTenant.getId());
+        tenantAdmin.setEmail("tenant2@thingsboard.org");
+        tenantAdmin.setFirstName("Joe");
+        tenantAdmin.setLastName("Downs");
+
+        createUserAndLogin(tenantAdmin, "testPassword1");
+    }
+
+    @After
+    public void afterTest() throws Exception {
+        loginSysAdmin();
+        if (savedTenant != null) {
+            doDelete("/api/tenant/" + savedTenant.getId().getId().toString()).andExpect(status().isOk());
+        }
+    }
+
+    @Test
+    public void testRuleChainWithOneRule() throws Exception {
+        // Creating Rule Chain
+        RuleChain ruleChain = new RuleChain();
+        ruleChain.setName("Simple Rule Chain");
+        ruleChain.setTenantId(savedTenant.getId());
+        ruleChain.setRoot(true);
+        ruleChain.setDebugMode(true);
+        ruleChain = saveRuleChain(ruleChain);
+        Assert.assertNull(ruleChain.getFirstRuleNodeId());
+
+        RuleChainMetaData metaData = new RuleChainMetaData();
+        metaData.setRuleChainId(ruleChain.getId());
+
+        RuleNode ruleNode = new RuleNode();
+        ruleNode.setName("Simple Rule Node");
+        ruleNode.setType(org.thingsboard.rule.engine.metadata.TbGetAttributesNode.class.getName());
+        ruleNode.setDebugMode(true);
+        TbGetAttributesNodeConfiguration configuration = new TbGetAttributesNodeConfiguration();
+        configuration.setServerAttributeNames(Collections.singletonList("serverAttributeKey"));
+        ruleNode.setConfiguration(mapper.valueToTree(configuration));
+
+        metaData.setNodes(Collections.singletonList(ruleNode));
+        metaData.setFirstNodeIndex(0);
+
+        metaData = saveRuleChainMetaData(metaData);
+        Assert.assertNotNull(metaData);
+
+        ruleChain = getRuleChain(ruleChain.getId());
+        Assert.assertNotNull(ruleChain.getFirstRuleNodeId());
+
+        // Saving the device
+        Device device = new Device();
+        device.setName("My device");
+        device.setType("default");
+        device = doPost("/api/device", device, Device.class);
+
+        attributesService.save(device.getId(), DataConstants.SERVER_SCOPE,
+                Collections.singletonList(new BaseAttributeKvEntry(new StringDataEntry("serverAttributeKey", "serverAttributeValue"), System.currentTimeMillis())));
+
+        Thread.sleep(1000);
+
+        // Pushing Message to the system
+        TbMsg tbMsg = new TbMsg(UUIDs.timeBased(),
+                "CUSTOM",
+                device.getId(),
+                new TbMsgMetaData(),
+                "{}",
+                null, null, 0L);
+        actorService.onMsg(new ServiceToRuleEngineMsg(savedTenant.getId(), tbMsg));
+
+        Thread.sleep(3000);
+
+        TimePageData<Event> eventsPage = getDebugEvents(savedTenant.getId(), ruleChain.getFirstRuleNodeId(), 1000);
+        List<Event> events = eventsPage.getData().stream().filter(filterByCustomEvent()).collect(Collectors.toList());
+
+        Assert.assertEquals(2, events.size());
+
+        Event inEvent = events.stream().filter(e -> e.getBody().get("type").asText().equals(DataConstants.IN)).findFirst().get();
+        Assert.assertEquals(ruleChain.getFirstRuleNodeId(), inEvent.getEntityId());
+        Assert.assertEquals(device.getId().getId().toString(), inEvent.getBody().get("entityId").asText());
+
+        Event outEvent = events.stream().filter(e -> e.getBody().get("type").asText().equals(DataConstants.OUT)).findFirst().get();
+        Assert.assertEquals(ruleChain.getFirstRuleNodeId(), outEvent.getEntityId());
+        Assert.assertEquals(device.getId().getId().toString(), outEvent.getBody().get("entityId").asText());
+
+        Assert.assertEquals("serverAttributeValue", getMetadata(outEvent).get("ss_serverAttributeKey").asText());
+    }
+
+    @Test
+    public void testRuleChainWithOneRuleAndMsgFromQueue() throws Exception {
+        // Creating Rule Chain
+        RuleChain ruleChain = new RuleChain();
+        ruleChain.setName("Simple Rule Chain");
+        ruleChain.setTenantId(savedTenant.getId());
+        ruleChain.setRoot(true);
+        ruleChain.setDebugMode(true);
+        ruleChain = saveRuleChain(ruleChain);
+        Assert.assertNull(ruleChain.getFirstRuleNodeId());
+
+        // Saving the device
+        Device device = new Device();
+        device.setName("My device");
+        device.setType("default");
+        device = doPost("/api/device", device, Device.class);
+
+        attributesService.save(device.getId(), DataConstants.SERVER_SCOPE,
+                Collections.singletonList(new BaseAttributeKvEntry(new StringDataEntry("serverAttributeKey", "serverAttributeValue"), System.currentTimeMillis())));
+
+        // Pushing Message to the system
+        TbMsg tbMsg = new TbMsg(UUIDs.timeBased(),
+                "CUSTOM",
+                device.getId(),
+                new TbMsgMetaData(),
+                "{}",
+                ruleChain.getId(), null, 0L);
+        msgQueueService.put(device.getTenantId(), tbMsg, ruleChain.getId().getId(), 0L);
+
+        Thread.sleep(1000);
+
+        RuleChainMetaData metaData = new RuleChainMetaData();
+        metaData.setRuleChainId(ruleChain.getId());
+
+        RuleNode ruleNode = new RuleNode();
+        ruleNode.setName("Simple Rule Node");
+        ruleNode.setType(org.thingsboard.rule.engine.metadata.TbGetAttributesNode.class.getName());
+        ruleNode.setDebugMode(true);
+        TbGetAttributesNodeConfiguration configuration = new TbGetAttributesNodeConfiguration();
+        configuration.setServerAttributeNames(Collections.singletonList("serverAttributeKey"));
+        ruleNode.setConfiguration(mapper.valueToTree(configuration));
+
+        metaData.setNodes(Collections.singletonList(ruleNode));
+        metaData.setFirstNodeIndex(0);
+
+        metaData = saveRuleChainMetaData(metaData);
+        Assert.assertNotNull(metaData);
+
+        ruleChain = getRuleChain(ruleChain.getId());
+        Assert.assertNotNull(ruleChain.getFirstRuleNodeId());
+
+        Thread.sleep(3000);
+
+        TimePageData<Event> eventsPage = getDebugEvents(savedTenant.getId(), ruleChain.getFirstRuleNodeId(), 1000);
+        List<Event> events = eventsPage.getData().stream().filter(filterByCustomEvent()).collect(Collectors.toList());
+
+        Assert.assertEquals(2, events.size());
+
+        Event inEvent = events.stream().filter(e -> e.getBody().get("type").asText().equals(DataConstants.IN)).findFirst().get();
+        Assert.assertEquals(ruleChain.getFirstRuleNodeId(), inEvent.getEntityId());
+        Assert.assertEquals(device.getId().getId().toString(), inEvent.getBody().get("entityId").asText());
+
+        Event outEvent = events.stream().filter(e -> e.getBody().get("type").asText().equals(DataConstants.OUT)).findFirst().get();
+        Assert.assertEquals(ruleChain.getFirstRuleNodeId(), outEvent.getEntityId());
+        Assert.assertEquals(device.getId().getId().toString(), outEvent.getBody().get("entityId").asText());
+
+        Assert.assertEquals("serverAttributeValue", getMetadata(outEvent).get("ss_serverAttributeKey").asText());
+    }
+
+}
diff --git a/application/src/test/java/org/thingsboard/server/rules/RuleEngineNoSqlTestSuite.java b/application/src/test/java/org/thingsboard/server/rules/RuleEngineNoSqlTestSuite.java
new file mode 100644
index 0000000..bffe491
--- /dev/null
+++ b/application/src/test/java/org/thingsboard/server/rules/RuleEngineNoSqlTestSuite.java
@@ -0,0 +1,42 @@
+/**
+ * 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.server.rules;
+
+import org.cassandraunit.dataset.cql.ClassPathCQLDataSet;
+import org.junit.ClassRule;
+import org.junit.extensions.cpsuite.ClasspathSuite;
+import org.junit.runner.RunWith;
+import org.thingsboard.server.dao.CustomCassandraCQLUnit;
+import org.thingsboard.server.dao.CustomSqlUnit;
+
+import java.util.Arrays;
+
+@RunWith(ClasspathSuite.class)
+@ClasspathSuite.ClassnameFilters({
+        "org.thingsboard.server.rules.flow.nosql.*Test",
+        "org.thingsboard.server.rules.lifecycle.nosql.*Test"
+})
+public class RuleEngineNoSqlTestSuite {
+
+    @ClassRule
+    public static CustomCassandraCQLUnit cassandraUnit =
+            new CustomCassandraCQLUnit(
+                    Arrays.asList(
+                            new ClassPathCQLDataSet("cassandra/schema.cql", false, false),
+                            new ClassPathCQLDataSet("cassandra/system-data.cql", false, false)),
+                    "cassandra-test.yaml", 30000l);
+
+}
diff --git a/application/src/test/java/org/thingsboard/server/rules/RuleEngineSqlTestSuite.java b/application/src/test/java/org/thingsboard/server/rules/RuleEngineSqlTestSuite.java
new file mode 100644
index 0000000..7b13e2f
--- /dev/null
+++ b/application/src/test/java/org/thingsboard/server/rules/RuleEngineSqlTestSuite.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.server.rules;
+
+import org.junit.ClassRule;
+import org.junit.extensions.cpsuite.ClasspathSuite;
+import org.junit.runner.RunWith;
+import org.thingsboard.server.dao.CustomSqlUnit;
+
+import java.util.Arrays;
+
+@RunWith(ClasspathSuite.class)
+@ClasspathSuite.ClassnameFilters({
+        "org.thingsboard.server.rules.flow.sql.*Test",
+        "org.thingsboard.server.rules.lifecycle.sql.*Test"})
+public class RuleEngineSqlTestSuite {
+
+    @ClassRule
+    public static CustomSqlUnit sqlUnit = new CustomSqlUnit(
+            Arrays.asList("sql/schema.sql", "sql/system-data.sql"),
+            "sql/drop-all-tables.sql",
+            "sql-test.properties");
+}
diff --git a/application/src/test/java/org/thingsboard/server/service/mail/TestMailService.java b/application/src/test/java/org/thingsboard/server/service/mail/TestMailService.java
index ed3750d..ba2bb65 100644
--- a/application/src/test/java/org/thingsboard/server/service/mail/TestMailService.java
+++ b/application/src/test/java/org/thingsboard/server/service/mail/TestMailService.java
@@ -22,7 +22,8 @@ import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
 import org.springframework.context.annotation.Primary;
 import org.springframework.context.annotation.Profile;
-import org.thingsboard.server.exception.ThingsboardException;
+import org.thingsboard.rule.engine.api.MailService;
+import org.thingsboard.server.common.data.exception.ThingsboardException;
 
 @Profile("test")
 @Configuration
diff --git a/application/src/test/java/org/thingsboard/server/service/script/RuleNodeJsScriptEngineTest.java b/application/src/test/java/org/thingsboard/server/service/script/RuleNodeJsScriptEngineTest.java
new file mode 100644
index 0000000..ea70384
--- /dev/null
+++ b/application/src/test/java/org/thingsboard/server/service/script/RuleNodeJsScriptEngineTest.java
@@ -0,0 +1,171 @@
+/**
+ * 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.server.service.script;
+
+import com.datastax.driver.core.utils.UUIDs;
+import com.google.common.collect.Sets;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.thingsboard.rule.engine.api.ScriptEngine;
+import org.thingsboard.server.common.msg.TbMsg;
+import org.thingsboard.server.common.msg.TbMsgMetaData;
+
+import javax.script.ScriptException;
+
+import java.util.Set;
+
+import static org.junit.Assert.*;
+
+public class RuleNodeJsScriptEngineTest {
+
+    private ScriptEngine scriptEngine;
+    private TestNashornJsSandboxService jsSandboxService;
+
+    @Before
+    public void beforeTest() throws Exception {
+        jsSandboxService = new TestNashornJsSandboxService(false, 1, 100, 3);
+    }
+
+    @After
+    public void afterTest() throws Exception {
+        jsSandboxService.stop();
+    }
+
+    @Test
+    public void msgCanBeUpdated() throws ScriptException {
+        String function = "metadata.temp = metadata.temp * 10; return {metadata: metadata};";
+        scriptEngine = new RuleNodeJsScriptEngine(jsSandboxService, function);
+
+        TbMsgMetaData metaData = new TbMsgMetaData();
+        metaData.putValue("temp", "7");
+        metaData.putValue("humidity", "99");
+        String rawJson = "{\"name\": \"Vit\", \"passed\": 5, \"bigObj\": {\"prop\":42}}";
+
+        TbMsg msg = new TbMsg(UUIDs.timeBased(), "USER", null, metaData, rawJson, null, null, 0L);
+
+        TbMsg actual = scriptEngine.executeUpdate(msg);
+        assertEquals("70", actual.getMetaData().getValue("temp"));
+        scriptEngine.destroy();
+    }
+
+    @Test
+    public void newAttributesCanBeAddedInMsg() throws ScriptException {
+        String function = "metadata.newAttr = metadata.humidity - msg.passed; return {metadata: metadata};";
+        scriptEngine = new RuleNodeJsScriptEngine(jsSandboxService, function);
+        TbMsgMetaData metaData = new TbMsgMetaData();
+        metaData.putValue("temp", "7");
+        metaData.putValue("humidity", "99");
+        String rawJson = "{\"name\": \"Vit\", \"passed\": 5, \"bigObj\": {\"prop\":42}}";
+
+        TbMsg msg = new TbMsg(UUIDs.timeBased(), "USER", null, metaData, rawJson, null, null, 0L);
+
+        TbMsg actual = scriptEngine.executeUpdate(msg);
+        assertEquals("94", actual.getMetaData().getValue("newAttr"));
+        scriptEngine.destroy();
+    }
+
+    @Test
+    public void payloadCanBeUpdated() throws ScriptException {
+        String function = "msg.passed = msg.passed * metadata.temp; msg.bigObj.newProp = 'Ukraine'; return {msg: msg};";
+        scriptEngine = new RuleNodeJsScriptEngine(jsSandboxService, function);
+        TbMsgMetaData metaData = new TbMsgMetaData();
+        metaData.putValue("temp", "7");
+        metaData.putValue("humidity", "99");
+        String rawJson = "{\"name\":\"Vit\",\"passed\": 5,\"bigObj\":{\"prop\":42}}";
+
+        TbMsg msg = new TbMsg(UUIDs.timeBased(), "USER", null, metaData, rawJson, null, null, 0L);
+
+        TbMsg actual = scriptEngine.executeUpdate(msg);
+
+        String expectedJson = "{\"name\":\"Vit\",\"passed\":35,\"bigObj\":{\"prop\":42,\"newProp\":\"Ukraine\"}}";
+        assertEquals(expectedJson, actual.getData());
+        scriptEngine.destroy();
+    }
+
+    @Test
+    public void metadataAccessibleForFilter() throws ScriptException {
+        String function = "return metadata.humidity < 15;";
+        scriptEngine = new RuleNodeJsScriptEngine(jsSandboxService, function);
+        TbMsgMetaData metaData = new TbMsgMetaData();
+        metaData.putValue("temp", "7");
+        metaData.putValue("humidity", "99");
+        String rawJson = "{\"name\": \"Vit\", \"passed\": 5, \"bigObj\": {\"prop\":42}}";
+
+        TbMsg msg = new TbMsg(UUIDs.timeBased(), "USER", null, metaData, rawJson, null, null, 0L);
+        assertFalse(scriptEngine.executeFilter(msg));
+        scriptEngine.destroy();
+    }
+
+    @Test
+    public void dataAccessibleForFilter() throws ScriptException {
+        String function = "return msg.passed < 15 && msg.name === 'Vit' && metadata.temp == 7 && msg.bigObj.prop == 42;";
+        scriptEngine = new RuleNodeJsScriptEngine(jsSandboxService, function);
+        TbMsgMetaData metaData = new TbMsgMetaData();
+        metaData.putValue("temp", "7");
+        metaData.putValue("humidity", "99");
+        String rawJson = "{\"name\": \"Vit\", \"passed\": 5, \"bigObj\": {\"prop\":42}}";
+
+        TbMsg msg = new TbMsg(UUIDs.timeBased(), "USER", null, metaData, rawJson, null, null, 0L);
+        assertTrue(scriptEngine.executeFilter(msg));
+        scriptEngine.destroy();
+    }
+
+    @Test
+    public void dataAccessibleForSwitch() throws ScriptException {
+        String jsCode = "function nextRelation(metadata, msg) {\n" +
+                "    if(msg.passed == 5 && metadata.temp == 10)\n" +
+                "        return 'one'\n" +
+                "    else\n" +
+                "        return 'two';\n" +
+                "};\n" +
+                "\n" +
+                "return nextRelation(metadata, msg);";
+        scriptEngine = new RuleNodeJsScriptEngine(jsSandboxService, jsCode);
+        TbMsgMetaData metaData = new TbMsgMetaData();
+        metaData.putValue("temp", "10");
+        metaData.putValue("humidity", "99");
+        String rawJson = "{\"name\": \"Vit\", \"passed\": 5, \"bigObj\": {\"prop\":42}}";
+
+        TbMsg msg = new TbMsg(UUIDs.timeBased(), "USER", null, metaData, rawJson, null, null, 0L);
+        Set<String> actual = scriptEngine.executeSwitch(msg);
+        assertEquals(Sets.newHashSet("one"), actual);
+        scriptEngine.destroy();
+    }
+
+    @Test
+    public void multipleRelationsReturnedFromSwitch() throws ScriptException {
+        String jsCode = "function nextRelation(metadata, msg) {\n" +
+                "    if(msg.passed == 5 && metadata.temp == 10)\n" +
+                "        return ['three', 'one']\n" +
+                "    else\n" +
+                "        return 'two';\n" +
+                "};\n" +
+                "\n" +
+                "return nextRelation(metadata, msg);";
+        scriptEngine = new RuleNodeJsScriptEngine(jsSandboxService, jsCode);
+        TbMsgMetaData metaData = new TbMsgMetaData();
+        metaData.putValue("temp", "10");
+        metaData.putValue("humidity", "99");
+        String rawJson = "{\"name\": \"Vit\", \"passed\": 5, \"bigObj\": {\"prop\":42}}";
+
+        TbMsg msg = new TbMsg(UUIDs.timeBased(), "USER", null, metaData, rawJson, null, null, 0L);
+        Set<String> actual = scriptEngine.executeSwitch(msg);
+        assertEquals(Sets.newHashSet("one", "three"), actual);
+        scriptEngine.destroy();
+    }
+
+}
\ No newline at end of file
diff --git a/application/src/test/java/org/thingsboard/server/service/script/TestNashornJsSandboxService.java b/application/src/test/java/org/thingsboard/server/service/script/TestNashornJsSandboxService.java
new file mode 100644
index 0000000..731c4bb
--- /dev/null
+++ b/application/src/test/java/org/thingsboard/server/service/script/TestNashornJsSandboxService.java
@@ -0,0 +1,62 @@
+/**
+ * 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.server.service.script;
+
+import com.google.common.util.concurrent.ListenableFuture;
+import delight.nashornsandbox.NashornSandbox;
+import delight.nashornsandbox.NashornSandboxes;
+
+import javax.script.ScriptException;
+import java.util.UUID;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+public class TestNashornJsSandboxService extends AbstractNashornJsSandboxService {
+
+    private boolean useJsSandbox;
+    private final int monitorThreadPoolSize;
+    private final long maxCpuTime;
+    private final int maxErrors;
+
+    public TestNashornJsSandboxService(boolean useJsSandbox, int monitorThreadPoolSize, long maxCpuTime, int maxErrors) {
+        this.useJsSandbox = useJsSandbox;
+        this.monitorThreadPoolSize = monitorThreadPoolSize;
+        this.maxCpuTime = maxCpuTime;
+        this.maxErrors = maxErrors;
+        init();
+    }
+
+    @Override
+    protected boolean useJsSandbox() {
+        return useJsSandbox;
+    }
+
+    @Override
+    protected int getMonitorThreadPoolSize() {
+        return monitorThreadPoolSize;
+    }
+
+    @Override
+    protected long getMaxCpuTime() {
+        return maxCpuTime;
+    }
+
+    @Override
+    protected int getMaxErrors() {
+        return maxErrors;
+    }
+}
diff --git a/application/src/test/java/org/thingsboard/server/system/SystemSqlTestSuite.java b/application/src/test/java/org/thingsboard/server/system/SystemSqlTestSuite.java
index cfa0c58..97c6749 100644
--- a/application/src/test/java/org/thingsboard/server/system/SystemSqlTestSuite.java
+++ b/application/src/test/java/org/thingsboard/server/system/SystemSqlTestSuite.java
@@ -35,5 +35,4 @@ public class SystemSqlTestSuite {
             "sql/drop-all-tables.sql",
             "sql-test.properties");
 
-
 }
diff --git a/common/data/pom.xml b/common/data/pom.xml
index 577fcff..793d889 100644
--- a/common/data/pom.xml
+++ b/common/data/pom.xml
@@ -20,7 +20,7 @@
     <modelVersion>4.0.0</modelVersion>
     <parent>
         <groupId>org.thingsboard</groupId>
-        <version>1.4.1-SNAPSHOT</version>
+        <version>2.0.0-SNAPSHOT</version>
         <artifactId>common</artifactId>
     </parent>
     <groupId>org.thingsboard.common</groupId>
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/Alarm.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/Alarm.java
index 70f5042..125406c 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/Alarm.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/Alarm.java
@@ -22,6 +22,7 @@ import lombok.Builder;
 import lombok.Data;
 import org.thingsboard.server.common.data.BaseData;
 import org.thingsboard.server.common.data.HasName;
+import org.thingsboard.server.common.data.HasTenantId;
 import org.thingsboard.server.common.data.id.EntityId;
 import org.thingsboard.server.common.data.id.TenantId;
 
@@ -31,7 +32,7 @@ import org.thingsboard.server.common.data.id.TenantId;
 @Data
 @Builder
 @AllArgsConstructor
-public class Alarm extends BaseData<AlarmId> implements HasName {
+public class Alarm extends BaseData<AlarmId> implements HasName, HasTenantId {
 
     private TenantId tenantId;
     private String type;
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/asset/Asset.java b/common/data/src/main/java/org/thingsboard/server/common/data/asset/Asset.java
index cc3c111..c7b246c 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/asset/Asset.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/asset/Asset.java
@@ -17,16 +17,13 @@ package org.thingsboard.server.common.data.asset;
 
 import com.fasterxml.jackson.databind.JsonNode;
 import lombok.EqualsAndHashCode;
-import org.thingsboard.server.common.data.HasAdditionalInfo;
-import org.thingsboard.server.common.data.HasName;
-import org.thingsboard.server.common.data.SearchTextBased;
-import org.thingsboard.server.common.data.SearchTextBasedWithAdditionalInfo;
+import org.thingsboard.server.common.data.*;
 import org.thingsboard.server.common.data.id.AssetId;
 import org.thingsboard.server.common.data.id.CustomerId;
 import org.thingsboard.server.common.data.id.TenantId;
 
 @EqualsAndHashCode(callSuper = true)
-public class Asset extends SearchTextBasedWithAdditionalInfo<AssetId> implements HasName {
+public class Asset extends SearchTextBasedWithAdditionalInfo<AssetId> implements HasName, HasTenantId, HasCustomerId {
 
     private static final long serialVersionUID = 2807343040519543363L;
 
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/Customer.java b/common/data/src/main/java/org/thingsboard/server/common/data/Customer.java
index 03115a9..078c97b 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/Customer.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/Customer.java
@@ -23,7 +23,7 @@ import org.thingsboard.server.common.data.id.TenantId;
 
 import com.fasterxml.jackson.databind.JsonNode;
 
-public class Customer extends ContactBased<CustomerId> implements HasName {
+public class Customer extends ContactBased<CustomerId> implements HasName, HasTenantId {
     
     private static final long serialVersionUID = -1599722990298929275L;
     
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/DataConstants.java b/common/data/src/main/java/org/thingsboard/server/common/data/DataConstants.java
index a776d7b..6b1c4a4 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/DataConstants.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/DataConstants.java
@@ -37,7 +37,25 @@ public class DataConstants {
     public static final String ERROR = "ERROR";
     public static final String LC_EVENT = "LC_EVENT";
     public static final String STATS = "STATS";
+    public static final String DEBUG_RULE_NODE = "DEBUG_RULE_NODE";
 
     public static final String ONEWAY = "ONEWAY";
     public static final String TWOWAY = "TWOWAY";
+
+    public static final String IN = "IN";
+    public static final String OUT = "OUT";
+
+    public static final String INACTIVITY_EVENT = "INACTIVITY_EVENT";
+    public static final String CONNECT_EVENT = "CONNECT_EVENT";
+    public static final String DISCONNECT_EVENT = "DISCONNECT_EVENT";
+    public static final String ACTIVITY_EVENT = "ACTIVITY_EVENT";
+
+    public static final String ENTITY_CREATED = "ENTITY_CREATED";
+    public static final String ENTITY_UPDATED = "ENTITY_UPDATED";
+    public static final String ENTITY_DELETED = "ENTITY_DELETED";
+    public static final String ENTITY_ASSIGNED = "ENTITY_ASSIGNED";
+    public static final String ENTITY_UNASSIGNED = "ENTITY_UNASSIGNED";
+    public static final String ATTRIBUTES_UPDATED = "ATTRIBUTES_UPDATED";
+    public static final String ATTRIBUTES_DELETED = "ATTRIBUTES_DELETED";
+
 }
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/Device.java b/common/data/src/main/java/org/thingsboard/server/common/data/Device.java
index 13fa011..95662c1 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/Device.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/Device.java
@@ -23,7 +23,7 @@ import org.thingsboard.server.common.data.id.TenantId;
 import com.fasterxml.jackson.databind.JsonNode;
 
 @EqualsAndHashCode(callSuper = true)
-public class Device extends SearchTextBasedWithAdditionalInfo<DeviceId> implements HasName {
+public class Device extends SearchTextBasedWithAdditionalInfo<DeviceId> implements HasName, HasTenantId, HasCustomerId {
 
     private static final long serialVersionUID = 2807343040519543363L;
 
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/EntityType.java b/common/data/src/main/java/org/thingsboard/server/common/data/EntityType.java
index 05b558e..fe9c018 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/EntityType.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/EntityType.java
@@ -19,5 +19,5 @@ package org.thingsboard.server.common.data;
  * @author Andrew Shvayka
  */
 public enum EntityType {
-    TENANT, CUSTOMER, USER, RULE, PLUGIN, DASHBOARD, ASSET, DEVICE, ALARM
+    TENANT, CUSTOMER, USER, DASHBOARD, ASSET, DEVICE, ALARM, RULE_CHAIN, RULE_NODE;
 }
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityIdFactory.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityIdFactory.java
index e45cb91..0ecc7c6 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityIdFactory.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityIdFactory.java
@@ -33,6 +33,10 @@ public class EntityIdFactory {
         return getByTypeAndUuid(EntityType.valueOf(type), uuid);
     }
 
+    public static EntityId getByTypeAndUuid(EntityType type, String uuid) {
+        return getByTypeAndUuid(type, UUID.fromString(uuid));
+    }
+
     public static EntityId getByTypeAndUuid(EntityType type, UUID uuid) {
         switch (type) {
             case TENANT:
@@ -41,10 +45,6 @@ public class EntityIdFactory {
                 return new CustomerId(uuid);
             case USER:
                 return new UserId(uuid);
-            case RULE:
-                return new RuleId(uuid);
-            case PLUGIN:
-                return new PluginId(uuid);
             case DASHBOARD:
                 return new DashboardId(uuid);
             case DEVICE:
@@ -53,6 +53,10 @@ public class EntityIdFactory {
                 return new AssetId(uuid);
             case ALARM:
                 return new AlarmId(uuid);
+            case RULE_CHAIN:
+                return new RuleChainId(uuid);
+            case RULE_NODE:
+                return new RuleNodeId(uuid);
         }
         throw new IllegalArgumentException("EntityType " + type + " is not supported!");
     }
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/IdBased.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/IdBased.java
index 6ac5136..e832807 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/id/IdBased.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/IdBased.java
@@ -17,9 +17,10 @@ package org.thingsboard.server.common.data.id;
 
 import com.fasterxml.jackson.annotation.JsonIgnore;
 
+import java.io.Serializable;
 import java.util.UUID;
 
-public abstract class IdBased<I extends UUIDBased> {
+public abstract class IdBased<I extends UUIDBased> implements Serializable {
 	
 	protected I id;
 	
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/page/PageDataIterable.java b/common/data/src/main/java/org/thingsboard/server/common/data/page/PageDataIterable.java
index 34f8c3a..ffd7822 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/page/PageDataIterable.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/page/PageDataIterable.java
@@ -20,6 +20,7 @@ import java.util.List;
 import java.util.NoSuchElementException;
 
 import org.thingsboard.server.common.data.SearchTextBased;
+import org.thingsboard.server.common.data.id.EntityId;
 import org.thingsboard.server.common.data.id.UUIDBased;
 
 public class PageDataIterable<T extends SearchTextBased<? extends UUIDBased>> implements Iterable<T>, Iterator<T> {
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/plugin/ComponentType.java b/common/data/src/main/java/org/thingsboard/server/common/data/plugin/ComponentType.java
index 45fb590..1335147 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/plugin/ComponentType.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/plugin/ComponentType.java
@@ -20,6 +20,6 @@ package org.thingsboard.server.common.data.plugin;
  */
 public enum ComponentType {
 
-    FILTER, PROCESSOR, ACTION, PLUGIN
+    ENRICHMENT, FILTER, TRANSFORMATION, ACTION, EXTERNAL
 
 }
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/relation/RelationTypeGroup.java b/common/data/src/main/java/org/thingsboard/server/common/data/relation/RelationTypeGroup.java
index 90e0253..5599055 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/relation/RelationTypeGroup.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/relation/RelationTypeGroup.java
@@ -19,6 +19,8 @@ public enum RelationTypeGroup {
 
     COMMON,
     ALARM,
-    DASHBOARD
+    DASHBOARD,
+    RULE_CHAIN,
+    RULE_NODE
 
 }
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChainMetaData.java b/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChainMetaData.java
new file mode 100644
index 0000000..7ecd6df
--- /dev/null
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChainMetaData.java
@@ -0,0 +1,62 @@
+/**
+ * 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.server.common.data.rule;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import lombok.Data;
+import org.thingsboard.server.common.data.id.RuleChainId;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Created by igor on 3/13/18.
+ */
+@Data
+public class RuleChainMetaData {
+
+    private RuleChainId ruleChainId;
+
+    private Integer firstNodeIndex;
+
+    private List<RuleNode> nodes;
+
+    private List<NodeConnectionInfo> connections;
+
+    private List<RuleChainConnectionInfo> ruleChainConnections;
+
+    public void addConnectionInfo(int fromIndex, int toIndex, String type) {
+        NodeConnectionInfo connectionInfo = new NodeConnectionInfo();
+        connectionInfo.setFromIndex(fromIndex);
+        connectionInfo.setToIndex(toIndex);
+        connectionInfo.setType(type);
+        if (connections == null) {
+            connections = new ArrayList<>();
+        }
+        connections.add(connectionInfo);
+    }
+    public void addRuleChainConnectionInfo(int fromIndex, RuleChainId targetRuleChainId, String type, JsonNode additionalInfo) {
+        RuleChainConnectionInfo connectionInfo = new RuleChainConnectionInfo();
+        connectionInfo.setFromIndex(fromIndex);
+        connectionInfo.setTargetRuleChainId(targetRuleChainId);
+        connectionInfo.setType(type);
+        connectionInfo.setAdditionalInfo(additionalInfo);
+        if (ruleChainConnections == null) {
+            ruleChainConnections = new ArrayList<>();
+        }
+        ruleChainConnections.add(connectionInfo);
+    }
+}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/User.java b/common/data/src/main/java/org/thingsboard/server/common/data/User.java
index c893d64..15c52dc 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/User.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/User.java
@@ -15,9 +15,11 @@
  */
 package org.thingsboard.server.common.data;
 
+import com.fasterxml.jackson.annotation.JsonIgnore;
 import com.fasterxml.jackson.annotation.JsonProperty;
 import lombok.EqualsAndHashCode;
 import org.thingsboard.server.common.data.id.CustomerId;
+import org.thingsboard.server.common.data.id.EntityId;
 import org.thingsboard.server.common.data.id.TenantId;
 import org.thingsboard.server.common.data.id.UserId;
 import org.thingsboard.server.common.data.security.Authority;
@@ -25,7 +27,7 @@ import org.thingsboard.server.common.data.security.Authority;
 import com.fasterxml.jackson.databind.JsonNode;
 
 @EqualsAndHashCode(callSuper = true)
-public class User extends SearchTextBasedWithAdditionalInfo<UserId> implements HasName {
+public class User extends SearchTextBasedWithAdditionalInfo<UserId> implements HasName, HasTenantId, HasCustomerId {
 
     private static final long serialVersionUID = 8250339805336035966L;
 
@@ -138,4 +140,18 @@ public class User extends SearchTextBasedWithAdditionalInfo<UserId> implements H
         return builder.toString();
     }
 
+    @JsonIgnore
+    public boolean isSystemAdmin() {
+        return tenantId == null || EntityId.NULL_UUID.equals(tenantId.getId());
+    }
+
+    @JsonIgnore
+    public boolean isTenantAdmin() {
+        return !isSystemAdmin() && (customerId == null || EntityId.NULL_UUID.equals(customerId.getId()));
+    }
+
+    @JsonIgnore
+    public boolean isCustomerUser() {
+        return !isSystemAdmin() && !isTenantAdmin();
+    }
 }
diff --git a/common/message/pom.xml b/common/message/pom.xml
index 9e97d34..c24cfd3 100644
--- a/common/message/pom.xml
+++ b/common/message/pom.xml
@@ -20,7 +20,7 @@
     <modelVersion>4.0.0</modelVersion>
     <parent>
         <groupId>org.thingsboard</groupId>
-        <version>1.4.1-SNAPSHOT</version>
+        <version>2.0.0-SNAPSHOT</version>
         <artifactId>common</artifactId>
     </parent>
     <groupId>org.thingsboard.common</groupId>
@@ -57,6 +57,11 @@
             <artifactId>logback-classic</artifactId>
         </dependency>
         <dependency>
+            <groupId>com.google.protobuf</groupId>
+            <artifactId>protobuf-java</artifactId>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
             <groupId>junit</groupId>
             <artifactId>junit</artifactId>
             <scope>test</scope>
@@ -70,6 +75,10 @@
 
     <build>
         <plugins>
+            <plugin>
+                <groupId>org.xolstice.maven.plugins</groupId>
+                <artifactId>protobuf-maven-plugin</artifactId>
+            </plugin>
         </plugins>
     </build>
 
diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/cluster/ClusterEventMsg.java b/common/message/src/main/java/org/thingsboard/server/common/msg/cluster/ClusterEventMsg.java
index 67f4de7..7d157f6 100644
--- a/common/message/src/main/java/org/thingsboard/server/common/msg/cluster/ClusterEventMsg.java
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/cluster/ClusterEventMsg.java
@@ -16,14 +16,20 @@
 package org.thingsboard.server.common.msg.cluster;
 
 import lombok.Data;
+import org.thingsboard.server.common.msg.MsgType;
+import org.thingsboard.server.common.msg.TbActorMsg;
 
 /**
  * @author Andrew Shvayka
  */
 @Data
-public final class ClusterEventMsg {
+public final class ClusterEventMsg implements TbActorMsg {
 
     private final ServerAddress serverAddress;
     private final boolean added;
 
+    @Override
+    public MsgType getMsgType() {
+        return MsgType.CLUSTER_EVENT_MSG;
+    }
 }
diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/cluster/ToAllNodesMsg.java b/common/message/src/main/java/org/thingsboard/server/common/msg/cluster/ToAllNodesMsg.java
index 1dc33c0..877689d 100644
--- a/common/message/src/main/java/org/thingsboard/server/common/msg/cluster/ToAllNodesMsg.java
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/cluster/ToAllNodesMsg.java
@@ -15,10 +15,12 @@
  */
 package org.thingsboard.server.common.msg.cluster;
 
+import org.thingsboard.server.common.msg.TbActorMsg;
+
 import java.io.Serializable;
 
 /**
  * @author Andrew Shvayka
  */
-public interface ToAllNodesMsg extends Serializable {
+public interface ToAllNodesMsg extends Serializable, TbActorMsg {
 }
diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/core/AttributesSubscribeMsg.java b/common/message/src/main/java/org/thingsboard/server/common/msg/core/AttributesSubscribeMsg.java
index 6894ac5..cadaf3c 100644
--- a/common/message/src/main/java/org/thingsboard/server/common/msg/core/AttributesSubscribeMsg.java
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/core/AttributesSubscribeMsg.java
@@ -16,14 +16,14 @@
 package org.thingsboard.server.common.msg.core;
 
 import org.thingsboard.server.common.msg.session.FromDeviceMsg;
-import org.thingsboard.server.common.msg.session.MsgType;
+import org.thingsboard.server.common.msg.session.SessionMsgType;
 
 /**
  * @author Andrew Shvayka
  */
 public class AttributesSubscribeMsg implements FromDeviceMsg {
     @Override
-    public MsgType getMsgType() {
-        return MsgType.SUBSCRIBE_ATTRIBUTES_REQUEST;
+    public SessionMsgType getMsgType() {
+        return SessionMsgType.SUBSCRIBE_ATTRIBUTES_REQUEST;
     }
 }
diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/core/AttributesUnsubscribeMsg.java b/common/message/src/main/java/org/thingsboard/server/common/msg/core/AttributesUnsubscribeMsg.java
index e3fcd6f..c98ad2a 100644
--- a/common/message/src/main/java/org/thingsboard/server/common/msg/core/AttributesUnsubscribeMsg.java
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/core/AttributesUnsubscribeMsg.java
@@ -16,14 +16,15 @@
 package org.thingsboard.server.common.msg.core;
 
 import org.thingsboard.server.common.msg.session.FromDeviceMsg;
-import org.thingsboard.server.common.msg.session.MsgType;
+import org.thingsboard.server.common.msg.session.SessionMsgType;
+import org.thingsboard.server.common.msg.session.SessionMsgType;
 
 /**
  * @author Andrew Shvayka
  */
 public class AttributesUnsubscribeMsg implements FromDeviceMsg {
     @Override
-    public MsgType getMsgType() {
-        return MsgType.UNSUBSCRIBE_ATTRIBUTES_REQUEST;
+    public SessionMsgType getMsgType() {
+        return SessionMsgType.UNSUBSCRIBE_ATTRIBUTES_REQUEST;
     }
 }
diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/core/AttributesUpdateNotification.java b/common/message/src/main/java/org/thingsboard/server/common/msg/core/AttributesUpdateNotification.java
index 1329489..1f0f9dd 100644
--- a/common/message/src/main/java/org/thingsboard/server/common/msg/core/AttributesUpdateNotification.java
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/core/AttributesUpdateNotification.java
@@ -17,7 +17,8 @@ package org.thingsboard.server.common.msg.core;
 
 import lombok.ToString;
 import org.thingsboard.server.common.msg.kv.AttributesKVMsg;
-import org.thingsboard.server.common.msg.session.MsgType;
+import org.thingsboard.server.common.msg.session.SessionMsgType;
+import org.thingsboard.server.common.msg.session.SessionMsgType;
 import org.thingsboard.server.common.msg.session.ToDeviceMsg;
 
 @ToString
@@ -36,9 +37,8 @@ public class AttributesUpdateNotification implements ToDeviceMsg {
         return true;
     }
 
-    @Override
-    public MsgType getMsgType() {
-        return MsgType.ATTRIBUTES_UPDATE_NOTIFICATION;
+    public SessionMsgType getSessionMsgType() {
+        return SessionMsgType.ATTRIBUTES_UPDATE_NOTIFICATION;
     }
 
     public AttributesKVMsg getData() {
diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/core/BasicCommandAckResponse.java b/common/message/src/main/java/org/thingsboard/server/common/msg/core/BasicCommandAckResponse.java
index ef772c3..3ee04ba 100644
--- a/common/message/src/main/java/org/thingsboard/server/common/msg/core/BasicCommandAckResponse.java
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/core/BasicCommandAckResponse.java
@@ -15,26 +15,27 @@
  */
 package org.thingsboard.server.common.msg.core;
 
-import org.thingsboard.server.common.msg.session.MsgType;
+import org.thingsboard.server.common.msg.session.SessionMsgType;
+import org.thingsboard.server.common.msg.session.SessionMsgType;
 
 public class BasicCommandAckResponse extends BasicResponseMsg<Integer> implements StatusCodeResponse {
 
     private static final long serialVersionUID = 1L;
 
-    public static BasicCommandAckResponse onSuccess(MsgType requestMsgType, Integer requestId) {
+    public static BasicCommandAckResponse onSuccess(SessionMsgType requestMsgType, Integer requestId) {
         return BasicCommandAckResponse.onSuccess(requestMsgType, requestId, 200);
     }
 
-    public static BasicCommandAckResponse onSuccess(MsgType requestMsgType, Integer requestId, Integer code) {
+    public static BasicCommandAckResponse onSuccess(SessionMsgType requestMsgType, Integer requestId, Integer code) {
         return new BasicCommandAckResponse(requestMsgType, requestId, true, null, code);
     }
 
-    public static BasicCommandAckResponse onError(MsgType requestMsgType, Integer requestId, Exception error) {
+    public static BasicCommandAckResponse onError(SessionMsgType requestMsgType, Integer requestId, Exception error) {
         return new BasicCommandAckResponse(requestMsgType, requestId, false, error, null);
     }
 
-    private BasicCommandAckResponse(MsgType requestMsgType, Integer requestId, boolean success, Exception error, Integer code) {
-        super(requestMsgType, requestId, MsgType.TO_DEVICE_RPC_RESPONSE_ACK, success, error, code);
+    private BasicCommandAckResponse(SessionMsgType requestMsgType, Integer requestId, boolean success, Exception error, Integer code) {
+        super(requestMsgType, requestId, SessionMsgType.TO_DEVICE_RPC_RESPONSE_ACK, success, error, code);
     }
 
     @Override
diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/core/BasicGetAttributesRequest.java b/common/message/src/main/java/org/thingsboard/server/common/msg/core/BasicGetAttributesRequest.java
index b431c14..e0f6d7e 100644
--- a/common/message/src/main/java/org/thingsboard/server/common/msg/core/BasicGetAttributesRequest.java
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/core/BasicGetAttributesRequest.java
@@ -16,7 +16,8 @@
 package org.thingsboard.server.common.msg.core;
 
 import lombok.ToString;
-import org.thingsboard.server.common.msg.session.MsgType;
+import org.thingsboard.server.common.msg.session.SessionMsgType;
+import org.thingsboard.server.common.msg.session.SessionMsgType;
 
 import java.util.Collections;
 import java.util.Optional;
@@ -41,8 +42,8 @@ public class BasicGetAttributesRequest extends BasicRequest implements GetAttrib
     }
 
     @Override
-    public MsgType getMsgType() {
-        return MsgType.GET_ATTRIBUTES_REQUEST;
+    public SessionMsgType getMsgType() {
+        return SessionMsgType.GET_ATTRIBUTES_REQUEST;
     }
 
     @Override
diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/core/BasicGetAttributesResponse.java b/common/message/src/main/java/org/thingsboard/server/common/msg/core/BasicGetAttributesResponse.java
index 5072de2..e3eb15d 100644
--- a/common/message/src/main/java/org/thingsboard/server/common/msg/core/BasicGetAttributesResponse.java
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/core/BasicGetAttributesResponse.java
@@ -17,23 +17,24 @@ package org.thingsboard.server.common.msg.core;
 
 import lombok.ToString;
 import org.thingsboard.server.common.msg.kv.AttributesKVMsg;
-import org.thingsboard.server.common.msg.session.MsgType;
+import org.thingsboard.server.common.msg.session.SessionMsgType;
+import org.thingsboard.server.common.msg.session.SessionMsgType;
 
 @ToString
 public class BasicGetAttributesResponse extends BasicResponseMsg<AttributesKVMsg> implements GetAttributesResponse {
 
     private static final long serialVersionUID = 1L;
 
-    public static BasicGetAttributesResponse onSuccess(MsgType requestMsgType, int requestId, AttributesKVMsg code) {
+    public static BasicGetAttributesResponse onSuccess(SessionMsgType requestMsgType, int requestId, AttributesKVMsg code) {
         return new BasicGetAttributesResponse(requestMsgType, requestId, true, null, code);
     }
 
-    public static BasicGetAttributesResponse onError(MsgType requestMsgType, int requestId, Exception error) {
+    public static BasicGetAttributesResponse onError(SessionMsgType requestMsgType, int requestId, Exception error) {
         return new BasicGetAttributesResponse(requestMsgType, requestId, false, error, null);
     }
 
-    private BasicGetAttributesResponse(MsgType requestMsgType, int requestId, boolean success, Exception error, AttributesKVMsg code) {
-        super(requestMsgType, requestId, MsgType.GET_ATTRIBUTES_RESPONSE, success, error, code);
+    private BasicGetAttributesResponse(SessionMsgType requestMsgType, int requestId, boolean success, Exception error, AttributesKVMsg code) {
+        super(requestMsgType, requestId, SessionMsgType.GET_ATTRIBUTES_RESPONSE, success, error, code);
     }
 
 }
diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/core/BasicResponseMsg.java b/common/message/src/main/java/org/thingsboard/server/common/msg/core/BasicResponseMsg.java
index caaa15c..c61d8e5 100644
--- a/common/message/src/main/java/org/thingsboard/server/common/msg/core/BasicResponseMsg.java
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/core/BasicResponseMsg.java
@@ -18,32 +18,33 @@ package org.thingsboard.server.common.msg.core;
 import java.io.Serializable;
 import java.util.Optional;
 
-import org.thingsboard.server.common.msg.session.MsgType;
+import org.thingsboard.server.common.msg.session.SessionMsgType;
+import org.thingsboard.server.common.msg.session.SessionMsgType;
 
 
 public class BasicResponseMsg<T extends Serializable> implements ResponseMsg<T> {
 
     private static final long serialVersionUID = 1L;
 
-    private final MsgType requestMsgType;
+    private final SessionMsgType requestMsgType;
     private final Integer requestId;
-    private final MsgType msgType;
+    private final SessionMsgType sessionMsgType;
     private final boolean success;
     private final T data;
     private final Exception error;
 
-    protected BasicResponseMsg(MsgType requestMsgType, Integer requestId, MsgType msgType, boolean success, Exception error, T data) {
+    protected BasicResponseMsg(SessionMsgType requestMsgType, Integer requestId, SessionMsgType sessionMsgType, boolean success, Exception error, T data) {
         super();
         this.requestMsgType = requestMsgType;
         this.requestId = requestId;
-        this.msgType = msgType;
+        this.sessionMsgType = sessionMsgType;
         this.success = success;
         this.error = error;
         this.data = data;
     }
 
     @Override
-    public MsgType getRequestMsgType() {
+    public SessionMsgType getRequestMsgType() {
         return requestMsgType;
     }
 
@@ -72,8 +73,7 @@ public class BasicResponseMsg<T extends Serializable> implements ResponseMsg<T> 
         return "BasicResponseMsg [success=" + success + ", data=" + data + ", error=" + error + "]";
     }
 
-    @Override
-    public MsgType getMsgType() {
-        return msgType;
+    public SessionMsgType getSessionMsgType() {
+        return sessionMsgType;
     }
 }
diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/core/BasicStatusCodeResponse.java b/common/message/src/main/java/org/thingsboard/server/common/msg/core/BasicStatusCodeResponse.java
index 22b525b..f21aa85 100644
--- a/common/message/src/main/java/org/thingsboard/server/common/msg/core/BasicStatusCodeResponse.java
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/core/BasicStatusCodeResponse.java
@@ -16,26 +16,27 @@
 package org.thingsboard.server.common.msg.core;
 
 import lombok.ToString;
-import org.thingsboard.server.common.msg.session.MsgType;
+import org.thingsboard.server.common.msg.session.SessionMsgType;
+import org.thingsboard.server.common.msg.session.SessionMsgType;
 
 @ToString
 public class BasicStatusCodeResponse extends BasicResponseMsg<Integer> implements StatusCodeResponse {
 
     private static final long serialVersionUID = 1L;
 
-    public static BasicStatusCodeResponse onSuccess(MsgType requestMsgType, Integer requestId) {
+    public static BasicStatusCodeResponse onSuccess(SessionMsgType requestMsgType, Integer requestId) {
         return BasicStatusCodeResponse.onSuccess(requestMsgType, requestId, 0);
     }
 
-    public static BasicStatusCodeResponse onSuccess(MsgType requestMsgType, Integer requestId, Integer code) {
+    public static BasicStatusCodeResponse onSuccess(SessionMsgType requestMsgType, Integer requestId, Integer code) {
         return new BasicStatusCodeResponse(requestMsgType, requestId, true, null, code);
     }
 
-    public static BasicStatusCodeResponse onError(MsgType requestMsgType, Integer requestId, Exception error) {
+    public static BasicStatusCodeResponse onError(SessionMsgType requestMsgType, Integer requestId, Exception error) {
         return new BasicStatusCodeResponse(requestMsgType, requestId, false, error, null);
     }
 
-    private BasicStatusCodeResponse(MsgType requestMsgType, Integer requestId, boolean success, Exception error, Integer code) {
-        super(requestMsgType, requestId, MsgType.STATUS_CODE_RESPONSE, success, error, code);
+    private BasicStatusCodeResponse(SessionMsgType requestMsgType, Integer requestId, boolean success, Exception error, Integer code) {
+        super(requestMsgType, requestId, SessionMsgType.STATUS_CODE_RESPONSE, success, error, code);
     }
 }
diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/core/BasicTelemetryUploadRequest.java b/common/message/src/main/java/org/thingsboard/server/common/msg/core/BasicTelemetryUploadRequest.java
index 1d96a65..60faeb1 100644
--- a/common/message/src/main/java/org/thingsboard/server/common/msg/core/BasicTelemetryUploadRequest.java
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/core/BasicTelemetryUploadRequest.java
@@ -21,7 +21,8 @@ import java.util.List;
 import java.util.Map;
 
 import org.thingsboard.server.common.data.kv.KvEntry;
-import org.thingsboard.server.common.msg.session.MsgType;
+import org.thingsboard.server.common.msg.session.SessionMsgType;
+import org.thingsboard.server.common.msg.session.SessionMsgType;
 
 public class BasicTelemetryUploadRequest extends BasicRequest implements TelemetryUploadRequest {
 
@@ -48,8 +49,8 @@ public class BasicTelemetryUploadRequest extends BasicRequest implements Telemet
     }
 
     @Override
-    public MsgType getMsgType() {
-        return MsgType.POST_TELEMETRY_REQUEST;
+    public SessionMsgType getMsgType() {
+        return SessionMsgType.POST_TELEMETRY_REQUEST;
     }
 
     @Override
diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/core/ResponseMsg.java b/common/message/src/main/java/org/thingsboard/server/common/msg/core/ResponseMsg.java
index 2eb0959..3e70460 100644
--- a/common/message/src/main/java/org/thingsboard/server/common/msg/core/ResponseMsg.java
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/core/ResponseMsg.java
@@ -18,12 +18,12 @@ package org.thingsboard.server.common.msg.core;
 import java.io.Serializable;
 import java.util.Optional;
 
-import org.thingsboard.server.common.msg.session.MsgType;
+import org.thingsboard.server.common.msg.session.SessionMsgType;
 import org.thingsboard.server.common.msg.session.ToDeviceMsg;
 
 public interface ResponseMsg<T extends Serializable> extends ToDeviceMsg {
 
-    MsgType getRequestMsgType();
+    SessionMsgType getRequestMsgType();
 
     Integer getRequestId();
 
diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/core/RpcSubscribeMsg.java b/common/message/src/main/java/org/thingsboard/server/common/msg/core/RpcSubscribeMsg.java
index d4fc1d7..f8f24e8 100644
--- a/common/message/src/main/java/org/thingsboard/server/common/msg/core/RpcSubscribeMsg.java
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/core/RpcSubscribeMsg.java
@@ -16,14 +16,15 @@
 package org.thingsboard.server.common.msg.core;
 
 import org.thingsboard.server.common.msg.session.FromDeviceMsg;
-import org.thingsboard.server.common.msg.session.MsgType;
+import org.thingsboard.server.common.msg.session.SessionMsgType;
+import org.thingsboard.server.common.msg.session.SessionMsgType;
 
 /**
  * @author Andrew Shvayka
  */
 public class RpcSubscribeMsg implements FromDeviceMsg {
     @Override
-    public MsgType getMsgType() {
-        return MsgType.SUBSCRIBE_RPC_COMMANDS_REQUEST;
+    public SessionMsgType getMsgType() {
+        return SessionMsgType.SUBSCRIBE_RPC_COMMANDS_REQUEST;
     }
 }
diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/core/RpcUnsubscribeMsg.java b/common/message/src/main/java/org/thingsboard/server/common/msg/core/RpcUnsubscribeMsg.java
index b1532d7..23eb238 100644
--- a/common/message/src/main/java/org/thingsboard/server/common/msg/core/RpcUnsubscribeMsg.java
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/core/RpcUnsubscribeMsg.java
@@ -16,14 +16,15 @@
 package org.thingsboard.server.common.msg.core;
 
 import org.thingsboard.server.common.msg.session.FromDeviceMsg;
-import org.thingsboard.server.common.msg.session.MsgType;
+import org.thingsboard.server.common.msg.session.SessionMsgType;
+import org.thingsboard.server.common.msg.session.SessionMsgType;
 
 /**
  * @author Andrew Shvayka
  */
 public class RpcUnsubscribeMsg implements FromDeviceMsg {
     @Override
-    public MsgType getMsgType() {
-        return MsgType.UNSUBSCRIBE_RPC_COMMANDS_REQUEST;
+    public SessionMsgType getMsgType() {
+        return SessionMsgType.UNSUBSCRIBE_RPC_COMMANDS_REQUEST;
     }
 }
diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/core/RuleEngineError.java b/common/message/src/main/java/org/thingsboard/server/common/msg/core/RuleEngineError.java
index bc8ceb4..dcfde0f 100644
--- a/common/message/src/main/java/org/thingsboard/server/common/msg/core/RuleEngineError.java
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/core/RuleEngineError.java
@@ -21,7 +21,7 @@ package org.thingsboard.server.common.msg.core;
 
 public enum RuleEngineError {
 
-    NO_RULES, NO_ACTIVE_RULES, NO_FILTERS_MATCHED, NO_REQUEST_FROM_ACTIONS, NO_TWO_WAY_ACTIONS, NO_RESPONSE_FROM_ACTIONS, PLUGIN_TIMEOUT(true);
+    QUEUE_PUT_TIMEOUT(true), SERVER_ERROR(true), TIMEOUT;
 
     private final boolean critical;
 
diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/core/RuleEngineErrorMsg.java b/common/message/src/main/java/org/thingsboard/server/common/msg/core/RuleEngineErrorMsg.java
index 5cb3314..e0ff23b 100644
--- a/common/message/src/main/java/org/thingsboard/server/common/msg/core/RuleEngineErrorMsg.java
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/core/RuleEngineErrorMsg.java
@@ -16,7 +16,8 @@
 package org.thingsboard.server.common.msg.core;
 
 import lombok.Data;
-import org.thingsboard.server.common.msg.session.MsgType;
+import org.thingsboard.server.common.msg.session.SessionMsgType;
+import org.thingsboard.server.common.msg.session.SessionMsgType;
 import org.thingsboard.server.common.msg.session.ToDeviceMsg;
 
 /**
@@ -25,7 +26,7 @@ import org.thingsboard.server.common.msg.session.ToDeviceMsg;
 @Data
 public class RuleEngineErrorMsg implements ToDeviceMsg {
 
-    private final MsgType inMsgType;
+    private final SessionMsgType inSessionMsgType;
     private final RuleEngineError error;
 
     @Override
@@ -33,27 +34,18 @@ public class RuleEngineErrorMsg implements ToDeviceMsg {
         return false;
     }
 
-    @Override
-    public MsgType getMsgType() {
-        return MsgType.RULE_ENGINE_ERROR;
+    public SessionMsgType getSessionMsgType() {
+        return SessionMsgType.RULE_ENGINE_ERROR;
     }
 
     public String getErrorMsg() {
         switch (error) {
-            case NO_RULES:
-                return "No rules configured!";
-            case NO_ACTIVE_RULES:
-                return "No active rules!";
-            case NO_FILTERS_MATCHED:
-                return "No rules that match current message!";
-            case NO_REQUEST_FROM_ACTIONS:
-                return "Rule filters match, but no plugin message produced by rule action!";
-            case NO_TWO_WAY_ACTIONS:
-                return "Rule filters match, but no rule with two-way action configured!";
-            case NO_RESPONSE_FROM_ACTIONS:
-                return "Rule filters match, message processed by plugin, but no response produced by rule action!";
-            case PLUGIN_TIMEOUT:
-                return "Timeout during processing of message by plugin!";
+            case QUEUE_PUT_TIMEOUT:
+                return "Timeout during persistence of the message to the queue!";
+            case SERVER_ERROR:
+                return "Error during processing of message by the server!";
+            case TIMEOUT:
+                return "Timeout during processing of message by the server!";
             default:
                 throw new RuntimeException("Error " + error + " is not supported!");
         }
diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/core/SessionCloseMsg.java b/common/message/src/main/java/org/thingsboard/server/common/msg/core/SessionCloseMsg.java
index 738bc6e..6d3ad5e 100644
--- a/common/message/src/main/java/org/thingsboard/server/common/msg/core/SessionCloseMsg.java
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/core/SessionCloseMsg.java
@@ -16,14 +16,15 @@
 package org.thingsboard.server.common.msg.core;
 
 import org.thingsboard.server.common.msg.session.FromDeviceMsg;
-import org.thingsboard.server.common.msg.session.MsgType;
+import org.thingsboard.server.common.msg.session.SessionMsgType;
+import org.thingsboard.server.common.msg.session.SessionMsgType;
 
 /**
  * @author Andrew Shvayka
  */
 public class SessionCloseMsg implements FromDeviceMsg {
     @Override
-    public MsgType getMsgType() {
-        return MsgType.SESSION_CLOSE;
+    public SessionMsgType getMsgType() {
+        return SessionMsgType.SESSION_CLOSE;
     }
 }
diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/core/SessionCloseNotification.java b/common/message/src/main/java/org/thingsboard/server/common/msg/core/SessionCloseNotification.java
index bf3c982..bf36a9b 100644
--- a/common/message/src/main/java/org/thingsboard/server/common/msg/core/SessionCloseNotification.java
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/core/SessionCloseNotification.java
@@ -17,7 +17,8 @@ package org.thingsboard.server.common.msg.core;
 
 import lombok.ToString;
 import org.thingsboard.server.common.msg.kv.AttributesKVMsg;
-import org.thingsboard.server.common.msg.session.MsgType;
+import org.thingsboard.server.common.msg.session.SessionMsgType;
+import org.thingsboard.server.common.msg.session.SessionMsgType;
 import org.thingsboard.server.common.msg.session.ToDeviceMsg;
 
 @ToString
@@ -30,9 +31,8 @@ public class SessionCloseNotification implements ToDeviceMsg {
         return true;
     }
 
-    @Override
-    public MsgType getMsgType() {
-        return MsgType.SESSION_CLOSE;
+    public SessionMsgType getSessionMsgType() {
+        return SessionMsgType.SESSION_CLOSE;
     }
 
 }
diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/core/SessionOpenMsg.java b/common/message/src/main/java/org/thingsboard/server/common/msg/core/SessionOpenMsg.java
index f3a7bc7..28e319e 100644
--- a/common/message/src/main/java/org/thingsboard/server/common/msg/core/SessionOpenMsg.java
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/core/SessionOpenMsg.java
@@ -15,15 +15,21 @@
  */
 package org.thingsboard.server.common.msg.core;
 
+import lombok.Data;
+import org.thingsboard.server.common.data.id.SessionId;
+import org.thingsboard.server.common.msg.aware.SessionAwareMsg;
 import org.thingsboard.server.common.msg.session.FromDeviceMsg;
-import org.thingsboard.server.common.msg.session.MsgType;
+import org.thingsboard.server.common.msg.session.SessionMsgType;
+import org.thingsboard.server.common.msg.session.SessionMsgType;
 
 /**
  * @author Andrew Shvayka
  */
+@Data
 public class SessionOpenMsg implements FromDeviceMsg {
+
     @Override
-    public MsgType getMsgType() {
-        return MsgType.SESSION_OPEN;
+    public SessionMsgType getMsgType() {
+        return SessionMsgType.SESSION_OPEN;
     }
 }
diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/core/ToDeviceRpcRequestMsg.java b/common/message/src/main/java/org/thingsboard/server/common/msg/core/ToDeviceRpcRequestMsg.java
index c9eeb4e..d1a1f96 100644
--- a/common/message/src/main/java/org/thingsboard/server/common/msg/core/ToDeviceRpcRequestMsg.java
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/core/ToDeviceRpcRequestMsg.java
@@ -16,7 +16,8 @@
 package org.thingsboard.server.common.msg.core;
 
 import lombok.Data;
-import org.thingsboard.server.common.msg.session.MsgType;
+import org.thingsboard.server.common.msg.session.SessionMsgType;
+import org.thingsboard.server.common.msg.session.SessionMsgType;
 import org.thingsboard.server.common.msg.session.ToDeviceMsg;
 
 /**
@@ -29,9 +30,8 @@ public class ToDeviceRpcRequestMsg implements ToDeviceMsg {
     private final String method;
     private final String params;
 
-    @Override
-    public MsgType getMsgType() {
-        return MsgType.TO_DEVICE_RPC_REQUEST;
+    public SessionMsgType getSessionMsgType() {
+        return SessionMsgType.TO_DEVICE_RPC_REQUEST;
     }
 
     @Override
diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/core/ToDeviceRpcResponseMsg.java b/common/message/src/main/java/org/thingsboard/server/common/msg/core/ToDeviceRpcResponseMsg.java
index ec739b3..4fa3024 100644
--- a/common/message/src/main/java/org/thingsboard/server/common/msg/core/ToDeviceRpcResponseMsg.java
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/core/ToDeviceRpcResponseMsg.java
@@ -17,7 +17,8 @@ package org.thingsboard.server.common.msg.core;
 
 import lombok.Data;
 import org.thingsboard.server.common.msg.session.FromDeviceMsg;
-import org.thingsboard.server.common.msg.session.MsgType;
+import org.thingsboard.server.common.msg.session.SessionMsgType;
+import org.thingsboard.server.common.msg.session.SessionMsgType;
 
 /**
  * @author Andrew Shvayka
@@ -29,7 +30,7 @@ public class ToDeviceRpcResponseMsg implements FromDeviceMsg {
     private final String data;
 
     @Override
-    public MsgType getMsgType() {
-        return MsgType.TO_DEVICE_RPC_RESPONSE;
+    public SessionMsgType getMsgType() {
+        return SessionMsgType.TO_DEVICE_RPC_RESPONSE;
     }
 }
diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/core/ToServerRpcRequestMsg.java b/common/message/src/main/java/org/thingsboard/server/common/msg/core/ToServerRpcRequestMsg.java
index 87708a7..3823aca 100644
--- a/common/message/src/main/java/org/thingsboard/server/common/msg/core/ToServerRpcRequestMsg.java
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/core/ToServerRpcRequestMsg.java
@@ -17,7 +17,7 @@ package org.thingsboard.server.common.msg.core;
 
 import lombok.Data;
 import org.thingsboard.server.common.msg.session.FromDeviceRequestMsg;
-import org.thingsboard.server.common.msg.session.MsgType;
+import org.thingsboard.server.common.msg.session.SessionMsgType;
 
 /**
  * @author Andrew Shvayka
@@ -30,7 +30,7 @@ public class ToServerRpcRequestMsg implements FromDeviceRequestMsg {
     private final String params;
 
     @Override
-    public MsgType getMsgType() {
-        return MsgType.TO_SERVER_RPC_REQUEST;
+    public SessionMsgType getMsgType() {
+        return SessionMsgType.TO_SERVER_RPC_REQUEST;
     }
 }
diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/core/ToServerRpcResponseMsg.java b/common/message/src/main/java/org/thingsboard/server/common/msg/core/ToServerRpcResponseMsg.java
index b9a43d1..82f44e9 100644
--- a/common/message/src/main/java/org/thingsboard/server/common/msg/core/ToServerRpcResponseMsg.java
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/core/ToServerRpcResponseMsg.java
@@ -17,7 +17,8 @@ package org.thingsboard.server.common.msg.core;
 
 import lombok.Data;
 import org.thingsboard.server.common.msg.session.FromDeviceMsg;
-import org.thingsboard.server.common.msg.session.MsgType;
+import org.thingsboard.server.common.msg.session.SessionMsgType;
+import org.thingsboard.server.common.msg.session.SessionMsgType;
 import org.thingsboard.server.common.msg.session.ToDeviceMsg;
 
 /**
@@ -29,9 +30,8 @@ public class ToServerRpcResponseMsg implements ToDeviceMsg {
     private final int requestId;
     private final String data;
 
-    @Override
-    public MsgType getMsgType() {
-        return MsgType.TO_SERVER_RPC_RESPONSE;
+    public SessionMsgType getSessionMsgType() {
+        return SessionMsgType.TO_SERVER_RPC_RESPONSE;
     }
 
     @Override
diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/MsgType.java b/common/message/src/main/java/org/thingsboard/server/common/msg/MsgType.java
new file mode 100644
index 0000000..7702788
--- /dev/null
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/MsgType.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.server.common.msg;
+
+/**
+ * Created by ashvayka on 15.03.18.
+ */
+//TODO: add all "See" references
+public enum MsgType {
+
+    /**
+     * ADDED/UPDATED/DELETED events for server nodes.
+     *
+     * See {@link org.thingsboard.server.common.msg.cluster.ClusterEventMsg}
+     */
+    CLUSTER_EVENT_MSG,
+
+    /**
+     * All messages, could be send  to cluster
+    */
+    SEND_TO_CLUSTER_MSG,
+
+    /**
+     * ADDED/UPDATED/DELETED events for main entities.
+     *
+     * See {@link org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg}
+     */
+    COMPONENT_LIFE_CYCLE_MSG,
+
+    /**
+     * Misc messages from the REST API/SERVICE layer to the new rule engine.
+     *
+     * See {@link org.thingsboard.server.common.msg.system.ServiceToRuleEngineMsg}
+     */
+    SERVICE_TO_RULE_ENGINE_MSG,
+
+    /**
+     * Message that is sent by RuleChainActor to RuleActor with command to process TbMsg.
+     */
+    RULE_CHAIN_TO_RULE_MSG,
+
+    /**
+     * Message that is sent by RuleChainActor to other RuleChainActor with command to process TbMsg.
+     */
+    RULE_CHAIN_TO_RULE_CHAIN_MSG,
+
+    /**
+     * Message that is sent by RuleActor to RuleChainActor with command to process TbMsg by next nodes in chain.
+     */
+    RULE_TO_RULE_CHAIN_TELL_NEXT_MSG,
+
+    /**
+     * Message that is sent by RuleActor implementation to RuleActor itself to log the error.
+     */
+    RULE_TO_SELF_ERROR_MSG,
+
+    /**
+     * Message that is sent by RuleActor implementation to RuleActor itself to process the message.
+     */
+    RULE_TO_SELF_MSG,
+
+    /**
+     * Message that is sent by Session Actor to Device Actor. Represents messages from the device itself.
+     */
+    DEVICE_SESSION_TO_DEVICE_ACTOR_MSG,
+
+    DEVICE_ATTRIBUTES_UPDATE_TO_DEVICE_ACTOR_MSG,
+
+    DEVICE_CREDENTIALS_UPDATE_TO_DEVICE_ACTOR_MSG,
+
+    DEVICE_NAME_OR_TYPE_UPDATE_TO_DEVICE_ACTOR_MSG,
+
+    DEVICE_RPC_REQUEST_TO_DEVICE_ACTOR_MSG,
+
+    SERVER_RPC_RESPONSE_TO_DEVICE_ACTOR_MSG,
+
+    DEVICE_ACTOR_SERVER_SIDE_RPC_TIMEOUT_MSG,
+
+    DEVICE_ACTOR_CLIENT_SIDE_RPC_TIMEOUT_MSG,
+
+    DEVICE_ACTOR_QUEUE_TIMEOUT_MSG,
+
+    /**
+     * Message that is sent from the Device Actor to Rule Engine. Requires acknowledgement
+     */
+    DEVICE_ACTOR_TO_RULE_ENGINE_MSG,
+
+    /**
+     * Message that is sent from Rule Engine to the Device Actor when message is successfully pushed to queue.
+     */
+    RULE_ENGINE_QUEUE_PUT_ACK_MSG, ACTOR_SYSTEM_TO_DEVICE_SESSION_ACTOR_MSG, TRANSPORT_TO_DEVICE_SESSION_ACTOR_MSG, SESSION_TIMEOUT_MSG, SESSION_CTRL_MSG;
+
+}
diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/plugin/ComponentLifecycleMsg.java b/common/message/src/main/java/org/thingsboard/server/common/msg/plugin/ComponentLifecycleMsg.java
index d48c3fe..eec2de0 100644
--- a/common/message/src/main/java/org/thingsboard/server/common/msg/plugin/ComponentLifecycleMsg.java
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/plugin/ComponentLifecycleMsg.java
@@ -15,14 +15,15 @@
  */
 package org.thingsboard.server.common.msg.plugin;
 
-import lombok.Data;
 import lombok.Getter;
 import lombok.ToString;
-import org.thingsboard.server.common.data.id.PluginId;
-import org.thingsboard.server.common.data.id.RuleId;
+import org.thingsboard.server.common.data.EntityType;
+import org.thingsboard.server.common.data.id.EntityId;
+import org.thingsboard.server.common.data.id.RuleChainId;
 import org.thingsboard.server.common.data.id.TenantId;
 import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent;
-import org.thingsboard.server.common.data.plugin.ComponentLifecycleState;
+import org.thingsboard.server.common.msg.MsgType;
+import org.thingsboard.server.common.msg.TbActorMsg;
 import org.thingsboard.server.common.msg.aware.TenantAwareMsg;
 import org.thingsboard.server.common.msg.cluster.ToAllNodesMsg;
 
@@ -32,34 +33,26 @@ import java.util.Optional;
  * @author Andrew Shvayka
  */
 @ToString
-public class ComponentLifecycleMsg implements TenantAwareMsg, ToAllNodesMsg {
+public class ComponentLifecycleMsg implements TbActorMsg, TenantAwareMsg, ToAllNodesMsg {
     @Getter
     private final TenantId tenantId;
-    private final PluginId pluginId;
-    private final RuleId ruleId;
+    @Getter
+    private final EntityId entityId;
     @Getter
     private final ComponentLifecycleEvent event;
 
-    public static ComponentLifecycleMsg forPlugin(TenantId tenantId, PluginId pluginId, ComponentLifecycleEvent event) {
-        return new ComponentLifecycleMsg(tenantId, pluginId, null, event);
-    }
-
-    public static ComponentLifecycleMsg forRule(TenantId tenantId, RuleId ruleId, ComponentLifecycleEvent event) {
-        return new ComponentLifecycleMsg(tenantId, null, ruleId, event);
-    }
-
-    private ComponentLifecycleMsg(TenantId tenantId, PluginId pluginId, RuleId ruleId, ComponentLifecycleEvent event) {
+    public ComponentLifecycleMsg(TenantId tenantId, EntityId entityId, ComponentLifecycleEvent event) {
         this.tenantId = tenantId;
-        this.pluginId = pluginId;
-        this.ruleId = ruleId;
+        this.entityId = entityId;
         this.event = event;
     }
 
-    public Optional<PluginId> getPluginId() {
-        return Optional.ofNullable(pluginId);
+    public Optional<RuleChainId> getRuleChainId() {
+        return entityId.getEntityType() == EntityType.RULE_CHAIN ? Optional.of((RuleChainId) entityId) : Optional.empty();
     }
 
-    public Optional<RuleId> getRuleId() {
-        return Optional.ofNullable(ruleId);
+    @Override
+    public MsgType getMsgType() {
+        return MsgType.COMPONENT_LIFE_CYCLE_MSG;
     }
 }
diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/session/ctrl/SessionCloseMsg.java b/common/message/src/main/java/org/thingsboard/server/common/msg/session/ctrl/SessionCloseMsg.java
index a24bdcd..49fad13 100644
--- a/common/message/src/main/java/org/thingsboard/server/common/msg/session/ctrl/SessionCloseMsg.java
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/session/ctrl/SessionCloseMsg.java
@@ -16,6 +16,7 @@
 package org.thingsboard.server.common.msg.session.ctrl;
 
 import org.thingsboard.server.common.data.id.SessionId;
+import org.thingsboard.server.common.msg.MsgType;
 import org.thingsboard.server.common.msg.session.SessionCtrlMsg;
 
 public class SessionCloseMsg implements SessionCtrlMsg {
@@ -60,4 +61,8 @@ public class SessionCloseMsg implements SessionCtrlMsg {
         return timeout;
     }
 
+    @Override
+    public MsgType getMsgType() {
+        return MsgType.SESSION_CTRL_MSG;
+    }
 }
diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/session/FromDeviceMsg.java b/common/message/src/main/java/org/thingsboard/server/common/msg/session/FromDeviceMsg.java
index 19d45a7..71b6057 100644
--- a/common/message/src/main/java/org/thingsboard/server/common/msg/session/FromDeviceMsg.java
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/session/FromDeviceMsg.java
@@ -19,6 +19,6 @@ import java.io.Serializable;
 
 public interface FromDeviceMsg extends Serializable {
 
-    MsgType getMsgType();
+    SessionMsgType getMsgType();
 
 }
diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/session/SessionCtrlMsg.java b/common/message/src/main/java/org/thingsboard/server/common/msg/session/SessionCtrlMsg.java
index 19ca219..8082f72 100644
--- a/common/message/src/main/java/org/thingsboard/server/common/msg/session/SessionCtrlMsg.java
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/session/SessionCtrlMsg.java
@@ -15,8 +15,9 @@
  */
 package org.thingsboard.server.common.msg.session;
 
+import org.thingsboard.server.common.msg.TbActorMsg;
 import org.thingsboard.server.common.msg.aware.SessionAwareMsg;
 
-public interface SessionCtrlMsg extends SessionAwareMsg {
+public interface SessionCtrlMsg extends SessionAwareMsg, TbActorMsg {
 
 }
diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/session/ToDeviceMsg.java b/common/message/src/main/java/org/thingsboard/server/common/msg/session/ToDeviceMsg.java
index 31ec1fa..705e864 100644
--- a/common/message/src/main/java/org/thingsboard/server/common/msg/session/ToDeviceMsg.java
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/session/ToDeviceMsg.java
@@ -21,6 +21,6 @@ public interface ToDeviceMsg extends Serializable {
 
     boolean isSuccess();
 
-    MsgType getMsgType();
+    SessionMsgType getSessionMsgType();
 
 }
diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/TbMsg.java b/common/message/src/main/java/org/thingsboard/server/common/msg/TbMsg.java
new file mode 100644
index 0000000..a6eb64e
--- /dev/null
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/TbMsg.java
@@ -0,0 +1,112 @@
+/**
+ * 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.server.common.msg;
+
+import com.google.protobuf.InvalidProtocolBufferException;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import org.thingsboard.server.common.data.id.EntityId;
+import org.thingsboard.server.common.data.id.EntityIdFactory;
+import org.thingsboard.server.common.data.id.RuleChainId;
+import org.thingsboard.server.common.data.id.RuleNodeId;
+import org.thingsboard.server.common.msg.gen.MsgProtos;
+
+import java.io.Serializable;
+import java.nio.ByteBuffer;
+import java.util.UUID;
+
+/**
+ * Created by ashvayka on 13.01.18.
+ */
+@Data
+@AllArgsConstructor
+public final class TbMsg implements Serializable {
+
+    private final UUID id;
+    private final String type;
+    private final EntityId originator;
+    private final TbMsgMetaData metaData;
+    private final TbMsgDataType dataType;
+    private final String data;
+
+    //The following fields are not persisted to DB, because they can always be recovered from the context;
+    private final RuleChainId ruleChainId;
+    private final RuleNodeId ruleNodeId;
+    private final long clusterPartition;
+
+    public TbMsg(UUID id, String type, EntityId originator, TbMsgMetaData metaData, String data,
+                 RuleChainId ruleChainId, RuleNodeId ruleNodeId, long clusterPartition) {
+        this.id = id;
+        this.type = type;
+        this.originator = originator;
+        this.metaData = metaData;
+        this.data = data;
+        this.dataType = TbMsgDataType.JSON;
+        this.ruleChainId = ruleChainId;
+        this.ruleNodeId = ruleNodeId;
+        this.clusterPartition = clusterPartition;
+    }
+
+    public static ByteBuffer toBytes(TbMsg msg) {
+        MsgProtos.TbMsgProto.Builder builder = MsgProtos.TbMsgProto.newBuilder();
+        builder.setId(msg.getId().toString());
+        builder.setType(msg.getType());
+        builder.setEntityType(msg.getOriginator().getEntityType().name());
+        builder.setEntityIdMSB(msg.getOriginator().getId().getMostSignificantBits());
+        builder.setEntityIdLSB(msg.getOriginator().getId().getLeastSignificantBits());
+
+        if (msg.getRuleChainId() != null) {
+            builder.setRuleChainIdMSB(msg.getRuleChainId().getId().getMostSignificantBits());
+            builder.setRuleChainIdLSB(msg.getRuleChainId().getId().getLeastSignificantBits());
+        }
+
+        if (msg.getRuleNodeId() != null) {
+            builder.setRuleNodeIdMSB(msg.getRuleNodeId().getId().getMostSignificantBits());
+            builder.setRuleNodeIdLSB(msg.getRuleNodeId().getId().getLeastSignificantBits());
+        }
+
+        if (msg.getMetaData() != null) {
+            builder.setMetaData(MsgProtos.TbMsgMetaDataProto.newBuilder().putAllData(msg.getMetaData().getData()).build());
+        }
+
+        builder.setDataType(msg.getDataType().ordinal());
+        builder.setData(msg.getData());
+        byte[] bytes = builder.build().toByteArray();
+        return ByteBuffer.wrap(bytes);
+    }
+
+    public static TbMsg fromBytes(ByteBuffer buffer) {
+        try {
+            MsgProtos.TbMsgProto proto = MsgProtos.TbMsgProto.parseFrom(buffer.array());
+            TbMsgMetaData metaData = new TbMsgMetaData(proto.getMetaData().getDataMap());
+            EntityId entityId = EntityIdFactory.getByTypeAndUuid(proto.getEntityType(), new UUID(proto.getEntityIdMSB(), proto.getEntityIdLSB()));
+            RuleChainId ruleChainId = new RuleChainId(new UUID(proto.getRuleChainIdMSB(), proto.getRuleChainIdLSB()));
+            RuleNodeId ruleNodeId = null;
+            if(proto.getRuleNodeIdMSB() != 0L && proto.getRuleNodeIdLSB() != 0L) {
+                 ruleNodeId = new RuleNodeId(new UUID(proto.getRuleNodeIdMSB(), proto.getRuleNodeIdLSB()));
+            }
+            TbMsgDataType dataType = TbMsgDataType.values()[proto.getDataType()];
+            return new TbMsg(UUID.fromString(proto.getId()), proto.getType(), entityId, metaData, dataType, proto.getData(), ruleChainId, ruleNodeId, proto.getClusterPartition());
+        } catch (InvalidProtocolBufferException e) {
+            throw new IllegalStateException("Could not parse protobuf for TbMsg", e);
+        }
+    }
+
+    public TbMsg copy(UUID newId, RuleChainId ruleChainId, RuleNodeId ruleNodeId, long clusterPartition) {
+        return new TbMsg(newId, type, originator, metaData.copy(), dataType, data, ruleChainId, ruleNodeId, clusterPartition);
+    }
+
+}
diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/TbMsgMetaData.java b/common/message/src/main/java/org/thingsboard/server/common/msg/TbMsgMetaData.java
new file mode 100644
index 0000000..e157aaa
--- /dev/null
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/TbMsgMetaData.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.server.common.msg;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.io.Serializable;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * Created by ashvayka on 13.01.18.
+ */
+@Data
+@NoArgsConstructor
+public final class TbMsgMetaData implements Serializable {
+
+    private final Map<String, String> data = new ConcurrentHashMap<>();
+
+    public TbMsgMetaData(Map<String, String> data) {
+        this.data.putAll(data);
+    }
+
+    public String getValue(String key) {
+        return data.get(key);
+    }
+
+    public void putValue(String key, String value) {
+        data.put(key, value);
+    }
+
+    public Map<String, String> values() {
+        return new HashMap<>(data);
+    }
+
+    public TbMsgMetaData copy() {
+        return new TbMsgMetaData(new ConcurrentHashMap<>(data));
+    }
+}
diff --git a/common/message/src/main/proto/tbmsg.proto b/common/message/src/main/proto/tbmsg.proto
new file mode 100644
index 0000000..60003dc
--- /dev/null
+++ b/common/message/src/main/proto/tbmsg.proto
@@ -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.
+ */
+syntax = "proto3";
+package msgqueue;
+
+option java_package = "org.thingsboard.server.common.msg.gen";
+option java_outer_classname = "MsgProtos";
+
+message TbMsgMetaDataProto {
+    map<string, string> data = 1;
+}
+
+message TbMsgProto {
+    string id = 1;
+    string type = 2;
+    string entityType = 3;
+    int64 entityIdMSB = 4;
+    int64 entityIdLSB = 5;
+
+    int64 ruleChainIdMSB = 6;
+    int64 ruleChainIdLSB = 7;
+
+    int64 ruleNodeIdMSB = 8;
+    int64 ruleNodeIdLSB = 9;
+    int64 clusterPartition = 10;
+
+    TbMsgMetaDataProto metaData = 11;
+
+    int32 dataType = 12;
+    string data = 13;
+
+}
\ No newline at end of file

common/pom.xml 2(+1 -1)

diff --git a/common/pom.xml b/common/pom.xml
index 0f92175..8644257 100644
--- a/common/pom.xml
+++ b/common/pom.xml
@@ -20,7 +20,7 @@
     <modelVersion>4.0.0</modelVersion>
     <parent>
         <groupId>org.thingsboard</groupId>
-        <version>1.4.1-SNAPSHOT</version>
+        <version>2.0.0-SNAPSHOT</version>
         <artifactId>thingsboard</artifactId>
     </parent>
     <groupId>org.thingsboard</groupId>
diff --git a/common/transport/pom.xml b/common/transport/pom.xml
index 18f2de6..fe0ab72 100644
--- a/common/transport/pom.xml
+++ b/common/transport/pom.xml
@@ -20,7 +20,7 @@
     <modelVersion>4.0.0</modelVersion>
     <parent>
         <groupId>org.thingsboard</groupId>
-        <version>1.4.1-SNAPSHOT</version>
+        <version>2.0.0-SNAPSHOT</version>
         <artifactId>common</artifactId>
     </parent>
     <groupId>org.thingsboard.common</groupId>
diff --git a/common/transport/src/main/java/org/thingsboard/server/common/transport/adaptor/JsonConverter.java b/common/transport/src/main/java/org/thingsboard/server/common/transport/adaptor/JsonConverter.java
index 390266a..e7c1734 100644
--- a/common/transport/src/main/java/org/thingsboard/server/common/transport/adaptor/JsonConverter.java
+++ b/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);
     }
 
+    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 {
+        return convertToTelemetry(jsonObject, System.currentTimeMillis(), requestId);
+    }
+
+    private static TelemetryUploadRequest convertToTelemetry(JsonElement jsonObject, long systemTs, int requestId) throws JsonSyntaxException {
         BasicTelemetryUploadRequest request = new BasicTelemetryUploadRequest(requestId);
-        long systemTs = System.currentTimeMillis();
         if (jsonObject.isJsonObject()) {
             parseObject(request, systemTs, jsonObject);
         } else if (jsonObject.isJsonArray()) {
@@ -118,13 +125,13 @@ public class JsonConverter {
         }
     }
 
-    public static UpdateAttributesRequest convertToAttributes(JsonElement element) {
+    public static AttributesUpdateRequest convertToAttributes(JsonElement element) {
         return convertToAttributes(element, BasicRequest.DEFAULT_REQUEST_ID);
     }
 
-    public static UpdateAttributesRequest convertToAttributes(JsonElement element, int requestId) {
+    public static AttributesUpdateRequest convertToAttributes(JsonElement element, int requestId) {
         if (element.isJsonObject()) {
-            BasicUpdateAttributesRequest request = new BasicUpdateAttributesRequest(requestId);
+            BasicAttributesUpdateRequest request = new BasicAttributesUpdateRequest(requestId);
             long ts = System.currentTimeMillis();
             request.add(parseValues(element.getAsJsonObject()).stream().map(kv -> new BaseAttributeKvEntry(kv, ts)).collect(Collectors.toList()));
             return request;
diff --git a/common/transport/src/main/java/org/thingsboard/server/common/transport/quota/host/HostIntervalRegistryLogger.java b/common/transport/src/main/java/org/thingsboard/server/common/transport/quota/host/HostIntervalRegistryLogger.java
new file mode 100644
index 0000000..65767f1
--- /dev/null
+++ b/common/transport/src/main/java/org/thingsboard/server/common/transport/quota/host/HostIntervalRegistryLogger.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.server.common.transport.quota.host;
+
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Component;
+import org.thingsboard.server.common.transport.quota.inmemory.IntervalRegistryLogger;
+
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+
+@Component
+@Slf4j
+public class HostIntervalRegistryLogger extends IntervalRegistryLogger {
+
+    private final long logIntervalMin;
+
+    public HostIntervalRegistryLogger(@Value("${quota.host.log.topSize}") int topSize,
+                                      @Value("${quota.host.log.intervalMin}") long logIntervalMin,
+                                      HostRequestIntervalRegistry intervalRegistry) {
+        super(topSize, logIntervalMin, intervalRegistry);
+        this.logIntervalMin = logIntervalMin;
+    }
+
+    protected void log(Map<String, Long> top, int uniqHosts, long requestsCount) {
+        long rps = requestsCount / TimeUnit.MINUTES.toSeconds(logIntervalMin);
+        StringBuilder builder = new StringBuilder("Quota Statistic : ");
+        builder.append("uniqHosts : ").append(uniqHosts).append("; ");
+        builder.append("requestsCount : ").append(requestsCount).append("; ");
+        builder.append("RPS : ").append(rps).append(" ");
+        builder.append("top -> ");
+        for (Map.Entry<String, Long> host : top.entrySet()) {
+            builder.append(host.getKey()).append(" : ").append(host.getValue()).append("; ");
+        }
+
+        log.info(builder.toString());
+    }
+}
diff --git a/common/transport/src/main/java/org/thingsboard/server/common/transport/quota/host/HostRequestIntervalRegistry.java b/common/transport/src/main/java/org/thingsboard/server/common/transport/quota/host/HostRequestIntervalRegistry.java
new file mode 100644
index 0000000..9b3b461
--- /dev/null
+++ b/common/transport/src/main/java/org/thingsboard/server/common/transport/quota/host/HostRequestIntervalRegistry.java
@@ -0,0 +1,37 @@
+/**
+ * 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.server.common.transport.quota.host;
+
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Component;
+import org.thingsboard.server.common.transport.quota.inmemory.KeyBasedIntervalRegistry;
+
+/**
+ * @author Vitaliy Paromskiy
+ * @version 1.0
+ */
+@Component
+@Slf4j
+public class HostRequestIntervalRegistry extends KeyBasedIntervalRegistry {
+
+    public HostRequestIntervalRegistry(@Value("${quota.host.intervalMs}") long intervalDurationMs,
+                                       @Value("${quota.host.ttlMs}") long ttlMs,
+                                       @Value("${quota.host.whitelist}") String whiteList,
+                                       @Value("${quota.host.blacklist}") String blackList) {
+        super(intervalDurationMs, ttlMs, whiteList, blackList, "host");
+    }
+}
diff --git a/common/transport/src/main/java/org/thingsboard/server/common/transport/quota/host/HostRequestsQuotaService.java b/common/transport/src/main/java/org/thingsboard/server/common/transport/quota/host/HostRequestsQuotaService.java
new file mode 100644
index 0000000..69342b5
--- /dev/null
+++ b/common/transport/src/main/java/org/thingsboard/server/common/transport/quota/host/HostRequestsQuotaService.java
@@ -0,0 +1,37 @@
+/**
+ * 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.server.common.transport.quota.host;
+
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Service;
+import org.thingsboard.server.common.transport.quota.AbstractQuotaService;
+
+/**
+ * @author Vitaliy Paromskiy
+ * @version 1.0
+ */
+@Service
+@Slf4j
+public class HostRequestsQuotaService extends AbstractQuotaService {
+
+    public HostRequestsQuotaService(HostRequestIntervalRegistry requestRegistry, HostRequestLimitPolicy requestsPolicy,
+                                    HostIntervalRegistryCleaner registryCleaner, HostIntervalRegistryLogger registryLogger,
+                                    @Value("${quota.host.enabled}") boolean enabled) {
+        super(requestRegistry, requestsPolicy, registryCleaner, registryLogger, enabled);
+    }
+
+}
diff --git a/common/transport/src/main/java/org/thingsboard/server/common/transport/quota/inmemory/IntervalRegistryCleaner.java b/common/transport/src/main/java/org/thingsboard/server/common/transport/quota/inmemory/IntervalRegistryCleaner.java
index a227d2a..0c510ff 100644
--- a/common/transport/src/main/java/org/thingsboard/server/common/transport/quota/inmemory/IntervalRegistryCleaner.java
+++ b/common/transport/src/main/java/org/thingsboard/server/common/transport/quota/inmemory/IntervalRegistryCleaner.java
@@ -16,10 +16,7 @@
 package org.thingsboard.server.common.transport.quota.inmemory;
 
 import lombok.extern.slf4j.Slf4j;
-import org.springframework.beans.factory.annotation.Value;
-import org.springframework.stereotype.Component;
 
-import javax.annotation.PreDestroy;
 import java.util.concurrent.Executors;
 import java.util.concurrent.ScheduledExecutorService;
 import java.util.concurrent.TimeUnit;
@@ -28,15 +25,14 @@ import java.util.concurrent.TimeUnit;
  * @author Vitaliy Paromskiy
  * @version 1.0
  */
-@Component
 @Slf4j
-public class IntervalRegistryCleaner {
+public abstract class IntervalRegistryCleaner {
 
-    private final HostRequestIntervalRegistry intervalRegistry;
+    private final KeyBasedIntervalRegistry intervalRegistry;
     private final long cleanPeriodMs;
     private ScheduledExecutorService executor;
 
-    public IntervalRegistryCleaner(HostRequestIntervalRegistry intervalRegistry, @Value("${quota.host.cleanPeriodMs}") long cleanPeriodMs) {
+    public IntervalRegistryCleaner(KeyBasedIntervalRegistry intervalRegistry, long cleanPeriodMs) {
         this.intervalRegistry = intervalRegistry;
         this.cleanPeriodMs = cleanPeriodMs;
     }
diff --git a/common/transport/src/main/java/org/thingsboard/server/common/transport/quota/inmemory/IntervalRegistryLogger.java b/common/transport/src/main/java/org/thingsboard/server/common/transport/quota/inmemory/IntervalRegistryLogger.java
index 8b34a6b..30399a1 100644
--- a/common/transport/src/main/java/org/thingsboard/server/common/transport/quota/inmemory/IntervalRegistryLogger.java
+++ b/common/transport/src/main/java/org/thingsboard/server/common/transport/quota/inmemory/IntervalRegistryLogger.java
@@ -17,8 +17,6 @@ package org.thingsboard.server.common.transport.quota.inmemory;
 
 import com.google.common.collect.MinMaxPriorityQueue;
 import lombok.extern.slf4j.Slf4j;
-import org.springframework.beans.factory.annotation.Value;
-import org.springframework.stereotype.Component;
 
 import java.util.Comparator;
 import java.util.Map;
@@ -32,17 +30,15 @@ import java.util.stream.Collectors;
  * @author Vitaliy Paromskiy
  * @version 1.0
  */
-@Component
 @Slf4j
-public class IntervalRegistryLogger {
+public abstract class IntervalRegistryLogger {
 
     private final int topSize;
-    private final HostRequestIntervalRegistry intervalRegistry;
+    private final KeyBasedIntervalRegistry intervalRegistry;
     private final long logIntervalMin;
     private ScheduledExecutorService executor;
 
-    public IntervalRegistryLogger(@Value("${quota.log.topSize}") int topSize, @Value("${quota.log.intervalMin}") long logIntervalMin,
-                                  HostRequestIntervalRegistry intervalRegistry) {
+    public IntervalRegistryLogger(int topSize, long logIntervalMin, KeyBasedIntervalRegistry intervalRegistry) {
         this.topSize = topSize;
         this.logIntervalMin = logIntervalMin;
         this.intervalRegistry = intervalRegistry;
@@ -79,17 +75,5 @@ public class IntervalRegistryLogger {
         return topQueue.stream().collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
     }
 
-    private void log(Map<String, Long> top, int uniqHosts, long requestsCount) {
-        long rps = requestsCount / TimeUnit.MINUTES.toSeconds(logIntervalMin);
-        StringBuilder builder = new StringBuilder("Quota Statistic : ");
-        builder.append("uniqHosts : ").append(uniqHosts).append("; ");
-        builder.append("requestsCount : ").append(requestsCount).append("; ");
-        builder.append("RPS : ").append(rps).append(" ");
-        builder.append("top -> ");
-        for (Map.Entry<String, Long> host : top.entrySet()) {
-            builder.append(host.getKey()).append(" : ").append(host.getValue()).append("; ");
-        }
-
-        log.info(builder.toString());
-    }
+    protected abstract void log(Map<String, Long> top, int uniqHosts, long requestsCount);
 }
diff --git a/common/transport/src/main/java/org/thingsboard/server/common/transport/quota/tenant/TenantIntervalRegistryLogger.java b/common/transport/src/main/java/org/thingsboard/server/common/transport/quota/tenant/TenantIntervalRegistryLogger.java
new file mode 100644
index 0000000..c56f457
--- /dev/null
+++ b/common/transport/src/main/java/org/thingsboard/server/common/transport/quota/tenant/TenantIntervalRegistryLogger.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.server.common.transport.quota.tenant;
+
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Component;
+import org.thingsboard.server.common.transport.quota.inmemory.IntervalRegistryLogger;
+
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+
+@Slf4j
+@Component
+public class TenantIntervalRegistryLogger extends IntervalRegistryLogger {
+
+    private final long logIntervalMin;
+
+    public TenantIntervalRegistryLogger(@Value("${quota.rule.tenant.log.topSize}") int topSize,
+                                        @Value("${quota.rule.tenant.log.intervalMin}") long logIntervalMin,
+                                        TenantMsgsIntervalRegistry intervalRegistry) {
+        super(topSize, logIntervalMin, intervalRegistry);
+        this.logIntervalMin = logIntervalMin;
+    }
+
+    protected void log(Map<String, Long> top, int uniqHosts, long requestsCount) {
+        long rps = requestsCount / TimeUnit.MINUTES.toSeconds(logIntervalMin);
+        StringBuilder builder = new StringBuilder("Tenant Quota Statistic : ");
+        builder.append("uniqTenants : ").append(uniqHosts).append("; ");
+        builder.append("requestsCount : ").append(requestsCount).append("; ");
+        builder.append("RPS : ").append(rps).append(" ");
+        builder.append("top -> ");
+        for (Map.Entry<String, Long> host : top.entrySet()) {
+            builder.append(host.getKey()).append(" : ").append(host.getValue()).append("; ");
+        }
+
+        log.info(builder.toString());
+    }
+}
diff --git a/common/transport/src/main/java/org/thingsboard/server/common/transport/TransportAdaptor.java b/common/transport/src/main/java/org/thingsboard/server/common/transport/TransportAdaptor.java
index 93a6e99..080f874 100644
--- a/common/transport/src/main/java/org/thingsboard/server/common/transport/TransportAdaptor.java
+++ b/common/transport/src/main/java/org/thingsboard/server/common/transport/TransportAdaptor.java
@@ -16,7 +16,7 @@
 package org.thingsboard.server.common.transport;
 
 import org.thingsboard.server.common.msg.session.AdaptorToSessionActorMsg;
-import org.thingsboard.server.common.msg.session.MsgType;
+import org.thingsboard.server.common.msg.session.SessionMsgType;
 import org.thingsboard.server.common.msg.session.SessionActorToAdaptorMsg;
 import org.thingsboard.server.common.msg.session.SessionContext;
 import org.thingsboard.server.common.transport.adaptor.AdaptorException;
@@ -25,7 +25,7 @@ import java.util.Optional;
 
 public interface TransportAdaptor<C extends SessionContext, T, V> {
 
-    AdaptorToSessionActorMsg convertToActorMsg(C ctx, MsgType type, T inbound) throws AdaptorException;
+    AdaptorToSessionActorMsg convertToActorMsg(C ctx, SessionMsgType type, T inbound) throws AdaptorException;
 
     Optional<V> convertToAdaptorMsg(C ctx, SessionActorToAdaptorMsg msg) throws AdaptorException;
 
diff --git a/common/transport/src/test/java/org/thingsboard/server/common/transport/quota/HostRequestLimitPolicyTest.java b/common/transport/src/test/java/org/thingsboard/server/common/transport/quota/HostRequestLimitPolicyTest.java
index 174d182..07e03ef 100644
--- a/common/transport/src/test/java/org/thingsboard/server/common/transport/quota/HostRequestLimitPolicyTest.java
+++ b/common/transport/src/test/java/org/thingsboard/server/common/transport/quota/HostRequestLimitPolicyTest.java
@@ -16,6 +16,7 @@
 package org.thingsboard.server.common.transport.quota;
 
 import org.junit.Test;
+import org.thingsboard.server.common.transport.quota.host.HostRequestLimitPolicy;
 
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
diff --git a/common/transport/src/test/java/org/thingsboard/server/common/transport/quota/HostRequestsQuotaServiceTest.java b/common/transport/src/test/java/org/thingsboard/server/common/transport/quota/HostRequestsQuotaServiceTest.java
index 547f0cf..20f8a55 100644
--- a/common/transport/src/test/java/org/thingsboard/server/common/transport/quota/HostRequestsQuotaServiceTest.java
+++ b/common/transport/src/test/java/org/thingsboard/server/common/transport/quota/HostRequestsQuotaServiceTest.java
@@ -17,9 +17,7 @@ package org.thingsboard.server.common.transport.quota;
 
 import org.junit.Before;
 import org.junit.Test;
-import org.thingsboard.server.common.transport.quota.inmemory.HostRequestIntervalRegistry;
-import org.thingsboard.server.common.transport.quota.inmemory.IntervalRegistryCleaner;
-import org.thingsboard.server.common.transport.quota.inmemory.IntervalRegistryLogger;
+import org.thingsboard.server.common.transport.quota.host.*;
 
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
@@ -35,8 +33,8 @@ public class HostRequestsQuotaServiceTest {
 
     private HostRequestIntervalRegistry requestRegistry = mock(HostRequestIntervalRegistry.class);
     private HostRequestLimitPolicy requestsPolicy = mock(HostRequestLimitPolicy.class);
-    private IntervalRegistryCleaner registryCleaner = mock(IntervalRegistryCleaner.class);
-    private IntervalRegistryLogger registryLogger = mock(IntervalRegistryLogger.class);
+    private HostIntervalRegistryCleaner registryCleaner = mock(HostIntervalRegistryCleaner.class);
+    private HostIntervalRegistryLogger registryLogger = mock(HostIntervalRegistryLogger.class);
 
     @Before
     public void init() {
diff --git a/common/transport/src/test/java/org/thingsboard/server/common/transport/quota/inmemory/HostRequestIntervalRegistryTest.java b/common/transport/src/test/java/org/thingsboard/server/common/transport/quota/inmemory/HostRequestIntervalRegistryTest.java
index 78b82ee..b49dd00 100644
--- a/common/transport/src/test/java/org/thingsboard/server/common/transport/quota/inmemory/HostRequestIntervalRegistryTest.java
+++ b/common/transport/src/test/java/org/thingsboard/server/common/transport/quota/inmemory/HostRequestIntervalRegistryTest.java
@@ -15,11 +15,9 @@
  */
 package org.thingsboard.server.common.transport.quota.inmemory;
 
-import com.google.common.collect.Sets;
 import org.junit.Before;
 import org.junit.Test;
-
-import java.util.Collections;
+import org.thingsboard.server.common.transport.quota.host.HostRequestIntervalRegistry;
 
 import static org.junit.Assert.assertEquals;
 
diff --git a/common/transport/src/test/java/org/thingsboard/server/common/transport/quota/inmemory/IntervalRegistryLoggerTest.java b/common/transport/src/test/java/org/thingsboard/server/common/transport/quota/inmemory/IntervalRegistryLoggerTest.java
index c9139ae..6e51420 100644
--- a/common/transport/src/test/java/org/thingsboard/server/common/transport/quota/inmemory/IntervalRegistryLoggerTest.java
+++ b/common/transport/src/test/java/org/thingsboard/server/common/transport/quota/inmemory/IntervalRegistryLoggerTest.java
@@ -18,6 +18,8 @@ package org.thingsboard.server.common.transport.quota.inmemory;
 import com.google.common.collect.ImmutableMap;
 import org.junit.Before;
 import org.junit.Test;
+import org.thingsboard.server.common.transport.quota.host.HostIntervalRegistryLogger;
+import org.thingsboard.server.common.transport.quota.host.HostRequestIntervalRegistry;
 
 import java.util.Collections;
 import java.util.Map;
@@ -37,7 +39,7 @@ public class IntervalRegistryLoggerTest {
 
     @Before
     public void init() {
-        logger = new IntervalRegistryLogger(3, 10, requestRegistry);
+        logger = new HostIntervalRegistryLogger(3, 10, requestRegistry);
     }
 
     @Test

dao/pom.xml 25(+10 -15)

diff --git a/dao/pom.xml b/dao/pom.xml
index 3eb267e..b9a180a 100644
--- a/dao/pom.xml
+++ b/dao/pom.xml
@@ -20,10 +20,9 @@
     <modelVersion>4.0.0</modelVersion>
     <parent>
         <groupId>org.thingsboard</groupId>
-        <version>1.4.1-SNAPSHOT</version>
+        <version>2.0.0-SNAPSHOT</version>
         <artifactId>thingsboard</artifactId>
     </parent>
-    <groupId>org.thingsboard</groupId>
     <artifactId>dao</artifactId>
     <packaging>jar</packaging>
 
@@ -39,7 +38,11 @@
         <dependency>
             <groupId>org.thingsboard.common</groupId>
             <artifactId>data</artifactId>
-        </dependency>    
+        </dependency>
+        <dependency>
+            <groupId>org.thingsboard.common</groupId>
+            <artifactId>message</artifactId>
+        </dependency>
         <dependency>
             <groupId>org.slf4j</groupId>
             <artifactId>slf4j-api</artifactId>
@@ -142,26 +145,18 @@
             <scope>test</scope>
         </dependency>
         <dependency>
-            <groupId>org.apache.curator</groupId>
-            <artifactId>curator-x-discovery</artifactId>
-        </dependency>
-        <dependency>
-            <groupId>com.hazelcast</groupId>
-            <artifactId>hazelcast-zookeeper</artifactId>
+            <groupId>com.google.protobuf</groupId>
+            <artifactId>protobuf-java</artifactId>
         </dependency>
         <dependency>
-            <groupId>com.hazelcast</groupId>
-            <artifactId>hazelcast</artifactId>
+            <groupId>org.apache.curator</groupId>
+            <artifactId>curator-x-discovery</artifactId>
         </dependency>
         <dependency>
             <groupId>com.github.ben-manes.caffeine</groupId>
             <artifactId>caffeine</artifactId>
         </dependency>
         <dependency>
-            <groupId>com.hazelcast</groupId>
-            <artifactId>hazelcast-spring</artifactId>
-        </dependency>
-        <dependency>
             <groupId>org.springframework.boot</groupId>
             <artifactId>spring-boot-autoconfigure</artifactId>
         </dependency>
diff --git a/dao/src/main/java/org/thingsboard/server/dao/alarm/AlarmService.java b/dao/src/main/java/org/thingsboard/server/dao/alarm/AlarmService.java
index 4da7df2..6638818 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/alarm/AlarmService.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/alarm/AlarmService.java
@@ -15,8 +15,15 @@
  */
 package org.thingsboard.server.dao.alarm;
 
+import com.fasterxml.jackson.databind.JsonNode;
 import com.google.common.util.concurrent.ListenableFuture;
-import org.thingsboard.server.common.data.alarm.*;
+import org.thingsboard.server.common.data.alarm.Alarm;
+import org.thingsboard.server.common.data.alarm.AlarmId;
+import org.thingsboard.server.common.data.alarm.AlarmInfo;
+import org.thingsboard.server.common.data.alarm.AlarmQuery;
+import org.thingsboard.server.common.data.alarm.AlarmSearchStatus;
+import org.thingsboard.server.common.data.alarm.AlarmSeverity;
+import org.thingsboard.server.common.data.alarm.AlarmStatus;
 import org.thingsboard.server.common.data.id.EntityId;
 import org.thingsboard.server.common.data.id.TenantId;
 import org.thingsboard.server.common.data.page.TimePageData;
@@ -30,7 +37,7 @@ public interface AlarmService {
 
     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);
 
diff --git a/dao/src/main/java/org/thingsboard/server/dao/alarm/BaseAlarmService.java b/dao/src/main/java/org/thingsboard/server/dao/alarm/BaseAlarmService.java
index e669ae2..23fe85a 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/alarm/BaseAlarmService.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/alarm/BaseAlarmService.java
@@ -16,8 +16,8 @@
 package org.thingsboard.server.dao.alarm;
 
 
+import com.fasterxml.jackson.databind.JsonNode;
 import com.google.common.base.Function;
-import com.google.common.util.concurrent.AsyncFunction;
 import com.google.common.util.concurrent.Futures;
 import com.google.common.util.concurrent.ListenableFuture;
 import lombok.extern.slf4j.Slf4j;
@@ -25,19 +25,25 @@ import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 import org.springframework.util.StringUtils;
 import org.thingsboard.server.common.data.Tenant;
-import org.thingsboard.server.common.data.alarm.*;
+import org.thingsboard.server.common.data.alarm.Alarm;
+import org.thingsboard.server.common.data.alarm.AlarmId;
+import org.thingsboard.server.common.data.alarm.AlarmInfo;
+import org.thingsboard.server.common.data.alarm.AlarmQuery;
+import org.thingsboard.server.common.data.alarm.AlarmSearchStatus;
+import org.thingsboard.server.common.data.alarm.AlarmSeverity;
+import org.thingsboard.server.common.data.alarm.AlarmStatus;
 import org.thingsboard.server.common.data.id.EntityId;
 import org.thingsboard.server.common.data.id.TenantId;
 import org.thingsboard.server.common.data.page.TimePageData;
 import org.thingsboard.server.common.data.page.TimePageLink;
 import org.thingsboard.server.common.data.relation.EntityRelation;
+import org.thingsboard.server.common.data.relation.EntityRelationsQuery;
+import org.thingsboard.server.common.data.relation.EntitySearchDirection;
 import org.thingsboard.server.common.data.relation.RelationTypeGroup;
+import org.thingsboard.server.common.data.relation.RelationsSearchParameters;
 import org.thingsboard.server.dao.entity.AbstractEntityService;
 import org.thingsboard.server.dao.entity.EntityService;
 import org.thingsboard.server.dao.exception.DataValidationException;
-import org.thingsboard.server.common.data.relation.EntityRelationsQuery;
-import org.thingsboard.server.common.data.relation.EntitySearchDirection;
-import org.thingsboard.server.common.data.relation.RelationsSearchParameters;
 import org.thingsboard.server.dao.service.DataValidator;
 import org.thingsboard.server.dao.tenant.TenantDao;
 
@@ -187,7 +193,7 @@ public class BaseAlarmService extends AbstractEntityService implements AlarmServ
     }
 
     @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>() {
             @Nullable
             @Override
@@ -199,6 +205,9 @@ public class BaseAlarmService extends AbstractEntityService implements AlarmServ
                     AlarmStatus newStatus = oldStatus.isAck() ? AlarmStatus.CLEARED_ACK : AlarmStatus.CLEARED_UNACK;
                     alarm.setStatus(newStatus);
                     alarm.setClearTs(clearTime);
+                    if (details != null) {
+                        alarm.setDetails(details);
+                    }
                     alarmDao.save(alarm);
                     updateRelations(alarm, oldStatus, newStatus);
                     return true;
@@ -218,15 +227,14 @@ public class BaseAlarmService extends AbstractEntityService implements AlarmServ
     public ListenableFuture<AlarmInfo> findAlarmInfoByIdAsync(AlarmId alarmId) {
         log.trace("Executing findAlarmInfoByIdAsync [{}]", alarmId);
         validateId(alarmId, "Incorrect alarmId " + alarmId);
-        return Futures.transform(alarmDao.findAlarmByIdAsync(alarmId.getId()),
-                (AsyncFunction<Alarm, AlarmInfo>) alarm1 -> {
-                    AlarmInfo alarmInfo = new AlarmInfo(alarm1);
+        return Futures.transformAsync(alarmDao.findAlarmByIdAsync(alarmId.getId()),
+                a -> {
+                    AlarmInfo alarmInfo = new AlarmInfo(a);
                     return Futures.transform(
-                            entityService.fetchEntityNameAsync(alarmInfo.getOriginator()), (Function<String, AlarmInfo>)
-                                    originatorName -> {
-                                        alarmInfo.setOriginatorName(originatorName);
-                                        return alarmInfo;
-                                    }
+                            entityService.fetchEntityNameAsync(alarmInfo.getOriginator()), originatorName -> {
+                                alarmInfo.setOriginatorName(originatorName);
+                                return alarmInfo;
+                            }
                     );
                 });
     }
@@ -235,18 +243,17 @@ public class BaseAlarmService extends AbstractEntityService implements AlarmServ
     public ListenableFuture<TimePageData<AlarmInfo>> findAlarms(AlarmQuery query) {
         ListenableFuture<List<AlarmInfo>> alarms = alarmDao.findAlarms(query);
         if (query.getFetchOriginator() != null && query.getFetchOriginator().booleanValue()) {
-            alarms = Futures.transform(alarms, (AsyncFunction<List<AlarmInfo>, List<AlarmInfo>>) input -> {
+            alarms = Futures.transformAsync(alarms, input -> {
                 List<ListenableFuture<AlarmInfo>> alarmFutures = new ArrayList<>(input.size());
                 for (AlarmInfo alarmInfo : input) {
                     alarmFutures.add(Futures.transform(
-                            entityService.fetchEntityNameAsync(alarmInfo.getOriginator()), (Function<String, AlarmInfo>)
-                                    originatorName -> {
-                                        if (originatorName == null) {
-                                            originatorName = "Deleted";
-                                        }
-                                        alarmInfo.setOriginatorName(originatorName);
-                                        return alarmInfo;
-                                    }
+                            entityService.fetchEntityNameAsync(alarmInfo.getOriginator()), originatorName -> {
+                                if (originatorName == null) {
+                                    originatorName = "Deleted";
+                                }
+                                alarmInfo.setOriginatorName(originatorName);
+                                return alarmInfo;
+                            }
                     ));
                 }
                 return Futures.successfulAsList(alarmFutures);
diff --git a/dao/src/main/java/org/thingsboard/server/dao/alarm/CassandraAlarmDao.java b/dao/src/main/java/org/thingsboard/server/dao/alarm/CassandraAlarmDao.java
index 1233c7f..646c055 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/alarm/CassandraAlarmDao.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/alarm/CassandraAlarmDao.java
@@ -17,8 +17,6 @@ package org.thingsboard.server.dao.alarm;
 
 import com.datastax.driver.core.querybuilder.QueryBuilder;
 import com.datastax.driver.core.querybuilder.Select;
-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;
@@ -45,7 +43,12 @@ import java.util.UUID;
 
 import static com.datastax.driver.core.querybuilder.QueryBuilder.eq;
 import static com.datastax.driver.core.querybuilder.QueryBuilder.select;
-import static org.thingsboard.server.dao.model.ModelConstants.*;
+import static org.thingsboard.server.dao.model.ModelConstants.ALARM_BY_ID_VIEW_NAME;
+import static org.thingsboard.server.dao.model.ModelConstants.ALARM_COLUMN_FAMILY_NAME;
+import static org.thingsboard.server.dao.model.ModelConstants.ALARM_ORIGINATOR_ID_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.ALARM_ORIGINATOR_TYPE_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.ALARM_TENANT_ID_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.ALARM_TYPE_PROPERTY;
 
 @Component
 @Slf4j
@@ -102,12 +105,12 @@ public class CassandraAlarmDao extends CassandraAbstractModelDao<AlarmEntity, Al
         }
         String relationType = BaseAlarmService.ALARM_RELATION_PREFIX + searchStatusName;
         ListenableFuture<List<EntityRelation>> relations = relationDao.findRelations(affectedEntity, relationType, RelationTypeGroup.ALARM, EntityType.ALARM, query.getPageLink());
-        return Futures.transform(relations, (AsyncFunction<List<EntityRelation>, List<AlarmInfo>>) input -> {
+        return Futures.transformAsync(relations, input -> {
             List<ListenableFuture<AlarmInfo>> alarmFutures = new ArrayList<>(input.size());
             for (EntityRelation relation : input) {
                 alarmFutures.add(Futures.transform(
                         findAlarmByIdAsync(relation.getTo().getId()),
-                        (Function<Alarm, AlarmInfo>) AlarmInfo::new));
+                        AlarmInfo::new));
             }
             return Futures.successfulAsList(alarmFutures);
         });
diff --git a/dao/src/main/java/org/thingsboard/server/dao/asset/BaseAssetService.java b/dao/src/main/java/org/thingsboard/server/dao/asset/BaseAssetService.java
index dcd9523..1eafb07 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/asset/BaseAssetService.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/asset/BaseAssetService.java
@@ -17,7 +17,6 @@ package org.thingsboard.server.dao.asset;
 
 
 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;
@@ -45,12 +44,19 @@ import org.thingsboard.server.dao.service.DataValidator;
 import org.thingsboard.server.dao.service.PaginatedRemover;
 import org.thingsboard.server.dao.tenant.TenantDao;
 
-import java.util.*;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Optional;
 import java.util.stream.Collectors;
 
 import static org.thingsboard.server.dao.DaoUtil.toUUIDs;
 import static org.thingsboard.server.dao.model.ModelConstants.NULL_UUID;
-import static org.thingsboard.server.dao.service.Validator.*;
+import static org.thingsboard.server.dao.service.Validator.validateId;
+import static org.thingsboard.server.dao.service.Validator.validateIds;
+import static org.thingsboard.server.dao.service.Validator.validatePageLink;
+import static org.thingsboard.server.dao.service.Validator.validateString;
 
 @Service
 @Slf4j
@@ -194,10 +200,10 @@ public class BaseAssetService extends AbstractEntityService implements AssetServ
     @Override
     public ListenableFuture<List<Asset>> findAssetsByQuery(AssetSearchQuery query) {
         ListenableFuture<List<EntityRelation>> relations = relationService.findByQuery(query.toEntitySearchQuery());
-        ListenableFuture<List<Asset>> assets = Futures.transform(relations, (AsyncFunction<List<EntityRelation>, List<Asset>>) relations1 -> {
+        ListenableFuture<List<Asset>> assets = Futures.transformAsync(relations, r -> {
             EntitySearchDirection direction = query.toEntitySearchQuery().getParameters().getDirection();
             List<ListenableFuture<Asset>> futures = new ArrayList<>();
-            for (EntityRelation relation : relations1) {
+            for (EntityRelation relation : r) {
                 EntityId entityId = direction == EntitySearchDirection.FROM ? relation.getTo() : relation.getFrom();
                 if (entityId.getEntityType() == EntityType.ASSET) {
                     futures.add(findAssetByIdAsync(new AssetId(entityId.getId())));
diff --git a/dao/src/main/java/org/thingsboard/server/dao/asset/CassandraAssetDao.java b/dao/src/main/java/org/thingsboard/server/dao/asset/CassandraAssetDao.java
index 64ec718..80a6f43 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/asset/CassandraAssetDao.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/asset/CassandraAssetDao.java
@@ -36,10 +36,30 @@ import org.thingsboard.server.dao.nosql.CassandraAbstractSearchTextDao;
 import org.thingsboard.server.dao.util.NoSqlDao;
 
 import javax.annotation.Nullable;
-import java.util.*;
-
-import static com.datastax.driver.core.querybuilder.QueryBuilder.*;
-import static org.thingsboard.server.dao.model.ModelConstants.*;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Optional;
+import java.util.UUID;
+
+import static com.datastax.driver.core.querybuilder.QueryBuilder.eq;
+import static com.datastax.driver.core.querybuilder.QueryBuilder.in;
+import static com.datastax.driver.core.querybuilder.QueryBuilder.select;
+import static org.thingsboard.server.dao.model.ModelConstants.ASSET_BY_CUSTOMER_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME;
+import static org.thingsboard.server.dao.model.ModelConstants.ASSET_BY_CUSTOMER_BY_TYPE_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME;
+import static org.thingsboard.server.dao.model.ModelConstants.ASSET_BY_TENANT_AND_NAME_VIEW_NAME;
+import static org.thingsboard.server.dao.model.ModelConstants.ASSET_BY_TENANT_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME;
+import static org.thingsboard.server.dao.model.ModelConstants.ASSET_BY_TENANT_BY_TYPE_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME;
+import static org.thingsboard.server.dao.model.ModelConstants.ASSET_COLUMN_FAMILY_NAME;
+import static org.thingsboard.server.dao.model.ModelConstants.ASSET_CUSTOMER_ID_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.ASSET_NAME_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.ASSET_TENANT_ID_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.ASSET_TYPE_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.ENTITY_SUBTYPE_COLUMN_FAMILY_NAME;
+import static org.thingsboard.server.dao.model.ModelConstants.ENTITY_SUBTYPE_ENTITY_TYPE_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.ENTITY_SUBTYPE_TENANT_ID_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.ID_PROPERTY;
 
 @Component
 @Slf4j
diff --git a/dao/src/main/java/org/thingsboard/server/dao/attributes/CassandraBaseAttributesDao.java b/dao/src/main/java/org/thingsboard/server/dao/attributes/CassandraBaseAttributesDao.java
index 8ae9dc8..a736a69 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/attributes/CassandraBaseAttributesDao.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/attributes/CassandraBaseAttributesDao.java
@@ -15,7 +15,11 @@
  */
 package org.thingsboard.server.dao.attributes;
 
-import com.datastax.driver.core.*;
+import com.datastax.driver.core.BoundStatement;
+import com.datastax.driver.core.PreparedStatement;
+import com.datastax.driver.core.ResultSet;
+import com.datastax.driver.core.Row;
+import com.datastax.driver.core.Statement;
 import com.datastax.driver.core.querybuilder.QueryBuilder;
 import com.datastax.driver.core.querybuilder.Select;
 import com.google.common.base.Function;
@@ -41,7 +45,12 @@ import java.util.stream.Collectors;
 
 import static com.datastax.driver.core.querybuilder.QueryBuilder.eq;
 import static com.datastax.driver.core.querybuilder.QueryBuilder.select;
-import static org.thingsboard.server.dao.model.ModelConstants.*;
+import static org.thingsboard.server.dao.model.ModelConstants.ATTRIBUTES_KV_CF;
+import static org.thingsboard.server.dao.model.ModelConstants.ATTRIBUTE_KEY_COLUMN;
+import static org.thingsboard.server.dao.model.ModelConstants.ATTRIBUTE_TYPE_COLUMN;
+import static org.thingsboard.server.dao.model.ModelConstants.ENTITY_ID_COLUMN;
+import static org.thingsboard.server.dao.model.ModelConstants.ENTITY_TYPE_COLUMN;
+import static org.thingsboard.server.dao.model.ModelConstants.LAST_UPDATE_TS_COLUMN;
 
 /**
  * @author Andrew Shvayka
diff --git a/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogService.java b/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogService.java
index 19651a0..d2e6f05 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogService.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogService.java
@@ -15,15 +15,16 @@
  */
 package org.thingsboard.server.dao.audit;
 
-import com.fasterxml.jackson.databind.JsonNode;
 import com.google.common.util.concurrent.ListenableFuture;
 import org.thingsboard.server.common.data.BaseData;
 import org.thingsboard.server.common.data.HasName;
-import org.thingsboard.server.common.data.User;
-import org.thingsboard.server.common.data.audit.ActionStatus;
 import org.thingsboard.server.common.data.audit.ActionType;
 import org.thingsboard.server.common.data.audit.AuditLog;
-import org.thingsboard.server.common.data.id.*;
+import org.thingsboard.server.common.data.id.CustomerId;
+import org.thingsboard.server.common.data.id.EntityId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.id.UUIDBased;
+import org.thingsboard.server.common.data.id.UserId;
 import org.thingsboard.server.common.data.page.TimePageData;
 import org.thingsboard.server.common.data.page.TimePageLink;
 
diff --git a/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogServiceImpl.java
index 23fadeb..a30c1b4 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogServiceImpl.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogServiceImpl.java
@@ -34,10 +34,16 @@ import org.thingsboard.server.common.data.HasName;
 import org.thingsboard.server.common.data.audit.ActionStatus;
 import org.thingsboard.server.common.data.audit.ActionType;
 import org.thingsboard.server.common.data.audit.AuditLog;
-import org.thingsboard.server.common.data.id.*;
+import org.thingsboard.server.common.data.id.AuditLogId;
+import org.thingsboard.server.common.data.id.CustomerId;
+import org.thingsboard.server.common.data.id.EntityId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.id.UUIDBased;
+import org.thingsboard.server.common.data.id.UserId;
 import org.thingsboard.server.common.data.kv.AttributeKvEntry;
 import org.thingsboard.server.common.data.page.TimePageData;
 import org.thingsboard.server.common.data.page.TimePageLink;
+import org.thingsboard.server.common.data.rule.RuleChainMetaData;
 import org.thingsboard.server.common.data.security.DeviceCredentials;
 import org.thingsboard.server.dao.audit.sink.AuditLogSink;
 import org.thingsboard.server.dao.entity.EntityService;
@@ -158,11 +164,20 @@ public class AuditLogServiceImpl implements AuditLogService {
         switch(actionType) {
             case ADDED:
             case UPDATED:
-                ObjectNode entityNode = objectMapper.valueToTree(entity);
-                if (entityId.getEntityType() == EntityType.DASHBOARD) {
-                    entityNode.put("configuration", "");
+                if (entity != null) {
+                    ObjectNode entityNode = objectMapper.valueToTree(entity);
+                    if (entityId.getEntityType() == EntityType.DASHBOARD) {
+                        entityNode.put("configuration", "");
+                    }
+                    actionData.set("entity", entityNode);
+                }
+                if (entityId.getEntityType() == EntityType.RULE_CHAIN) {
+                    RuleChainMetaData ruleChainMetaData = extractParameter(RuleChainMetaData.class, additionalInfo);
+                    if (ruleChainMetaData != null) {
+                        ObjectNode ruleChainMetaDataNode = objectMapper.valueToTree(ruleChainMetaData);
+                        actionData.set("metadata", ruleChainMetaDataNode);
+                    }
                 }
-                actionData.set("entity", entityNode);
                 break;
             case DELETED:
             case ACTIVATED:
diff --git a/dao/src/main/java/org/thingsboard/server/dao/audit/CassandraAuditLogDao.java b/dao/src/main/java/org/thingsboard/server/dao/audit/CassandraAuditLogDao.java
index fd02b5f..764f468 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/audit/CassandraAuditLogDao.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/audit/CassandraAuditLogDao.java
@@ -48,13 +48,22 @@ import java.time.Instant;
 import java.time.LocalDate;
 import java.time.LocalDateTime;
 import java.time.ZoneOffset;
-import java.util.*;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Optional;
+import java.util.StringJoiner;
+import java.util.UUID;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
 import java.util.stream.Collectors;
 
 import static com.datastax.driver.core.querybuilder.QueryBuilder.eq;
-import static org.thingsboard.server.dao.model.ModelConstants.*;
+import static org.thingsboard.server.dao.model.ModelConstants.AUDIT_LOG_BY_CUSTOMER_ID_CF;
+import static org.thingsboard.server.dao.model.ModelConstants.AUDIT_LOG_BY_ENTITY_ID_CF;
+import static org.thingsboard.server.dao.model.ModelConstants.AUDIT_LOG_BY_TENANT_ID_CF;
+import static org.thingsboard.server.dao.model.ModelConstants.AUDIT_LOG_BY_USER_ID_CF;
+import static org.thingsboard.server.dao.model.ModelConstants.AUDIT_LOG_COLUMN_FAMILY_NAME;
 
 @Component
 @Slf4j
diff --git a/dao/src/main/java/org/thingsboard/server/dao/audit/DummyAuditLogServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/audit/DummyAuditLogServiceImpl.java
index 3706e50..b14d6b1 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/audit/DummyAuditLogServiceImpl.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/audit/DummyAuditLogServiceImpl.java
@@ -22,7 +22,11 @@ import org.thingsboard.server.common.data.BaseData;
 import org.thingsboard.server.common.data.HasName;
 import org.thingsboard.server.common.data.audit.ActionType;
 import org.thingsboard.server.common.data.audit.AuditLog;
-import org.thingsboard.server.common.data.id.*;
+import org.thingsboard.server.common.data.id.CustomerId;
+import org.thingsboard.server.common.data.id.EntityId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.id.UUIDBased;
+import org.thingsboard.server.common.data.id.UserId;
 import org.thingsboard.server.common.data.page.TimePageData;
 import org.thingsboard.server.common.data.page.TimePageLink;
 
diff --git a/dao/src/main/java/org/thingsboard/server/dao/cache/TBRedisCacheConfiguration.java b/dao/src/main/java/org/thingsboard/server/dao/cache/TBRedisCacheConfiguration.java
index cfb69f3..3be33f3 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/cache/TBRedisCacheConfiguration.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/cache/TBRedisCacheConfiguration.java
@@ -18,7 +18,6 @@ package org.thingsboard.server.dao.cache;
 import lombok.Data;
 import org.springframework.beans.factory.annotation.Value;
 import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
-import org.springframework.boot.context.properties.ConfigurationProperties;
 import org.springframework.cache.CacheManager;
 import org.springframework.cache.annotation.EnableCaching;
 import org.springframework.cache.interceptor.KeyGenerator;
diff --git a/dao/src/main/java/org/thingsboard/server/dao/cassandra/AbstractCassandraCluster.java b/dao/src/main/java/org/thingsboard/server/dao/cassandra/AbstractCassandraCluster.java
index ca94822..fd61976 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/cassandra/AbstractCassandraCluster.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/cassandra/AbstractCassandraCluster.java
@@ -16,10 +16,17 @@
 package org.thingsboard.server.dao.cassandra;
 
 
-import com.datastax.driver.core.*;
+import com.datastax.driver.core.Cluster;
+import com.datastax.driver.core.ConsistencyLevel;
+import com.datastax.driver.core.HostDistance;
+import com.datastax.driver.core.PoolingOptions;
 import com.datastax.driver.core.ProtocolOptions.Compression;
+import com.datastax.driver.core.Session;
+import com.datastax.driver.mapping.DefaultPropertyMapper;
 import com.datastax.driver.mapping.Mapper;
+import com.datastax.driver.mapping.MappingConfiguration;
 import com.datastax.driver.mapping.MappingManager;
+import com.datastax.driver.mapping.PropertyAccessStrategy;
 import lombok.extern.slf4j.Slf4j;
 import org.apache.commons.lang3.StringUtils;
 import org.springframework.beans.factory.annotation.Autowired;
@@ -149,7 +156,13 @@ public abstract class AbstractCassandraCluster {
                 } else {
                     session = cluster.connect();
                 }
-                mappingManager = new MappingManager(session);
+//                For Cassandra Driver version 3.5.0
+                DefaultPropertyMapper propertyMapper = new DefaultPropertyMapper();
+                propertyMapper.setPropertyAccessStrategy(PropertyAccessStrategy.FIELDS);
+                MappingConfiguration configuration = MappingConfiguration.builder().withPropertyMapper(propertyMapper).build();
+                mappingManager = new MappingManager(session, configuration);
+//                For Cassandra Driver version 3.0.0
+//                mappingManager = new MappingManager(session);
                 break;
             } catch (Exception e) {
                 log.warn("Failed to initialize cassandra cluster due to {}. Will retry in {} ms", e.getMessage(), initRetryInterval);
diff --git a/dao/src/main/java/org/thingsboard/server/dao/customer/CassandraCustomerDao.java b/dao/src/main/java/org/thingsboard/server/dao/customer/CassandraCustomerDao.java
index 9fea618..598f98a 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/customer/CassandraCustomerDao.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/customer/CassandraCustomerDao.java
@@ -33,7 +33,9 @@ import java.util.UUID;
 
 import static com.datastax.driver.core.querybuilder.QueryBuilder.eq;
 import static com.datastax.driver.core.querybuilder.QueryBuilder.select;
-import static org.thingsboard.server.dao.model.ModelConstants.*;
+import static org.thingsboard.server.dao.model.ModelConstants.CUSTOMER_BY_TENANT_AND_TITLE_VIEW_NAME;
+import static org.thingsboard.server.dao.model.ModelConstants.CUSTOMER_TENANT_ID_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.CUSTOMER_TITLE_PROPERTY;
 @Component
 @Slf4j
 @NoSqlDao
diff --git a/dao/src/main/java/org/thingsboard/server/dao/DaoUtil.java b/dao/src/main/java/org/thingsboard/server/dao/DaoUtil.java
index fc434bf..c7bcfbc 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/DaoUtil.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/DaoUtil.java
@@ -18,7 +18,11 @@ package org.thingsboard.server.dao;
 import org.thingsboard.server.common.data.id.UUIDBased;
 import org.thingsboard.server.dao.model.ToData;
 
-import java.util.*;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.UUID;
 
 public abstract class DaoUtil {
 
diff --git a/dao/src/main/java/org/thingsboard/server/dao/dashboard/CassandraDashboardInfoDao.java b/dao/src/main/java/org/thingsboard/server/dao/dashboard/CassandraDashboardInfoDao.java
index 8091b2a..13a27c2 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/dashboard/CassandraDashboardInfoDao.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/dashboard/CassandraDashboardInfoDao.java
@@ -15,7 +15,6 @@
  */
 package org.thingsboard.server.dao.dashboard;
 
-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;
@@ -40,7 +39,9 @@ import java.util.List;
 import java.util.UUID;
 
 import static com.datastax.driver.core.querybuilder.QueryBuilder.eq;
-import static org.thingsboard.server.dao.model.ModelConstants.*;
+import static org.thingsboard.server.dao.model.ModelConstants.DASHBOARD_BY_TENANT_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME;
+import static org.thingsboard.server.dao.model.ModelConstants.DASHBOARD_COLUMN_FAMILY_NAME;
+import static org.thingsboard.server.dao.model.ModelConstants.DASHBOARD_TENANT_ID_PROPERTY;
 
 @Component
 @Slf4j
@@ -77,7 +78,7 @@ public class CassandraDashboardInfoDao extends CassandraAbstractSearchTextDao<Da
 
         ListenableFuture<List<EntityRelation>> relations = relationDao.findRelations(new CustomerId(customerId), EntityRelation.CONTAINS_TYPE, RelationTypeGroup.DASHBOARD, EntityType.DASHBOARD, pageLink);
 
-        return Futures.transform(relations, (AsyncFunction<List<EntityRelation>, List<DashboardInfo>>) input -> {
+        return Futures.transformAsync(relations, input -> {
             List<ListenableFuture<DashboardInfo>> dashboardFutures = new ArrayList<>(input.size());
             for (EntityRelation relation : input) {
                 dashboardFutures.add(findByIdAsync(relation.getTo().getId()));
diff --git a/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardService.java b/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardService.java
index 44d18ac..c69364b 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardService.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardService.java
@@ -26,8 +26,6 @@ import org.thingsboard.server.common.data.page.TextPageLink;
 import org.thingsboard.server.common.data.page.TimePageData;
 import org.thingsboard.server.common.data.page.TimePageLink;
 
-import java.util.Set;
-
 public interface DashboardService {
     
     Dashboard findDashboardById(DashboardId dashboardId);
diff --git a/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardServiceImpl.java
index 6ce5286..e1f4e3c 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardServiceImpl.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardServiceImpl.java
@@ -22,8 +22,10 @@ import lombok.extern.slf4j.Slf4j;
 import org.apache.commons.lang3.StringUtils;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
-import org.thingsboard.server.common.data.*;
-import org.thingsboard.server.common.data.alarm.AlarmInfo;
+import org.thingsboard.server.common.data.Customer;
+import org.thingsboard.server.common.data.Dashboard;
+import org.thingsboard.server.common.data.DashboardInfo;
+import org.thingsboard.server.common.data.Tenant;
 import org.thingsboard.server.common.data.id.CustomerId;
 import org.thingsboard.server.common.data.id.DashboardId;
 import org.thingsboard.server.common.data.id.TenantId;
@@ -36,8 +38,6 @@ import org.thingsboard.server.common.data.relation.RelationTypeGroup;
 import org.thingsboard.server.dao.customer.CustomerDao;
 import org.thingsboard.server.dao.entity.AbstractEntityService;
 import org.thingsboard.server.dao.exception.DataValidationException;
-import org.thingsboard.server.dao.model.ModelConstants;
-import org.thingsboard.server.dao.relation.RelationDao;
 import org.thingsboard.server.dao.service.DataValidator;
 import org.thingsboard.server.dao.service.PaginatedRemover;
 import org.thingsboard.server.dao.service.TimePaginatedRemover;
@@ -45,11 +45,7 @@ import org.thingsboard.server.dao.service.Validator;
 import org.thingsboard.server.dao.tenant.TenantDao;
 
 import javax.annotation.Nullable;
-import java.sql.Time;
-import java.util.ArrayList;
-import java.util.HashSet;
 import java.util.List;
-import java.util.Set;
 import java.util.concurrent.ExecutionException;
 
 import static org.thingsboard.server.dao.service.Validator.validateId;
diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/CassandraDeviceDao.java b/dao/src/main/java/org/thingsboard/server/dao/device/CassandraDeviceDao.java
index 641c464..110f207 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/device/CassandraDeviceDao.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/device/CassandraDeviceDao.java
@@ -36,10 +36,30 @@ import org.thingsboard.server.dao.nosql.CassandraAbstractSearchTextDao;
 import org.thingsboard.server.dao.util.NoSqlDao;
 
 import javax.annotation.Nullable;
-import java.util.*;
-
-import static com.datastax.driver.core.querybuilder.QueryBuilder.*;
-import static org.thingsboard.server.dao.model.ModelConstants.*;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Optional;
+import java.util.UUID;
+
+import static com.datastax.driver.core.querybuilder.QueryBuilder.eq;
+import static com.datastax.driver.core.querybuilder.QueryBuilder.in;
+import static com.datastax.driver.core.querybuilder.QueryBuilder.select;
+import static org.thingsboard.server.dao.model.ModelConstants.DEVICE_BY_CUSTOMER_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME;
+import static org.thingsboard.server.dao.model.ModelConstants.DEVICE_BY_CUSTOMER_BY_TYPE_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME;
+import static org.thingsboard.server.dao.model.ModelConstants.DEVICE_BY_TENANT_AND_NAME_VIEW_NAME;
+import static org.thingsboard.server.dao.model.ModelConstants.DEVICE_BY_TENANT_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME;
+import static org.thingsboard.server.dao.model.ModelConstants.DEVICE_BY_TENANT_BY_TYPE_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME;
+import static org.thingsboard.server.dao.model.ModelConstants.DEVICE_COLUMN_FAMILY_NAME;
+import static org.thingsboard.server.dao.model.ModelConstants.DEVICE_CUSTOMER_ID_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.DEVICE_NAME_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.DEVICE_TENANT_ID_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.DEVICE_TYPE_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.ENTITY_SUBTYPE_COLUMN_FAMILY_NAME;
+import static org.thingsboard.server.dao.model.ModelConstants.ENTITY_SUBTYPE_ENTITY_TYPE_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.ENTITY_SUBTYPE_TENANT_ID_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.ID_PROPERTY;
 
 @Component
 @Slf4j
diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceService.java b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceService.java
index 3a7c5e3..c5edd9e 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceService.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceService.java
@@ -26,7 +26,6 @@ import org.thingsboard.server.common.data.page.TextPageData;
 import org.thingsboard.server.common.data.page.TextPageLink;
 
 import java.util.List;
-import java.util.Optional;
 
 public interface DeviceService {
     
diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceServiceImpl.java
index 9120619..3930e3a 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceServiceImpl.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceServiceImpl.java
@@ -16,7 +16,6 @@
 package org.thingsboard.server.dao.device;
 
 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;
@@ -28,7 +27,11 @@ import org.springframework.cache.annotation.CacheEvict;
 import org.springframework.cache.annotation.Cacheable;
 import org.springframework.stereotype.Service;
 import org.springframework.util.StringUtils;
-import org.thingsboard.server.common.data.*;
+import org.thingsboard.server.common.data.Customer;
+import org.thingsboard.server.common.data.Device;
+import org.thingsboard.server.common.data.EntitySubtype;
+import org.thingsboard.server.common.data.EntityType;
+import org.thingsboard.server.common.data.Tenant;
 import org.thingsboard.server.common.data.device.DeviceSearchQuery;
 import org.thingsboard.server.common.data.id.CustomerId;
 import org.thingsboard.server.common.data.id.DeviceId;
@@ -48,13 +51,20 @@ import org.thingsboard.server.dao.service.PaginatedRemover;
 import org.thingsboard.server.dao.tenant.TenantDao;
 
 import javax.annotation.Nullable;
-import java.util.*;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Optional;
 import java.util.stream.Collectors;
 
 import static org.thingsboard.server.common.data.CacheConstants.DEVICE_CACHE;
 import static org.thingsboard.server.dao.DaoUtil.toUUIDs;
 import static org.thingsboard.server.dao.model.ModelConstants.NULL_UUID;
-import static org.thingsboard.server.dao.service.Validator.*;
+import static org.thingsboard.server.dao.service.Validator.validateId;
+import static org.thingsboard.server.dao.service.Validator.validateIds;
+import static org.thingsboard.server.dao.service.Validator.validatePageLink;
+import static org.thingsboard.server.dao.service.Validator.validateString;
 
 @Service
 @Slf4j
@@ -227,10 +237,10 @@ public class DeviceServiceImpl extends AbstractEntityService implements DeviceSe
     @Override
     public ListenableFuture<List<Device>> findDevicesByQuery(DeviceSearchQuery query) {
         ListenableFuture<List<EntityRelation>> relations = relationService.findByQuery(query.toEntitySearchQuery());
-        ListenableFuture<List<Device>> devices = Futures.transform(relations, (AsyncFunction<List<EntityRelation>, List<Device>>) relations1 -> {
+        ListenableFuture<List<Device>> devices = Futures.transformAsync(relations, r -> {
             EntitySearchDirection direction = query.toEntitySearchQuery().getParameters().getDirection();
             List<ListenableFuture<Device>> futures = new ArrayList<>();
-            for (EntityRelation relation : relations1) {
+            for (EntityRelation relation : r) {
                 EntityId entityId = direction == EntitySearchDirection.FROM ? relation.getTo() : relation.getFrom();
                 if (entityId.getEntityType() == EntityType.DEVICE) {
                     futures.add(findDeviceByIdAsync(new DeviceId(entityId.getId())));
diff --git a/dao/src/main/java/org/thingsboard/server/dao/entity/AbstractEntityService.java b/dao/src/main/java/org/thingsboard/server/dao/entity/AbstractEntityService.java
index 50723f3..bc530ab 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/entity/AbstractEntityService.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/entity/AbstractEntityService.java
@@ -20,8 +20,6 @@ import org.springframework.beans.factory.annotation.Autowired;
 import org.thingsboard.server.common.data.id.EntityId;
 import org.thingsboard.server.dao.relation.RelationService;
 
-import java.util.concurrent.ExecutionException;
-
 @Slf4j
 public abstract class AbstractEntityService {
 
diff --git a/dao/src/main/java/org/thingsboard/server/dao/entity/BaseEntityService.java b/dao/src/main/java/org/thingsboard/server/dao/entity/BaseEntityService.java
index acc88eb..1b67ca0 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/entity/BaseEntityService.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/entity/BaseEntityService.java
@@ -29,8 +29,7 @@ import org.thingsboard.server.dao.asset.AssetService;
 import org.thingsboard.server.dao.customer.CustomerService;
 import org.thingsboard.server.dao.dashboard.DashboardService;
 import org.thingsboard.server.dao.device.DeviceService;
-import org.thingsboard.server.dao.plugin.PluginService;
-import org.thingsboard.server.dao.rule.RuleService;
+import org.thingsboard.server.dao.rule.RuleChainService;
 import org.thingsboard.server.dao.tenant.TenantService;
 import org.thingsboard.server.dao.user.UserService;
 
@@ -48,12 +47,6 @@ public class BaseEntityService extends AbstractEntityService implements EntitySe
     private DeviceService deviceService;
 
     @Autowired
-    private RuleService ruleService;
-
-    @Autowired
-    private PluginService pluginService;
-
-    @Autowired
     private TenantService tenantService;
 
     @Autowired
@@ -68,6 +61,9 @@ public class BaseEntityService extends AbstractEntityService implements EntitySe
     @Autowired
     private AlarmService alarmService;
 
+    @Autowired
+    private RuleChainService ruleChainService;
+
     @Override
     public void deleteEntityRelations(EntityId entityId) {
         super.deleteEntityRelations(entityId);
@@ -85,12 +81,6 @@ public class BaseEntityService extends AbstractEntityService implements EntitySe
             case DEVICE:
                 hasName = deviceService.findDeviceByIdAsync(new DeviceId(entityId.getId()));
                 break;
-            case RULE:
-                hasName = ruleService.findRuleByIdAsync(new RuleId(entityId.getId()));
-                break;
-            case PLUGIN:
-                hasName = pluginService.findPluginByIdAsync(new PluginId(entityId.getId()));
-                break;
             case TENANT:
                 hasName = tenantService.findTenantByIdAsync(new TenantId(entityId.getId()));
                 break;
@@ -106,6 +96,9 @@ public class BaseEntityService extends AbstractEntityService implements EntitySe
             case ALARM:
                 hasName = alarmService.findAlarmByIdAsync(new AlarmId(entityId.getId()));
                 break;
+            case RULE_CHAIN:
+                hasName = ruleChainService.findRuleChainByIdAsync(new RuleChainId(entityId.getId()));
+                break;
             default:
                 throw new IllegalStateException("Not Implemented!");
         }
diff --git a/dao/src/main/java/org/thingsboard/server/dao/event/BaseEventService.java b/dao/src/main/java/org/thingsboard/server/dao/event/BaseEventService.java
index b8ba4a7..7dddec1 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/event/BaseEventService.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/event/BaseEventService.java
@@ -15,6 +15,7 @@
  */
 package org.thingsboard.server.dao.event;
 
+import com.google.common.util.concurrent.ListenableFuture;
 import lombok.extern.slf4j.Slf4j;
 import org.apache.commons.lang3.StringUtils;
 import org.springframework.beans.factory.annotation.Autowired;
@@ -44,6 +45,12 @@ public class BaseEventService implements EventService {
     }
 
     @Override
+    public ListenableFuture<Event> saveAsync(Event event) {
+        eventValidator.validate(event);
+        return eventDao.saveAsync(event);
+    }
+
+    @Override
     public Optional<Event> saveIfNotExists(Event event) {
         eventValidator.validate(event);
         if (StringUtils.isEmpty(event.getUid())) {
@@ -82,6 +89,11 @@ public class BaseEventService implements EventService {
         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 =
             new DataValidator<Event>() {
                 @Override
diff --git a/dao/src/main/java/org/thingsboard/server/dao/event/CassandraBaseEventDao.java b/dao/src/main/java/org/thingsboard/server/dao/event/CassandraBaseEventDao.java
index 43d3fd5..7549e40 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/event/CassandraBaseEventDao.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/event/CassandraBaseEventDao.java
@@ -15,11 +15,13 @@
  */
 package org.thingsboard.server.dao.event;
 
-import com.datastax.driver.core.ResultSet;
+import com.datastax.driver.core.ResultSetFuture;
 import com.datastax.driver.core.querybuilder.Insert;
 import com.datastax.driver.core.querybuilder.QueryBuilder;
 import com.datastax.driver.core.querybuilder.Select;
 import com.datastax.driver.core.utils.UUIDs;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
 import lombok.extern.slf4j.Slf4j;
 import org.apache.commons.lang3.StringUtils;
 import org.springframework.stereotype.Component;
@@ -38,6 +40,7 @@ import java.util.Arrays;
 import java.util.List;
 import java.util.Optional;
 import java.util.UUID;
+import java.util.concurrent.ExecutionException;
 
 import static com.datastax.driver.core.querybuilder.QueryBuilder.eq;
 import static com.datastax.driver.core.querybuilder.QueryBuilder.select;
@@ -62,6 +65,15 @@ public class CassandraBaseEventDao extends CassandraAbstractSearchTimeDao<EventE
 
     @Override
     public Event save(Event event) {
+        try {
+            return saveAsync(event).get();
+        } catch (InterruptedException | ExecutionException e) {
+            throw new IllegalStateException("Could not save EventEntity", e);
+        }
+    }
+
+    @Override
+    public ListenableFuture<Event> saveAsync(Event event) {
         log.debug("Save event [{}] ", event);
         if (event.getTenantId() == null) {
             log.trace("Save system event with predefined id {}", systemTenantId);
@@ -73,7 +85,8 @@ public class CassandraBaseEventDao extends CassandraAbstractSearchTimeDao<EventE
         if (StringUtils.isEmpty(event.getUid())) {
             event.setUid(event.getId().toString());
         }
-        return save(new EventEntity(event), false).orElse(null);
+        ListenableFuture<Optional<Event>> optionalSave = saveAsync(new EventEntity(event), false);
+        return Futures.transform(optionalSave, opt -> opt.orElse(null));
     }
 
     @Override
@@ -134,7 +147,30 @@ public class CassandraBaseEventDao extends CassandraAbstractSearchTimeDao<EventE
         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) {
+        try {
+            return saveAsync(entity, ifNotExists).get();
+        } catch (InterruptedException | ExecutionException e) {
+            throw new IllegalStateException("Could not save EventEntity", e);
+        }
+    }
+
+    private ListenableFuture<Optional<Event>> saveAsync(EventEntity entity, boolean ifNotExists) {
         if (entity.getId() == null) {
             entity.setId(UUIDs.timeBased());
         }
@@ -149,11 +185,13 @@ public class CassandraBaseEventDao extends CassandraAbstractSearchTimeDao<EventE
         if (ifNotExists) {
             insert = insert.ifNotExists();
         }
-        ResultSet rs = executeWrite(insert);
-        if (rs.wasApplied()) {
-            return Optional.of(DaoUtil.getData(entity));
-        } else {
-            return Optional.empty();
-        }
+        ResultSetFuture resultSetFuture = executeAsyncWrite(insert);
+        return Futures.transform(resultSetFuture, rs -> {
+            if (rs.wasApplied()) {
+                return Optional.of(DaoUtil.getData(entity));
+            } else {
+                return Optional.empty();
+            }
+        });
     }
 }
diff --git a/dao/src/main/java/org/thingsboard/server/dao/event/EventDao.java b/dao/src/main/java/org/thingsboard/server/dao/event/EventDao.java
index fb5c0fb..9469c61 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/event/EventDao.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/event/EventDao.java
@@ -15,6 +15,7 @@
  */
 package org.thingsboard.server.dao.event;
 
+import com.google.common.util.concurrent.ListenableFuture;
 import org.thingsboard.server.common.data.Event;
 import org.thingsboard.server.common.data.id.EntityId;
 import org.thingsboard.server.common.data.page.TimePageLink;
@@ -38,6 +39,14 @@ public interface EventDao extends Dao<Event> {
     Event save(Event event);
 
     /**
+     * Save or update event object async
+     *
+     * @param event the event object
+     * @return saved event object future
+     */
+    ListenableFuture<Event> saveAsync(Event event);
+
+    /**
      * Save event object if it is not yet saved
      *
      * @param event the event object
@@ -76,4 +85,16 @@ public interface EventDao extends Dao<Event> {
      * @return the event list
      */
     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);
+
 }
diff --git a/dao/src/main/java/org/thingsboard/server/dao/event/EventService.java b/dao/src/main/java/org/thingsboard/server/dao/event/EventService.java
index edee190..0698c6b 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/event/EventService.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/event/EventService.java
@@ -15,18 +15,22 @@
  */
 package org.thingsboard.server.dao.event;
 
+import com.google.common.util.concurrent.ListenableFuture;
 import org.thingsboard.server.common.data.Event;
 import org.thingsboard.server.common.data.id.EntityId;
 import org.thingsboard.server.common.data.id.TenantId;
 import org.thingsboard.server.common.data.page.TimePageData;
 import org.thingsboard.server.common.data.page.TimePageLink;
 
+import java.util.List;
 import java.util.Optional;
 
 public interface EventService {
 
     Event save(Event event);
 
+    ListenableFuture<Event> saveAsync(Event event);
+
     Optional<Event> saveIfNotExists(Event event);
 
     Optional<Event> findEvent(TenantId tenantId, EntityId entityId, String eventType, String eventUid);
@@ -34,4 +38,7 @@ public interface EventService {
     TimePageData<Event> findEvents(TenantId tenantId, EntityId entityId, TimePageLink pageLink);
 
     TimePageData<Event> findEvents(TenantId tenantId, EntityId entityId, String eventType, TimePageLink pageLink);
+
+    List<Event> findLatestEvents(TenantId tenantId, EntityId entityId, String eventType, int limit);
+
 }
diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/BaseEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/BaseEntity.java
index 0aaf9c2..01ea41b 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/model/BaseEntity.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/model/BaseEntity.java
@@ -15,7 +15,6 @@
  */
 package org.thingsboard.server.dao.model;
 
-import java.io.Serializable;
 import java.util.UUID;
 
 public interface BaseEntity<D> extends ToData<D> {
diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/EntitySubtypeEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/EntitySubtypeEntity.java
index e4e4d61..16622de 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/model/EntitySubtypeEntity.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/model/EntitySubtypeEntity.java
@@ -25,7 +25,10 @@ import org.thingsboard.server.dao.model.type.EntityTypeCodec;
 
 import java.util.UUID;
 
-import static org.thingsboard.server.dao.model.ModelConstants.*;
+import static org.thingsboard.server.dao.model.ModelConstants.ENTITY_SUBTYPE_COLUMN_FAMILY_NAME;
+import static org.thingsboard.server.dao.model.ModelConstants.ENTITY_SUBTYPE_ENTITY_TYPE_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.ENTITY_SUBTYPE_TENANT_ID_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.ENTITY_SUBTYPE_TYPE_PROPERTY;
 
 @Table(name = ENTITY_SUBTYPE_COLUMN_FAMILY_NAME)
 public class EntitySubtypeEntity {
diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java b/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java
index c2b55c9..3a934eb 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java
@@ -18,6 +18,7 @@ package org.thingsboard.server.dao.model;
 import com.datastax.driver.core.utils.UUIDs;
 import org.apache.commons.lang3.ArrayUtils;
 import org.thingsboard.server.common.data.UUIDConverter;
+import org.thingsboard.server.common.data.id.TenantId;
 import org.thingsboard.server.common.data.kv.Aggregation;
 
 import java.util.UUID;
@@ -29,6 +30,7 @@ public class ModelConstants {
 
     public static final UUID NULL_UUID = UUIDs.startOf(0);
     public static final String NULL_UUID_STR = UUIDConverter.fromTimeUUID(NULL_UUID);
+    public static final TenantId SYSTEM_TENANT = new TenantId(ModelConstants.NULL_UUID);
 
     /**
      * Generic constants.
@@ -273,21 +275,6 @@ public class ModelConstants {
     public static final String DASHBOARD_BY_TENANT_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME = "dashboard_by_tenant_and_search_text";
 
     /**
-     * Cassandra plugin metadata constants.
-     */
-    public static final String PLUGIN_COLUMN_FAMILY_NAME = "plugin";
-    public static final String PLUGIN_TENANT_ID_PROPERTY = TENANT_ID_PROPERTY;
-    public static final String PLUGIN_NAME_PROPERTY = "name";
-    public static final String PLUGIN_API_TOKEN_PROPERTY = "api_token";
-    public static final String PLUGIN_CLASS_PROPERTY = "plugin_class";
-    public static final String PLUGIN_ACCESS_PROPERTY = "public_access";
-    public static final String PLUGIN_STATE_PROPERTY = STATE_PROPERTY;
-    public static final String PLUGIN_CONFIGURATION_PROPERTY = "configuration";
-
-    public static final String PLUGIN_BY_API_TOKEN_COLUMN_FAMILY_NAME = "plugin_by_api_token";
-    public static final String PLUGIN_BY_TENANT_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME = "plugin_by_tenant_and_search_text";
-
-    /**
      * Cassandra plugin component metadata constants.
      */
     public static final String COMPONENT_DESCRIPTOR_COLUMN_FAMILY_NAME = "component_descriptor";
@@ -303,22 +290,6 @@ public class ModelConstants {
     public static final String COMPONENT_DESCRIPTOR_BY_ID = "component_desc_by_id";
 
     /**
-     * Cassandra rule metadata constants.
-     */
-    public static final String RULE_COLUMN_FAMILY_NAME = "rule";
-    public static final String RULE_TENANT_ID_PROPERTY = TENANT_ID_PROPERTY;
-    public static final String RULE_NAME_PROPERTY = "name";
-    public static final String RULE_STATE_PROPERTY = STATE_PROPERTY;
-    public static final String RULE_WEIGHT_PROPERTY = "weight";
-    public static final String RULE_PLUGIN_TOKEN_PROPERTY = "plugin_token";
-    public static final String RULE_FILTERS = "filters";
-    public static final String RULE_PROCESSOR = "processor";
-    public static final String RULE_ACTION = "action";
-
-    public static final String RULE_BY_PLUGIN_TOKEN = "rule_by_plugin_token";
-    public static final String RULE_BY_TENANT_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME = "rule_by_tenant_and_search_text";
-
-    /**
      * Cassandra event constants.
      */
     public static final String EVENT_COLUMN_FAMILY_NAME = "event";
@@ -332,6 +303,29 @@ public class ModelConstants {
     public static final String EVENT_BY_TYPE_AND_ID_VIEW_NAME = "event_by_type_and_id";
     public static final String EVENT_BY_ID_VIEW_NAME = "event_by_id";
 
+    public static final String DEBUG_MODE = "debug_mode";
+
+    /**
+     * Cassandra rule chain constants.
+     */
+    public static final String RULE_CHAIN_COLUMN_FAMILY_NAME = "rule_chain";
+    public static final String RULE_CHAIN_TENANT_ID_PROPERTY = TENANT_ID_PROPERTY;
+    public static final String RULE_CHAIN_NAME_PROPERTY = "name";
+    public static final String RULE_CHAIN_FIRST_RULE_NODE_ID_PROPERTY = "first_rule_node_id";
+    public static final String RULE_CHAIN_ROOT_PROPERTY = "root";
+    public static final String RULE_CHAIN_CONFIGURATION_PROPERTY = "configuration";
+
+    public static final String RULE_CHAIN_BY_TENANT_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME = "rule_chain_by_tenant_and_search_text";
+
+    /**
+     * Cassandra rule node constants.
+     */
+    public static final String RULE_NODE_COLUMN_FAMILY_NAME = "rule_node";
+    public static final String RULE_NODE_CHAIN_ID_PROPERTY = "rule_chain_id";
+    public static final String RULE_NODE_TYPE_PROPERTY = "type";
+    public static final String RULE_NODE_NAME_PROPERTY = "name";
+    public static final String RULE_NODE_CONFIGURATION_PROPERTY = "configuration";
+
     /**
      * Cassandra attributes and timeseries constants.
      */
diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/nosql/AdminSettingsEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/nosql/AdminSettingsEntity.java
index 29c9859..7b8e847 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/model/nosql/AdminSettingsEntity.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/model/nosql/AdminSettingsEntity.java
@@ -29,7 +29,10 @@ import org.thingsboard.server.dao.model.type.JsonCodec;
 
 import java.util.UUID;
 
-import static org.thingsboard.server.dao.model.ModelConstants.*;
+import static org.thingsboard.server.dao.model.ModelConstants.ADMIN_SETTINGS_COLUMN_FAMILY_NAME;
+import static org.thingsboard.server.dao.model.ModelConstants.ADMIN_SETTINGS_JSON_VALUE_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.ADMIN_SETTINGS_KEY_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.ID_PROPERTY;
 
 @Table(name = ADMIN_SETTINGS_COLUMN_FAMILY_NAME)
 @EqualsAndHashCode
diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/nosql/AlarmEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/nosql/AlarmEntity.java
index 72bf4df..99df84f 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/model/nosql/AlarmEntity.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/model/nosql/AlarmEntity.java
@@ -16,7 +16,10 @@
 package org.thingsboard.server.dao.model.nosql;
 
 import com.datastax.driver.core.utils.UUIDs;
-import com.datastax.driver.mapping.annotations.*;
+import com.datastax.driver.mapping.annotations.ClusteringColumn;
+import com.datastax.driver.mapping.annotations.Column;
+import com.datastax.driver.mapping.annotations.PartitionKey;
+import com.datastax.driver.mapping.annotations.Table;
 import com.fasterxml.jackson.databind.JsonNode;
 import lombok.EqualsAndHashCode;
 import lombok.ToString;
@@ -35,7 +38,20 @@ import org.thingsboard.server.dao.model.type.JsonCodec;
 
 import java.util.UUID;
 
-import static org.thingsboard.server.dao.model.ModelConstants.*;
+import static org.thingsboard.server.dao.model.ModelConstants.ALARM_ACK_TS_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.ALARM_CLEAR_TS_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.ALARM_COLUMN_FAMILY_NAME;
+import static org.thingsboard.server.dao.model.ModelConstants.ALARM_DETAILS_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.ALARM_END_TS_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.ALARM_ORIGINATOR_ID_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.ALARM_ORIGINATOR_TYPE_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.ALARM_PROPAGATE_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.ALARM_SEVERITY_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.ALARM_START_TS_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.ALARM_STATUS_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.ALARM_TENANT_ID_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.ALARM_TYPE_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.ID_PROPERTY;
 
 @Table(name = ALARM_COLUMN_FAMILY_NAME)
 @EqualsAndHashCode
diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/nosql/AssetEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/nosql/AssetEntity.java
index 11eb905..da9bdb4 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/model/nosql/AssetEntity.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/model/nosql/AssetEntity.java
@@ -31,7 +31,14 @@ import org.thingsboard.server.dao.model.type.JsonCodec;
 
 import java.util.UUID;
 
-import static org.thingsboard.server.dao.model.ModelConstants.*;
+import static org.thingsboard.server.dao.model.ModelConstants.ASSET_ADDITIONAL_INFO_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.ASSET_COLUMN_FAMILY_NAME;
+import static org.thingsboard.server.dao.model.ModelConstants.ASSET_CUSTOMER_ID_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.ASSET_NAME_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.ASSET_TENANT_ID_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.ASSET_TYPE_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.ID_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.SEARCH_TEXT_PROPERTY;
 
 @Table(name = ASSET_COLUMN_FAMILY_NAME)
 @EqualsAndHashCode
diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/nosql/AuditLogEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/nosql/AuditLogEntity.java
index ab2e3bc..aa145ed 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/model/nosql/AuditLogEntity.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/model/nosql/AuditLogEntity.java
@@ -25,7 +25,11 @@ import org.thingsboard.server.common.data.EntityType;
 import org.thingsboard.server.common.data.audit.ActionStatus;
 import org.thingsboard.server.common.data.audit.ActionType;
 import org.thingsboard.server.common.data.audit.AuditLog;
-import org.thingsboard.server.common.data.id.*;
+import org.thingsboard.server.common.data.id.AuditLogId;
+import org.thingsboard.server.common.data.id.CustomerId;
+import org.thingsboard.server.common.data.id.EntityIdFactory;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.id.UserId;
 import org.thingsboard.server.dao.model.BaseEntity;
 import org.thingsboard.server.dao.model.type.ActionStatusCodec;
 import org.thingsboard.server.dao.model.type.ActionTypeCodec;
@@ -34,7 +38,19 @@ import org.thingsboard.server.dao.model.type.JsonCodec;
 
 import java.util.UUID;
 
-import static org.thingsboard.server.dao.model.ModelConstants.*;
+import static org.thingsboard.server.dao.model.ModelConstants.AUDIT_LOG_ACTION_DATA_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.AUDIT_LOG_ACTION_FAILURE_DETAILS_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.AUDIT_LOG_ACTION_STATUS_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.AUDIT_LOG_ACTION_TYPE_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.AUDIT_LOG_BY_ENTITY_ID_CF;
+import static org.thingsboard.server.dao.model.ModelConstants.AUDIT_LOG_CUSTOMER_ID_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.AUDIT_LOG_ENTITY_ID_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.AUDIT_LOG_ENTITY_NAME_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.AUDIT_LOG_ENTITY_TYPE_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.AUDIT_LOG_TENANT_ID_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.AUDIT_LOG_USER_ID_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.AUDIT_LOG_USER_NAME_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.ID_PROPERTY;
 
 @Table(name = AUDIT_LOG_BY_ENTITY_ID_CF)
 @Data
diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/nosql/ComponentDescriptorEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/nosql/ComponentDescriptorEntity.java
index 4998ae5..6593304 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/model/nosql/ComponentDescriptorEntity.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/model/nosql/ComponentDescriptorEntity.java
@@ -28,7 +28,15 @@ import org.thingsboard.server.dao.model.type.JsonCodec;
 
 import java.util.UUID;
 
-import static org.thingsboard.server.dao.model.ModelConstants.*;
+import static org.thingsboard.server.dao.model.ModelConstants.COMPONENT_DESCRIPTOR_ACTIONS_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.COMPONENT_DESCRIPTOR_CLASS_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.COMPONENT_DESCRIPTOR_COLUMN_FAMILY_NAME;
+import static org.thingsboard.server.dao.model.ModelConstants.COMPONENT_DESCRIPTOR_CONFIGURATION_DESCRIPTOR_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.COMPONENT_DESCRIPTOR_NAME_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.COMPONENT_DESCRIPTOR_SCOPE_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.COMPONENT_DESCRIPTOR_TYPE_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.ID_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.SEARCH_TEXT_PROPERTY;
 
 /**
  * @author Andrew Shvayka
diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/nosql/CustomerEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/nosql/CustomerEntity.java
index 0952037..d13a54e 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/model/nosql/CustomerEntity.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/model/nosql/CustomerEntity.java
@@ -30,7 +30,20 @@ import org.thingsboard.server.dao.model.type.JsonCodec;
 
 import java.util.UUID;
 
-import static org.thingsboard.server.dao.model.ModelConstants.*;
+import static org.thingsboard.server.dao.model.ModelConstants.ADDRESS2_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.ADDRESS_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.CITY_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.COUNTRY_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.CUSTOMER_ADDITIONAL_INFO_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.CUSTOMER_COLUMN_FAMILY_NAME;
+import static org.thingsboard.server.dao.model.ModelConstants.CUSTOMER_TENANT_ID_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.CUSTOMER_TITLE_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.EMAIL_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.ID_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.PHONE_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.SEARCH_TEXT_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.STATE_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.ZIP_PROPERTY;
 
 @Table(name = CUSTOMER_COLUMN_FAMILY_NAME)
 @EqualsAndHashCode
diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/nosql/DashboardEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/nosql/DashboardEntity.java
index 622d6df..74963f8 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/model/nosql/DashboardEntity.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/model/nosql/DashboardEntity.java
@@ -38,7 +38,13 @@ import java.io.IOException;
 import java.util.HashSet;
 import java.util.UUID;
 
-import static org.thingsboard.server.dao.model.ModelConstants.*;
+import static org.thingsboard.server.dao.model.ModelConstants.DASHBOARD_ASSIGNED_CUSTOMERS_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.DASHBOARD_COLUMN_FAMILY_NAME;
+import static org.thingsboard.server.dao.model.ModelConstants.DASHBOARD_CONFIGURATION_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.DASHBOARD_TENANT_ID_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.DASHBOARD_TITLE_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.ID_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.SEARCH_TEXT_PROPERTY;
 
 @Table(name = DASHBOARD_COLUMN_FAMILY_NAME)
 @EqualsAndHashCode
diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/nosql/DashboardInfoEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/nosql/DashboardInfoEntity.java
index ab0ec1c..ce5f723 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/model/nosql/DashboardInfoEntity.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/model/nosql/DashboardInfoEntity.java
@@ -36,7 +36,12 @@ import java.io.IOException;
 import java.util.HashSet;
 import java.util.UUID;
 
-import static org.thingsboard.server.dao.model.ModelConstants.*;
+import static org.thingsboard.server.dao.model.ModelConstants.DASHBOARD_ASSIGNED_CUSTOMERS_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.DASHBOARD_COLUMN_FAMILY_NAME;
+import static org.thingsboard.server.dao.model.ModelConstants.DASHBOARD_TENANT_ID_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.DASHBOARD_TITLE_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.ID_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.SEARCH_TEXT_PROPERTY;
 
 @Table(name = DASHBOARD_COLUMN_FAMILY_NAME)
 @EqualsAndHashCode
diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/nosql/DeviceCredentialsEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/nosql/DeviceCredentialsEntity.java
index 8483865..5e4bb0f 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/model/nosql/DeviceCredentialsEntity.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/model/nosql/DeviceCredentialsEntity.java
@@ -30,7 +30,12 @@ import org.thingsboard.server.dao.model.type.DeviceCredentialsTypeCodec;
 
 import java.util.UUID;
 
-import static org.thingsboard.server.dao.model.ModelConstants.*;
+import static org.thingsboard.server.dao.model.ModelConstants.DEVICE_CREDENTIALS_COLUMN_FAMILY_NAME;
+import static org.thingsboard.server.dao.model.ModelConstants.DEVICE_CREDENTIALS_CREDENTIALS_ID_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.DEVICE_CREDENTIALS_CREDENTIALS_TYPE_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.DEVICE_CREDENTIALS_CREDENTIALS_VALUE_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.DEVICE_CREDENTIALS_DEVICE_ID_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.ID_PROPERTY;
 
 @Table(name = DEVICE_CREDENTIALS_COLUMN_FAMILY_NAME)
 @EqualsAndHashCode
diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/nosql/DeviceEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/nosql/DeviceEntity.java
index ef0c5fe..3543840 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/model/nosql/DeviceEntity.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/model/nosql/DeviceEntity.java
@@ -31,7 +31,14 @@ import org.thingsboard.server.dao.model.type.JsonCodec;
 
 import java.util.UUID;
 
-import static org.thingsboard.server.dao.model.ModelConstants.*;
+import static org.thingsboard.server.dao.model.ModelConstants.DEVICE_ADDITIONAL_INFO_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.DEVICE_COLUMN_FAMILY_NAME;
+import static org.thingsboard.server.dao.model.ModelConstants.DEVICE_CUSTOMER_ID_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.DEVICE_NAME_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.DEVICE_TENANT_ID_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.DEVICE_TYPE_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.ID_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.SEARCH_TEXT_PROPERTY;
 
 @Table(name = DEVICE_COLUMN_FAMILY_NAME)
 @EqualsAndHashCode
diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/nosql/EventEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/nosql/EventEntity.java
index 3b55d38..7f12359 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/model/nosql/EventEntity.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/model/nosql/EventEntity.java
@@ -16,7 +16,10 @@
 package org.thingsboard.server.dao.model.nosql;
 
 import com.datastax.driver.core.utils.UUIDs;
-import com.datastax.driver.mapping.annotations.*;
+import com.datastax.driver.mapping.annotations.ClusteringColumn;
+import com.datastax.driver.mapping.annotations.Column;
+import com.datastax.driver.mapping.annotations.PartitionKey;
+import com.datastax.driver.mapping.annotations.Table;
 import com.fasterxml.jackson.databind.JsonNode;
 import lombok.Data;
 import lombok.NoArgsConstructor;
@@ -31,7 +34,14 @@ import org.thingsboard.server.dao.model.type.JsonCodec;
 
 import java.util.UUID;
 
-import static org.thingsboard.server.dao.model.ModelConstants.*;
+import static org.thingsboard.server.dao.model.ModelConstants.EVENT_BODY_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.EVENT_COLUMN_FAMILY_NAME;
+import static org.thingsboard.server.dao.model.ModelConstants.EVENT_ENTITY_ID_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.EVENT_ENTITY_TYPE_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.EVENT_TENANT_ID_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.EVENT_TYPE_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.EVENT_UID_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.ID_PROPERTY;
 
 /**
  * @author Andrew Shvayka
diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/nosql/TenantEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/nosql/TenantEntity.java
index 9140f9e..b16a733 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/model/nosql/TenantEntity.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/model/nosql/TenantEntity.java
@@ -29,7 +29,20 @@ import org.thingsboard.server.dao.model.type.JsonCodec;
 
 import java.util.UUID;
 
-import static org.thingsboard.server.dao.model.ModelConstants.*;
+import static org.thingsboard.server.dao.model.ModelConstants.ADDRESS2_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.ADDRESS_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.CITY_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.COUNTRY_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.EMAIL_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.ID_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.PHONE_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.SEARCH_TEXT_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.STATE_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.TENANT_ADDITIONAL_INFO_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.TENANT_COLUMN_FAMILY_NAME;
+import static org.thingsboard.server.dao.model.ModelConstants.TENANT_REGION_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.TENANT_TITLE_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.ZIP_PROPERTY;
 
 @Table(name = TENANT_COLUMN_FAMILY_NAME)
 @EqualsAndHashCode
diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/nosql/UserCredentialsEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/nosql/UserCredentialsEntity.java
index ef441de..1e7fbe2 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/model/nosql/UserCredentialsEntity.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/model/nosql/UserCredentialsEntity.java
@@ -27,7 +27,13 @@ import org.thingsboard.server.dao.model.BaseEntity;
 
 import java.util.UUID;
 
-import static org.thingsboard.server.dao.model.ModelConstants.*;
+import static org.thingsboard.server.dao.model.ModelConstants.ID_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.USER_CREDENTIALS_ACTIVATE_TOKEN_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.USER_CREDENTIALS_COLUMN_FAMILY_NAME;
+import static org.thingsboard.server.dao.model.ModelConstants.USER_CREDENTIALS_ENABLED_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.USER_CREDENTIALS_PASSWORD_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.USER_CREDENTIALS_RESET_TOKEN_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.USER_CREDENTIALS_USER_ID_PROPERTY;
 
 @Table(name = USER_CREDENTIALS_COLUMN_FAMILY_NAME)
 @EqualsAndHashCode
diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/nosql/UserEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/nosql/UserEntity.java
index 69b5a63..f3f845d 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/model/nosql/UserEntity.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/model/nosql/UserEntity.java
@@ -33,7 +33,16 @@ import org.thingsboard.server.dao.model.type.JsonCodec;
 
 import java.util.UUID;
 
-import static org.thingsboard.server.dao.model.ModelConstants.*;
+import static org.thingsboard.server.dao.model.ModelConstants.ID_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.SEARCH_TEXT_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.USER_ADDITIONAL_INFO_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.USER_AUTHORITY_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.USER_COLUMN_FAMILY_NAME;
+import static org.thingsboard.server.dao.model.ModelConstants.USER_CUSTOMER_ID_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.USER_EMAIL_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.USER_FIRST_NAME_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.USER_LAST_NAME_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.USER_TENANT_ID_PROPERTY;
 
 @Table(name = USER_COLUMN_FAMILY_NAME)
 @EqualsAndHashCode
diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/nosql/WidgetsBundleEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/nosql/WidgetsBundleEntity.java
index 26cd88d..6704f1a 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/model/nosql/WidgetsBundleEntity.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/model/nosql/WidgetsBundleEntity.java
@@ -30,7 +30,13 @@ import org.thingsboard.server.dao.model.SearchTextEntity;
 import java.nio.ByteBuffer;
 import java.util.UUID;
 
-import static org.thingsboard.server.dao.model.ModelConstants.*;
+import static org.thingsboard.server.dao.model.ModelConstants.ID_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.SEARCH_TEXT_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.WIDGETS_BUNDLE_ALIAS_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.WIDGETS_BUNDLE_COLUMN_FAMILY_NAME;
+import static org.thingsboard.server.dao.model.ModelConstants.WIDGETS_BUNDLE_IMAGE_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.WIDGETS_BUNDLE_TENANT_ID_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.WIDGETS_BUNDLE_TITLE_PROPERTY;
 
 @Table(name = WIDGETS_BUNDLE_COLUMN_FAMILY_NAME)
 @EqualsAndHashCode
diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/nosql/WidgetTypeEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/nosql/WidgetTypeEntity.java
index 1d37aaf..fe2d76e 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/model/nosql/WidgetTypeEntity.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/model/nosql/WidgetTypeEntity.java
@@ -30,7 +30,13 @@ import org.thingsboard.server.dao.model.type.JsonCodec;
 
 import java.util.UUID;
 
-import static org.thingsboard.server.dao.model.ModelConstants.*;
+import static org.thingsboard.server.dao.model.ModelConstants.ID_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.WIDGET_TYPE_ALIAS_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.WIDGET_TYPE_BUNDLE_ALIAS_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.WIDGET_TYPE_COLUMN_FAMILY_NAME;
+import static org.thingsboard.server.dao.model.ModelConstants.WIDGET_TYPE_DESCRIPTOR_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.WIDGET_TYPE_NAME_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.WIDGET_TYPE_TENANT_ID_PROPERTY;
 
 @Table(name = WIDGET_TYPE_COLUMN_FAMILY_NAME)
 @EqualsAndHashCode
diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/AdminSettingsEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/AdminSettingsEntity.java
index 7891bfc..b5851f0 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/AdminSettingsEntity.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/AdminSettingsEntity.java
@@ -32,7 +32,9 @@ import javax.persistence.Column;
 import javax.persistence.Entity;
 import javax.persistence.Table;
 
-import static org.thingsboard.server.dao.model.ModelConstants.*;
+import static org.thingsboard.server.dao.model.ModelConstants.ADMIN_SETTINGS_COLUMN_FAMILY_NAME;
+import static org.thingsboard.server.dao.model.ModelConstants.ADMIN_SETTINGS_JSON_VALUE_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.ADMIN_SETTINGS_KEY_PROPERTY;
 
 @Data
 @EqualsAndHashCode(callSuper = true)
diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/AlarmEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/AlarmEntity.java
index 65ca386..32f95e1 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/AlarmEntity.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/AlarmEntity.java
@@ -34,9 +34,24 @@ import org.thingsboard.server.dao.model.BaseSqlEntity;
 import org.thingsboard.server.dao.model.ModelConstants;
 import org.thingsboard.server.dao.util.mapping.JsonStringType;
 
-import javax.persistence.*;
-
-import static org.thingsboard.server.dao.model.ModelConstants.*;
+import javax.persistence.Column;
+import javax.persistence.Entity;
+import javax.persistence.EnumType;
+import javax.persistence.Enumerated;
+import javax.persistence.Table;
+
+import static org.thingsboard.server.dao.model.ModelConstants.ALARM_ACK_TS_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.ALARM_CLEAR_TS_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.ALARM_COLUMN_FAMILY_NAME;
+import static org.thingsboard.server.dao.model.ModelConstants.ALARM_END_TS_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.ALARM_ORIGINATOR_ID_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.ALARM_ORIGINATOR_TYPE_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.ALARM_PROPAGATE_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.ALARM_SEVERITY_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.ALARM_START_TS_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.ALARM_STATUS_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.ALARM_TENANT_ID_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.ALARM_TYPE_PROPERTY;
 
 @Data
 @EqualsAndHashCode(callSuper = true)
diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/AssetEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/AssetEntity.java
index a32f2d6..2da7396 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/AssetEntity.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/AssetEntity.java
@@ -35,7 +35,12 @@ import javax.persistence.Column;
 import javax.persistence.Entity;
 import javax.persistence.Table;
 
-import static org.thingsboard.server.dao.model.ModelConstants.*;
+import static org.thingsboard.server.dao.model.ModelConstants.ASSET_COLUMN_FAMILY_NAME;
+import static org.thingsboard.server.dao.model.ModelConstants.ASSET_CUSTOMER_ID_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.ASSET_NAME_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.ASSET_TENANT_ID_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.ASSET_TYPE_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.SEARCH_TEXT_PROPERTY;
 
 @Data
 @EqualsAndHashCode(callSuper = true)
diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/AttributeKvEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/AttributeKvEntity.java
index 81722a1..587a314 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/AttributeKvEntity.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/AttributeKvEntity.java
@@ -17,13 +17,33 @@ package org.thingsboard.server.dao.model.sql;
 
 import lombok.Data;
 import org.thingsboard.server.common.data.EntityType;
-import org.thingsboard.server.common.data.kv.*;
+import org.thingsboard.server.common.data.kv.AttributeKvEntry;
+import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry;
+import org.thingsboard.server.common.data.kv.BooleanDataEntry;
+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.StringDataEntry;
 import org.thingsboard.server.dao.model.ToData;
 
-import javax.persistence.*;
+import javax.persistence.Column;
+import javax.persistence.Entity;
+import javax.persistence.EnumType;
+import javax.persistence.Enumerated;
+import javax.persistence.Id;
+import javax.persistence.IdClass;
+import javax.persistence.Table;
 import java.io.Serializable;
 
-import static org.thingsboard.server.dao.model.ModelConstants.*;
+import static org.thingsboard.server.dao.model.ModelConstants.ATTRIBUTE_KEY_COLUMN;
+import static org.thingsboard.server.dao.model.ModelConstants.ATTRIBUTE_TYPE_COLUMN;
+import static org.thingsboard.server.dao.model.ModelConstants.BOOLEAN_VALUE_COLUMN;
+import static org.thingsboard.server.dao.model.ModelConstants.DOUBLE_VALUE_COLUMN;
+import static org.thingsboard.server.dao.model.ModelConstants.ENTITY_ID_COLUMN;
+import static org.thingsboard.server.dao.model.ModelConstants.ENTITY_TYPE_COLUMN;
+import static org.thingsboard.server.dao.model.ModelConstants.LAST_UPDATE_TS_COLUMN;
+import static org.thingsboard.server.dao.model.ModelConstants.LONG_VALUE_COLUMN;
+import static org.thingsboard.server.dao.model.ModelConstants.STRING_VALUE_COLUMN;
 
 @Data
 @Entity
diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/AuditLogEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/AuditLogEntity.java
index 6d13561..d4e8e27 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/AuditLogEntity.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/AuditLogEntity.java
@@ -25,15 +25,33 @@ import org.thingsboard.server.common.data.EntityType;
 import org.thingsboard.server.common.data.audit.ActionStatus;
 import org.thingsboard.server.common.data.audit.ActionType;
 import org.thingsboard.server.common.data.audit.AuditLog;
-import org.thingsboard.server.common.data.id.*;
+import org.thingsboard.server.common.data.id.AuditLogId;
+import org.thingsboard.server.common.data.id.CustomerId;
+import org.thingsboard.server.common.data.id.EntityIdFactory;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.id.UserId;
 import org.thingsboard.server.dao.model.BaseEntity;
 import org.thingsboard.server.dao.model.BaseSqlEntity;
 import org.thingsboard.server.dao.model.ModelConstants;
 import org.thingsboard.server.dao.util.mapping.JsonStringType;
 
-import javax.persistence.*;
-
-import static org.thingsboard.server.dao.model.ModelConstants.*;
+import javax.persistence.Column;
+import javax.persistence.Entity;
+import javax.persistence.EnumType;
+import javax.persistence.Enumerated;
+import javax.persistence.Table;
+
+import static org.thingsboard.server.dao.model.ModelConstants.AUDIT_LOG_ACTION_DATA_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.AUDIT_LOG_ACTION_FAILURE_DETAILS_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.AUDIT_LOG_ACTION_STATUS_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.AUDIT_LOG_ACTION_TYPE_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.AUDIT_LOG_CUSTOMER_ID_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.AUDIT_LOG_ENTITY_ID_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.AUDIT_LOG_ENTITY_NAME_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.AUDIT_LOG_ENTITY_TYPE_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.AUDIT_LOG_TENANT_ID_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.AUDIT_LOG_USER_ID_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.AUDIT_LOG_USER_NAME_PROPERTY;
 
 @Data
 @EqualsAndHashCode(callSuper = true)
diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/ComponentDescriptorEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/ComponentDescriptorEntity.java
index 9739889..66fbcc3 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/ComponentDescriptorEntity.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/ComponentDescriptorEntity.java
@@ -29,7 +29,11 @@ import org.thingsboard.server.dao.model.ModelConstants;
 import org.thingsboard.server.dao.model.SearchTextEntity;
 import org.thingsboard.server.dao.util.mapping.JsonStringType;
 
-import javax.persistence.*;
+import javax.persistence.Column;
+import javax.persistence.Entity;
+import javax.persistence.EnumType;
+import javax.persistence.Enumerated;
+import javax.persistence.Table;
 
 @Data
 @EqualsAndHashCode(callSuper = true)
diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/DeviceCredentialsEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/DeviceCredentialsEntity.java
index 676ae2d..faa036e 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/DeviceCredentialsEntity.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/DeviceCredentialsEntity.java
@@ -26,7 +26,11 @@ import org.thingsboard.server.dao.model.BaseEntity;
 import org.thingsboard.server.dao.model.BaseSqlEntity;
 import org.thingsboard.server.dao.model.ModelConstants;
 
-import javax.persistence.*;
+import javax.persistence.Column;
+import javax.persistence.Entity;
+import javax.persistence.EnumType;
+import javax.persistence.Enumerated;
+import javax.persistence.Table;
 
 @Data
 @EqualsAndHashCode(callSuper = true)
diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/EventEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/EventEntity.java
index 6f6b942..aaf2c36 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/EventEntity.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/EventEntity.java
@@ -31,9 +31,19 @@ import org.thingsboard.server.dao.model.BaseEntity;
 import org.thingsboard.server.dao.model.BaseSqlEntity;
 import org.thingsboard.server.dao.util.mapping.JsonStringType;
 
-import javax.persistence.*;
+import javax.persistence.Column;
+import javax.persistence.Entity;
+import javax.persistence.EnumType;
+import javax.persistence.Enumerated;
+import javax.persistence.Table;
 
-import static org.thingsboard.server.dao.model.ModelConstants.*;
+import static org.thingsboard.server.dao.model.ModelConstants.EVENT_BODY_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.EVENT_COLUMN_FAMILY_NAME;
+import static org.thingsboard.server.dao.model.ModelConstants.EVENT_ENTITY_ID_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.EVENT_ENTITY_TYPE_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.EVENT_TENANT_ID_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.EVENT_TYPE_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.EVENT_UID_PROPERTY;
 
 @Data
 @EqualsAndHashCode(callSuper = true)
diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/RelationEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/RelationEntity.java
index 2f94669..6d0f6d0 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/RelationEntity.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/RelationEntity.java
@@ -26,9 +26,20 @@ import org.thingsboard.server.common.data.relation.RelationTypeGroup;
 import org.thingsboard.server.dao.model.ToData;
 import org.thingsboard.server.dao.util.mapping.JsonStringType;
 
-import javax.persistence.*;
+import javax.persistence.Column;
+import javax.persistence.Entity;
+import javax.persistence.Id;
+import javax.persistence.IdClass;
+import javax.persistence.Table;
 
-import static org.thingsboard.server.dao.model.ModelConstants.*;
+import static org.thingsboard.server.dao.model.ModelConstants.ADDITIONAL_INFO_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.RELATION_COLUMN_FAMILY_NAME;
+import static org.thingsboard.server.dao.model.ModelConstants.RELATION_FROM_ID_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.RELATION_FROM_TYPE_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.RELATION_TO_ID_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.RELATION_TO_TYPE_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.RELATION_TYPE_GROUP_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.RELATION_TYPE_PROPERTY;
 
 @Data
 @Entity
diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/TsKvEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/TsKvEntity.java
index 3afe007..a6d3ea6 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/TsKvEntity.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/TsKvEntity.java
@@ -17,12 +17,31 @@ package org.thingsboard.server.dao.model.sql;
 
 import lombok.Data;
 import org.thingsboard.server.common.data.EntityType;
-import org.thingsboard.server.common.data.kv.*;
+import org.thingsboard.server.common.data.kv.BasicTsKvEntry;
+import org.thingsboard.server.common.data.kv.BooleanDataEntry;
+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.StringDataEntry;
+import org.thingsboard.server.common.data.kv.TsKvEntry;
 import org.thingsboard.server.dao.model.ToData;
 
-import javax.persistence.*;
-
-import static org.thingsboard.server.dao.model.ModelConstants.*;
+import javax.persistence.Column;
+import javax.persistence.Entity;
+import javax.persistence.EnumType;
+import javax.persistence.Enumerated;
+import javax.persistence.Id;
+import javax.persistence.IdClass;
+import javax.persistence.Table;
+
+import static org.thingsboard.server.dao.model.ModelConstants.BOOLEAN_VALUE_COLUMN;
+import static org.thingsboard.server.dao.model.ModelConstants.DOUBLE_VALUE_COLUMN;
+import static org.thingsboard.server.dao.model.ModelConstants.ENTITY_ID_COLUMN;
+import static org.thingsboard.server.dao.model.ModelConstants.ENTITY_TYPE_COLUMN;
+import static org.thingsboard.server.dao.model.ModelConstants.KEY_COLUMN;
+import static org.thingsboard.server.dao.model.ModelConstants.LONG_VALUE_COLUMN;
+import static org.thingsboard.server.dao.model.ModelConstants.STRING_VALUE_COLUMN;
+import static org.thingsboard.server.dao.model.ModelConstants.TS_COLUMN;
 
 @Data
 @Entity
diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/TsKvLatestEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/TsKvLatestEntity.java
index 67267c9..cb7851c 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/TsKvLatestEntity.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/TsKvLatestEntity.java
@@ -17,12 +17,31 @@ package org.thingsboard.server.dao.model.sql;
 
 import lombok.Data;
 import org.thingsboard.server.common.data.EntityType;
-import org.thingsboard.server.common.data.kv.*;
+import org.thingsboard.server.common.data.kv.BasicTsKvEntry;
+import org.thingsboard.server.common.data.kv.BooleanDataEntry;
+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.StringDataEntry;
+import org.thingsboard.server.common.data.kv.TsKvEntry;
 import org.thingsboard.server.dao.model.ToData;
 
-import javax.persistence.*;
+import javax.persistence.Column;
+import javax.persistence.Entity;
+import javax.persistence.EnumType;
+import javax.persistence.Enumerated;
+import javax.persistence.Id;
+import javax.persistence.IdClass;
+import javax.persistence.Table;
 
-import static org.thingsboard.server.dao.model.ModelConstants.*;
+import static org.thingsboard.server.dao.model.ModelConstants.BOOLEAN_VALUE_COLUMN;
+import static org.thingsboard.server.dao.model.ModelConstants.DOUBLE_VALUE_COLUMN;
+import static org.thingsboard.server.dao.model.ModelConstants.ENTITY_ID_COLUMN;
+import static org.thingsboard.server.dao.model.ModelConstants.ENTITY_TYPE_COLUMN;
+import static org.thingsboard.server.dao.model.ModelConstants.KEY_COLUMN;
+import static org.thingsboard.server.dao.model.ModelConstants.LONG_VALUE_COLUMN;
+import static org.thingsboard.server.dao.model.ModelConstants.STRING_VALUE_COLUMN;
+import static org.thingsboard.server.dao.model.ModelConstants.TS_COLUMN;
 
 @Data
 @Entity
diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/UserEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/UserEntity.java
index 3df2aa2..7f18cbe 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/UserEntity.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/UserEntity.java
@@ -31,7 +31,11 @@ import org.thingsboard.server.dao.model.ModelConstants;
 import org.thingsboard.server.dao.model.SearchTextEntity;
 import org.thingsboard.server.dao.util.mapping.JsonStringType;
 
-import javax.persistence.*;
+import javax.persistence.Column;
+import javax.persistence.Entity;
+import javax.persistence.EnumType;
+import javax.persistence.Enumerated;
+import javax.persistence.Table;
 
 import static org.thingsboard.server.common.data.UUIDConverter.fromString;
 import static org.thingsboard.server.common.data.UUIDConverter.fromTimeUUID;
diff --git a/dao/src/main/java/org/thingsboard/server/dao/nosql/CassandraAbstractDao.java b/dao/src/main/java/org/thingsboard/server/dao/nosql/CassandraAbstractDao.java
index 5c93066..d1af167 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/nosql/CassandraAbstractDao.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/nosql/CassandraAbstractDao.java
@@ -15,20 +15,39 @@
  */
 package org.thingsboard.server.dao.nosql;
 
-import com.datastax.driver.core.*;
+import com.datastax.driver.core.BoundStatement;
+import com.datastax.driver.core.CodecRegistry;
+import com.datastax.driver.core.ConsistencyLevel;
+import com.datastax.driver.core.PreparedStatement;
+import com.datastax.driver.core.ResultSet;
+import com.datastax.driver.core.ResultSetFuture;
+import com.datastax.driver.core.Session;
+import com.datastax.driver.core.Statement;
+import com.datastax.driver.core.TypeCodec;
 import com.datastax.driver.core.exceptions.CodecNotFoundException;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.thingsboard.server.dao.cassandra.CassandraCluster;
-import org.thingsboard.server.dao.model.type.*;
+import org.thingsboard.server.dao.model.type.AuthorityCodec;
+import org.thingsboard.server.dao.model.type.ComponentLifecycleStateCodec;
+import org.thingsboard.server.dao.model.type.ComponentScopeCodec;
+import org.thingsboard.server.dao.model.type.ComponentTypeCodec;
+import org.thingsboard.server.dao.model.type.DeviceCredentialsTypeCodec;
+import org.thingsboard.server.dao.model.type.EntityTypeCodec;
+import org.thingsboard.server.dao.model.type.JsonCodec;
 import org.thingsboard.server.dao.util.BufferedRateLimiter;
 
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+
 @Slf4j
 public abstract class CassandraAbstractDao {
 
     @Autowired
     protected CassandraCluster cluster;
 
+    private ConcurrentMap<String, PreparedStatement> preparedStatementMap = new ConcurrentHashMap<>();
+
     @Autowired
     private BufferedRateLimiter rateLimiter;
 
@@ -55,7 +74,7 @@ public abstract class CassandraAbstractDao {
     }
 
     protected PreparedStatement prepare(String query) {
-        return getSession().prepare(query);
+        return preparedStatementMap.computeIfAbsent(query, i -> getSession().prepare(i));
     }
 
     private void registerCodecIfNotFound(CodecRegistry registry, TypeCodec<?> codec) {
@@ -83,15 +102,27 @@ public abstract class CassandraAbstractDao {
     }
 
     private ResultSet execute(Statement statement, ConsistencyLevel level) {
-        log.debug("Execute cassandra statement {}", statement);
+        if (log.isDebugEnabled()) {
+            log.debug("Execute cassandra statement {}", statementToString(statement));
+        }
         return executeAsync(statement, level).getUninterruptibly();
     }
 
     private ResultSetFuture executeAsync(Statement statement, ConsistencyLevel level) {
-        log.debug("Execute cassandra async statement {}", statement);
+        if (log.isDebugEnabled()) {
+            log.debug("Execute cassandra async statement {}", statementToString(statement));
+        }
         if (statement.getConsistencyLevel() == null) {
             statement.setConsistencyLevel(level);
         }
         return new RateLimitedResultSetFuture(getSession(), rateLimiter, statement);
     }
+
+    private static String statementToString(Statement statement) {
+        if (statement instanceof BoundStatement) {
+            return ((BoundStatement)statement).preparedStatement().getQueryString();
+        } else {
+            return statement.toString();
+        }
+    }
 }
\ No newline at end of file
diff --git a/dao/src/main/java/org/thingsboard/server/dao/nosql/RateLimitedResultSetFuture.java b/dao/src/main/java/org/thingsboard/server/dao/nosql/RateLimitedResultSetFuture.java
index d250563..2b10bbc 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/nosql/RateLimitedResultSetFuture.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/nosql/RateLimitedResultSetFuture.java
@@ -19,7 +19,6 @@ import com.datastax.driver.core.ResultSet;
 import com.datastax.driver.core.ResultSetFuture;
 import com.datastax.driver.core.Session;
 import com.datastax.driver.core.Statement;
-import com.google.common.base.Function;
 import com.google.common.util.concurrent.FutureCallback;
 import com.google.common.util.concurrent.Futures;
 import com.google.common.util.concurrent.ListenableFuture;
@@ -28,7 +27,11 @@ import org.thingsboard.server.dao.exception.BufferLimitException;
 import org.thingsboard.server.dao.util.AsyncRateLimiter;
 
 import javax.annotation.Nullable;
-import java.util.concurrent.*;
+import java.util.concurrent.CancellationException;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Executor;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
 
 public class RateLimitedResultSetFuture implements ResultSetFuture {
 
@@ -36,14 +39,14 @@ public class RateLimitedResultSetFuture implements ResultSetFuture {
     private final ListenableFuture<Void> rateLimitFuture;
 
     public RateLimitedResultSetFuture(Session session, AsyncRateLimiter rateLimiter, Statement statement) {
-        this.rateLimitFuture = Futures.withFallback(rateLimiter.acquireAsync(), t -> {
+        this.rateLimitFuture = Futures.catchingAsync(rateLimiter.acquireAsync(), Throwable.class, t -> {
             if (!(t instanceof BufferLimitException)) {
                 rateLimiter.release();
             }
             return Futures.immediateFailedFuture(t);
         });
         this.originalFuture = Futures.transform(rateLimitFuture,
-                (Function<Void, ResultSetFuture>) i -> executeAsyncWithRelease(rateLimiter, session, statement));
+                i -> executeAsyncWithRelease(rateLimiter, session, statement));
 
     }
 
diff --git a/dao/src/main/java/org/thingsboard/server/dao/queue/memory/InMemoryMsgQueue.java b/dao/src/main/java/org/thingsboard/server/dao/queue/memory/InMemoryMsgQueue.java
new file mode 100644
index 0000000..9305778
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/queue/memory/InMemoryMsgQueue.java
@@ -0,0 +1,123 @@
+/**
+ * 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.server.dao.queue.memory;
+
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.ListeningExecutorService;
+import com.google.common.util.concurrent.MoreExecutors;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.stereotype.Component;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.msg.TbMsg;
+import org.thingsboard.server.dao.queue.MsgQueue;
+
+import javax.annotation.PostConstruct;
+import javax.annotation.PreDestroy;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Executors;
+
+/**
+ * Created by ashvayka on 27.04.18.
+ */
+@Component
+@ConditionalOnProperty(prefix = "actors.rule.queue", value = "type", havingValue = "memory", matchIfMissing = true)
+@Slf4j
+public class InMemoryMsgQueue implements MsgQueue {
+
+    private ListeningExecutorService queueExecutor;
+    private Map<TenantId, Map<InMemoryMsgKey, Map<UUID, TbMsg>>> data = new HashMap<>();
+
+    @PostConstruct
+    public void init() {
+        // Should be always single threaded due to absence of locks.
+        queueExecutor = MoreExecutors.listeningDecorator(Executors.newSingleThreadExecutor());
+    }
+
+    @PreDestroy
+    public void stop() {
+        if (queueExecutor != null) {
+            queueExecutor.shutdownNow();
+        }
+    }
+
+    @Override
+    public ListenableFuture<Void> put(TenantId tenantId, TbMsg msg, UUID nodeId, long clusterPartition) {
+        return queueExecutor.submit(() -> {
+            data.computeIfAbsent(tenantId, key -> new HashMap<>()).
+                    computeIfAbsent(new InMemoryMsgKey(nodeId, clusterPartition), key -> new HashMap<>()).put(msg.getId(), msg);
+            return null;
+        });
+    }
+
+    @Override
+    public ListenableFuture<Void> ack(TenantId tenantId, TbMsg msg, UUID nodeId, long clusterPartition) {
+        return queueExecutor.submit(() -> {
+            Map<InMemoryMsgKey, Map<UUID, TbMsg>> tenantMap = data.get(tenantId);
+            if (tenantMap != null) {
+                InMemoryMsgKey key = new InMemoryMsgKey(nodeId, clusterPartition);
+                Map<UUID, TbMsg> map = tenantMap.get(key);
+                if (map != null) {
+                    map.remove(msg.getId());
+                    if (map.isEmpty()) {
+                        tenantMap.remove(key);
+                    }
+                }
+                if (tenantMap.isEmpty()) {
+                    data.remove(tenantId);
+                }
+            }
+            return null;
+        });
+    }
+
+    @Override
+    public Iterable<TbMsg> findUnprocessed(TenantId tenantId, UUID nodeId, long clusterPartition) {
+        ListenableFuture<List<TbMsg>> list = queueExecutor.submit(() -> {
+            Map<InMemoryMsgKey, Map<UUID, TbMsg>> tenantMap = data.get(tenantId);
+            if (tenantMap != null) {
+                InMemoryMsgKey key = new InMemoryMsgKey(nodeId, clusterPartition);
+                Map<UUID, TbMsg> map = tenantMap.get(key);
+                if (map != null) {
+                    return new ArrayList<>(map.values());
+                } else {
+                    return Collections.emptyList();
+                }
+            } else {
+                return Collections.emptyList();
+            }
+        });
+        try {
+            return list.get();
+        } catch (InterruptedException | ExecutionException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    @Override
+    public ListenableFuture<Void> cleanUp(TenantId tenantId) {
+        return queueExecutor.submit(() -> {
+            data.remove(tenantId);
+            return null;
+        });
+    }
+}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/queue/QueueBenchmark.java b/dao/src/main/java/org/thingsboard/server/dao/queue/QueueBenchmark.java
new file mode 100644
index 0000000..ca61a63
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/queue/QueueBenchmark.java
@@ -0,0 +1,152 @@
+/**
+ * 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.server.dao.queue;
+
+import com.datastax.driver.core.Cluster;
+import com.datastax.driver.core.HostDistance;
+import com.datastax.driver.core.PoolingOptions;
+import com.datastax.driver.core.Session;
+import com.datastax.driver.core.utils.UUIDs;
+import com.google.common.util.concurrent.FutureCallback;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.CommandLineRunner;
+import org.springframework.boot.SpringApplication;
+import org.springframework.context.annotation.Bean;
+import org.thingsboard.server.common.data.id.EntityId;
+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.msg.TbMsg;
+import org.thingsboard.server.common.msg.TbMsgDataType;
+import org.thingsboard.server.common.msg.TbMsgMetaData;
+
+import javax.annotation.Nullable;
+import java.net.InetSocketAddress;
+import java.util.UUID;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicLong;
+
+//@SpringBootApplication
+//@EnableAutoConfiguration
+//@ComponentScan({"org.thingsboard.rule.engine"})
+//@PropertySource("classpath:processing-pipeline.properties")
+@Slf4j
+public class QueueBenchmark implements CommandLineRunner {
+
+    public static void main(String[] args) {
+        try {
+            SpringApplication.run(QueueBenchmark.class, args);
+        } catch (Throwable th) {
+            th.printStackTrace();
+            System.exit(0);
+        }
+    }
+
+    @Autowired
+    private MsgQueue msgQueue;
+
+    @Override
+    public void run(String... strings) throws Exception {
+        System.out.println("It works + " + msgQueue);
+
+
+        long start = System.currentTimeMillis();
+        int msgCount = 10000000;
+        AtomicLong count = new AtomicLong(0);
+        ExecutorService service = Executors.newFixedThreadPool(100);
+
+        CountDownLatch latch = new CountDownLatch(msgCount);
+        for (int i = 0; i < msgCount; i++) {
+            service.submit(() -> {
+                boolean isFinished = false;
+                while (!isFinished) {
+                    try {
+                        TbMsg msg = randomMsg();
+                        UUID nodeId = UUIDs.timeBased();
+                        ListenableFuture<Void> put = msgQueue.put(new TenantId(EntityId.NULL_UUID), msg, nodeId, 100L);
+//                    ListenableFuture<Void> put = msgQueue.ack(msg, nodeId, 100L);
+                        Futures.addCallback(put, new FutureCallback<Void>() {
+                            @Override
+                            public void onSuccess(@Nullable Void result) {
+                                latch.countDown();
+                            }
+
+                            @Override
+                            public void onFailure(Throwable t) {
+//                                t.printStackTrace();
+                                System.out.println("onFailure, because:" + t.getMessage());
+                                latch.countDown();
+                            }
+                        });
+                        isFinished = true;
+                    } catch (Throwable th) {
+//                        th.printStackTrace();
+                        System.out.println("Repeat query, because:" + th.getMessage());
+//                        latch.countDown();
+                    }
+                }
+            });
+        }
+
+        long prev = 0L;
+        while (latch.getCount() != 0) {
+            TimeUnit.SECONDS.sleep(1);
+            long curr = latch.getCount();
+            long rps = prev - curr;
+            prev = curr;
+            System.out.println("rps = " + rps);
+        }
+
+        long end = System.currentTimeMillis();
+        System.out.println("final rps = " + (msgCount / (end - start) * 1000));
+
+        System.out.println("Finished");
+
+    }
+
+    private TbMsg randomMsg() {
+        TbMsgMetaData metaData = new TbMsgMetaData();
+        metaData.putValue("key", "value");
+        String dataStr = "someContent";
+        return new TbMsg(UUIDs.timeBased(), "type", null, metaData, TbMsgDataType.JSON, dataStr, new RuleChainId(UUIDs.timeBased()), new RuleNodeId(UUIDs.timeBased()), 0L);
+    }
+
+    @Bean
+    public Session session() {
+        Cluster thingsboard = Cluster.builder()
+                .addContactPointsWithPorts(new InetSocketAddress("127.0.0.1", 9042))
+                .withClusterName("thingsboard")
+//                .withSocketOptions(socketOpts.getOpts())
+                .withPoolingOptions(new PoolingOptions()
+                        .setMaxRequestsPerConnection(HostDistance.LOCAL, 32768)
+                        .setMaxRequestsPerConnection(HostDistance.REMOTE, 32768)).build();
+
+        Session session = thingsboard.connect("thingsboard");
+        return session;
+    }
+
+    @Bean
+    public int defaultTtl() {
+        return 6000;
+    }
+
+}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationDao.java b/dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationDao.java
index 55838d6..ba6d09d 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationDao.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationDao.java
@@ -15,7 +15,11 @@
  */
 package org.thingsboard.server.dao.relation;
 
-import com.datastax.driver.core.*;
+import com.datastax.driver.core.BoundStatement;
+import com.datastax.driver.core.PreparedStatement;
+import com.datastax.driver.core.ResultSet;
+import com.datastax.driver.core.ResultSetFuture;
+import com.datastax.driver.core.Row;
 import com.datastax.driver.core.querybuilder.QueryBuilder;
 import com.datastax.driver.core.querybuilder.Select;
 import com.fasterxml.jackson.databind.JsonNode;
diff --git a/dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationService.java b/dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationService.java
index e9f808a..ea29f6f 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationService.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationService.java
@@ -29,12 +29,23 @@ import org.springframework.cache.annotation.Caching;
 import org.springframework.stereotype.Service;
 import org.springframework.util.StringUtils;
 import org.thingsboard.server.common.data.id.EntityId;
-import org.thingsboard.server.common.data.relation.*;
+import org.thingsboard.server.common.data.relation.EntityRelation;
+import org.thingsboard.server.common.data.relation.EntityRelationInfo;
+import org.thingsboard.server.common.data.relation.EntityRelationsQuery;
+import org.thingsboard.server.common.data.relation.EntitySearchDirection;
+import org.thingsboard.server.common.data.relation.EntityTypeFilter;
+import org.thingsboard.server.common.data.relation.RelationTypeGroup;
+import org.thingsboard.server.common.data.relation.RelationsSearchParameters;
 import org.thingsboard.server.dao.entity.EntityService;
 import org.thingsboard.server.dao.exception.DataValidationException;
 
 import javax.annotation.Nullable;
-import java.util.*;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.ExecutionException;
 import java.util.function.BiConsumer;
@@ -166,129 +177,96 @@ public class BaseRelationService implements RelationService {
     }
 
     @Override
-    public boolean deleteEntityRelations(EntityId entity) {
-        Cache cache = cacheManager.getCache(RELATIONS_CACHE);
-        log.trace("Executing deleteEntityRelations [{}]", entity);
-        validate(entity);
-        List<ListenableFuture<List<EntityRelation>>> inboundRelationsList = new ArrayList<>();
-        for (RelationTypeGroup typeGroup : RelationTypeGroup.values()) {
-            inboundRelationsList.add(relationDao.findAllByTo(entity, typeGroup));
-        }
-        ListenableFuture<List<List<EntityRelation>>> inboundRelations = Futures.allAsList(inboundRelationsList);
-        ListenableFuture<List<Boolean>> inboundDeletions = Futures.transform(inboundRelations, (List<List<EntityRelation>> relations) ->
-                getBooleans(relations, cache, true));
-
-        ListenableFuture<Boolean> inboundFuture = Futures.transform(inboundDeletions, getListToBooleanFunction());
-        boolean inboundDeleteResult = false;
+    public void deleteEntityRelations(EntityId entityId) {
         try {
-            inboundDeleteResult = inboundFuture.get();
+            deleteEntityRelationsAsync(entityId).get();
         } catch (InterruptedException | ExecutionException e) {
-            log.error("Error deleting entity inbound relations", e);
-        }
-
-        List<ListenableFuture<List<EntityRelation>>> outboundRelationsList = new ArrayList<>();
-        for (RelationTypeGroup typeGroup : RelationTypeGroup.values()) {
-            outboundRelationsList.add(relationDao.findAllByFrom(entity, typeGroup));
-        }
-        ListenableFuture<List<List<EntityRelation>>> outboundRelations = Futures.allAsList(outboundRelationsList);
-        Futures.transform(outboundRelations, (Function<List<List<EntityRelation>>, List<Boolean>>) relations ->
-                getBooleans(relations, cache, false));
-
-        boolean outboundDeleteResult = relationDao.deleteOutboundRelations(entity);
-        return inboundDeleteResult && outboundDeleteResult;
-    }
-
-    private List<Boolean> getBooleans(List<List<EntityRelation>> relations, Cache cache, boolean isRemove) {
-        List<Boolean> results = new ArrayList<>();
-        for (List<EntityRelation> relationList : relations) {
-            relationList.stream().forEach(relation -> checkFromDeleteSync(cache, results, relation, isRemove));
-        }
-        return results;
-    }
-
-    private void checkFromDeleteSync(Cache cache, List<Boolean> results, EntityRelation relation, boolean isRemove) {
-        if (isRemove) {
-            results.add(relationDao.deleteRelation(relation));
+            throw new RuntimeException(e);
         }
-        cacheEviction(relation, cache);
     }
 
     @Override
-    public ListenableFuture<Boolean> deleteEntityRelationsAsync(EntityId entity) {
+    public ListenableFuture<Void> deleteEntityRelationsAsync(EntityId entityId) {
         Cache cache = cacheManager.getCache(RELATIONS_CACHE);
-        log.trace("Executing deleteEntityRelationsAsync [{}]", entity);
-        validate(entity);
+        log.trace("Executing deleteEntityRelationsAsync [{}]", entityId);
+        validate(entityId);
         List<ListenableFuture<List<EntityRelation>>> inboundRelationsList = new ArrayList<>();
         for (RelationTypeGroup typeGroup : RelationTypeGroup.values()) {
-            inboundRelationsList.add(relationDao.findAllByTo(entity, typeGroup));
+            inboundRelationsList.add(relationDao.findAllByTo(entityId, typeGroup));
         }
-        ListenableFuture<List<List<EntityRelation>>> inboundRelations = Futures.allAsList(inboundRelationsList);
-        ListenableFuture<List<Boolean>> inboundDeletions = Futures.transform(inboundRelations,
-                (AsyncFunction<List<List<EntityRelation>>, List<Boolean>>) relations -> {
-                    List<ListenableFuture<Boolean>> results = getListenableFutures(relations, cache, true);
-                    return Futures.allAsList(results);
-                });
 
-        ListenableFuture<Boolean> inboundFuture = Futures.transform(inboundDeletions, getListToBooleanFunction());
+        ListenableFuture<List<List<EntityRelation>>> inboundRelations = Futures.allAsList(inboundRelationsList);
 
         List<ListenableFuture<List<EntityRelation>>> outboundRelationsList = new ArrayList<>();
         for (RelationTypeGroup typeGroup : RelationTypeGroup.values()) {
-            outboundRelationsList.add(relationDao.findAllByFrom(entity, typeGroup));
+            outboundRelationsList.add(relationDao.findAllByFrom(entityId, typeGroup));
         }
+
         ListenableFuture<List<List<EntityRelation>>> outboundRelations = Futures.allAsList(outboundRelationsList);
-        Futures.transform(outboundRelations, (AsyncFunction<List<List<EntityRelation>>, List<Boolean>>) relations -> {
-            List<ListenableFuture<Boolean>> results = getListenableFutures(relations, cache, false);
-            return Futures.allAsList(results);
-        });
 
-        ListenableFuture<Boolean> outboundFuture = relationDao.deleteOutboundRelationsAsync(entity);
-        return Futures.transform(Futures.allAsList(Arrays.asList(inboundFuture, outboundFuture)), getListToBooleanFunction());
+        ListenableFuture<List<Boolean>> inboundDeletions = Futures.transformAsync(inboundRelations,
+                relations -> {
+                    List<ListenableFuture<Boolean>> results = deleteRelationGroupsAsync(relations, cache, true);
+                    return Futures.allAsList(results);
+                });
+
+        ListenableFuture<List<Boolean>> outboundDeletions = Futures.transformAsync(outboundRelations,
+                relations -> {
+                    List<ListenableFuture<Boolean>> results = deleteRelationGroupsAsync(relations, cache, false);
+                    return Futures.allAsList(results);
+                });
+
+        ListenableFuture<List<List<Boolean>>> deletionsFuture = Futures.allAsList(inboundDeletions, outboundDeletions);
+
+        return Futures.transform(Futures.transformAsync(deletionsFuture, (deletions) -> relationDao.deleteOutboundRelationsAsync(entityId)), result -> null);
     }
 
-    private List<ListenableFuture<Boolean>> getListenableFutures(List<List<EntityRelation>> relations, Cache cache, boolean isRemove) {
+    private List<ListenableFuture<Boolean>> deleteRelationGroupsAsync(List<List<EntityRelation>> relations, Cache cache, boolean deleteFromDb) {
         List<ListenableFuture<Boolean>> results = new ArrayList<>();
         for (List<EntityRelation> relationList : relations) {
-            relationList.stream().forEach(relation -> checkFromDeleteAsync(cache, results, relation, isRemove));
+            relationList.forEach(relation -> results.add(deleteAsync(cache, relation, deleteFromDb)));
         }
         return results;
     }
 
-    private void checkFromDeleteAsync(Cache cache, List<ListenableFuture<Boolean>> results, EntityRelation relation, boolean isRemove) {
-        if (isRemove) {
-            results.add(relationDao.deleteRelationAsync(relation));
-        }
+    private ListenableFuture<Boolean> deleteAsync(Cache cache, EntityRelation relation, boolean deleteFromDb) {
         cacheEviction(relation, cache);
+        if (deleteFromDb) {
+            return relationDao.deleteRelationAsync(relation);
+        } else {
+            return Futures.immediateFuture(false);
+        }
     }
 
     private void cacheEviction(EntityRelation relation, Cache cache) {
-        List<Object> toAndGroup = new ArrayList<>();
-        toAndGroup.add(relation.getTo());
-        toAndGroup.add(relation.getTypeGroup());
-        cache.evict(toAndGroup);
-
-        List<Object> toTypeAndGroup = new ArrayList<>();
-        toTypeAndGroup.add(relation.getTo());
-        toTypeAndGroup.add(relation.getType());
-        toTypeAndGroup.add(relation.getTypeGroup());
-        cache.evict(toTypeAndGroup);
-
-        List<Object> fromAndGroup = new ArrayList<>();
-        fromAndGroup.add(relation.getFrom());
-        fromAndGroup.add(relation.getTypeGroup());
-        cache.evict(fromAndGroup);
-
-        List<Object> fromTypeAndGroup = new ArrayList<>();
-        fromTypeAndGroup.add(relation.getFrom());
-        fromTypeAndGroup.add(relation.getType());
-        fromTypeAndGroup.add(relation.getTypeGroup());
-        cache.evict(fromTypeAndGroup);
-
-        List<Object> fromToTypeAndGroup = new ArrayList<>();
-        fromToTypeAndGroup.add(relation.getFrom());
-        fromToTypeAndGroup.add(relation.getTo());
-        fromToTypeAndGroup.add(relation.getType());
-        fromToTypeAndGroup.add(relation.getTypeGroup());
-        cache.evict(fromToTypeAndGroup);
+        List<Object> fromToTypeAndTypeGroup = new ArrayList<>();
+        fromToTypeAndTypeGroup.add(relation.getFrom());
+        fromToTypeAndTypeGroup.add(relation.getTo());
+        fromToTypeAndTypeGroup.add(relation.getType());
+        fromToTypeAndTypeGroup.add(relation.getTypeGroup());
+        cache.evict(fromToTypeAndTypeGroup);
+
+        List<Object> fromTypeAndTypeGroup = new ArrayList<>();
+        fromTypeAndTypeGroup.add(relation.getFrom());
+        fromTypeAndTypeGroup.add(relation.getType());
+        fromTypeAndTypeGroup.add(relation.getTypeGroup());
+        cache.evict(fromTypeAndTypeGroup);
+
+        List<Object> fromAndTypeGroup = new ArrayList<>();
+        fromAndTypeGroup.add(relation.getFrom());
+        fromAndTypeGroup.add(relation.getTypeGroup());
+        cache.evict(fromAndTypeGroup);
+
+        List<Object> toAndTypeGroup = new ArrayList<>();
+        toAndTypeGroup.add(relation.getTo());
+        toAndTypeGroup.add(relation.getTypeGroup());
+        cache.evict(toAndTypeGroup);
+
+        List<Object> toTypeAndTypeGroup = new ArrayList<>();
+        fromTypeAndTypeGroup.add(relation.getTo());
+        fromTypeAndTypeGroup.add(relation.getType());
+        fromTypeAndTypeGroup.add(relation.getTypeGroup());
+        cache.evict(toTypeAndTypeGroup);
     }
 
     @Cacheable(cacheNames = RELATIONS_CACHE, key = "{#from, #typeGroup}")
@@ -315,17 +293,16 @@ public class BaseRelationService implements RelationService {
         validate(from);
         validateTypeGroup(typeGroup);
         ListenableFuture<List<EntityRelation>> relations = relationDao.findAllByFrom(from, typeGroup);
-        ListenableFuture<List<EntityRelationInfo>> relationsInfo = Futures.transform(relations,
-                (AsyncFunction<List<EntityRelation>, List<EntityRelationInfo>>) relations1 -> {
+        return Futures.transformAsync(relations,
+                relations1 -> {
                     List<ListenableFuture<EntityRelationInfo>> futures = new ArrayList<>();
-                    relations1.stream().forEach(relation ->
+                    relations1.forEach(relation ->
                             futures.add(fetchRelationInfoAsync(relation,
-                                    relation2 -> relation2.getTo(),
-                                    (EntityRelationInfo relationInfo, String entityName) -> relationInfo.setToName(entityName)))
+                                    EntityRelation::getTo,
+                                    EntityRelationInfo::setToName))
                     );
                     return Futures.successfulAsList(futures);
                 });
-        return relationsInfo;
     }
 
     @Cacheable(cacheNames = RELATIONS_CACHE, key = "{#from, #relationType, #typeGroup}")
@@ -371,30 +348,27 @@ public class BaseRelationService implements RelationService {
         validate(to);
         validateTypeGroup(typeGroup);
         ListenableFuture<List<EntityRelation>> relations = relationDao.findAllByTo(to, typeGroup);
-        ListenableFuture<List<EntityRelationInfo>> relationsInfo = Futures.transform(relations,
-                (AsyncFunction<List<EntityRelation>, List<EntityRelationInfo>>) relations1 -> {
+        return Futures.transformAsync(relations,
+                relations1 -> {
                     List<ListenableFuture<EntityRelationInfo>> futures = new ArrayList<>();
-                    relations1.stream().forEach(relation ->
+                    relations1.forEach(relation ->
                             futures.add(fetchRelationInfoAsync(relation,
-                                    relation2 -> relation2.getFrom(),
-                                    (EntityRelationInfo relationInfo, String entityName) -> relationInfo.setFromName(entityName)))
+                                    EntityRelation::getFrom,
+                                    EntityRelationInfo::setFromName))
                     );
                     return Futures.successfulAsList(futures);
                 });
-        return relationsInfo;
     }
 
     private ListenableFuture<EntityRelationInfo> fetchRelationInfoAsync(EntityRelation relation,
                                                                         Function<EntityRelation, EntityId> entityIdGetter,
                                                                         BiConsumer<EntityRelationInfo, String> entityNameSetter) {
         ListenableFuture<String> entityName = entityService.fetchEntityNameAsync(entityIdGetter.apply(relation));
-        ListenableFuture<EntityRelationInfo> entityRelationInfo =
-                Futures.transform(entityName, (Function<String, EntityRelationInfo>) entityName1 -> {
-                    EntityRelationInfo entityRelationInfo1 = new EntityRelationInfo(relation);
-                    entityNameSetter.accept(entityRelationInfo1, entityName1);
-                    return entityRelationInfo1;
-                });
-        return entityRelationInfo;
+        return Futures.transform(entityName, entityName1 -> {
+            EntityRelationInfo entityRelationInfo1 = new EntityRelationInfo(relation);
+            entityNameSetter.accept(entityRelationInfo1, entityName1);
+            return entityRelationInfo1;
+        });
     }
 
     @Cacheable(cacheNames = RELATIONS_CACHE, key = "{#to, #relationType, #typeGroup}")
@@ -429,7 +403,7 @@ public class BaseRelationService implements RelationService {
 
         try {
             ListenableFuture<Set<EntityRelation>> relationSet = findRelationsRecursively(params.getEntityId(), params.getDirection(), maxLvl, new ConcurrentHashMap<>());
-            return Futures.transform(relationSet, (Function<Set<EntityRelation>, List<EntityRelation>>) input -> {
+            return Futures.transform(relationSet, input -> {
                 List<EntityRelation> relations = new ArrayList<>();
                 if (filters == null || filters.isEmpty()) {
                     relations.addAll(input);
@@ -453,10 +427,10 @@ public class BaseRelationService implements RelationService {
         log.trace("Executing findInfoByQuery [{}]", query);
         ListenableFuture<List<EntityRelation>> relations = findByQuery(query);
         EntitySearchDirection direction = query.getParameters().getDirection();
-        ListenableFuture<List<EntityRelationInfo>> relationsInfo = Futures.transform(relations,
-                (AsyncFunction<List<EntityRelation>, List<EntityRelationInfo>>) relations1 -> {
+        return Futures.transformAsync(relations,
+                relations1 -> {
                     List<ListenableFuture<EntityRelationInfo>> futures = new ArrayList<>();
-                    relations1.stream().forEach(relation ->
+                    relations1.forEach(relation ->
                             futures.add(fetchRelationInfoAsync(relation,
                                     relation2 -> direction == EntitySearchDirection.FROM ? relation2.getTo() : relation2.getFrom(),
                                     (EntityRelationInfo relationInfo, String entityName) -> {
@@ -469,7 +443,6 @@ public class BaseRelationService implements RelationService {
                     );
                     return Futures.successfulAsList(futures);
                 });
-        return relationsInfo;
     }
 
     protected void validate(EntityRelation relation) {
@@ -575,7 +548,7 @@ public class BaseRelationService implements RelationService {
         }
         //TODO: try to remove this blocking operation
         List<Set<EntityRelation>> relations = Futures.successfulAsList(futures).get();
-        relations.forEach(r -> r.forEach(d -> children.add(d)));
+        relations.forEach(r -> r.forEach(children::add));
         return Futures.immediateFuture(children);
     }
 
diff --git a/dao/src/main/java/org/thingsboard/server/dao/relation/RelationService.java b/dao/src/main/java/org/thingsboard/server/dao/relation/RelationService.java
index ca1b959..da58b11 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/relation/RelationService.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/relation/RelationService.java
@@ -23,6 +23,7 @@ import org.thingsboard.server.common.data.relation.EntityRelationsQuery;
 import org.thingsboard.server.common.data.relation.RelationTypeGroup;
 
 import java.util.List;
+import java.util.concurrent.ExecutionException;
 
 /**
  * Created by ashvayka on 27.04.17.
@@ -47,9 +48,9 @@ public interface RelationService {
 
     ListenableFuture<Boolean> deleteRelationAsync(EntityId from, EntityId to, String relationType, RelationTypeGroup typeGroup);
 
-    boolean deleteEntityRelations(EntityId entity);
+    void deleteEntityRelations(EntityId entity);
 
-    ListenableFuture<Boolean> deleteEntityRelationsAsync(EntityId entity);
+    ListenableFuture<Void> deleteEntityRelationsAsync(EntityId entity);
 
     List<EntityRelation> findByFrom(EntityId from, RelationTypeGroup typeGroup);
 
diff --git a/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java b/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java
new file mode 100644
index 0000000..14de1a4
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java
@@ -0,0 +1,375 @@
+/**
+ * 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.server.dao.rule;
+
+import com.google.common.util.concurrent.ListenableFuture;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.thingsboard.server.common.data.EntityType;
+import org.thingsboard.server.common.data.Tenant;
+import org.thingsboard.server.common.data.id.EntityId;
+import org.thingsboard.server.common.data.id.RuleChainId;
+import org.thingsboard.server.common.data.id.RuleNodeId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.page.TextPageData;
+import org.thingsboard.server.common.data.page.TextPageLink;
+import org.thingsboard.server.common.data.relation.EntityRelation;
+import org.thingsboard.server.common.data.relation.RelationTypeGroup;
+import org.thingsboard.server.common.data.rule.NodeConnectionInfo;
+import org.thingsboard.server.common.data.rule.RuleChain;
+import org.thingsboard.server.common.data.rule.RuleChainConnectionInfo;
+import org.thingsboard.server.common.data.rule.RuleChainMetaData;
+import org.thingsboard.server.common.data.rule.RuleNode;
+import org.thingsboard.server.dao.entity.AbstractEntityService;
+import org.thingsboard.server.dao.exception.DataValidationException;
+import org.thingsboard.server.dao.service.DataValidator;
+import org.thingsboard.server.dao.service.PaginatedRemover;
+import org.thingsboard.server.dao.service.Validator;
+import org.thingsboard.server.dao.tenant.TenantDao;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ExecutionException;
+import java.util.stream.Collectors;
+
+/**
+ * Created by igor on 3/12/18.
+ */
+@Service
+@Slf4j
+public class BaseRuleChainService extends AbstractEntityService implements RuleChainService {
+
+    @Autowired
+    private RuleChainDao ruleChainDao;
+
+    @Autowired
+    private RuleNodeDao ruleNodeDao;
+
+    @Autowired
+    private TenantDao tenantDao;
+
+    @Override
+    public RuleChain saveRuleChain(RuleChain ruleChain) {
+        ruleChainValidator.validate(ruleChain);
+        RuleChain savedRuleChain = ruleChainDao.save(ruleChain);
+        if (ruleChain.isRoot() && ruleChain.getId() == null) {
+            try {
+                createRelation(new EntityRelation(savedRuleChain.getTenantId(), savedRuleChain.getId(),
+                        EntityRelation.CONTAINS_TYPE, RelationTypeGroup.RULE_CHAIN));
+            } catch (ExecutionException | InterruptedException e) {
+                log.warn("[{}] Failed to create tenant to root rule chain relation. from: [{}], to: [{}]",
+                        savedRuleChain.getTenantId(), savedRuleChain.getId());
+                throw new RuntimeException(e);
+            }
+        }
+        return savedRuleChain;
+    }
+
+    @Override
+    public boolean setRootRuleChain(RuleChainId ruleChainId) {
+        RuleChain ruleChain = ruleChainDao.findById(ruleChainId.getId());
+        if (!ruleChain.isRoot()) {
+            RuleChain previousRootRuleChain = getRootTenantRuleChain(ruleChain.getTenantId());
+            if (!previousRootRuleChain.getId().equals(ruleChain.getId())) {
+                try {
+                    deleteRelation(new EntityRelation(previousRootRuleChain.getTenantId(), previousRootRuleChain.getId(),
+                            EntityRelation.CONTAINS_TYPE, RelationTypeGroup.RULE_CHAIN));
+                    previousRootRuleChain.setRoot(false);
+                    ruleChainDao.save(previousRootRuleChain);
+                    createRelation(new EntityRelation(ruleChain.getTenantId(), ruleChain.getId(),
+                            EntityRelation.CONTAINS_TYPE, RelationTypeGroup.RULE_CHAIN));
+                    ruleChain.setRoot(true);
+                    ruleChainDao.save(ruleChain);
+                    return true;
+                } catch (ExecutionException | InterruptedException e) {
+                    log.warn("[{}] Failed to set root rule chain, ruleChainId: [{}]", ruleChainId);
+                    throw new RuntimeException(e);
+                }
+            }
+        }
+        return false;
+    }
+
+    @Override
+    public RuleChainMetaData saveRuleChainMetaData(RuleChainMetaData ruleChainMetaData) {
+        Validator.validateId(ruleChainMetaData.getRuleChainId(), "Incorrect rule chain id.");
+        RuleChain ruleChain = findRuleChainById(ruleChainMetaData.getRuleChainId());
+        if (ruleChain == null) {
+            return null;
+        }
+
+        List<RuleNode> nodes = ruleChainMetaData.getNodes();
+        List<RuleNode> toAddOrUpdate = new ArrayList<>();
+        List<RuleNode> toDelete = new ArrayList<>();
+
+        Map<RuleNodeId, Integer> ruleNodeIndexMap = new HashMap<>();
+        if (nodes != null) {
+            for (RuleNode node : nodes) {
+                if (node.getId() != null) {
+                    ruleNodeIndexMap.put(node.getId(), nodes.indexOf(node));
+                } else {
+                    toAddOrUpdate.add(node);
+                }
+            }
+        }
+
+        List<RuleNode> existingRuleNodes = getRuleChainNodes(ruleChainMetaData.getRuleChainId());
+        for (RuleNode existingNode : existingRuleNodes) {
+            deleteEntityRelations(existingNode.getId());
+            Integer index = ruleNodeIndexMap.get(existingNode.getId());
+            if (index != null) {
+                toAddOrUpdate.add(ruleChainMetaData.getNodes().get(index));
+            } else {
+                toDelete.add(existingNode);
+            }
+        }
+        for (RuleNode node : toAddOrUpdate) {
+            node.setRuleChainId(ruleChain.getId());
+            RuleNode savedNode = ruleNodeDao.save(node);
+            try {
+                createRelation(new EntityRelation(ruleChainMetaData.getRuleChainId(), savedNode.getId(),
+                        EntityRelation.CONTAINS_TYPE, RelationTypeGroup.RULE_CHAIN));
+            } catch (ExecutionException | InterruptedException e) {
+                log.warn("[{}] Failed to create rule chain to rule node relation. from: [{}], to: [{}]",
+                        ruleChainMetaData.getRuleChainId(), savedNode.getId());
+                throw new RuntimeException(e);
+            }
+            int index = nodes.indexOf(node);
+            nodes.set(index, savedNode);
+            ruleNodeIndexMap.put(savedNode.getId(), index);
+        }
+        for (RuleNode node : toDelete) {
+            deleteRuleNode(node.getId());
+        }
+        RuleNodeId firstRuleNodeId = null;
+        if (ruleChainMetaData.getFirstNodeIndex() != null) {
+            firstRuleNodeId = nodes.get(ruleChainMetaData.getFirstNodeIndex()).getId();
+        }
+        if ((ruleChain.getFirstRuleNodeId() != null && !ruleChain.getFirstRuleNodeId().equals(firstRuleNodeId))
+                || (ruleChain.getFirstRuleNodeId() == null && firstRuleNodeId != null)) {
+            ruleChain.setFirstRuleNodeId(firstRuleNodeId);
+            ruleChainDao.save(ruleChain);
+        }
+        if (ruleChainMetaData.getConnections() != null) {
+            for (NodeConnectionInfo nodeConnection : ruleChainMetaData.getConnections()) {
+                EntityId from = nodes.get(nodeConnection.getFromIndex()).getId();
+                EntityId to = nodes.get(nodeConnection.getToIndex()).getId();
+                String type = nodeConnection.getType();
+                try {
+                    createRelation(new EntityRelation(from, to, type, RelationTypeGroup.RULE_NODE));
+                } catch (ExecutionException | InterruptedException e) {
+                    log.warn("[{}] Failed to create rule node relation. from: [{}], to: [{}]", from, to);
+                    throw new RuntimeException(e);
+                }
+            }
+        }
+        if (ruleChainMetaData.getRuleChainConnections() != null) {
+            for (RuleChainConnectionInfo nodeToRuleChainConnection : ruleChainMetaData.getRuleChainConnections()) {
+                EntityId from = nodes.get(nodeToRuleChainConnection.getFromIndex()).getId();
+                EntityId to = nodeToRuleChainConnection.getTargetRuleChainId();
+                String type = nodeToRuleChainConnection.getType();
+                try {
+                    createRelation(new EntityRelation(from, to, type, RelationTypeGroup.RULE_NODE, nodeToRuleChainConnection.getAdditionalInfo()));
+                } catch (ExecutionException | InterruptedException e) {
+                    log.warn("[{}] Failed to create rule node to rule chain relation. from: [{}], to: [{}]", from, to);
+                    throw new RuntimeException(e);
+                }
+            }
+        }
+
+        return loadRuleChainMetaData(ruleChainMetaData.getRuleChainId());
+    }
+
+    @Override
+    public RuleChainMetaData loadRuleChainMetaData(RuleChainId ruleChainId) {
+        Validator.validateId(ruleChainId, "Incorrect rule chain id.");
+        RuleChain ruleChain = findRuleChainById(ruleChainId);
+        if (ruleChain == null) {
+            return null;
+        }
+        RuleChainMetaData ruleChainMetaData = new RuleChainMetaData();
+        ruleChainMetaData.setRuleChainId(ruleChainId);
+        List<RuleNode> ruleNodes = getRuleChainNodes(ruleChainId);
+        Map<RuleNodeId, Integer> ruleNodeIndexMap = new HashMap<>();
+        for (RuleNode node : ruleNodes) {
+            ruleNodeIndexMap.put(node.getId(), ruleNodes.indexOf(node));
+        }
+        ruleChainMetaData.setNodes(ruleNodes);
+        if (ruleChain.getFirstRuleNodeId() != null) {
+            ruleChainMetaData.setFirstNodeIndex(ruleNodeIndexMap.get(ruleChain.getFirstRuleNodeId()));
+        }
+        for (RuleNode node : ruleNodes) {
+            int fromIndex = ruleNodeIndexMap.get(node.getId());
+            List<EntityRelation> nodeRelations = getRuleNodeRelations(node.getId());
+            for (EntityRelation nodeRelation : nodeRelations) {
+                String type = nodeRelation.getType();
+                if (nodeRelation.getTo().getEntityType() == EntityType.RULE_NODE) {
+                    RuleNodeId toNodeId = new RuleNodeId(nodeRelation.getTo().getId());
+                    int toIndex = ruleNodeIndexMap.get(toNodeId);
+                    ruleChainMetaData.addConnectionInfo(fromIndex, toIndex, type);
+                } else if (nodeRelation.getTo().getEntityType() == EntityType.RULE_CHAIN) {
+                    RuleChainId targetRuleChainId = new RuleChainId(nodeRelation.getTo().getId());
+                    ruleChainMetaData.addRuleChainConnectionInfo(fromIndex, targetRuleChainId, type, nodeRelation.getAdditionalInfo());
+                }
+            }
+        }
+        return ruleChainMetaData;
+    }
+
+    @Override
+    public RuleChain findRuleChainById(RuleChainId ruleChainId) {
+        Validator.validateId(ruleChainId, "Incorrect rule chain id for search request.");
+        return ruleChainDao.findById(ruleChainId.getId());
+    }
+
+    @Override
+    public RuleNode findRuleNodeById(RuleNodeId ruleNodeId) {
+        Validator.validateId(ruleNodeId, "Incorrect rule node id for search request.");
+        return ruleNodeDao.findById(ruleNodeId.getId());
+    }
+
+    @Override
+    public ListenableFuture<RuleChain> findRuleChainByIdAsync(RuleChainId ruleChainId) {
+        Validator.validateId(ruleChainId, "Incorrect rule chain id for search request.");
+        return ruleChainDao.findByIdAsync(ruleChainId.getId());
+    }
+
+    @Override
+    public ListenableFuture<RuleNode> findRuleNodeByIdAsync(RuleNodeId ruleNodeId) {
+        Validator.validateId(ruleNodeId, "Incorrect rule node id for search request.");
+        return ruleNodeDao.findByIdAsync(ruleNodeId.getId());
+    }
+
+    @Override
+    public RuleChain getRootTenantRuleChain(TenantId tenantId) {
+        Validator.validateId(tenantId, "Incorrect tenant id for search request.");
+        List<EntityRelation> relations = relationService.findByFrom(tenantId, RelationTypeGroup.RULE_CHAIN);
+        if (relations != null && !relations.isEmpty()) {
+            EntityRelation relation = relations.get(0);
+            RuleChainId ruleChainId = new RuleChainId(relation.getTo().getId());
+            return findRuleChainById(ruleChainId);
+        } else {
+            return null;
+        }
+    }
+
+    @Override
+    public List<RuleNode> getRuleChainNodes(RuleChainId ruleChainId) {
+        Validator.validateId(ruleChainId, "Incorrect rule chain id for search request.");
+        List<EntityRelation> relations = getRuleChainToNodeRelations(ruleChainId);
+        List<RuleNode> ruleNodes = relations.stream().map(relation -> ruleNodeDao.findById(relation.getTo().getId())).collect(Collectors.toList());
+        return ruleNodes;
+    }
+
+    @Override
+    public List<EntityRelation> getRuleNodeRelations(RuleNodeId ruleNodeId) {
+        Validator.validateId(ruleNodeId, "Incorrect rule node id for search request.");
+        return relationService.findByFrom(ruleNodeId, RelationTypeGroup.RULE_NODE);
+    }
+
+    @Override
+    public TextPageData<RuleChain> findTenantRuleChains(TenantId tenantId, TextPageLink pageLink) {
+        Validator.validateId(tenantId, "Incorrect tenant id for search rule chain request.");
+        Validator.validatePageLink(pageLink, "Incorrect PageLink object for search rule chain request.");
+        List<RuleChain> ruleChains = ruleChainDao.findRuleChainsByTenantId(tenantId.getId(), pageLink);
+        return new TextPageData<>(ruleChains, pageLink);
+    }
+
+    @Override
+    public void deleteRuleChainById(RuleChainId ruleChainId) {
+        Validator.validateId(ruleChainId, "Incorrect rule chain id for delete request.");
+        RuleChain ruleChain = ruleChainDao.findById(ruleChainId.getId());
+        if (ruleChain != null && ruleChain.isRoot()) {
+            throw new DataValidationException("Deletion of Root Tenant Rule Chain is prohibited!");
+        }
+        checkRuleNodesAndDelete(ruleChainId);
+    }
+
+    @Override
+    public void deleteRuleChainsByTenantId(TenantId tenantId) {
+        Validator.validateId(tenantId, "Incorrect tenant id for delete rule chains request.");
+        tenantRuleChainsRemover.removeEntities(tenantId);
+    }
+
+    private void checkRuleNodesAndDelete(RuleChainId ruleChainId) {
+        List<EntityRelation> nodeRelations = getRuleChainToNodeRelations(ruleChainId);
+        for (EntityRelation relation : nodeRelations) {
+            deleteRuleNode(relation.getTo());
+        }
+        deleteEntityRelations(ruleChainId);
+        ruleChainDao.removeById(ruleChainId.getId());
+    }
+
+    private List<EntityRelation> getRuleChainToNodeRelations(RuleChainId ruleChainId) {
+        return relationService.findByFrom(ruleChainId, RelationTypeGroup.RULE_CHAIN);
+    }
+
+    private void deleteRuleNode(EntityId entityId) {
+        deleteEntityRelations(entityId);
+        ruleNodeDao.removeById(entityId.getId());
+    }
+
+    private void createRelation(EntityRelation relation) throws ExecutionException, InterruptedException {
+        log.debug("Creating relation: {}", relation);
+        relationService.saveRelation(relation);
+    }
+
+    private void deleteRelation(EntityRelation relation) throws ExecutionException, InterruptedException {
+        log.debug("Deleting relation: {}", relation);
+        relationService.deleteRelation(relation);
+    }
+
+    private DataValidator<RuleChain> ruleChainValidator =
+            new DataValidator<RuleChain>() {
+                @Override
+                protected void validateDataImpl(RuleChain ruleChain) {
+                    if (StringUtils.isEmpty(ruleChain.getName())) {
+                        throw new DataValidationException("Rule chain name should be specified!.");
+                    }
+                    if (ruleChain.getTenantId() == null || ruleChain.getTenantId().isNullUid()) {
+                        throw new DataValidationException("Rule chain should be assigned to tenant!");
+                    }
+                    Tenant tenant = tenantDao.findById(ruleChain.getTenantId().getId());
+                    if (tenant == null) {
+                        throw new DataValidationException("Rule chain is referencing to non-existent tenant!");
+                    }
+                    if (ruleChain.isRoot()) {
+                        RuleChain rootRuleChain = getRootTenantRuleChain(ruleChain.getTenantId());
+                        if (rootRuleChain != null && !rootRuleChain.getId().equals(ruleChain.getId())) {
+                            throw new DataValidationException("Another root rule chain is present in scope of current tenant!");
+                        }
+                    }
+                }
+            };
+
+    private PaginatedRemover<TenantId, RuleChain> tenantRuleChainsRemover =
+            new PaginatedRemover<TenantId, RuleChain>() {
+
+                @Override
+                protected List<RuleChain> findEntities(TenantId id, TextPageLink pageLink) {
+                    return ruleChainDao.findRuleChainsByTenantId(id.getId(), pageLink);
+                }
+
+                @Override
+                protected void removeEntity(RuleChain entity) {
+                    checkRuleNodesAndDelete(entity.getId());
+                }
+            };
+}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/rule/CassandraRuleChainDao.java b/dao/src/main/java/org/thingsboard/server/dao/rule/CassandraRuleChainDao.java
new file mode 100644
index 0000000..ff672a7
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/rule/CassandraRuleChainDao.java
@@ -0,0 +1,62 @@
+/**
+ * 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.server.dao.rule;
+
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Component;
+import org.thingsboard.server.common.data.page.TextPageLink;
+import org.thingsboard.server.common.data.rule.RuleChain;
+import org.thingsboard.server.dao.DaoUtil;
+import org.thingsboard.server.dao.model.nosql.RuleChainEntity;
+import org.thingsboard.server.dao.nosql.CassandraAbstractSearchTextDao;
+import org.thingsboard.server.dao.util.NoSqlDao;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.UUID;
+
+import static com.datastax.driver.core.querybuilder.QueryBuilder.eq;
+import static org.thingsboard.server.dao.model.ModelConstants.RULE_CHAIN_BY_TENANT_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME;
+import static org.thingsboard.server.dao.model.ModelConstants.RULE_CHAIN_COLUMN_FAMILY_NAME;
+import static org.thingsboard.server.dao.model.ModelConstants.RULE_CHAIN_TENANT_ID_PROPERTY;
+
+@Component
+@Slf4j
+@NoSqlDao
+public class CassandraRuleChainDao extends CassandraAbstractSearchTextDao<RuleChainEntity, RuleChain> implements RuleChainDao {
+
+    @Override
+    protected Class<RuleChainEntity> getColumnFamilyClass() {
+        return RuleChainEntity.class;
+    }
+
+    @Override
+    protected String getColumnFamilyName() {
+        return RULE_CHAIN_COLUMN_FAMILY_NAME;
+    }
+
+    @Override
+    public List<RuleChain> findRuleChainsByTenantId(UUID tenantId, TextPageLink pageLink) {
+        log.debug("Try to find rule chains by tenantId [{}] and pageLink [{}]", tenantId, pageLink);
+        List<RuleChainEntity> ruleChainEntities = findPageWithTextSearch(RULE_CHAIN_BY_TENANT_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME,
+                Collections.singletonList(eq(RULE_CHAIN_TENANT_ID_PROPERTY, tenantId)),
+                pageLink);
+
+        log.trace("Found rule chains [{}] by tenantId [{}] and pageLink [{}]", ruleChainEntities, tenantId, pageLink);
+        return DaoUtil.convertDataList(ruleChainEntities);
+    }
+
+}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/TimePaginatedRemover.java b/dao/src/main/java/org/thingsboard/server/dao/service/TimePaginatedRemover.java
index c8bb38d..52fcf85 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/service/TimePaginatedRemover.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/service/TimePaginatedRemover.java
@@ -18,7 +18,6 @@ package org.thingsboard.server.dao.service;
 import org.thingsboard.server.common.data.id.IdBased;
 import org.thingsboard.server.common.data.page.TimePageLink;
 
-import java.sql.Time;
 import java.util.List;
 import java.util.UUID;
 
diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/Validator.java b/dao/src/main/java/org/thingsboard/server/dao/service/Validator.java
index c8af7cb..1947f4a 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/service/Validator.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/service/Validator.java
@@ -18,7 +18,6 @@ package org.thingsboard.server.dao.service;
 import org.thingsboard.server.common.data.id.EntityId;
 import org.thingsboard.server.common.data.id.UUIDBased;
 import org.thingsboard.server.common.data.page.BasePageLink;
-import org.thingsboard.server.common.data.page.TextPageLink;
 import org.thingsboard.server.dao.exception.IncorrectParameterException;
 
 import java.util.List;
diff --git a/dao/src/main/java/org/thingsboard/server/dao/settings/CassandraAdminSettingsDao.java b/dao/src/main/java/org/thingsboard/server/dao/settings/CassandraAdminSettingsDao.java
index 6cd3719..ebc66d7 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/settings/CassandraAdminSettingsDao.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/settings/CassandraAdminSettingsDao.java
@@ -26,7 +26,9 @@ import org.thingsboard.server.dao.util.NoSqlDao;
 
 import static com.datastax.driver.core.querybuilder.QueryBuilder.eq;
 import static com.datastax.driver.core.querybuilder.QueryBuilder.select;
-import static org.thingsboard.server.dao.model.ModelConstants.*;
+import static org.thingsboard.server.dao.model.ModelConstants.ADMIN_SETTINGS_BY_KEY_COLUMN_FAMILY_NAME;
+import static org.thingsboard.server.dao.model.ModelConstants.ADMIN_SETTINGS_COLUMN_FAMILY_NAME;
+import static org.thingsboard.server.dao.model.ModelConstants.ADMIN_SETTINGS_KEY_PROPERTY;
 
 @Component
 @Slf4j
diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmDao.java
index 0d64d5c..ff6e213 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmDao.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmDao.java
@@ -15,8 +15,6 @@
  */
 package org.thingsboard.server.dao.sql.alarm;
 
-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;
@@ -102,12 +100,12 @@ public class JpaAlarmDao extends JpaAbstractDao<AlarmEntity, Alarm> implements A
         }
         String relationType = BaseAlarmService.ALARM_RELATION_PREFIX + searchStatusName;
         ListenableFuture<List<EntityRelation>> relations = relationDao.findRelations(affectedEntity, relationType, RelationTypeGroup.ALARM, EntityType.ALARM, query.getPageLink());
-        return Futures.transform(relations, (AsyncFunction<List<EntityRelation>, List<AlarmInfo>>) input -> {
+        return Futures.transformAsync(relations, input -> {
             List<ListenableFuture<AlarmInfo>> alarmFutures = new ArrayList<>(input.size());
             for (EntityRelation relation : input) {
                 alarmFutures.add(Futures.transform(
                         findAlarmByIdAsync(relation.getTo().getId()),
-                        (Function<Alarm, AlarmInfo>) AlarmInfo::new));
+                        AlarmInfo::new));
             }
             return Futures.successfulAsList(alarmFutures);
         });
diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/asset/AssetRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/asset/AssetRepository.java
index bf1e289..7887ea8 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/sql/asset/AssetRepository.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/sql/asset/AssetRepository.java
@@ -19,8 +19,6 @@ import org.springframework.data.domain.Pageable;
 import org.springframework.data.jpa.repository.Query;
 import org.springframework.data.repository.CrudRepository;
 import org.springframework.data.repository.query.Param;
-import org.thingsboard.server.common.data.EntitySubtype;
-import org.thingsboard.server.common.data.EntityType;
 import org.thingsboard.server.dao.model.sql.AssetEntity;
 import org.thingsboard.server.dao.util.SqlDao;
 
diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/asset/JpaAssetDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/asset/JpaAssetDao.java
index 9f8f673..fd5817c 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/sql/asset/JpaAssetDao.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/sql/asset/JpaAssetDao.java
@@ -31,7 +31,12 @@ import org.thingsboard.server.dao.model.sql.AssetEntity;
 import org.thingsboard.server.dao.sql.JpaAbstractSearchTextDao;
 import org.thingsboard.server.dao.util.SqlDao;
 
-import java.util.*;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.UUID;
 
 import static org.thingsboard.server.common.data.UUIDConverter.fromTimeUUID;
 import static org.thingsboard.server.common.data.UUIDConverter.fromTimeUUIDs;
diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/component/ComponentDescriptorRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/component/ComponentDescriptorRepository.java
index 6a7d119..4c0ff69 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/sql/component/ComponentDescriptorRepository.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/sql/component/ComponentDescriptorRepository.java
@@ -25,7 +25,6 @@ import org.thingsboard.server.dao.model.sql.ComponentDescriptorEntity;
 import org.thingsboard.server.dao.util.SqlDao;
 
 import java.util.List;
-import java.util.UUID;
 
 /**
  * Created by Valerii Sosliuk on 5/6/2017.
diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/dashboard/DashboardInfoRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/dashboard/DashboardInfoRepository.java
index f481387..ee55038 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/sql/dashboard/DashboardInfoRepository.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/sql/dashboard/DashboardInfoRepository.java
@@ -23,7 +23,6 @@ import org.thingsboard.server.dao.model.sql.DashboardInfoEntity;
 import org.thingsboard.server.dao.util.SqlDao;
 
 import java.util.List;
-import java.util.UUID;
 
 /**
  * Created by Valerii Sosliuk on 5/6/2017.
diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/dashboard/JpaDashboardInfoDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/dashboard/JpaDashboardInfoDao.java
index 4d8d0b2..7f40741 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/sql/dashboard/JpaDashboardInfoDao.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/sql/dashboard/JpaDashboardInfoDao.java
@@ -15,7 +15,6 @@
  */
 package org.thingsboard.server.dao.sql.dashboard;
 
-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;
@@ -38,7 +37,6 @@ import org.thingsboard.server.dao.relation.RelationDao;
 import org.thingsboard.server.dao.sql.JpaAbstractSearchTextDao;
 import org.thingsboard.server.dao.util.SqlDao;
 
-import java.sql.Time;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Objects;
@@ -86,7 +84,7 @@ public class JpaDashboardInfoDao extends JpaAbstractSearchTextDao<DashboardInfoE
 
         ListenableFuture<List<EntityRelation>> relations = relationDao.findRelations(new CustomerId(customerId), EntityRelation.CONTAINS_TYPE, RelationTypeGroup.DASHBOARD, EntityType.DASHBOARD, pageLink);
 
-        return Futures.transform(relations, (AsyncFunction<List<EntityRelation>, List<DashboardInfo>>) input -> {
+        return Futures.transformAsync(relations, input -> {
             List<ListenableFuture<DashboardInfo>> dashboardFutures = new ArrayList<>(input.size());
             for (EntityRelation relation : input) {
                 dashboardFutures.add(findByIdAsync(relation.getTo().getId()));
diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/device/JpaDeviceDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/device/JpaDeviceDao.java
index 4f3cd7d..e464c65 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/sql/device/JpaDeviceDao.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/sql/device/JpaDeviceDao.java
@@ -32,7 +32,12 @@ import org.thingsboard.server.dao.model.sql.DeviceEntity;
 import org.thingsboard.server.dao.sql.JpaAbstractSearchTextDao;
 import org.thingsboard.server.dao.util.SqlDao;
 
-import java.util.*;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.UUID;
 
 import static org.thingsboard.server.common.data.UUIDConverter.fromTimeUUID;
 import static org.thingsboard.server.common.data.UUIDConverter.fromTimeUUIDs;
diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/event/EventRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/event/EventRepository.java
index 89cd944..07fb5a0 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/sql/event/EventRepository.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/sql/event/EventRepository.java
@@ -15,12 +15,17 @@
  */
 package org.thingsboard.server.dao.sql.event;
 
+import org.springframework.data.domain.Pageable;
 import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
+import org.springframework.data.jpa.repository.Query;
 import org.springframework.data.repository.CrudRepository;
+import org.springframework.data.repository.query.Param;
 import org.thingsboard.server.common.data.EntityType;
 import org.thingsboard.server.dao.model.sql.EventEntity;
 import org.thingsboard.server.dao.util.SqlDao;
 
+import java.util.List;
+
 /**
  * Created by Valerii Sosliuk on 5/3/2017.
  */
@@ -36,4 +41,14 @@ public interface EventRepository extends CrudRepository<EventEntity, String>, Jp
     EventEntity findByTenantIdAndEntityTypeAndEntityId(String tenantId,
                                                        EntityType entityType,
                                                        String entityId);
+
+    @Query("SELECT e FROM EventEntity e WHERE e.tenantId = :tenantId AND e.entityType = :entityType " +
+            "AND e.entityId = :entityId AND e.eventType = :eventType ORDER BY e.eventType DESC, e.id DESC")
+    List<EventEntity> findLatestByTenantIdAndEntityTypeAndEntityIdAndEventType(
+                                                    @Param("tenantId") String tenantId,
+                                                    @Param("entityType") EntityType entityType,
+                                                    @Param("entityId") String entityId,
+                                                    @Param("eventType") String eventType,
+                                                    Pageable pageable);
+
 }
diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/event/JpaBaseEventDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/event/JpaBaseEventDao.java
index 9b22773..5a63ed8 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/sql/event/JpaBaseEventDao.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/sql/event/JpaBaseEventDao.java
@@ -16,6 +16,7 @@
 package org.thingsboard.server.dao.sql.event;
 
 import com.datastax.driver.core.utils.UUIDs;
+import com.google.common.util.concurrent.ListenableFuture;
 import lombok.extern.slf4j.Slf4j;
 import org.apache.commons.lang3.StringUtils;
 import org.springframework.beans.factory.annotation.Autowired;
@@ -36,10 +37,7 @@ import org.thingsboard.server.dao.model.sql.EventEntity;
 import org.thingsboard.server.dao.sql.JpaAbstractSearchTimeDao;
 import org.thingsboard.server.dao.util.SqlDao;
 
-import javax.persistence.criteria.CriteriaBuilder;
-import javax.persistence.criteria.CriteriaQuery;
 import javax.persistence.criteria.Predicate;
-import javax.persistence.criteria.Root;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Optional;
@@ -85,6 +83,18 @@ public class JpaBaseEventDao extends JpaAbstractSearchTimeDao<EventEntity, Event
     }
 
     @Override
+    public ListenableFuture<Event> saveAsync(Event event) {
+        log.debug("Save event [{}] ", event);
+        if (event.getId() == null) {
+            event.setId(new EventId(UUIDs.timeBased()));
+        }
+        if (StringUtils.isEmpty(event.getUid())) {
+            event.setUid(event.getId().toString());
+        }
+        return service.submit(() -> save(new EventEntity(event), false).orElse(null));
+    }
+
+    @Override
     public Optional<Event> saveIfNotExists(Event event) {
         return save(new EventEntity(event), true);
     }
@@ -92,7 +102,7 @@ public class JpaBaseEventDao extends JpaAbstractSearchTimeDao<EventEntity, Event
     @Override
     public Event findEvent(UUID tenantId, EntityId entityId, String eventType, String eventUid) {
         return DaoUtil.getData(eventRepository.findByTenantIdAndEntityTypeAndEntityIdAndEventTypeAndEventUid(
-                UUIDConverter.fromTimeUUID(tenantId), entityId.getEntityType(),  UUIDConverter.fromTimeUUID(entityId.getId()), eventType, eventUid));
+                UUIDConverter.fromTimeUUID(tenantId), entityId.getEntityType(), UUIDConverter.fromTimeUUID(entityId.getId()), eventType, eventUid));
     }
 
     @Override
@@ -109,6 +119,17 @@ public class JpaBaseEventDao extends JpaAbstractSearchTimeDao<EventEntity, Event
         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) {
         log.debug("Save event [{}] ", entity);
         if (entity.getTenantId() == null) {
diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/relation/JpaRelationDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/relation/JpaRelationDao.java
index 6776f56..a8de677 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/sql/relation/JpaRelationDao.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/sql/relation/JpaRelationDao.java
@@ -21,7 +21,6 @@ import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.data.domain.PageRequest;
 import org.springframework.data.domain.Pageable;
 import org.springframework.data.domain.Sort;
-import org.springframework.data.domain.Sort.Order;
 import org.springframework.data.jpa.domain.Specification;
 import org.springframework.stereotype.Component;
 import org.thingsboard.server.common.data.EntityType;
@@ -38,14 +37,10 @@ import org.thingsboard.server.dao.sql.JpaAbstractDaoListeningExecutorService;
 import org.thingsboard.server.dao.sql.JpaAbstractSearchTimeDao;
 import org.thingsboard.server.dao.util.SqlDao;
 
-import javax.persistence.criteria.CriteriaBuilder;
-import javax.persistence.criteria.CriteriaQuery;
 import javax.persistence.criteria.Predicate;
-import javax.persistence.criteria.Root;
 import java.util.ArrayList;
 import java.util.List;
 
-import static org.springframework.data.domain.Sort.Direction.ASC;
 import static org.springframework.data.jpa.domain.Specifications.where;
 import static org.thingsboard.server.common.data.UUIDConverter.fromTimeUUID;
 
@@ -132,39 +127,35 @@ public class JpaRelationDao extends JpaAbstractDaoListeningExecutorService imple
     @Override
     public boolean deleteRelation(EntityRelation relation) {
         RelationCompositeKey key = new RelationCompositeKey(relation);
-        boolean relationExistsBeforeDelete = relationRepository.exists(key);
-        relationRepository.delete(key);
-        return relationExistsBeforeDelete;
+        return deleteRelationIfExists(key);
     }
 
     @Override
     public ListenableFuture<Boolean> deleteRelationAsync(EntityRelation relation) {
         RelationCompositeKey key = new RelationCompositeKey(relation);
         return service.submit(
-                () -> {
-                    boolean relationExistsBeforeDelete = relationRepository.exists(key);
-                    relationRepository.delete(key);
-                    return relationExistsBeforeDelete;
-                });
+                () -> deleteRelationIfExists(key));
     }
 
     @Override
     public boolean deleteRelation(EntityId from, EntityId to, String relationType, RelationTypeGroup typeGroup) {
         RelationCompositeKey key = getRelationCompositeKey(from, to, relationType, typeGroup);
-        boolean relationExistsBeforeDelete = relationRepository.exists(key);
-        relationRepository.delete(key);
-        return relationExistsBeforeDelete;
+        return deleteRelationIfExists(key);
     }
 
     @Override
     public ListenableFuture<Boolean> deleteRelationAsync(EntityId from, EntityId to, String relationType, RelationTypeGroup typeGroup) {
         RelationCompositeKey key = getRelationCompositeKey(from, to, relationType, typeGroup);
         return service.submit(
-                () -> {
-                    boolean relationExistsBeforeDelete = relationRepository.exists(key);
-                    relationRepository.delete(key);
-                    return relationExistsBeforeDelete;
-                });
+                () -> deleteRelationIfExists(key));
+    }
+
+    private boolean deleteRelationIfExists(RelationCompositeKey key) {
+        boolean relationExistsBeforeDelete = relationRepository.exists(key);
+        if (relationExistsBeforeDelete) {
+            relationRepository.delete(key);
+        }
+        return relationExistsBeforeDelete;
     }
 
     @Override
@@ -172,7 +163,9 @@ public class JpaRelationDao extends JpaAbstractDaoListeningExecutorService imple
         boolean relationExistsBeforeDelete = relationRepository
                 .findAllByFromIdAndFromType(UUIDConverter.fromTimeUUID(entity.getId()), entity.getEntityType().name())
                 .size() > 0;
-        relationRepository.deleteByFromIdAndFromType(UUIDConverter.fromTimeUUID(entity.getId()), entity.getEntityType().name());
+        if (relationExistsBeforeDelete) {
+            relationRepository.deleteByFromIdAndFromType(UUIDConverter.fromTimeUUID(entity.getId()), entity.getEntityType().name());
+        }
         return relationExistsBeforeDelete;
     }
 
@@ -183,7 +176,9 @@ public class JpaRelationDao extends JpaAbstractDaoListeningExecutorService imple
                     boolean relationExistsBeforeDelete = relationRepository
                             .findAllByFromIdAndFromType(UUIDConverter.fromTimeUUID(entity.getId()), entity.getEntityType().name())
                             .size() > 0;
-                    relationRepository.deleteByFromIdAndFromType(UUIDConverter.fromTimeUUID(entity.getId()), entity.getEntityType().name());
+                    if (relationExistsBeforeDelete) {
+                        relationRepository.deleteByFromIdAndFromType(UUIDConverter.fromTimeUUID(entity.getId()), entity.getEntityType().name());
+                    }
                     return relationExistsBeforeDelete;
                 });
     }
diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/rule/JpaRuleChainDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/rule/JpaRuleChainDao.java
new file mode 100644
index 0000000..fb5d20a
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/sql/rule/JpaRuleChainDao.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.server.dao.sql.rule;
+
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.data.domain.PageRequest;
+import org.springframework.data.repository.CrudRepository;
+import org.springframework.stereotype.Component;
+import org.thingsboard.server.common.data.UUIDConverter;
+import org.thingsboard.server.common.data.page.TextPageLink;
+import org.thingsboard.server.common.data.rule.RuleChain;
+import org.thingsboard.server.dao.DaoUtil;
+import org.thingsboard.server.dao.model.sql.RuleChainEntity;
+import org.thingsboard.server.dao.rule.RuleChainDao;
+import org.thingsboard.server.dao.sql.JpaAbstractSearchTextDao;
+import org.thingsboard.server.dao.util.SqlDao;
+
+import java.util.List;
+import java.util.Objects;
+import java.util.UUID;
+
+import static org.thingsboard.server.dao.model.ModelConstants.NULL_UUID_STR;
+
+@Slf4j
+@Component
+@SqlDao
+public class JpaRuleChainDao extends JpaAbstractSearchTextDao<RuleChainEntity, RuleChain> implements RuleChainDao {
+
+    @Autowired
+    private RuleChainRepository ruleChainRepository;
+
+    @Override
+    protected Class getEntityClass() {
+        return RuleChainEntity.class;
+    }
+
+    @Override
+    protected CrudRepository getCrudRepository() {
+        return ruleChainRepository;
+    }
+
+    @Override
+    public List<RuleChain> findRuleChainsByTenantId(UUID tenantId, TextPageLink pageLink) {
+        return DaoUtil.convertDataList(ruleChainRepository
+                .findByTenantId(
+                        UUIDConverter.fromTimeUUID(tenantId),
+                        Objects.toString(pageLink.getTextSearch(), ""),
+                        pageLink.getIdOffset() == null ? NULL_UUID_STR : UUIDConverter.fromTimeUUID(pageLink.getIdOffset()),
+                        new PageRequest(0, pageLink.getLimit())));
+    }
+
+}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/rule/JpaRuleNodeDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/rule/JpaRuleNodeDao.java
new file mode 100644
index 0000000..eec2894
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/sql/rule/JpaRuleNodeDao.java
@@ -0,0 +1,46 @@
+/**
+ * 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.server.dao.sql.rule;
+
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.data.repository.CrudRepository;
+import org.springframework.stereotype.Component;
+import org.thingsboard.server.common.data.rule.RuleNode;
+import org.thingsboard.server.dao.model.sql.RuleNodeEntity;
+import org.thingsboard.server.dao.rule.RuleNodeDao;
+import org.thingsboard.server.dao.sql.JpaAbstractSearchTextDao;
+import org.thingsboard.server.dao.util.SqlDao;
+
+@Slf4j
+@Component
+@SqlDao
+public class JpaRuleNodeDao extends JpaAbstractSearchTextDao<RuleNodeEntity, RuleNode> implements RuleNodeDao {
+
+    @Autowired
+    private RuleNodeRepository ruleNodeRepository;
+
+    @Override
+    protected Class getEntityClass() {
+        return RuleNodeEntity.class;
+    }
+
+    @Override
+    protected CrudRepository getCrudRepository() {
+        return ruleNodeRepository;
+    }
+
+}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/timeseries/JpaTimeseriesDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/timeseries/JpaTimeseriesDao.java
index 6350352..e5f145f 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/sql/timeseries/JpaTimeseriesDao.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/sql/timeseries/JpaTimeseriesDao.java
@@ -17,7 +17,11 @@ package org.thingsboard.server.dao.sql.timeseries;
 
 import com.google.common.base.Function;
 import com.google.common.collect.Lists;
-import com.google.common.util.concurrent.*;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.ListeningExecutorService;
+import com.google.common.util.concurrent.MoreExecutors;
+import com.google.common.util.concurrent.SettableFuture;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.beans.factory.annotation.Value;
@@ -25,7 +29,11 @@ import org.springframework.data.domain.PageRequest;
 import org.springframework.stereotype.Component;
 import org.thingsboard.server.common.data.UUIDConverter;
 import org.thingsboard.server.common.data.id.EntityId;
-import org.thingsboard.server.common.data.kv.*;
+import org.thingsboard.server.common.data.kv.Aggregation;
+import org.thingsboard.server.common.data.kv.BasicTsKvEntry;
+import org.thingsboard.server.common.data.kv.StringDataEntry;
+import org.thingsboard.server.common.data.kv.TsKvEntry;
+import org.thingsboard.server.common.data.kv.TsKvQuery;
 import org.thingsboard.server.dao.DaoUtil;
 import org.thingsboard.server.dao.model.sql.TsKvEntity;
 import org.thingsboard.server.dao.model.sql.TsKvLatestCompositeKey;
@@ -42,7 +50,6 @@ import java.util.ArrayList;
 import java.util.List;
 import java.util.Optional;
 import java.util.concurrent.CompletableFuture;
-import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
 import java.util.stream.Collectors;
 
diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/timeseries/TsKvLatestRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/timeseries/TsKvLatestRepository.java
index c5e8861..7171cef 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/sql/timeseries/TsKvLatestRepository.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/sql/timeseries/TsKvLatestRepository.java
@@ -22,7 +22,6 @@ import org.thingsboard.server.dao.model.sql.TsKvLatestEntity;
 import org.thingsboard.server.dao.util.SqlDao;
 
 import java.util.List;
-import java.util.UUID;
 
 @SqlDao
 public interface TsKvLatestRepository extends CrudRepository<TsKvLatestEntity, TsKvLatestCompositeKey> {
diff --git a/dao/src/main/java/org/thingsboard/server/dao/tenant/CassandraTenantDao.java b/dao/src/main/java/org/thingsboard/server/dao/tenant/CassandraTenantDao.java
index fc44403..1c3a779 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/tenant/CassandraTenantDao.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/tenant/CassandraTenantDao.java
@@ -28,7 +28,9 @@ import java.util.Arrays;
 import java.util.List;
 
 import static com.datastax.driver.core.querybuilder.QueryBuilder.eq;
-import static org.thingsboard.server.dao.model.ModelConstants.*;
+import static org.thingsboard.server.dao.model.ModelConstants.TENANT_BY_REGION_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME;
+import static org.thingsboard.server.dao.model.ModelConstants.TENANT_COLUMN_FAMILY_NAME;
+import static org.thingsboard.server.dao.model.ModelConstants.TENANT_REGION_PROPERTY;
 
 @Component
 @Slf4j
diff --git a/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java
index 24abe52..d92c941 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java
@@ -30,8 +30,7 @@ import org.thingsboard.server.dao.dashboard.DashboardService;
 import org.thingsboard.server.dao.device.DeviceService;
 import org.thingsboard.server.dao.entity.AbstractEntityService;
 import org.thingsboard.server.dao.exception.DataValidationException;
-import org.thingsboard.server.dao.plugin.PluginService;
-import org.thingsboard.server.dao.rule.RuleService;
+import org.thingsboard.server.dao.rule.RuleChainService;
 import org.thingsboard.server.dao.service.DataValidator;
 import org.thingsboard.server.dao.service.PaginatedRemover;
 import org.thingsboard.server.dao.service.Validator;
@@ -71,10 +70,7 @@ public class TenantServiceImpl extends AbstractEntityService implements TenantSe
     private DashboardService dashboardService;
 
     @Autowired
-    private RuleService ruleService;
-
-    @Autowired
-    private PluginService pluginService;
+    private RuleChainService ruleChainService;
 
     @Override
     public Tenant findTenantById(TenantId tenantId) {
@@ -108,8 +104,7 @@ public class TenantServiceImpl extends AbstractEntityService implements TenantSe
         assetService.deleteAssetsByTenantId(tenantId);
         deviceService.deleteDevicesByTenantId(tenantId);
         userService.deleteTenantAdmins(tenantId);
-        ruleService.deleteRulesByTenantId(tenantId);
-        pluginService.deletePluginsByTenantId(tenantId);
+        ruleChainService.deleteRuleChainsByTenantId(tenantId);
         tenantDao.removeById(tenantId.getId());
         deleteEntityRelations(tenantId);
     }
diff --git a/dao/src/main/java/org/thingsboard/server/dao/timeseries/AggregatePartitionsFunction.java b/dao/src/main/java/org/thingsboard/server/dao/timeseries/AggregatePartitionsFunction.java
index 99a2bf1..ac5ee64 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/timeseries/AggregatePartitionsFunction.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/timeseries/AggregatePartitionsFunction.java
@@ -18,7 +18,14 @@ package org.thingsboard.server.dao.timeseries;
 import com.datastax.driver.core.ResultSet;
 import com.datastax.driver.core.Row;
 import lombok.extern.slf4j.Slf4j;
-import org.thingsboard.server.common.data.kv.*;
+import org.thingsboard.server.common.data.kv.Aggregation;
+import org.thingsboard.server.common.data.kv.BasicTsKvEntry;
+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.LongDataEntry;
+import org.thingsboard.server.common.data.kv.StringDataEntry;
+import org.thingsboard.server.common.data.kv.TsKvEntry;
 
 import javax.annotation.Nullable;
 import java.util.List;
diff --git a/dao/src/main/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesDao.java b/dao/src/main/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesDao.java
index cda4b16..7aa317c 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesDao.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesDao.java
@@ -15,7 +15,11 @@
  */
 package org.thingsboard.server.dao.timeseries;
 
-import com.datastax.driver.core.*;
+import com.datastax.driver.core.BoundStatement;
+import com.datastax.driver.core.PreparedStatement;
+import com.datastax.driver.core.ResultSet;
+import com.datastax.driver.core.ResultSetFuture;
+import com.datastax.driver.core.Row;
 import com.datastax.driver.core.querybuilder.QueryBuilder;
 import com.datastax.driver.core.querybuilder.Select;
 import com.google.common.base.Function;
@@ -29,8 +33,17 @@ import org.springframework.beans.factory.annotation.Value;
 import org.springframework.core.env.Environment;
 import org.springframework.stereotype.Component;
 import org.thingsboard.server.common.data.id.EntityId;
-import org.thingsboard.server.common.data.kv.*;
+import org.thingsboard.server.common.data.kv.Aggregation;
+import org.thingsboard.server.common.data.kv.BaseTsKvQuery;
+import org.thingsboard.server.common.data.kv.BasicTsKvEntry;
+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.KvEntry;
+import org.thingsboard.server.common.data.kv.LongDataEntry;
+import org.thingsboard.server.common.data.kv.StringDataEntry;
+import org.thingsboard.server.common.data.kv.TsKvEntry;
+import org.thingsboard.server.common.data.kv.TsKvQuery;
 import org.thingsboard.server.dao.model.ModelConstants;
 import org.thingsboard.server.dao.nosql.CassandraAbstractAsyncDao;
 import org.thingsboard.server.dao.util.NoSqlDao;
@@ -69,6 +82,9 @@ public class CassandraBaseTimeseriesDao extends CassandraAbstractAsyncDao implem
     @Value("${cassandra.query.ts_key_value_partitioning}")
     private String partitioning;
 
+    @Value("${cassandra.query.ts_key_value_ttl}")
+    private long systemTtl;
+
     private TsPartitionDate tsFormat;
 
     private PreparedStatement partitionInsertStmt;
@@ -217,7 +233,7 @@ public class CassandraBaseTimeseriesDao extends CassandraAbstractAsyncDao implem
 
         ListenableFuture<List<Long>> partitionsListFuture = Futures.transform(partitionsFuture, getPartitionsArrayFunction(), readResultsProcessingExecutor);
 
-        ListenableFuture<List<ResultSet>> aggregationChunks = Futures.transform(partitionsListFuture,
+        ListenableFuture<List<ResultSet>> aggregationChunks = Futures.transformAsync(partitionsListFuture,
                 getFetchChunksAsyncFunction(entityId, key, aggregation, startTs, endTs), readResultsProcessingExecutor);
 
         return Futures.transform(aggregationChunks, new AggregatePartitionsFunction(aggregation, key, ts), readResultsProcessingExecutor);
@@ -274,6 +290,7 @@ public class CassandraBaseTimeseriesDao extends CassandraAbstractAsyncDao implem
 
     @Override
     public ListenableFuture<Void> save(EntityId entityId, TsKvEntry tsKvEntry, long ttl) {
+        ttl = computeTtl(ttl);
         long partition = toPartitionTs(tsKvEntry.getTs());
         DataType type = tsKvEntry.getDataType();
         BoundStatement stmt = (ttl == 0 ? getSaveStmt(type) : getSaveTtlStmt(type)).bind();
@@ -291,6 +308,7 @@ public class CassandraBaseTimeseriesDao extends CassandraAbstractAsyncDao implem
 
     @Override
     public ListenableFuture<Void> savePartition(EntityId entityId, long tsKvEntryTs, String key, long ttl) {
+        ttl = computeTtl(ttl);
         long partition = toPartitionTs(tsKvEntryTs);
         log.debug("Saving partition {} for the entity [{}-{}] and key {}", partition, entityId.getEntityType(), entityId.getId(), key);
         BoundStatement stmt = (ttl == 0 ? getPartitionInsertStmt() : getPartitionInsertTtlStmt()).bind();
@@ -304,6 +322,17 @@ public class CassandraBaseTimeseriesDao extends CassandraAbstractAsyncDao implem
         return getFuture(executeAsyncWrite(stmt), rs -> null);
     }
 
+    private long computeTtl(long ttl) {
+        if (systemTtl > 0) {
+            if (ttl == 0) {
+                ttl = systemTtl;
+            } else {
+                ttl = Math.min(systemTtl, ttl);
+            }
+        }
+        return ttl;
+    }
+
     @Override
     public ListenableFuture<Void> saveLatest(EntityId entityId, TsKvEntry tsKvEntry) {
         BoundStatement stmt = getLatestStmt().bind()
diff --git a/dao/src/main/java/org/thingsboard/server/dao/user/UserServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/user/UserServiceImpl.java
index 350bf4f..e1bd4e0 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/user/UserServiceImpl.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/user/UserServiceImpl.java
@@ -42,7 +42,9 @@ import org.thingsboard.server.dao.tenant.TenantDao;
 
 import java.util.List;
 
-import static org.thingsboard.server.dao.service.Validator.*;
+import static org.thingsboard.server.dao.service.Validator.validateId;
+import static org.thingsboard.server.dao.service.Validator.validatePageLink;
+import static org.thingsboard.server.dao.service.Validator.validateString;
 
 @Service
 @Slf4j
diff --git a/dao/src/main/java/org/thingsboard/server/dao/util/BufferedRateLimiter.java b/dao/src/main/java/org/thingsboard/server/dao/util/BufferedRateLimiter.java
index 03eb46f..817845b 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/util/BufferedRateLimiter.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/util/BufferedRateLimiter.java
@@ -25,7 +25,11 @@ import org.springframework.scheduling.annotation.Scheduled;
 import org.springframework.stereotype.Component;
 import org.thingsboard.server.dao.exception.BufferLimitException;
 
-import java.util.concurrent.*;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.Executors;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicInteger;
 
 @Component
@@ -125,6 +129,9 @@ public class BufferedRateLimiter implements AsyncRateLimiter {
                 if(permits.get() < permitsLimit) {
                     reprocessQueue();
                 }
+                if(permits.get() < permitsLimit) {
+                    reprocessQueue();
+                }
                 return lockedFuture.future;
             } catch (InterruptedException e) {
                 return Futures.immediateFailedFuture(new BufferLimitException());
diff --git a/dao/src/main/java/org/thingsboard/server/dao/widget/CassandraWidgetsBundleDao.java b/dao/src/main/java/org/thingsboard/server/dao/widget/CassandraWidgetsBundleDao.java
index b5d0e5f..f938a98 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/widget/CassandraWidgetsBundleDao.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/widget/CassandraWidgetsBundleDao.java
@@ -29,8 +29,15 @@ import java.util.Arrays;
 import java.util.List;
 import java.util.UUID;
 
-import static com.datastax.driver.core.querybuilder.QueryBuilder.*;
-import static org.thingsboard.server.dao.model.ModelConstants.*;
+import static com.datastax.driver.core.querybuilder.QueryBuilder.eq;
+import static com.datastax.driver.core.querybuilder.QueryBuilder.in;
+import static com.datastax.driver.core.querybuilder.QueryBuilder.select;
+import static org.thingsboard.server.dao.model.ModelConstants.NULL_UUID;
+import static org.thingsboard.server.dao.model.ModelConstants.WIDGETS_BUNDLE_ALIAS_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.WIDGETS_BUNDLE_BY_TENANT_AND_ALIAS_COLUMN_FAMILY_NAME;
+import static org.thingsboard.server.dao.model.ModelConstants.WIDGETS_BUNDLE_BY_TENANT_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME;
+import static org.thingsboard.server.dao.model.ModelConstants.WIDGETS_BUNDLE_COLUMN_FAMILY_NAME;
+import static org.thingsboard.server.dao.model.ModelConstants.WIDGETS_BUNDLE_TENANT_ID_PROPERTY;
 
 @Component
 @Slf4j
diff --git a/dao/src/main/java/org/thingsboard/server/dao/widget/CassandraWidgetTypeDao.java b/dao/src/main/java/org/thingsboard/server/dao/widget/CassandraWidgetTypeDao.java
index 43049f9..e31809f 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/widget/CassandraWidgetTypeDao.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/widget/CassandraWidgetTypeDao.java
@@ -29,7 +29,11 @@ import java.util.UUID;
 
 import static com.datastax.driver.core.querybuilder.QueryBuilder.eq;
 import static com.datastax.driver.core.querybuilder.QueryBuilder.select;
-import static org.thingsboard.server.dao.model.ModelConstants.*;
+import static org.thingsboard.server.dao.model.ModelConstants.WIDGET_TYPE_ALIAS_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.WIDGET_TYPE_BUNDLE_ALIAS_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.WIDGET_TYPE_BY_TENANT_AND_ALIASES_COLUMN_FAMILY_NAME;
+import static org.thingsboard.server.dao.model.ModelConstants.WIDGET_TYPE_COLUMN_FAMILY_NAME;
+import static org.thingsboard.server.dao.model.ModelConstants.WIDGET_TYPE_TENANT_ID_PROPERTY;
 
 @Component
 @Slf4j
diff --git a/dao/src/main/resources/cassandra/schema.cql b/dao/src/main/resources/cassandra/schema.cql
index fc557a9..f03122a 100644
--- a/dao/src/main/resources/cassandra/schema.cql
+++ b/dao/src/main/resources/cassandra/schema.cql
@@ -428,7 +428,7 @@ CREATE TABLE IF NOT EXISTS thingsboard.attributes_kv_cf (
 
 CREATE TABLE IF NOT EXISTS  thingsboard.component_descriptor (
     id timeuuid,
-    type text, //("FILTER", "PROCESSOR", "ACTION", "PLUGIN")
+    type text,
     scope text,
     name text,
     search_text text,
@@ -459,67 +459,12 @@ CREATE MATERIALIZED VIEW IF NOT EXISTS thingsboard.component_desc_by_id AS
     PRIMARY KEY ( id, clazz, scope, type )
     WITH CLUSTERING ORDER BY ( clazz ASC, scope ASC, type DESC);
 
-CREATE TABLE IF NOT EXISTS  thingsboard.rule (
-    id timeuuid,
-    tenant_id timeuuid,
-    name text,
-    state text,
-    search_text text,
-    weight int,
-    plugin_token text,
-    filters text, // Format: {"clazz":"A", "name": "Filter A", "configuration": {"types":["TELEMETRY"]}}
-    processor text, // Format: {"clazz":"A", "name": "Processor A", "configuration": null}
-    action text, // Format: {"clazz":"A", "name": "Action A", "configuration": null}
-    additional_info text,
-    PRIMARY KEY (id, tenant_id)
-);
-
-CREATE MATERIALIZED VIEW IF NOT EXISTS thingsboard.rule_by_plugin_token AS
-    SELECT *
-
-    FROM thingsboard.rule
-    WHERE tenant_id IS NOT NULL AND id IS NOT NULL AND plugin_token IS NOT NULL
-    PRIMARY KEY (plugin_token, tenant_id, id) WITH CLUSTERING ORDER BY (tenant_id DESC, id DESC);
-
-CREATE MATERIALIZED VIEW IF NOT EXISTS thingsboard.rule_by_tenant_and_search_text AS
-    SELECT *
-    FROM thingsboard.rule
-    WHERE tenant_id IS NOT NULL AND id IS NOT NULL AND search_text IS NOT NULL
-    PRIMARY KEY (tenant_id, search_text, id) WITH CLUSTERING ORDER BY (search_text ASC);
-
-CREATE TABLE IF NOT EXISTS  thingsboard.plugin (
-    id uuid,
-    tenant_id uuid,
-    name text,
-    state text,
-    search_text text,
-    api_token text,
-    plugin_class text,
-    public_access boolean,
-    configuration text,
-    additional_info text,
-    PRIMARY KEY (id, tenant_id)
-);
-
-CREATE MATERIALIZED VIEW IF NOT EXISTS thingsboard.plugin_by_api_token AS
-    SELECT *
-    FROM thingsboard.plugin
-    WHERE api_token IS NOT NULL AND id IS NOT NULL AND tenant_id IS NOT NULL
-    PRIMARY KEY (api_token, id, tenant_id) WITH CLUSTERING ORDER BY (id DESC);
-
-CREATE MATERIALIZED VIEW IF NOT EXISTS thingsboard.plugin_by_tenant_and_search_text AS
-    SELECT *
-    from thingsboard.plugin
-    WHERE tenant_id IS NOT NULL AND search_text IS NOT NULL AND id IS NOT NULL
-    PRIMARY KEY ( tenant_id, search_text, id )
-    WITH CLUSTERING ORDER BY ( search_text ASC, id DESC );
-
 CREATE TABLE IF NOT EXISTS thingsboard.event (
 	tenant_id timeuuid, // tenant or system
 	id timeuuid,
 	event_type text,
 	event_uid text,
-	entity_type text, // (device, customer, rule, plugin)
+	entity_type text,
 	entity_id timeuuid,
 	body text,
 	PRIMARY KEY ((tenant_id, entity_type, entity_id), event_type, event_uid)
@@ -542,7 +487,6 @@ CREATE MATERIALIZED VIEW IF NOT EXISTS thingsboard.event_by_id AS
     PRIMARY KEY ((tenant_id, entity_type, entity_id), id, event_type, event_uid)
     WITH CLUSTERING ORDER BY (id ASC, event_type ASC, event_uid ASC);
 
-
 CREATE TABLE IF NOT EXISTS thingsboard.audit_log_by_entity_id (
   tenant_id timeuuid,
   id timeuuid,
@@ -591,8 +535,6 @@ CREATE TABLE IF NOT EXISTS thingsboard.audit_log_by_user_id (
   PRIMARY KEY ((tenant_id, user_id), id)
 );
 
-
-
 CREATE TABLE IF NOT EXISTS thingsboard.audit_log_by_tenant_id (
   tenant_id timeuuid,
   id timeuuid,
@@ -615,4 +557,84 @@ CREATE TABLE IF NOT EXISTS thingsboard.audit_log_by_tenant_id_partitions (
   partition bigint,
   PRIMARY KEY (( tenant_id ), partition)
 ) WITH CLUSTERING ORDER BY ( partition ASC )
-AND compaction = { 'class' :  'LeveledCompactionStrategy'  };
\ No newline at end of file
+AND compaction = { 'class' :  'LeveledCompactionStrategy'  };
+
+CREATE TABLE IF NOT EXISTS thingsboard.msg_queue (
+    node_id         timeuuid,
+    cluster_partition    bigint,
+    ts_partition       bigint,
+    ts              bigint,
+    msg             blob,
+	PRIMARY KEY ((node_id, cluster_partition, ts_partition), ts))
+WITH CLUSTERING ORDER BY (ts DESC)
+AND compaction = {
+    'class': 'org.apache.cassandra.db.compaction.DateTieredCompactionStrategy',
+    'min_threshold': '5',
+    'base_time_seconds': '43200',
+    'max_window_size_seconds': '43200',
+    'tombstone_threshold': '0.9',
+    'unchecked_tombstone_compaction': 'true'
+};
+
+CREATE TABLE IF NOT EXISTS thingsboard.msg_ack_queue (
+    node_id         timeuuid,
+    cluster_partition    bigint,
+    ts_partition       bigint,
+    msg_id              timeuuid,
+	PRIMARY KEY ((node_id, cluster_partition, ts_partition), msg_id))
+WITH CLUSTERING ORDER BY (msg_id DESC)
+AND compaction = {
+    'class': 'org.apache.cassandra.db.compaction.DateTieredCompactionStrategy',
+    'min_threshold': '5',
+    'base_time_seconds': '43200',
+    'max_window_size_seconds': '43200',
+    'tombstone_threshold': '0.9',
+    'unchecked_tombstone_compaction': 'true'
+};
+
+CREATE TABLE IF NOT EXISTS thingsboard.processed_msg_partitions (
+    node_id         timeuuid,
+    cluster_partition    bigint,
+    ts_partition       bigint,
+	PRIMARY KEY ((node_id, cluster_partition), ts_partition))
+WITH CLUSTERING ORDER BY (ts_partition DESC)
+AND compaction = {
+    'class': 'org.apache.cassandra.db.compaction.DateTieredCompactionStrategy',
+    'min_threshold': '5',
+    'base_time_seconds': '43200',
+    'max_window_size_seconds': '43200',
+    'tombstone_threshold': '0.9',
+    'unchecked_tombstone_compaction': 'true'
+};
+
+CREATE TABLE IF NOT EXISTS  thingsboard.rule_chain (
+    id uuid,
+    tenant_id uuid,
+    name text,
+    search_text text,
+    first_rule_node_id uuid,
+    root boolean,
+    debug_mode boolean,
+    configuration text,
+    additional_info text,
+    PRIMARY KEY (id, tenant_id)
+);
+
+CREATE MATERIALIZED VIEW IF NOT EXISTS thingsboard.rule_chain_by_tenant_and_search_text AS
+    SELECT *
+    from thingsboard.rule_chain
+    WHERE tenant_id IS NOT NULL AND search_text IS NOT NULL AND id IS NOT NULL
+    PRIMARY KEY ( tenant_id, search_text, id )
+    WITH CLUSTERING ORDER BY ( search_text ASC, id DESC );
+
+CREATE TABLE IF NOT EXISTS  thingsboard.rule_node (
+    id uuid,
+    rule_chain_id uuid,
+    type text,
+    name text,
+    debug_mode boolean,
+    search_text text,
+    configuration text,
+    additional_info text,
+    PRIMARY KEY (id)
+);
diff --git a/dao/src/main/resources/cassandra/system-data.cql b/dao/src/main/resources/cassandra/system-data.cql
index 2455ac6..16a53ca 100644
--- a/dao/src/main/resources/cassandra/system-data.cql
+++ b/dao/src/main/resources/cassandra/system-data.cql
@@ -279,28 +279,4 @@ VALUES ( now ( ), minTimeuuid ( 0 ), 'charts', 'basic_timeseries',
 '{"type":"timeseries","sizeX":8,"sizeY":5,"resources":[],"templateHtml":"","templateCss":".legend {\n    font-size: 13px;\n    line-height: 10px;\n}\n\n.legend table { \n    border-spacing: 0px;\n    border-collapse: separate;\n}\n\n.mouse-events .flot-overlay {\n    cursor: crosshair; \n}\n\n","controllerScript":"self.onInit = function() {\n    self.ctx.flot = new TbFlot(self.ctx);    \n}\n\nself.onDataUpdated = function() {\n    self.ctx.flot.update();\n}\n\nself.onResize = function() {\n    self.ctx.flot.resize();\n}\n\nself.onEditModeChanged = function() {\n    self.ctx.flot.checkMouseEvents();\n}\n\nself.onMobileModeChanged = function() {\n    self.ctx.flot.checkMouseEvents();\n}\n\nself.getSettingsSchema = function() {\n    return TbFlot.settingsSchema;\n}\n\nself.getDataKeySettingsSchema = function() {\n    return TbFlot.datakeySettingsSchema(true);\n}\n\nself.onDestroy = function() {\n    self.ctx.flot.destroy();\n}\n","settingsSchema":"{}","dataKeySettingsSchema":"{}","defaultConfig":"{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"First\",\"color\":\"#2196f3\",\"settings\":{\"showLines\":true,\"fillLines\":true,\"showPoints\":false},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Second\",\"color\":\"#ffc107\",\"settings\":{\"showLines\":true,\"fillLines\":false,\"showPoints\":false},\"_hash\":0.12775350966079668,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"shadowSize\":4,\"fontColor\":\"#545454\",\"fontSize\":10,\"xaxis\":{\"showLabels\":true,\"color\":\"#545454\"},\"yaxis\":{\"showLabels\":true,\"color\":\"#545454\"},\"grid\":{\"color\":\"#545454\",\"tickColor\":\"#DDDDDD\",\"verticalLines\":true,\"horizontalLines\":true,\"outlineWidth\":1},\"legend\":{\"show\":true,\"position\":\"nw\",\"backgroundColor\":\"#f0f0f0\",\"backgroundOpacity\":0.85,\"labelBoxBorderColor\":\"rgba(1, 1, 1, 0.45)\"},\"decimals\":1,\"stack\":false,\"tooltipIndividual\":false},\"title\":\"Timeseries - Flot\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"mobileHeight\":null}"}',
 'Timeseries - Flot' );
 
-
-/** System plugins and rules **/
-INSERT INTO thingsboard.plugin ( id, tenant_id, name, state, search_text, api_token, plugin_class, public_access,
-configuration )
-VALUES ( minTimeuuid ( '2016-11-01 01:01:01+0000' ), minTimeuuid ( 0 ), 'System Telemetry Plugin', 'ACTIVE',
-'system telemetry plugin', 'telemetry',
-'org.thingsboard.server.extensions.core.plugin.telemetry.TelemetryStoragePlugin', true, '{}' );
-
-INSERT INTO thingsboard.rule ( id, tenant_id, name, plugin_token, state, search_text, weight, filters, processor,
-action )
-VALUES ( minTimeuuid ( '2016-11-01 01:01:02+0000' ), minTimeuuid ( 0 ), 'System Telemetry Rule', 'telemetry', 'ACTIVE',
-'system telemetry rule', 0,
-'[{"clazz":"org.thingsboard.server.extensions.core.filter.MsgTypeFilter", "name":"TelemetryFilter", "configuration": {"messageTypes":["POST_TELEMETRY","POST_ATTRIBUTES","GET_ATTRIBUTES"]}}]',
-null,
-'{"clazz":"org.thingsboard.server.extensions.core.action.telemetry.TelemetryPluginAction", "name":"TelemetryMsgConverterAction", "configuration":{}}'
-);
-
-INSERT INTO thingsboard.plugin ( id, tenant_id, name, state, search_text, api_token, plugin_class, public_access,
-configuration )
-VALUES ( minTimeuuid ( '2016-11-01 01:01:03+0000' ), minTimeuuid ( 0 ), 'System RPC Plugin', 'ACTIVE',
-'system rpc plugin', 'rpc', 'org.thingsboard.server.extensions.core.plugin.rpc.RpcPlugin', true, '{
-       "defaultTimeout": 20000
-     }' );
-
 /** SYSTEM **/
diff --git a/dao/src/main/resources/sql/schema.sql b/dao/src/main/resources/sql/schema.sql
index 9f03dc8..91e77da 100644
--- a/dao/src/main/resources/sql/schema.sql
+++ b/dao/src/main/resources/sql/schema.sql
@@ -140,19 +140,6 @@ CREATE TABLE IF NOT EXISTS event (
     CONSTRAINT event_unq_key UNIQUE (tenant_id, entity_type, entity_id, event_type, event_uid)
 );
 
-CREATE TABLE IF NOT EXISTS plugin (
-    id varchar(31) NOT NULL CONSTRAINT plugin_pkey PRIMARY KEY,
-    additional_info varchar,
-    api_token varchar(255),
-    plugin_class varchar(255),
-    configuration varchar,
-    name varchar(255),
-    public_access boolean,
-    search_text varchar(255),
-    state varchar(255),
-    tenant_id varchar(31)
-);
-
 CREATE TABLE IF NOT EXISTS relation (
     from_id varchar(31),
     from_type varchar(255),
@@ -164,20 +151,6 @@ CREATE TABLE IF NOT EXISTS relation (
     CONSTRAINT relation_unq_key UNIQUE (from_id, from_type, relation_type_group, relation_type, to_id, to_type)
 );
 
-CREATE TABLE IF NOT EXISTS rule (
-    id varchar(31) NOT NULL CONSTRAINT rule_pkey PRIMARY KEY,
-    action varchar,
-    additional_info varchar,
-    filters varchar,
-    name varchar(255),
-    plugin_token varchar(255),
-    processor varchar,
-    search_text varchar(255),
-    state varchar(255),
-    tenant_id varchar(31),
-    weight integer
-);
-
 CREATE TABLE IF NOT EXISTS tb_user (
     id varchar(31) NOT NULL CONSTRAINT tb_user_pkey PRIMARY KEY,
     additional_info varchar,
@@ -254,4 +227,27 @@ CREATE TABLE IF NOT EXISTS widgets_bundle (
     search_text varchar(255),
     tenant_id varchar(31),
     title varchar(255)
-);
\ No newline at end of file
+);
+
+CREATE TABLE IF NOT EXISTS rule_chain (
+    id varchar(31) NOT NULL CONSTRAINT rule_chain_pkey PRIMARY KEY,
+    additional_info varchar,
+    configuration varchar(10000000),
+    name varchar(255),
+    first_rule_node_id varchar(31),
+    root boolean,
+    debug_mode boolean,
+    search_text varchar(255),
+    tenant_id varchar(31)
+);
+
+CREATE TABLE IF NOT EXISTS rule_node (
+    id varchar(31) NOT NULL CONSTRAINT rule_node_pkey PRIMARY KEY,
+    rule_chain_id varchar(31),
+    additional_info varchar,
+    configuration varchar(10000000),
+    type varchar(255),
+    name varchar(255),
+    debug_mode boolean,
+    search_text varchar(255)
+);
diff --git a/dao/src/main/resources/sql/system-data.sql b/dao/src/main/resources/sql/system-data.sql
index 39fd6bd..d41521a 100644
--- a/dao/src/main/resources/sql/system-data.sql
+++ b/dao/src/main/resources/sql/system-data.sql
@@ -42,23 +42,3 @@ VALUES ( '1e746126eaaefa6a91992ebcb67fe33', 'mail', '{
 	"username": "",
 	"password": ""
 }' );
-
-/** System plugins and rules **/
-INSERT INTO plugin ( id, tenant_id, name, state, search_text, api_token, plugin_class, public_access, configuration )
-VALUES ( '1e7461160cb2da2a91992ebcb67fe33', '1b21dd2138140008080808080808080', 'System Telemetry Plugin', 'ACTIVE',
-         'system telemetry plugin', 'telemetry',
-         'org.thingsboard.server.extensions.core.plugin.telemetry.TelemetryStoragePlugin', true, '{}' );
-
-INSERT INTO rule ( id, tenant_id, name, plugin_token, state, search_text, weight, filters, processor, action )
-VALUES ( '1e7461165abad4ca91992ebcb67fe33', '1b21dd2138140008080808080808080', 'System Telemetry Rule', 'telemetry', 'ACTIVE',
-         'system telemetry rule', 0,
-         '[{"clazz":"org.thingsboard.server.extensions.core.filter.MsgTypeFilter", "name":"TelemetryFilter", "configuration": {"messageTypes":["POST_TELEMETRY","POST_ATTRIBUTES","GET_ATTRIBUTES"]}}]',
-         null,
-         '{"clazz":"org.thingsboard.server.extensions.core.action.telemetry.TelemetryPluginAction", "name":"TelemetryMsgConverterAction", "configuration":{}}'
-);
-
-INSERT INTO plugin ( id, tenant_id, name, state, search_text, api_token, plugin_class, public_access, configuration )
-VALUES ( '1e746116b3b8994a91992ebcb67fe33', '1b21dd2138140008080808080808080', 'System RPC Plugin', 'ACTIVE',
-         'system rpc plugin', 'rpc', 'org.thingsboard.server.extensions.core.plugin.rpc.RpcPlugin', true, '{
-       "defaultTimeout": 20000
-     }' );
diff --git a/dao/src/test/java/org/thingsboard/server/dao/CustomCassandraCQLUnit.java b/dao/src/test/java/org/thingsboard/server/dao/CustomCassandraCQLUnit.java
index d214a70..1dc3faa 100644
--- a/dao/src/test/java/org/thingsboard/server/dao/CustomCassandraCQLUnit.java
+++ b/dao/src/test/java/org/thingsboard/server/dao/CustomCassandraCQLUnit.java
@@ -15,18 +15,17 @@
  */
 package org.thingsboard.server.dao;
 
+import com.datastax.driver.core.Cluster;
+import com.datastax.driver.core.Session;
 import org.cassandraunit.BaseCassandraUnit;
 import org.cassandraunit.CQLDataLoader;
 import org.cassandraunit.dataset.CQLDataSet;
 import org.cassandraunit.utils.EmbeddedCassandraServerHelper;
 
-import com.datastax.driver.core.Cluster;
-import com.datastax.driver.core.Session;
-
 import java.util.List;
 
 public class CustomCassandraCQLUnit extends BaseCassandraUnit {
-    private List<CQLDataSet> dataSets;
+    protected List<CQLDataSet> dataSets;
 
     public Session session;
     public Cluster cluster;
diff --git a/dao/src/test/java/org/thingsboard/server/dao/JpaDbunitTestConfig.java b/dao/src/test/java/org/thingsboard/server/dao/JpaDbunitTestConfig.java
index d83293c..0bf9f19 100644
--- a/dao/src/test/java/org/thingsboard/server/dao/JpaDbunitTestConfig.java
+++ b/dao/src/test/java/org/thingsboard/server/dao/JpaDbunitTestConfig.java
@@ -19,7 +19,6 @@ import com.github.springtestdbunit.bean.DatabaseConfigBean;
 import com.github.springtestdbunit.bean.DatabaseDataSourceConnectionFactoryBean;
 import org.dbunit.DatabaseUnitException;
 import org.dbunit.ext.hsqldb.HsqldbDataTypeFactory;
-import org.dbunit.ext.postgresql.PostgresqlDataTypeFactory;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
diff --git a/dao/src/test/java/org/thingsboard/server/dao/nosql/RateLimitedResultSetFutureTest.java b/dao/src/test/java/org/thingsboard/server/dao/nosql/RateLimitedResultSetFutureTest.java
index f49668d..491409a 100644
--- a/dao/src/test/java/org/thingsboard/server/dao/nosql/RateLimitedResultSetFutureTest.java
+++ b/dao/src/test/java/org/thingsboard/server/dao/nosql/RateLimitedResultSetFutureTest.java
@@ -15,7 +15,12 @@
  */
 package org.thingsboard.server.dao.nosql;
 
-import com.datastax.driver.core.*;
+import com.datastax.driver.core.ProtocolVersion;
+import com.datastax.driver.core.ResultSet;
+import com.datastax.driver.core.ResultSetFuture;
+import com.datastax.driver.core.Row;
+import com.datastax.driver.core.Session;
+import com.datastax.driver.core.Statement;
 import com.datastax.driver.core.exceptions.UnsupportedFeatureException;
 import com.google.common.util.concurrent.Futures;
 import com.google.common.util.concurrent.ListenableFuture;
@@ -29,10 +34,18 @@ import org.mockito.stubbing.Answer;
 import org.thingsboard.server.dao.exception.BufferLimitException;
 import org.thingsboard.server.dao.util.AsyncRateLimiter;
 
-import java.util.concurrent.*;
-
-import static org.junit.Assert.*;
-import static org.mockito.Mockito.*;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeoutException;
+
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.mockito.Mockito.when;
 
 @RunWith(MockitoJUnitRunner.class)
 public class RateLimitedResultSetFutureTest {
diff --git a/dao/src/test/java/org/thingsboard/server/dao/NoSqlDaoServiceTestSuite.java b/dao/src/test/java/org/thingsboard/server/dao/NoSqlDaoServiceTestSuite.java
index 7e25baa..f10462d 100644
--- a/dao/src/test/java/org/thingsboard/server/dao/NoSqlDaoServiceTestSuite.java
+++ b/dao/src/test/java/org/thingsboard/server/dao/NoSqlDaoServiceTestSuite.java
@@ -17,7 +17,6 @@ package org.thingsboard.server.dao;
 
 import org.cassandraunit.dataset.cql.ClassPathCQLDataSet;
 import org.junit.ClassRule;
-import org.junit.Ignore;
 import org.junit.extensions.cpsuite.ClasspathSuite;
 import org.junit.extensions.cpsuite.ClasspathSuite.ClassnameFilters;
 import org.junit.runner.RunWith;
@@ -26,7 +25,9 @@ import java.util.Arrays;
 
 @RunWith(ClasspathSuite.class)
 @ClassnameFilters({
-        "org.thingsboard.server.dao.service.*ServiceNoSqlTest"
+        "org.thingsboard.server.dao.service.*ServiceNoSqlTest",
+        "org.thingsboard.server.dao.service.queue.cassandra.*.*.*Test",
+        "org.thingsboard.server.dao.service.queue.cassandra.*Test"
 })
 public class NoSqlDaoServiceTestSuite {
 
diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/AbstractServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/AbstractServiceTest.java
index ffa9f0e..1e1f15d 100644
--- a/dao/src/test/java/org/thingsboard/server/dao/service/AbstractServiceTest.java
+++ b/dao/src/test/java/org/thingsboard/server/dao/service/AbstractServiceTest.java
@@ -18,8 +18,6 @@ package org.thingsboard.server.dao.service;
 import com.datastax.driver.core.utils.UUIDs;
 import com.fasterxml.jackson.databind.JsonNode;
 import com.fasterxml.jackson.databind.ObjectMapper;
-import com.fasterxml.jackson.databind.node.ObjectNode;
-import com.fasterxml.jackson.databind.node.TextNode;
 import org.junit.runner.RunWith;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.context.annotation.Bean;
@@ -38,8 +36,6 @@ import org.thingsboard.server.common.data.id.UUIDBased;
 import org.thingsboard.server.common.data.plugin.ComponentDescriptor;
 import org.thingsboard.server.common.data.plugin.ComponentScope;
 import org.thingsboard.server.common.data.plugin.ComponentType;
-import org.thingsboard.server.common.data.plugin.PluginMetaData;
-import org.thingsboard.server.common.data.rule.RuleMetaData;
 import org.thingsboard.server.dao.alarm.AlarmService;
 import org.thingsboard.server.dao.asset.AssetService;
 import org.thingsboard.server.dao.audit.AuditLogLevelFilter;
@@ -50,9 +46,8 @@ import org.thingsboard.server.dao.dashboard.DashboardService;
 import org.thingsboard.server.dao.device.DeviceCredentialsService;
 import org.thingsboard.server.dao.device.DeviceService;
 import org.thingsboard.server.dao.event.EventService;
-import org.thingsboard.server.dao.plugin.PluginService;
 import org.thingsboard.server.dao.relation.RelationService;
-import org.thingsboard.server.dao.rule.RuleService;
+import org.thingsboard.server.dao.rule.RuleChainService;
 import org.thingsboard.server.dao.settings.AdminSettingsService;
 import org.thingsboard.server.dao.tenant.TenantService;
 import org.thingsboard.server.dao.timeseries.TimeseriesService;
@@ -64,8 +59,6 @@ import java.io.IOException;
 import java.util.Comparator;
 import java.util.HashMap;
 import java.util.Map;
-import java.util.UUID;
-import java.util.concurrent.ThreadLocalRandom;
 
 
 @RunWith(SpringRunner.class)
@@ -111,12 +104,6 @@ public abstract class AbstractServiceTest {
     protected TimeseriesService tsService;
 
     @Autowired
-    protected PluginService pluginService;
-
-    @Autowired
-    protected RuleService ruleService;
-
-    @Autowired
     protected EventService eventService;
 
     @Autowired
@@ -126,6 +113,9 @@ public abstract class AbstractServiceTest {
     protected AlarmService alarmService;
 
     @Autowired
+    protected RuleChainService ruleChainService;
+
+    @Autowired
     private ComponentDescriptorService componentDescriptorService;
 
     class IdComparator<D extends BaseData<? extends UUIDBased>> implements Comparator<D> {
@@ -149,32 +139,6 @@ public abstract class AbstractServiceTest {
         return event;
     }
 
-    protected PluginMetaData generatePlugin(TenantId tenantId, String token) throws IOException {
-        return generatePlugin(tenantId, token, "org.thingsboard.component.PluginTest", "org.thingsboard.component.ActionTest", "TestJsonDescriptor.json", "TestJsonData.json");
-    }
-
-    protected PluginMetaData generatePlugin(TenantId tenantId, String token, String clazz, String actions, String configurationDescriptorResource, String dataResource) throws IOException {
-        if (tenantId == null) {
-            tenantId = new TenantId(UUIDs.timeBased());
-        }
-        if (token == null) {
-            token = UUID.randomUUID().toString();
-        }
-        getOrCreateDescriptor(ComponentScope.TENANT, ComponentType.PLUGIN, clazz, configurationDescriptorResource, actions);
-        PluginMetaData pluginMetaData = new PluginMetaData();
-        pluginMetaData.setName("Testing");
-        pluginMetaData.setClazz(clazz);
-        pluginMetaData.setTenantId(tenantId);
-        pluginMetaData.setApiToken(token);
-        pluginMetaData.setAdditionalInfo(mapper.readTree("{\"test\":\"test\"}"));
-        try {
-            pluginMetaData.setConfiguration(readFromResource(dataResource));
-        } catch (IOException e) {
-            throw new RuntimeException(e);
-        }
-        return pluginMetaData;
-    }
-
     private ComponentDescriptor getOrCreateDescriptor(ComponentScope scope, ComponentType type, String clazz, String configurationDescriptorResource) throws IOException {
         return getOrCreateDescriptor(scope, type, clazz, configurationDescriptorResource, null);
     }
@@ -198,42 +162,6 @@ public abstract class AbstractServiceTest {
         return mapper.readTree(this.getClass().getClassLoader().getResourceAsStream(resourceName));
     }
 
-    protected RuleMetaData generateRule(TenantId tenantId, Integer weight, String pluginToken) throws IOException {
-        if (tenantId == null) {
-            tenantId = new TenantId(UUIDs.timeBased());
-        }
-        if (weight == null) {
-            weight = ThreadLocalRandom.current().nextInt();
-        }
-
-        RuleMetaData ruleMetaData = new RuleMetaData();
-        ruleMetaData.setName("Testing");
-        ruleMetaData.setTenantId(tenantId);
-        ruleMetaData.setWeight(weight);
-        ruleMetaData.setPluginToken(pluginToken);
-
-        ruleMetaData.setAction(createNode(ComponentScope.TENANT, ComponentType.ACTION,
-                "org.thingsboard.component.ActionTest", "TestJsonDescriptor.json", "TestJsonData.json"));
-        ruleMetaData.setProcessor(createNode(ComponentScope.TENANT, ComponentType.PROCESSOR,
-                "org.thingsboard.component.ProcessorTest", "TestJsonDescriptor.json", "TestJsonData.json"));
-        ruleMetaData.setFilters(mapper.createArrayNode().add(
-                createNode(ComponentScope.TENANT, ComponentType.FILTER,
-                        "org.thingsboard.component.FilterTest", "TestJsonDescriptor.json", "TestJsonData.json")
-        ));
-
-        ruleMetaData.setAdditionalInfo(mapper.readTree("{}"));
-        return ruleMetaData;
-    }
-
-    protected JsonNode createNode(ComponentScope scope, ComponentType type, String clazz, String configurationDescriptor, String configuration) throws IOException {
-        getOrCreateDescriptor(scope, type, clazz, configurationDescriptor);
-        ObjectNode oNode = mapper.createObjectNode();
-        oNode.set("name", new TextNode("test action"));
-        oNode.set("clazz", new TextNode(clazz));
-        oNode.set("configuration", readFromResource(configuration));
-        return oNode;
-    }
-
     @Bean
     public AuditLogLevelFilter auditLogLevelFilter() {
         Map<String,String> mask = new HashMap<>();
diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/BaseAdminSettingsServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/BaseAdminSettingsServiceTest.java
index 4ea39f0..41ba94f 100644
--- a/dao/src/test/java/org/thingsboard/server/dao/service/BaseAdminSettingsServiceTest.java
+++ b/dao/src/test/java/org/thingsboard/server/dao/service/BaseAdminSettingsServiceTest.java
@@ -16,7 +16,6 @@
 package org.thingsboard.server.dao.service;
 
 import com.fasterxml.jackson.databind.JsonNode;
-import com.fasterxml.jackson.databind.ObjectMapper;
 import com.fasterxml.jackson.databind.node.ObjectNode;
 import org.junit.Assert;
 import org.junit.Test;
diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/BaseAlarmServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/BaseAlarmServiceTest.java
index fd071e9..03099e9 100644
--- a/dao/src/test/java/org/thingsboard/server/dao/service/BaseAlarmServiceTest.java
+++ b/dao/src/test/java/org/thingsboard/server/dao/service/BaseAlarmServiceTest.java
@@ -21,7 +21,11 @@ import org.junit.Assert;
 import org.junit.Before;
 import org.junit.Test;
 import org.thingsboard.server.common.data.Tenant;
-import org.thingsboard.server.common.data.alarm.*;
+import org.thingsboard.server.common.data.alarm.Alarm;
+import org.thingsboard.server.common.data.alarm.AlarmInfo;
+import org.thingsboard.server.common.data.alarm.AlarmQuery;
+import org.thingsboard.server.common.data.alarm.AlarmSeverity;
+import org.thingsboard.server.common.data.alarm.AlarmStatus;
 import org.thingsboard.server.common.data.id.AssetId;
 import org.thingsboard.server.common.data.id.TenantId;
 import org.thingsboard.server.common.data.page.TimePageData;
@@ -168,7 +172,7 @@ public abstract class BaseAlarmServiceTest extends AbstractServiceTest {
         Assert.assertNotNull(alarms.getData());
         Assert.assertEquals(0, alarms.getData().size());
 
-        alarmService.clearAlarm(created.getId(), System.currentTimeMillis()).get();
+        alarmService.clearAlarm(created.getId(), null, System.currentTimeMillis()).get();
         created = alarmService.findAlarmByIdAsync(created.getId()).get();
 
         alarms = alarmService.findAlarms(AlarmQuery.builder()
diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/BaseDashboardServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/BaseDashboardServiceTest.java
index 993e525..02b5186 100644
--- a/dao/src/test/java/org/thingsboard/server/dao/service/BaseDashboardServiceTest.java
+++ b/dao/src/test/java/org/thingsboard/server/dao/service/BaseDashboardServiceTest.java
@@ -32,10 +32,8 @@ import org.thingsboard.server.common.data.page.TextPageLink;
 import org.thingsboard.server.common.data.page.TimePageData;
 import org.thingsboard.server.common.data.page.TimePageLink;
 import org.thingsboard.server.dao.exception.DataValidationException;
-import org.thingsboard.server.dao.model.ModelConstants;
 
 import java.io.IOException;
-import java.sql.Time;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/BaseDeviceCredentialsCacheTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/BaseDeviceCredentialsCacheTest.java
index 61fbf05..9269fc3 100644
--- a/dao/src/test/java/org/thingsboard/server/dao/service/BaseDeviceCredentialsCacheTest.java
+++ b/dao/src/test/java/org/thingsboard/server/dao/service/BaseDeviceCredentialsCacheTest.java
@@ -36,7 +36,10 @@ import org.thingsboard.server.dao.device.DeviceService;
 
 import java.util.UUID;
 
-import static org.mockito.Mockito.*;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
 
 public abstract class BaseDeviceCredentialsCacheTest extends AbstractServiceTest {
 
diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/BaseDeviceCredentialsServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/BaseDeviceCredentialsServiceTest.java
index 032534e..1d771b2 100644
--- a/dao/src/test/java/org/thingsboard/server/dao/service/BaseDeviceCredentialsServiceTest.java
+++ b/dao/src/test/java/org/thingsboard/server/dao/service/BaseDeviceCredentialsServiceTest.java
@@ -16,7 +16,6 @@
 package org.thingsboard.server.dao.service;
 
 import com.datastax.driver.core.utils.UUIDs;
-import org.apache.commons.lang3.RandomStringUtils;
 import org.junit.After;
 import org.junit.Assert;
 import org.junit.Before;
diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/BaseRelationCacheTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/BaseRelationCacheTest.java
index 3a0b6ae..bc78fd7 100644
--- a/dao/src/test/java/org/thingsboard/server/dao/service/BaseRelationCacheTest.java
+++ b/dao/src/test/java/org/thingsboard/server/dao/service/BaseRelationCacheTest.java
@@ -34,7 +34,10 @@ import org.thingsboard.server.dao.relation.RelationService;
 import java.util.UUID;
 import java.util.concurrent.ExecutionException;
 
-import static org.mockito.Mockito.*;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
 import static org.thingsboard.server.common.data.CacheConstants.RELATIONS_CACHE;
 
 public abstract class BaseRelationCacheTest extends AbstractServiceTest {
diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/BaseRelationServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/BaseRelationServiceTest.java
index 754454a..743aefc 100644
--- a/dao/src/test/java/org/thingsboard/server/dao/service/BaseRelationServiceTest.java
+++ b/dao/src/test/java/org/thingsboard/server/dao/service/BaseRelationServiceTest.java
@@ -24,12 +24,12 @@ import org.thingsboard.server.common.data.EntityType;
 import org.thingsboard.server.common.data.id.AssetId;
 import org.thingsboard.server.common.data.id.DeviceId;
 import org.thingsboard.server.common.data.relation.EntityRelation;
-import org.thingsboard.server.common.data.relation.RelationTypeGroup;
-import org.thingsboard.server.dao.exception.DataValidationException;
 import org.thingsboard.server.common.data.relation.EntityRelationsQuery;
 import org.thingsboard.server.common.data.relation.EntitySearchDirection;
 import org.thingsboard.server.common.data.relation.EntityTypeFilter;
+import org.thingsboard.server.common.data.relation.RelationTypeGroup;
 import org.thingsboard.server.common.data.relation.RelationsSearchParameters;
+import org.thingsboard.server.dao.exception.DataValidationException;
 
 import java.util.Collections;
 import java.util.List;
@@ -96,7 +96,7 @@ public abstract class BaseRelationServiceTest extends AbstractServiceTest {
         saveRelation(relationA);
         saveRelation(relationB);
 
-        Assert.assertTrue(relationService.deleteEntityRelationsAsync(childId).get());
+        Assert.assertNull(relationService.deleteEntityRelationsAsync(childId).get());
 
         Assert.assertFalse(relationService.checkRelation(parentId, childId, EntityRelation.CONTAINS_TYPE, RelationTypeGroup.COMMON).get());
 
diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/BaseRuleChainServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/BaseRuleChainServiceTest.java
new file mode 100644
index 0000000..28c6147
--- /dev/null
+++ b/dao/src/test/java/org/thingsboard/server/dao/service/BaseRuleChainServiceTest.java
@@ -0,0 +1,362 @@
+/**
+ * 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.server.dao.service;
+
+import com.datastax.driver.core.utils.UUIDs;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.apache.commons.lang3.RandomStringUtils;
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.thingsboard.server.common.data.Tenant;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.page.TextPageData;
+import org.thingsboard.server.common.data.page.TextPageLink;
+import org.thingsboard.server.common.data.relation.EntityRelation;
+import org.thingsboard.server.common.data.rule.RuleChain;
+import org.thingsboard.server.common.data.rule.RuleChainMetaData;
+import org.thingsboard.server.common.data.rule.RuleNode;
+import org.thingsboard.server.dao.exception.DataValidationException;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Created by igor on 3/13/18.
+ */
+public abstract class BaseRuleChainServiceTest extends AbstractServiceTest {
+
+    private IdComparator<RuleChain> idComparator = new IdComparator<>();
+    private IdComparator<RuleNode> ruleNodeIdComparator = new IdComparator<>();
+
+    private TenantId tenantId;
+
+    @Before
+    public void before() {
+        Tenant tenant = new Tenant();
+        tenant.setTitle("My tenant");
+        Tenant savedTenant = tenantService.saveTenant(tenant);
+        Assert.assertNotNull(savedTenant);
+        tenantId = savedTenant.getId();
+    }
+
+    @After
+    public void after() {
+        tenantService.deleteTenant(tenantId);
+    }
+
+    @Test
+    public void testSaveRuleChain() throws IOException {
+        RuleChain ruleChain = new RuleChain();
+        ruleChain.setTenantId(tenantId);
+        ruleChain.setName("My RuleChain");
+
+        RuleChain savedRuleChain = ruleChainService.saveRuleChain(ruleChain);
+        Assert.assertNotNull(savedRuleChain);
+        Assert.assertNotNull(savedRuleChain.getId());
+        Assert.assertTrue(savedRuleChain.getCreatedTime() > 0);
+        Assert.assertEquals(ruleChain.getTenantId(), savedRuleChain.getTenantId());
+        Assert.assertEquals(ruleChain.getName(), savedRuleChain.getName());
+
+        savedRuleChain.setName("My new RuleChain");
+
+        ruleChainService.saveRuleChain(savedRuleChain);
+        RuleChain foundRuleChain = ruleChainService.findRuleChainById(savedRuleChain.getId());
+        Assert.assertEquals(foundRuleChain.getName(), savedRuleChain.getName());
+
+        ruleChainService.deleteRuleChainById(savedRuleChain.getId());
+    }
+
+    @Test(expected = DataValidationException.class)
+    public void testSaveRuleChainWithEmptyName() {
+        RuleChain ruleChain = new RuleChain();
+        ruleChain.setTenantId(tenantId);
+        ruleChainService.saveRuleChain(ruleChain);
+    }
+
+    @Test(expected = DataValidationException.class)
+    public void testSaveRuleChainWithInvalidTenant() {
+        RuleChain ruleChain = new RuleChain();
+        ruleChain.setName("My RuleChain");
+        ruleChain.setTenantId(new TenantId(UUIDs.timeBased()));
+        ruleChainService.saveRuleChain(ruleChain);
+    }
+
+    @Test
+    public void testFindRuleChainById() {
+        RuleChain ruleChain = new RuleChain();
+        ruleChain.setTenantId(tenantId);
+        ruleChain.setName("My RuleChain");
+        RuleChain savedRuleChain = ruleChainService.saveRuleChain(ruleChain);
+        RuleChain foundRuleChain = ruleChainService.findRuleChainById(savedRuleChain.getId());
+        Assert.assertNotNull(foundRuleChain);
+        Assert.assertEquals(savedRuleChain, foundRuleChain);
+        ruleChainService.deleteRuleChainById(savedRuleChain.getId());
+    }
+
+    @Test
+    public void testDeleteRuleChain() {
+        RuleChain ruleChain = new RuleChain();
+        ruleChain.setTenantId(tenantId);
+        ruleChain.setName("My RuleChain");
+        RuleChain savedRuleChain = ruleChainService.saveRuleChain(ruleChain);
+        RuleChain foundRuleChain = ruleChainService.findRuleChainById(savedRuleChain.getId());
+        Assert.assertNotNull(foundRuleChain);
+        ruleChainService.deleteRuleChainById(savedRuleChain.getId());
+        foundRuleChain = ruleChainService.findRuleChainById(savedRuleChain.getId());
+        Assert.assertNull(foundRuleChain);
+    }
+
+    @Test
+    public void testFindRuleChainsByTenantId() {
+        Tenant tenant = new Tenant();
+        tenant.setTitle("Test tenant");
+        tenant = tenantService.saveTenant(tenant);
+
+        TenantId tenantId = tenant.getId();
+
+        List<RuleChain> ruleChains = new ArrayList<>();
+        for (int i = 0; i < 165; i++) {
+            RuleChain ruleChain = new RuleChain();
+            ruleChain.setTenantId(tenantId);
+            ruleChain.setName("RuleChain" + i);
+            ruleChains.add(ruleChainService.saveRuleChain(ruleChain));
+        }
+
+        List<RuleChain> loadedRuleChains = new ArrayList<>();
+        TextPageLink pageLink = new TextPageLink(16);
+        TextPageData<RuleChain> pageData = null;
+        do {
+            pageData = ruleChainService.findTenantRuleChains(tenantId, pageLink);
+            loadedRuleChains.addAll(pageData.getData());
+            if (pageData.hasNext()) {
+                pageLink = pageData.getNextPageLink();
+            }
+        } while (pageData.hasNext());
+
+        Collections.sort(ruleChains, idComparator);
+        Collections.sort(loadedRuleChains, idComparator);
+
+        Assert.assertEquals(ruleChains, loadedRuleChains);
+
+        ruleChainService.deleteRuleChainsByTenantId(tenantId);
+
+        pageLink = new TextPageLink(31);
+        pageData = ruleChainService.findTenantRuleChains(tenantId, pageLink);
+        Assert.assertFalse(pageData.hasNext());
+        Assert.assertTrue(pageData.getData().isEmpty());
+
+        tenantService.deleteTenant(tenantId);
+    }
+
+    @Test
+    public void testFindRuleChainsByTenantIdAndName() {
+        String name1 = "RuleChain name 1";
+        List<RuleChain> ruleChainsName1 = new ArrayList<>();
+        for (int i = 0; i < 123; i++) {
+            RuleChain ruleChain = new RuleChain();
+            ruleChain.setTenantId(tenantId);
+            String suffix = RandomStringUtils.randomAlphanumeric((int) (Math.random() * 17));
+            String name = name1 + suffix;
+            name = i % 2 == 0 ? name.toLowerCase() : name.toUpperCase();
+            ruleChain.setName(name);
+            ruleChainsName1.add(ruleChainService.saveRuleChain(ruleChain));
+        }
+        String name2 = "RuleChain name 2";
+        List<RuleChain> ruleChainsName2 = new ArrayList<>();
+        for (int i = 0; i < 193; i++) {
+            RuleChain ruleChain = new RuleChain();
+            ruleChain.setTenantId(tenantId);
+            String suffix = RandomStringUtils.randomAlphanumeric((int) (Math.random() * 15));
+            String name = name2 + suffix;
+            name = i % 2 == 0 ? name.toLowerCase() : name.toUpperCase();
+            ruleChain.setName(name);
+            ruleChainsName2.add(ruleChainService.saveRuleChain(ruleChain));
+        }
+
+        List<RuleChain> loadedRuleChainsName1 = new ArrayList<>();
+        TextPageLink pageLink = new TextPageLink(19, name1);
+        TextPageData<RuleChain> pageData = null;
+        do {
+            pageData = ruleChainService.findTenantRuleChains(tenantId, pageLink);
+            loadedRuleChainsName1.addAll(pageData.getData());
+            if (pageData.hasNext()) {
+                pageLink = pageData.getNextPageLink();
+            }
+        } while (pageData.hasNext());
+
+        Collections.sort(ruleChainsName1, idComparator);
+        Collections.sort(loadedRuleChainsName1, idComparator);
+
+        Assert.assertEquals(ruleChainsName1, loadedRuleChainsName1);
+
+        List<RuleChain> loadedRuleChainsName2 = new ArrayList<>();
+        pageLink = new TextPageLink(4, name2);
+        do {
+            pageData = ruleChainService.findTenantRuleChains(tenantId, pageLink);
+            loadedRuleChainsName2.addAll(pageData.getData());
+            if (pageData.hasNext()) {
+                pageLink = pageData.getNextPageLink();
+            }
+        } while (pageData.hasNext());
+
+        Collections.sort(ruleChainsName2, idComparator);
+        Collections.sort(loadedRuleChainsName2, idComparator);
+
+        Assert.assertEquals(ruleChainsName2, loadedRuleChainsName2);
+
+        for (RuleChain ruleChain : loadedRuleChainsName1) {
+            ruleChainService.deleteRuleChainById(ruleChain.getId());
+        }
+
+        pageLink = new TextPageLink(4, name1);
+        pageData = ruleChainService.findTenantRuleChains(tenantId, pageLink);
+        Assert.assertFalse(pageData.hasNext());
+        Assert.assertEquals(0, pageData.getData().size());
+
+        for (RuleChain ruleChain : loadedRuleChainsName2) {
+            ruleChainService.deleteRuleChainById(ruleChain.getId());
+        }
+
+        pageLink = new TextPageLink(4, name2);
+        pageData = ruleChainService.findTenantRuleChains(tenantId, pageLink);
+        Assert.assertFalse(pageData.hasNext());
+        Assert.assertEquals(0, pageData.getData().size());
+    }
+
+    @Test
+    public void testSaveRuleChainMetaData() throws Exception {
+
+        RuleChainMetaData savedRuleChainMetaData = createRuleChainMetadata();
+
+        Assert.assertEquals(3, savedRuleChainMetaData.getNodes().size());
+        Assert.assertEquals(3, savedRuleChainMetaData.getConnections().size());
+
+        for (RuleNode ruleNode : savedRuleChainMetaData.getNodes()) {
+            Assert.assertNotNull(ruleNode.getId());
+            List<EntityRelation> relations = ruleChainService.getRuleNodeRelations(ruleNode.getId());
+            if ("name1".equals(ruleNode.getName())) {
+                Assert.assertEquals(2, relations.size());
+            } else if ("name2".equals(ruleNode.getName())) {
+                Assert.assertEquals(1, relations.size());
+            } else if ("name3".equals(ruleNode.getName())) {
+                Assert.assertEquals(0, relations.size());
+            }
+        }
+
+        List<RuleNode> loadedRuleNodes = ruleChainService.getRuleChainNodes(savedRuleChainMetaData.getRuleChainId());
+
+        Collections.sort(savedRuleChainMetaData.getNodes(), ruleNodeIdComparator);
+        Collections.sort(loadedRuleNodes, ruleNodeIdComparator);
+
+        Assert.assertEquals(savedRuleChainMetaData.getNodes(), loadedRuleNodes);
+
+        ruleChainService.deleteRuleChainById(savedRuleChainMetaData.getRuleChainId());
+    }
+
+    @Test
+    public void testUpdateRuleChainMetaData() throws Exception {
+        RuleChainMetaData savedRuleChainMetaData = createRuleChainMetadata();
+
+        List<RuleNode> ruleNodes = savedRuleChainMetaData.getNodes();
+        int name3Index = -1;
+        for (int i=0;i<ruleNodes.size();i++) {
+            if ("name3".equals(ruleNodes.get(i).getName())) {
+                name3Index = i;
+                break;
+            }
+        }
+
+        RuleNode ruleNode4 = new RuleNode();
+        ruleNode4.setName("name4");
+        ruleNode4.setType("type4");
+        ruleNode4.setConfiguration(mapper.readTree("\"key4\": \"val4\""));
+
+        ruleNodes.set(name3Index, ruleNode4);
+
+        RuleChainMetaData updatedRuleChainMetaData = ruleChainService.saveRuleChainMetaData(savedRuleChainMetaData);
+
+        Assert.assertEquals(3, updatedRuleChainMetaData.getNodes().size());
+        Assert.assertEquals(3, updatedRuleChainMetaData.getConnections().size());
+
+        for (RuleNode ruleNode : updatedRuleChainMetaData.getNodes()) {
+            Assert.assertNotNull(ruleNode.getId());
+            List<EntityRelation> relations = ruleChainService.getRuleNodeRelations(ruleNode.getId());
+            if ("name1".equals(ruleNode.getName())) {
+                Assert.assertEquals(2, relations.size());
+            } else if ("name2".equals(ruleNode.getName())) {
+                Assert.assertEquals(1, relations.size());
+            } else if ("name4".equals(ruleNode.getName())) {
+                Assert.assertEquals(0, relations.size());
+            }
+        }
+
+        List<RuleNode> loadedRuleNodes = ruleChainService.getRuleChainNodes(savedRuleChainMetaData.getRuleChainId());
+
+        Collections.sort(updatedRuleChainMetaData.getNodes(), ruleNodeIdComparator);
+        Collections.sort(loadedRuleNodes, ruleNodeIdComparator);
+
+        Assert.assertEquals(updatedRuleChainMetaData.getNodes(), loadedRuleNodes);
+
+        ruleChainService.deleteRuleChainById(savedRuleChainMetaData.getRuleChainId());
+    }
+
+    private RuleChainMetaData createRuleChainMetadata() throws Exception {
+        RuleChain ruleChain = new RuleChain();
+        ruleChain.setName("My RuleChain");
+        ruleChain.setTenantId(tenantId);
+        RuleChain savedRuleChain = ruleChainService.saveRuleChain(ruleChain);
+
+        RuleChainMetaData ruleChainMetaData = new RuleChainMetaData();
+        ruleChainMetaData.setRuleChainId(savedRuleChain.getId());
+
+        ObjectMapper mapper = new ObjectMapper();
+
+        RuleNode ruleNode1 = new RuleNode();
+        ruleNode1.setName("name1");
+        ruleNode1.setType("type1");
+        ruleNode1.setConfiguration(mapper.readTree("\"key1\": \"val1\""));
+
+        RuleNode ruleNode2 = new RuleNode();
+        ruleNode2.setName("name2");
+        ruleNode2.setType("type2");
+        ruleNode2.setConfiguration(mapper.readTree("\"key2\": \"val2\""));
+
+        RuleNode ruleNode3 = new RuleNode();
+        ruleNode3.setName("name3");
+        ruleNode3.setType("type3");
+        ruleNode3.setConfiguration(mapper.readTree("\"key3\": \"val3\""));
+
+        List<RuleNode> ruleNodes = new ArrayList<>();
+        ruleNodes.add(ruleNode1);
+        ruleNodes.add(ruleNode2);
+        ruleNodes.add(ruleNode3);
+        ruleChainMetaData.setFirstNodeIndex(0);
+        ruleChainMetaData.setNodes(ruleNodes);
+
+        ruleChainMetaData.addConnectionInfo(0,1,"success");
+        ruleChainMetaData.addConnectionInfo(0,2,"fail");
+        ruleChainMetaData.addConnectionInfo(1,2,"success");
+
+        return ruleChainService.saveRuleChainMetaData(ruleChainMetaData);
+    }
+
+
+}
diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/DaoNoSqlTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/DaoNoSqlTest.java
index 9263bc4..82a1a57 100644
--- a/dao/src/test/java/org/thingsboard/server/dao/service/DaoNoSqlTest.java
+++ b/dao/src/test/java/org/thingsboard/server/dao/service/DaoNoSqlTest.java
@@ -17,7 +17,12 @@ package org.thingsboard.server.dao.service;
 
 import org.springframework.test.context.TestPropertySource;
 
-import java.lang.annotation.*;
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Inherited;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
 
 @Target(ElementType.TYPE)
 @Retention(RetentionPolicy.RUNTIME)
diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/DaoSqlTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/DaoSqlTest.java
index 285c653..ea95650 100644
--- a/dao/src/test/java/org/thingsboard/server/dao/service/DaoSqlTest.java
+++ b/dao/src/test/java/org/thingsboard/server/dao/service/DaoSqlTest.java
@@ -17,7 +17,12 @@ package org.thingsboard.server.dao.service;
 
 import org.springframework.test.context.TestPropertySource;
 
-import java.lang.annotation.*;
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Inherited;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
 
 @Target(ElementType.TYPE)
 @Retention(RetentionPolicy.RUNTIME)
diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/event/BaseEventServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/event/BaseEventServiceTest.java
index 62af5a5..eb25c86 100644
--- a/dao/src/test/java/org/thingsboard/server/dao/service/event/BaseEventServiceTest.java
+++ b/dao/src/test/java/org/thingsboard/server/dao/service/event/BaseEventServiceTest.java
@@ -20,10 +20,10 @@ import org.junit.Assert;
 import org.junit.Test;
 import org.thingsboard.server.common.data.DataConstants;
 import org.thingsboard.server.common.data.Event;
+import org.thingsboard.server.common.data.id.CustomerId;
 import org.thingsboard.server.common.data.id.DeviceId;
 import org.thingsboard.server.common.data.id.EntityId;
 import org.thingsboard.server.common.data.id.EventId;
-import org.thingsboard.server.common.data.id.RuleId;
 import org.thingsboard.server.common.data.id.TenantId;
 import org.thingsboard.server.common.data.page.TimePageData;
 import org.thingsboard.server.common.data.page.TimePageLink;
@@ -66,15 +66,15 @@ public abstract class BaseEventServiceTest extends AbstractServiceTest {
         long endTime = LocalDateTime.of(2016, Month.NOVEMBER, 1, 13, 0).toEpochSecond(ZoneOffset.UTC);
         long timeAfterEndTime = LocalDateTime.of(2016, Month.NOVEMBER, 1, 13, 30).toEpochSecond(ZoneOffset.UTC);
 
-        RuleId ruleId = new RuleId(UUIDs.timeBased());
+        CustomerId customerId = new CustomerId(UUIDs.timeBased());
         TenantId tenantId = new TenantId(UUIDs.timeBased());
-        saveEventWithProvidedTime(timeBeforeStartTime, ruleId, tenantId);
-        Event savedEvent = saveEventWithProvidedTime(eventTime, ruleId, tenantId);
-        Event savedEvent2 = saveEventWithProvidedTime(eventTime+1, ruleId, tenantId);
-        Event savedEvent3 = saveEventWithProvidedTime(eventTime+2, ruleId, tenantId);
-        saveEventWithProvidedTime(timeAfterEndTime, ruleId, tenantId);
+        saveEventWithProvidedTime(timeBeforeStartTime, customerId, tenantId);
+        Event savedEvent = saveEventWithProvidedTime(eventTime, customerId, tenantId);
+        Event savedEvent2 = saveEventWithProvidedTime(eventTime+1, customerId, tenantId);
+        Event savedEvent3 = saveEventWithProvidedTime(eventTime+2, customerId, tenantId);
+        saveEventWithProvidedTime(timeAfterEndTime, customerId, tenantId);
 
-        TimePageData<Event> events = eventService.findEvents(tenantId, ruleId, DataConstants.STATS,
+        TimePageData<Event> events = eventService.findEvents(tenantId, customerId, DataConstants.STATS,
                 new TimePageLink(2, startTime, endTime, true));
 
         Assert.assertNotNull(events.getData());
@@ -84,7 +84,7 @@ public abstract class BaseEventServiceTest extends AbstractServiceTest {
         Assert.assertTrue(events.hasNext());
         Assert.assertNotNull(events.getNextPageLink());
 
-        events = eventService.findEvents(tenantId, ruleId, DataConstants.STATS, events.getNextPageLink());
+        events = eventService.findEvents(tenantId, customerId, DataConstants.STATS, events.getNextPageLink());
 
         Assert.assertNotNull(events.getData());
         Assert.assertTrue(events.getData().size() == 1);
@@ -101,15 +101,15 @@ public abstract class BaseEventServiceTest extends AbstractServiceTest {
         long endTime = LocalDateTime.of(2016, Month.NOVEMBER, 1, 13, 0).toEpochSecond(ZoneOffset.UTC);
         long timeAfterEndTime = LocalDateTime.of(2016, Month.NOVEMBER, 1, 13, 30).toEpochSecond(ZoneOffset.UTC);
 
-        RuleId ruleId = new RuleId(UUIDs.timeBased());
+        CustomerId customerId = new CustomerId(UUIDs.timeBased());
         TenantId tenantId = new TenantId(UUIDs.timeBased());
-        saveEventWithProvidedTime(timeBeforeStartTime, ruleId, tenantId);
-        Event savedEvent = saveEventWithProvidedTime(eventTime, ruleId, tenantId);
-        Event savedEvent2 = saveEventWithProvidedTime(eventTime+1, ruleId, tenantId);
-        Event savedEvent3 = saveEventWithProvidedTime(eventTime+2, ruleId, tenantId);
-        saveEventWithProvidedTime(timeAfterEndTime, ruleId, tenantId);
+        saveEventWithProvidedTime(timeBeforeStartTime, customerId, tenantId);
+        Event savedEvent = saveEventWithProvidedTime(eventTime, customerId, tenantId);
+        Event savedEvent2 = saveEventWithProvidedTime(eventTime+1, customerId, tenantId);
+        Event savedEvent3 = saveEventWithProvidedTime(eventTime+2, customerId, tenantId);
+        saveEventWithProvidedTime(timeAfterEndTime, customerId, tenantId);
 
-        TimePageData<Event> events = eventService.findEvents(tenantId, ruleId, DataConstants.STATS,
+        TimePageData<Event> events = eventService.findEvents(tenantId, customerId, DataConstants.STATS,
                 new TimePageLink(2, startTime, endTime, false));
 
         Assert.assertNotNull(events.getData());
@@ -119,7 +119,7 @@ public abstract class BaseEventServiceTest extends AbstractServiceTest {
         Assert.assertTrue(events.hasNext());
         Assert.assertNotNull(events.getNextPageLink());
 
-        events = eventService.findEvents(tenantId, ruleId, DataConstants.STATS, events.getNextPageLink());
+        events = eventService.findEvents(tenantId, customerId, DataConstants.STATS, events.getNextPageLink());
 
         Assert.assertNotNull(events.getData());
         Assert.assertTrue(events.getData().size() == 1);
diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/nosql/AdminSettingsServiceNoSqlTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/nosql/AdminSettingsServiceNoSqlTest.java
index c30f864..97e9e81 100644
--- a/dao/src/test/java/org/thingsboard/server/dao/service/nosql/AdminSettingsServiceNoSqlTest.java
+++ b/dao/src/test/java/org/thingsboard/server/dao/service/nosql/AdminSettingsServiceNoSqlTest.java
@@ -15,8 +15,8 @@
  */
 package org.thingsboard.server.dao.service.nosql;
 
-import org.thingsboard.server.dao.service.DaoNoSqlTest;
 import org.thingsboard.server.dao.service.BaseAdminSettingsServiceTest;
+import org.thingsboard.server.dao.service.DaoNoSqlTest;
 
 @DaoNoSqlTest
 public class AdminSettingsServiceNoSqlTest extends BaseAdminSettingsServiceTest {
diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/nosql/AlarmServiceNoSqlTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/nosql/AlarmServiceNoSqlTest.java
index f822adb..499d3c6 100644
--- a/dao/src/test/java/org/thingsboard/server/dao/service/nosql/AlarmServiceNoSqlTest.java
+++ b/dao/src/test/java/org/thingsboard/server/dao/service/nosql/AlarmServiceNoSqlTest.java
@@ -15,8 +15,8 @@
  */
 package org.thingsboard.server.dao.service.nosql;
 
-import org.thingsboard.server.dao.service.DaoNoSqlTest;
 import org.thingsboard.server.dao.service.BaseAlarmServiceTest;
+import org.thingsboard.server.dao.service.DaoNoSqlTest;
 
 @DaoNoSqlTest
 public class AlarmServiceNoSqlTest extends BaseAlarmServiceTest {
diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/nosql/AssetServiceNoSqlTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/nosql/AssetServiceNoSqlTest.java
index 3887788..9543560 100644
--- a/dao/src/test/java/org/thingsboard/server/dao/service/nosql/AssetServiceNoSqlTest.java
+++ b/dao/src/test/java/org/thingsboard/server/dao/service/nosql/AssetServiceNoSqlTest.java
@@ -15,8 +15,8 @@
  */
 package org.thingsboard.server.dao.service.nosql;
 
-import org.thingsboard.server.dao.service.DaoNoSqlTest;
 import org.thingsboard.server.dao.service.BaseAssetServiceTest;
+import org.thingsboard.server.dao.service.DaoNoSqlTest;
 
 @DaoNoSqlTest
 public class AssetServiceNoSqlTest extends BaseAssetServiceTest {
diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/nosql/CustomerServiceNoSqlTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/nosql/CustomerServiceNoSqlTest.java
index 99c77de..905b723 100644
--- a/dao/src/test/java/org/thingsboard/server/dao/service/nosql/CustomerServiceNoSqlTest.java
+++ b/dao/src/test/java/org/thingsboard/server/dao/service/nosql/CustomerServiceNoSqlTest.java
@@ -15,8 +15,8 @@
  */
 package org.thingsboard.server.dao.service.nosql;
 
-import org.thingsboard.server.dao.service.DaoNoSqlTest;
 import org.thingsboard.server.dao.service.BaseCustomerServiceTest;
+import org.thingsboard.server.dao.service.DaoNoSqlTest;
 
 @DaoNoSqlTest
 public class CustomerServiceNoSqlTest extends BaseCustomerServiceTest {
diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/nosql/DashboardServiceNoSqlTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/nosql/DashboardServiceNoSqlTest.java
index 3234805..9712ee8 100644
--- a/dao/src/test/java/org/thingsboard/server/dao/service/nosql/DashboardServiceNoSqlTest.java
+++ b/dao/src/test/java/org/thingsboard/server/dao/service/nosql/DashboardServiceNoSqlTest.java
@@ -15,8 +15,8 @@
  */
 package org.thingsboard.server.dao.service.nosql;
 
-import org.thingsboard.server.dao.service.DaoNoSqlTest;
 import org.thingsboard.server.dao.service.BaseDashboardServiceTest;
+import org.thingsboard.server.dao.service.DaoNoSqlTest;
 
 @DaoNoSqlTest
 public class DashboardServiceNoSqlTest extends BaseDashboardServiceTest {
diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/nosql/DeviceCredentialCacheNoSqlTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/nosql/DeviceCredentialCacheNoSqlTest.java
index 218993f..7c1356d 100644
--- a/dao/src/test/java/org/thingsboard/server/dao/service/nosql/DeviceCredentialCacheNoSqlTest.java
+++ b/dao/src/test/java/org/thingsboard/server/dao/service/nosql/DeviceCredentialCacheNoSqlTest.java
@@ -15,8 +15,8 @@
  */
 package org.thingsboard.server.dao.service.nosql;
 
-import org.thingsboard.server.dao.service.DaoNoSqlTest;
 import org.thingsboard.server.dao.service.BaseDeviceCredentialsCacheTest;
+import org.thingsboard.server.dao.service.DaoNoSqlTest;
 
 @DaoNoSqlTest
 public class DeviceCredentialCacheNoSqlTest extends BaseDeviceCredentialsCacheTest {
diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/nosql/DeviceCredentialServiceNoSqlTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/nosql/DeviceCredentialServiceNoSqlTest.java
index a12658e..6b8cf64 100644
--- a/dao/src/test/java/org/thingsboard/server/dao/service/nosql/DeviceCredentialServiceNoSqlTest.java
+++ b/dao/src/test/java/org/thingsboard/server/dao/service/nosql/DeviceCredentialServiceNoSqlTest.java
@@ -15,8 +15,8 @@
  */
 package org.thingsboard.server.dao.service.nosql;
 
-import org.thingsboard.server.dao.service.DaoNoSqlTest;
 import org.thingsboard.server.dao.service.BaseDeviceCredentialsServiceTest;
+import org.thingsboard.server.dao.service.DaoNoSqlTest;
 
 @DaoNoSqlTest
 public class DeviceCredentialServiceNoSqlTest extends BaseDeviceCredentialsServiceTest {
diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/nosql/DeviceServiceNoSqlTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/nosql/DeviceServiceNoSqlTest.java
index 5d01278..99e327c 100644
--- a/dao/src/test/java/org/thingsboard/server/dao/service/nosql/DeviceServiceNoSqlTest.java
+++ b/dao/src/test/java/org/thingsboard/server/dao/service/nosql/DeviceServiceNoSqlTest.java
@@ -15,8 +15,8 @@
  */
 package org.thingsboard.server.dao.service.nosql;
 
-import org.thingsboard.server.dao.service.DaoNoSqlTest;
 import org.thingsboard.server.dao.service.BaseDeviceServiceTest;
+import org.thingsboard.server.dao.service.DaoNoSqlTest;
 
 @DaoNoSqlTest
 public class DeviceServiceNoSqlTest extends BaseDeviceServiceTest {
diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/nosql/RelationServiceNoSqlTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/nosql/RelationServiceNoSqlTest.java
index 48d2026..74728fb 100644
--- a/dao/src/test/java/org/thingsboard/server/dao/service/nosql/RelationServiceNoSqlTest.java
+++ b/dao/src/test/java/org/thingsboard/server/dao/service/nosql/RelationServiceNoSqlTest.java
@@ -15,8 +15,8 @@
  */
 package org.thingsboard.server.dao.service.nosql;
 
-import org.thingsboard.server.dao.service.DaoNoSqlTest;
 import org.thingsboard.server.dao.service.BaseRelationServiceTest;
+import org.thingsboard.server.dao.service.DaoNoSqlTest;
 
 @DaoNoSqlTest
 public class RelationServiceNoSqlTest extends BaseRelationServiceTest {
diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/nosql/TenantServiceNoSqlTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/nosql/TenantServiceNoSqlTest.java
index e19cd51..de474fd 100644
--- a/dao/src/test/java/org/thingsboard/server/dao/service/nosql/TenantServiceNoSqlTest.java
+++ b/dao/src/test/java/org/thingsboard/server/dao/service/nosql/TenantServiceNoSqlTest.java
@@ -15,8 +15,8 @@
  */
 package org.thingsboard.server.dao.service.nosql;
 
-import org.thingsboard.server.dao.service.DaoNoSqlTest;
 import org.thingsboard.server.dao.service.BaseTenantServiceTest;
+import org.thingsboard.server.dao.service.DaoNoSqlTest;
 
 @DaoNoSqlTest
 public class TenantServiceNoSqlTest extends BaseTenantServiceTest {
diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/nosql/UserServiceNoSqlTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/nosql/UserServiceNoSqlTest.java
index 0aa7175..af3c497 100644
--- a/dao/src/test/java/org/thingsboard/server/dao/service/nosql/UserServiceNoSqlTest.java
+++ b/dao/src/test/java/org/thingsboard/server/dao/service/nosql/UserServiceNoSqlTest.java
@@ -15,8 +15,8 @@
  */
 package org.thingsboard.server.dao.service.nosql;
 
-import org.thingsboard.server.dao.service.DaoNoSqlTest;
 import org.thingsboard.server.dao.service.BaseUserServiceTest;
+import org.thingsboard.server.dao.service.DaoNoSqlTest;
 
 @DaoNoSqlTest
 public class UserServiceNoSqlTest extends BaseUserServiceTest {
diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/nosql/WidgetsBundleServiceNoSqlTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/nosql/WidgetsBundleServiceNoSqlTest.java
index 00b131d..79b5507 100644
--- a/dao/src/test/java/org/thingsboard/server/dao/service/nosql/WidgetsBundleServiceNoSqlTest.java
+++ b/dao/src/test/java/org/thingsboard/server/dao/service/nosql/WidgetsBundleServiceNoSqlTest.java
@@ -15,8 +15,8 @@
  */
 package org.thingsboard.server.dao.service.nosql;
 
-import org.thingsboard.server.dao.service.DaoNoSqlTest;
 import org.thingsboard.server.dao.service.BaseWidgetsBundleServiceTest;
+import org.thingsboard.server.dao.service.DaoNoSqlTest;
 
 @DaoNoSqlTest
 public class WidgetsBundleServiceNoSqlTest extends BaseWidgetsBundleServiceTest {
diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/nosql/WidgetTypeServiceNoSqlTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/nosql/WidgetTypeServiceNoSqlTest.java
index 98d867c..90399c4 100644
--- a/dao/src/test/java/org/thingsboard/server/dao/service/nosql/WidgetTypeServiceNoSqlTest.java
+++ b/dao/src/test/java/org/thingsboard/server/dao/service/nosql/WidgetTypeServiceNoSqlTest.java
@@ -15,8 +15,8 @@
  */
 package org.thingsboard.server.dao.service.nosql;
 
-import org.thingsboard.server.dao.service.DaoNoSqlTest;
 import org.thingsboard.server.dao.service.BaseWidgetTypeServiceTest;
+import org.thingsboard.server.dao.service.DaoNoSqlTest;
 
 @DaoNoSqlTest
 public class WidgetTypeServiceNoSqlTest extends BaseWidgetTypeServiceTest {
diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/sql/AdminSettingsServiceSqlTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/sql/AdminSettingsServiceSqlTest.java
index d5a69c0..aa2015f 100644
--- a/dao/src/test/java/org/thingsboard/server/dao/service/sql/AdminSettingsServiceSqlTest.java
+++ b/dao/src/test/java/org/thingsboard/server/dao/service/sql/AdminSettingsServiceSqlTest.java
@@ -15,8 +15,8 @@
  */
 package org.thingsboard.server.dao.service.sql;
 
-import org.thingsboard.server.dao.service.DaoSqlTest;
 import org.thingsboard.server.dao.service.BaseAdminSettingsServiceTest;
+import org.thingsboard.server.dao.service.DaoSqlTest;
 
 @DaoSqlTest
 public class AdminSettingsServiceSqlTest extends BaseAdminSettingsServiceTest {
diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/sql/AlarmServiceSqlTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/sql/AlarmServiceSqlTest.java
index 38d6080..7b2d3a7 100644
--- a/dao/src/test/java/org/thingsboard/server/dao/service/sql/AlarmServiceSqlTest.java
+++ b/dao/src/test/java/org/thingsboard/server/dao/service/sql/AlarmServiceSqlTest.java
@@ -15,8 +15,8 @@
  */
 package org.thingsboard.server.dao.service.sql;
 
-import org.thingsboard.server.dao.service.DaoSqlTest;
 import org.thingsboard.server.dao.service.BaseAlarmServiceTest;
+import org.thingsboard.server.dao.service.DaoSqlTest;
 
 @DaoSqlTest
 public class AlarmServiceSqlTest extends BaseAlarmServiceTest {
diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/sql/AssetServiceSqlTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/sql/AssetServiceSqlTest.java
index 3020cec..fc77fe3 100644
--- a/dao/src/test/java/org/thingsboard/server/dao/service/sql/AssetServiceSqlTest.java
+++ b/dao/src/test/java/org/thingsboard/server/dao/service/sql/AssetServiceSqlTest.java
@@ -15,8 +15,8 @@
  */
 package org.thingsboard.server.dao.service.sql;
 
-import org.thingsboard.server.dao.service.DaoSqlTest;
 import org.thingsboard.server.dao.service.BaseAssetServiceTest;
+import org.thingsboard.server.dao.service.DaoSqlTest;
 
 @DaoSqlTest
 public class AssetServiceSqlTest extends BaseAssetServiceTest {
diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/sql/CustomerServiceSqlTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/sql/CustomerServiceSqlTest.java
index c900cc4..48b4bac 100644
--- a/dao/src/test/java/org/thingsboard/server/dao/service/sql/CustomerServiceSqlTest.java
+++ b/dao/src/test/java/org/thingsboard/server/dao/service/sql/CustomerServiceSqlTest.java
@@ -15,8 +15,8 @@
  */
 package org.thingsboard.server.dao.service.sql;
 
-import org.thingsboard.server.dao.service.DaoSqlTest;
 import org.thingsboard.server.dao.service.BaseCustomerServiceTest;
+import org.thingsboard.server.dao.service.DaoSqlTest;
 
 @DaoSqlTest
 public class CustomerServiceSqlTest extends BaseCustomerServiceTest {
diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/sql/DashboardServiceSqlTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/sql/DashboardServiceSqlTest.java
index 66aee43..4c6a2bb 100644
--- a/dao/src/test/java/org/thingsboard/server/dao/service/sql/DashboardServiceSqlTest.java
+++ b/dao/src/test/java/org/thingsboard/server/dao/service/sql/DashboardServiceSqlTest.java
@@ -15,8 +15,8 @@
  */
 package org.thingsboard.server.dao.service.sql;
 
-import org.thingsboard.server.dao.service.DaoSqlTest;
 import org.thingsboard.server.dao.service.BaseDashboardServiceTest;
+import org.thingsboard.server.dao.service.DaoSqlTest;
 
 @DaoSqlTest
 public class DashboardServiceSqlTest extends BaseDashboardServiceTest {
diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/sql/DeviceCredentialsCacheSqlTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/sql/DeviceCredentialsCacheSqlTest.java
index 27febf8..2f534f6 100644
--- a/dao/src/test/java/org/thingsboard/server/dao/service/sql/DeviceCredentialsCacheSqlTest.java
+++ b/dao/src/test/java/org/thingsboard/server/dao/service/sql/DeviceCredentialsCacheSqlTest.java
@@ -15,8 +15,8 @@
  */
 package org.thingsboard.server.dao.service.sql;
 
-import org.thingsboard.server.dao.service.DaoSqlTest;
 import org.thingsboard.server.dao.service.BaseDeviceCredentialsCacheTest;
+import org.thingsboard.server.dao.service.DaoSqlTest;
 
 @DaoSqlTest
 public class DeviceCredentialsCacheSqlTest extends BaseDeviceCredentialsCacheTest {
diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/sql/DeviceCredentialsServiceSqlTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/sql/DeviceCredentialsServiceSqlTest.java
index f44c004..78e84eb 100644
--- a/dao/src/test/java/org/thingsboard/server/dao/service/sql/DeviceCredentialsServiceSqlTest.java
+++ b/dao/src/test/java/org/thingsboard/server/dao/service/sql/DeviceCredentialsServiceSqlTest.java
@@ -15,8 +15,8 @@
  */
 package org.thingsboard.server.dao.service.sql;
 
-import org.thingsboard.server.dao.service.DaoSqlTest;
 import org.thingsboard.server.dao.service.BaseDeviceCredentialsServiceTest;
+import org.thingsboard.server.dao.service.DaoSqlTest;
 
 @DaoSqlTest
 public class DeviceCredentialsServiceSqlTest extends BaseDeviceCredentialsServiceTest {
diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/sql/DeviceServiceSqlTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/sql/DeviceServiceSqlTest.java
index f076237..5576a37 100644
--- a/dao/src/test/java/org/thingsboard/server/dao/service/sql/DeviceServiceSqlTest.java
+++ b/dao/src/test/java/org/thingsboard/server/dao/service/sql/DeviceServiceSqlTest.java
@@ -15,8 +15,8 @@
  */
 package org.thingsboard.server.dao.service.sql;
 
-import org.thingsboard.server.dao.service.DaoSqlTest;
 import org.thingsboard.server.dao.service.BaseDeviceServiceTest;
+import org.thingsboard.server.dao.service.DaoSqlTest;
 
 @DaoSqlTest
 public class DeviceServiceSqlTest extends BaseDeviceServiceTest {
diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/sql/RelationServiceSqlTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/sql/RelationServiceSqlTest.java
index b2410db..a88ecd2 100644
--- a/dao/src/test/java/org/thingsboard/server/dao/service/sql/RelationServiceSqlTest.java
+++ b/dao/src/test/java/org/thingsboard/server/dao/service/sql/RelationServiceSqlTest.java
@@ -15,8 +15,8 @@
  */
 package org.thingsboard.server.dao.service.sql;
 
-import org.thingsboard.server.dao.service.DaoSqlTest;
 import org.thingsboard.server.dao.service.BaseRelationServiceTest;
+import org.thingsboard.server.dao.service.DaoSqlTest;
 
 @DaoSqlTest
 public class RelationServiceSqlTest extends BaseRelationServiceTest {
diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/sql/TenantServiceSqlTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/sql/TenantServiceSqlTest.java
index 30c2d8a..abd847f 100644
--- a/dao/src/test/java/org/thingsboard/server/dao/service/sql/TenantServiceSqlTest.java
+++ b/dao/src/test/java/org/thingsboard/server/dao/service/sql/TenantServiceSqlTest.java
@@ -15,8 +15,8 @@
  */
 package org.thingsboard.server.dao.service.sql;
 
-import org.thingsboard.server.dao.service.DaoSqlTest;
 import org.thingsboard.server.dao.service.BaseTenantServiceTest;
+import org.thingsboard.server.dao.service.DaoSqlTest;
 
 @DaoSqlTest
 public class TenantServiceSqlTest extends BaseTenantServiceTest {
diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/sql/UserServiceSqlTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/sql/UserServiceSqlTest.java
index 2e9007c..b81c0be 100644
--- a/dao/src/test/java/org/thingsboard/server/dao/service/sql/UserServiceSqlTest.java
+++ b/dao/src/test/java/org/thingsboard/server/dao/service/sql/UserServiceSqlTest.java
@@ -15,8 +15,8 @@
  */
 package org.thingsboard.server.dao.service.sql;
 
-import org.thingsboard.server.dao.service.DaoSqlTest;
 import org.thingsboard.server.dao.service.BaseUserServiceTest;
+import org.thingsboard.server.dao.service.DaoSqlTest;
 
 @DaoSqlTest
 public class UserServiceSqlTest extends BaseUserServiceTest {
diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/sql/WidgetsBundleServiceSqlTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/sql/WidgetsBundleServiceSqlTest.java
index 79a7e7b..0b78bad 100644
--- a/dao/src/test/java/org/thingsboard/server/dao/service/sql/WidgetsBundleServiceSqlTest.java
+++ b/dao/src/test/java/org/thingsboard/server/dao/service/sql/WidgetsBundleServiceSqlTest.java
@@ -15,8 +15,8 @@
  */
 package org.thingsboard.server.dao.service.sql;
 
-import org.thingsboard.server.dao.service.DaoSqlTest;
 import org.thingsboard.server.dao.service.BaseWidgetsBundleServiceTest;
+import org.thingsboard.server.dao.service.DaoSqlTest;
 
 @DaoSqlTest
 public class WidgetsBundleServiceSqlTest extends BaseWidgetsBundleServiceTest {
diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/sql/WidgetTypeServiceSqlTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/sql/WidgetTypeServiceSqlTest.java
index de2b0e6..03b188c 100644
--- a/dao/src/test/java/org/thingsboard/server/dao/service/sql/WidgetTypeServiceSqlTest.java
+++ b/dao/src/test/java/org/thingsboard/server/dao/service/sql/WidgetTypeServiceSqlTest.java
@@ -15,8 +15,8 @@
  */
 package org.thingsboard.server.dao.service.sql;
 
-import org.thingsboard.server.dao.service.DaoSqlTest;
 import org.thingsboard.server.dao.service.BaseWidgetTypeServiceTest;
+import org.thingsboard.server.dao.service.DaoSqlTest;
 
 @DaoSqlTest
 public class WidgetTypeServiceSqlTest extends BaseWidgetTypeServiceTest {
diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/timeseries/BaseTimeseriesServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/timeseries/BaseTimeseriesServiceTest.java
index 0cb3f7f..f045bf1 100644
--- a/dao/src/test/java/org/thingsboard/server/dao/service/timeseries/BaseTimeseriesServiceTest.java
+++ b/dao/src/test/java/org/thingsboard/server/dao/service/timeseries/BaseTimeseriesServiceTest.java
@@ -20,7 +20,15 @@ import lombok.extern.slf4j.Slf4j;
 import org.junit.Assert;
 import org.junit.Test;
 import org.thingsboard.server.common.data.id.DeviceId;
-import org.thingsboard.server.common.data.kv.*;
+import org.thingsboard.server.common.data.kv.Aggregation;
+import org.thingsboard.server.common.data.kv.BaseTsKvQuery;
+import org.thingsboard.server.common.data.kv.BasicTsKvEntry;
+import org.thingsboard.server.common.data.kv.BooleanDataEntry;
+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.StringDataEntry;
+import org.thingsboard.server.common.data.kv.TsKvEntry;
 import org.thingsboard.server.dao.service.AbstractServiceTest;
 
 import java.util.ArrayList;
diff --git a/dao/src/test/java/org/thingsboard/server/dao/sql/asset/JpaAssetDaoTest.java b/dao/src/test/java/org/thingsboard/server/dao/sql/asset/JpaAssetDaoTest.java
index 19674f8..8139c3b 100644
--- a/dao/src/test/java/org/thingsboard/server/dao/sql/asset/JpaAssetDaoTest.java
+++ b/dao/src/test/java/org/thingsboard/server/dao/sql/asset/JpaAssetDaoTest.java
@@ -35,7 +35,9 @@ import java.util.UUID;
 import java.util.concurrent.ExecutionException;
 
 import static junit.framework.TestCase.assertFalse;
-import static org.junit.Assert.*;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
 
 /**
  * Created by Valerii Sosliuk on 5/21/2017.
diff --git a/dao/src/test/java/org/thingsboard/server/dao/sql/component/JpaBaseComponentDescriptorDaoTest.java b/dao/src/test/java/org/thingsboard/server/dao/sql/component/JpaBaseComponentDescriptorDaoTest.java
index 9874eff..cdc5515 100644
--- a/dao/src/test/java/org/thingsboard/server/dao/sql/component/JpaBaseComponentDescriptorDaoTest.java
+++ b/dao/src/test/java/org/thingsboard/server/dao/sql/component/JpaBaseComponentDescriptorDaoTest.java
@@ -41,23 +41,23 @@ public class JpaBaseComponentDescriptorDaoTest extends AbstractJpaDaoTest {
     @Test
     public void findByType() {
         for (int i = 0; i < 20; i++) {
-            createComponentDescriptor(ComponentType.PLUGIN, ComponentScope.SYSTEM, i);
+            createComponentDescriptor(ComponentType.FILTER, ComponentScope.SYSTEM, i);
             createComponentDescriptor(ComponentType.ACTION, ComponentScope.TENANT, i + 20);
         }
 
         TextPageLink pageLink1 = new TextPageLink(15, "COMPONENT_");
-        List<ComponentDescriptor> components1 = componentDescriptorDao.findByTypeAndPageLink(ComponentType.PLUGIN, pageLink1);
+        List<ComponentDescriptor> components1 = componentDescriptorDao.findByTypeAndPageLink(ComponentType.FILTER, pageLink1);
         assertEquals(15, components1.size());
 
         TextPageLink pageLink2 = new TextPageLink(15, "COMPONENT_", components1.get(14).getId().getId(), null);
-        List<ComponentDescriptor> components2 = componentDescriptorDao.findByTypeAndPageLink(ComponentType.PLUGIN, pageLink2);
+        List<ComponentDescriptor> components2 = componentDescriptorDao.findByTypeAndPageLink(ComponentType.FILTER, pageLink2);
         assertEquals(5, components2.size());
     }
 
     @Test
     public void findByTypeAndSocpe() {
         for (int i = 0; i < 20; i++) {
-            createComponentDescriptor(ComponentType.PLUGIN, ComponentScope.SYSTEM, i);
+            createComponentDescriptor(ComponentType.ENRICHMENT, ComponentScope.SYSTEM, i);
             createComponentDescriptor(ComponentType.ACTION, ComponentScope.TENANT, i + 20);
             createComponentDescriptor(ComponentType.FILTER, ComponentScope.SYSTEM, i + 40);
         }
diff --git a/dao/src/test/java/org/thingsboard/server/dao/sql/dashboard/JpaDashboardInfoDaoTest.java b/dao/src/test/java/org/thingsboard/server/dao/sql/dashboard/JpaDashboardInfoDaoTest.java
index aa80339..0d48d43 100644
--- a/dao/src/test/java/org/thingsboard/server/dao/sql/dashboard/JpaDashboardInfoDaoTest.java
+++ b/dao/src/test/java/org/thingsboard/server/dao/sql/dashboard/JpaDashboardInfoDaoTest.java
@@ -20,7 +20,6 @@ import org.junit.Assert;
 import org.junit.Test;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.thingsboard.server.common.data.DashboardInfo;
-import org.thingsboard.server.common.data.id.CustomerId;
 import org.thingsboard.server.common.data.id.DashboardId;
 import org.thingsboard.server.common.data.id.TenantId;
 import org.thingsboard.server.common.data.page.TextPageLink;
diff --git a/dao/src/test/java/org/thingsboard/server/dao/sql/event/JpaBaseEventDaoTest.java b/dao/src/test/java/org/thingsboard/server/dao/sql/event/JpaBaseEventDaoTest.java
index 67841f5..84ae307 100644
--- a/dao/src/test/java/org/thingsboard/server/dao/sql/event/JpaBaseEventDaoTest.java
+++ b/dao/src/test/java/org/thingsboard/server/dao/sql/event/JpaBaseEventDaoTest.java
@@ -36,7 +36,10 @@ import java.util.List;
 import java.util.Optional;
 import java.util.UUID;
 
-import static org.junit.Assert.*;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
 import static org.thingsboard.server.common.data.DataConstants.ALARM;
 import static org.thingsboard.server.common.data.DataConstants.STATS;
 
diff --git a/dao/src/test/java/org/thingsboard/server/dao/sql/tenant/JpaTenantDaoTest.java b/dao/src/test/java/org/thingsboard/server/dao/sql/tenant/JpaTenantDaoTest.java
index 43b87b4..bf36711 100644
--- a/dao/src/test/java/org/thingsboard/server/dao/sql/tenant/JpaTenantDaoTest.java
+++ b/dao/src/test/java/org/thingsboard/server/dao/sql/tenant/JpaTenantDaoTest.java
@@ -27,7 +27,7 @@ import org.thingsboard.server.dao.tenant.TenantDao;
 
 import java.util.List;
 
-import static org.junit.Assert.*;
+import static org.junit.Assert.assertEquals;
 
 /**
  * Created by Valerii Sosliuk on 4/30/2017.
diff --git a/dao/src/test/java/org/thingsboard/server/dao/sql/user/JpaUserDaoTest.java b/dao/src/test/java/org/thingsboard/server/dao/sql/user/JpaUserDaoTest.java
index 67491a2..bacd73d 100644
--- a/dao/src/test/java/org/thingsboard/server/dao/sql/user/JpaUserDaoTest.java
+++ b/dao/src/test/java/org/thingsboard/server/dao/sql/user/JpaUserDaoTest.java
@@ -34,7 +34,8 @@ import java.io.IOException;
 import java.util.List;
 import java.util.UUID;
 
-import static org.junit.Assert.*;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
 import static org.thingsboard.server.dao.model.ModelConstants.NULL_UUID;
 
 /**
diff --git a/dao/src/test/java/org/thingsboard/server/dao/sql/widget/JpaWidgetsBundleDaoTest.java b/dao/src/test/java/org/thingsboard/server/dao/sql/widget/JpaWidgetsBundleDaoTest.java
index 47cf034..12487a8 100644
--- a/dao/src/test/java/org/thingsboard/server/dao/sql/widget/JpaWidgetsBundleDaoTest.java
+++ b/dao/src/test/java/org/thingsboard/server/dao/sql/widget/JpaWidgetsBundleDaoTest.java
@@ -31,7 +31,7 @@ import org.thingsboard.server.dao.widget.WidgetsBundleDao;
 import java.util.List;
 import java.util.UUID;
 
-import static org.junit.Assert.*;
+import static org.junit.Assert.assertEquals;
 import static org.thingsboard.server.dao.model.ModelConstants.NULL_UUID;
 
 /**
diff --git a/dao/src/test/java/org/thingsboard/server/dao/util/BufferedRateLimiterTest.java b/dao/src/test/java/org/thingsboard/server/dao/util/BufferedRateLimiterTest.java
index 67c3ce8..366cefd 100644
--- a/dao/src/test/java/org/thingsboard/server/dao/util/BufferedRateLimiterTest.java
+++ b/dao/src/test/java/org/thingsboard/server/dao/util/BufferedRateLimiterTest.java
@@ -15,7 +15,11 @@
  */
 package org.thingsboard.server.dao.util;
 
-import com.google.common.util.concurrent.*;
+import com.google.common.util.concurrent.FutureCallback;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.ListeningExecutorService;
+import com.google.common.util.concurrent.MoreExecutors;
 import org.junit.Test;
 import org.thingsboard.server.dao.exception.BufferLimitException;
 
@@ -25,7 +29,10 @@ import java.util.concurrent.Executors;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicInteger;
 
-import static org.junit.Assert.*;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
 
 
 public class BufferedRateLimiterTest {
diff --git a/dao/src/test/resources/application-test.properties b/dao/src/test/resources/application-test.properties
index 42b71f8..f2dab45 100644
--- a/dao/src/test/resources/application-test.properties
+++ b/dao/src/test/resources/application-test.properties
@@ -28,3 +28,5 @@ redis.connection.host=localhost
 redis.connection.port=6379
 redis.connection.db=0
 redis.connection.password=
+
+rule.queue.type=memory
diff --git a/dao/src/test/resources/cassandra/system-test.cql b/dao/src/test/resources/cassandra/system-test.cql
index da5d1f1..de4e325 100644
--- a/dao/src/test/resources/cassandra/system-test.cql
+++ b/dao/src/test/resources/cassandra/system-test.cql
@@ -1,2 +1,27 @@
-TRUNCATE thingsboard.plugin;
-TRUNCATE thingsboard.rule;
\ No newline at end of file
+TRUNCATE thingsboard.rule_chain;
+TRUNCATE thingsboard.rule_node;
+
+-- msg_queue dataset
+
+INSERT INTO thingsboard.msg_queue (node_id, cluster_partition, ts_partition, ts, msg)
+    VALUES (055eee50-1883-11e8-b380-65b5d5335ba9, 101, 200, 201, null);
+INSERT INTO thingsboard.msg_queue (node_id, cluster_partition, ts_partition, ts, msg)
+    VALUES (055eee50-1883-11e8-b380-65b5d5335ba9, 101, 200, 202, null);
+INSERT INTO thingsboard.msg_queue (node_id, cluster_partition, ts_partition, ts, msg)
+    VALUES (055eee50-1883-11e8-b380-65b5d5335ba9, 101, 300, 301, null);
+
+-- ack_queue dataset
+INSERT INTO thingsboard.msg_ack_queue (node_id, cluster_partition, ts_partition, msg_id)
+    VALUES (055eee50-1883-11e8-b380-65b5d5335ba9, 101, 300, bebaeb60-1888-11e8-bf21-65b5d5335ba9);
+INSERT INTO thingsboard.msg_ack_queue (node_id, cluster_partition, ts_partition, msg_id)
+    VALUES (055eee50-1883-11e8-b380-65b5d5335ba9, 101, 300, 12baeb60-1888-11e8-bf21-65b5d5335ba9);
+INSERT INTO thingsboard.msg_ack_queue (node_id, cluster_partition, ts_partition, msg_id)
+    VALUES (055eee50-1883-11e8-b380-65b5d5335ba9, 101, 200, 32baeb60-1888-11e8-bf21-65b5d5335ba9);
+
+-- processed partition dataset
+INSERT INTO thingsboard.processed_msg_partitions (node_id, cluster_partition, ts_partition)
+    VALUES (055eee50-1883-11e8-b380-65b5d5335ba9, 101, 100);
+INSERT INTO thingsboard.processed_msg_partitions (node_id, cluster_partition, ts_partition)
+    VALUES (055eee50-1883-11e8-b380-65b5d5335ba9, 101, 777);
+INSERT INTO thingsboard.processed_msg_partitions (node_id, cluster_partition, ts_partition)
+    VALUES (055eee50-1883-11e8-b380-65b5d5335ba9, 202, 200);
\ No newline at end of file
diff --git a/dao/src/test/resources/cassandra-test.properties b/dao/src/test/resources/cassandra-test.properties
index 737687f..cf07b22 100644
--- a/dao/src/test/resources/cassandra-test.properties
+++ b/dao/src/test/resources/cassandra-test.properties
@@ -46,6 +46,8 @@ cassandra.query.default_fetch_size=2000
 
 cassandra.query.ts_key_value_partitioning=HOURS
 
+cassandra.query.ts_key_value_ttl=0
+
 cassandra.query.max_limit_per_request=1000
 cassandra.query.buffer_size=100000
 cassandra.query.concurrent_limit=1000
diff --git a/dao/src/test/resources/cassandra-test.yaml b/dao/src/test/resources/cassandra-test.yaml
index 6463f64..e60f248 100644
--- a/dao/src/test/resources/cassandra-test.yaml
+++ b/dao/src/test/resources/cassandra-test.yaml
@@ -103,6 +103,8 @@ commitlog_directory: target/embeddedCassandra/commitlog
 
 hints_directory: target/embeddedCassandra/hints
 
+cdc_raw_directory: target/embeddedCassandra/cdc
+
 # policy for data disk failures:
 # stop: shut down gossip and Thrift, leaving the node effectively dead, but
 #       can still be inspected via JMX.
diff --git a/dao/src/test/resources/nosql-test.properties b/dao/src/test/resources/nosql-test.properties
index 482c6e7..e37e228 100644
--- a/dao/src/test/resources/nosql-test.properties
+++ b/dao/src/test/resources/nosql-test.properties
@@ -1 +1,6 @@
-database.type=cassandra
\ No newline at end of file
+database.type=cassandra
+
+cassandra.queue.partitioning=HOURS
+cassandra.queue.ack.ttl=3600
+cassandra.queue.msg.ttl=3600
+cassandra.queue.partitions.ttl=3600
\ No newline at end of file
diff --git a/dao/src/test/resources/sql/drop-all-tables.sql b/dao/src/test/resources/sql/drop-all-tables.sql
index dfdc90f..23b6a56 100644
--- a/dao/src/test/resources/sql/drop-all-tables.sql
+++ b/dao/src/test/resources/sql/drop-all-tables.sql
@@ -9,13 +9,13 @@ DROP TABLE IF EXISTS dashboard;
 DROP TABLE IF EXISTS device;
 DROP TABLE IF EXISTS device_credentials;
 DROP TABLE IF EXISTS event;
-DROP TABLE IF EXISTS plugin;
 DROP TABLE IF EXISTS relation;
-DROP TABLE IF EXISTS rule;
 DROP TABLE IF EXISTS tb_user;
 DROP TABLE IF EXISTS tenant;
 DROP TABLE IF EXISTS ts_kv;
 DROP TABLE IF EXISTS ts_kv_latest;
 DROP TABLE IF EXISTS user_credentials;
 DROP TABLE IF EXISTS widget_type;
-DROP TABLE IF EXISTS widgets_bundle;
\ No newline at end of file
+DROP TABLE IF EXISTS widgets_bundle;
+DROP TABLE IF EXISTS rule_node;
+DROP TABLE IF EXISTS rule_chain;
\ No newline at end of file
diff --git a/dao/src/test/resources/sql/system-test.sql b/dao/src/test/resources/sql/system-test.sql
index 57d49dd..e59afd9 100644
--- a/dao/src/test/resources/sql/system-test.sql
+++ b/dao/src/test/resources/sql/system-test.sql
@@ -1,2 +1,2 @@
-TRUNCATE TABLE plugin;
-TRUNCATE TABLE rule;
\ No newline at end of file
+TRUNCATE TABLE rule_node;
+TRUNCATE TABLE rule_chain;
\ No newline at end of file
diff --git a/docker/cassandra/Makefile b/docker/cassandra/Makefile
index adc9c7e..e84be0f 100644
--- a/docker/cassandra/Makefile
+++ b/docker/cassandra/Makefile
@@ -1,4 +1,4 @@
-VERSION=1.4.0
+VERSION=2.0.0
 PROJECT=thingsboard
 APP=cassandra
 
diff --git a/docker/cassandra-setup/Makefile b/docker/cassandra-setup/Makefile
index fbe3ff9..c572987 100644
--- a/docker/cassandra-setup/Makefile
+++ b/docker/cassandra-setup/Makefile
@@ -1,4 +1,4 @@
-VERSION=1.4.0
+VERSION=2.0.0
 PROJECT=thingsboard
 APP=cassandra-setup
 
diff --git a/docker/cluster-mode-thirdparty.yml b/docker/cluster-mode-thirdparty.yml
new file mode 100644
index 0000000..3aa5bb5
--- /dev/null
+++ b/docker/cluster-mode-thirdparty.yml
@@ -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.
+#
+
+version: '3.3'
+services:
+  zookeeper:
+    image: wurstmeister/zookeeper
+    networks:
+      - core
+    ports:
+      - "2181:2181"
+
+  cassandra:
+    image: cassandra:3.11.2
+    networks:
+      - core
+    ports:
+      - "7199:7199"
+      - "9160:9160"
+      - "9042:9042"
+
+  redis:
+    image: redis:4.0
+    networks:
+      - core
+    command: redis-server --maxclients 2000
+    ports:
+      - "6379:6379"
+
+networks:
+  core:
+
diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml
index 6662a8f..28323b7 100644
--- a/docker/docker-compose.yml
+++ b/docker/docker-compose.yml
@@ -18,7 +18,7 @@ version: '2'
 
 services:
   tb:
-    image: "thingsboard/application:1.4.0"
+    image: "thingsboard/application:2.0.0"
     ports:
       - "8080:8080"
       - "1883:1883"
diff --git a/docker/k8s/cassandra.yaml b/docker/k8s/cassandra.yaml
index 15d0e55..d6264ae 100644
--- a/docker/k8s/cassandra.yaml
+++ b/docker/k8s/cassandra.yaml
@@ -54,7 +54,7 @@ spec:
               topologyKey: "kubernetes.io/hostname"
       containers:
       - name: cassandra
-        image: thingsboard/cassandra:1.4.0
+        image: thingsboard/cassandra:2.0.0
         imagePullPolicy: Always
         ports:
         - containerPort: 7000
diff --git a/docker/k8s/cassandra-setup.yaml b/docker/k8s/cassandra-setup.yaml
index bf50ba9..2f07238 100644
--- a/docker/k8s/cassandra-setup.yaml
+++ b/docker/k8s/cassandra-setup.yaml
@@ -22,7 +22,7 @@ spec:
   containers:
   - name: cassandra-setup
     imagePullPolicy: Always
-    image: thingsboard/cassandra-setup:1.4.0
+    image: thingsboard/cassandra-setup:2.0.0
     env:
     - name: ADD_DEMO_DATA
       value: "true"
diff --git a/docker/k8s/tb.yaml b/docker/k8s/tb.yaml
index 1d94cb1..2a4e6f8 100644
--- a/docker/k8s/tb.yaml
+++ b/docker/k8s/tb.yaml
@@ -84,7 +84,7 @@ spec:
       containers:
       - name: tb
         imagePullPolicy: Always
-        image: thingsboard/application:1.4.0
+        image: thingsboard/application:2.0.0
         ports:
         - containerPort: 8080
           name: ui
diff --git a/docker/k8s/zookeeper.yaml b/docker/k8s/zookeeper.yaml
index bea0833..b5c6dce 100644
--- a/docker/k8s/zookeeper.yaml
+++ b/docker/k8s/zookeeper.yaml
@@ -87,7 +87,7 @@ spec:
       containers:
       - name: zk
         imagePullPolicy: Always
-        image: thingsboard/zk:1.4.0
+        image: thingsboard/zk:2.0.0
         ports:
         - containerPort: 2181
           name: client
diff --git a/docker/tb/Makefile b/docker/tb/Makefile
index 05d3f4d..cfff5a4 100644
--- a/docker/tb/Makefile
+++ b/docker/tb/Makefile
@@ -1,4 +1,4 @@
-VERSION=1.4.0
+VERSION=2.0.0
 PROJECT=thingsboard
 APP=application
 
diff --git a/docker/zookeeper/Makefile b/docker/zookeeper/Makefile
index 3386ff8..52b80fb 100644
--- a/docker/zookeeper/Makefile
+++ b/docker/zookeeper/Makefile
@@ -1,4 +1,4 @@
-VERSION=1.4.0
+VERSION=2.0.0
 PROJECT=thingsboard
 APP=zk
 
diff --git a/netty-mqtt/.gitignore b/netty-mqtt/.gitignore
new file mode 100644
index 0000000..4d2302b
--- /dev/null
+++ b/netty-mqtt/.gitignore
@@ -0,0 +1,7 @@
+.idea/
+*.ipr
+*.iws
+*.ids
+*.iml
+logs
+target

netty-mqtt/pom.xml 99(+99 -0)

diff --git a/netty-mqtt/pom.xml b/netty-mqtt/pom.xml
new file mode 100644
index 0000000..411fc53
--- /dev/null
+++ b/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>2.0.0-SNAPSHOT</version>
+        <artifactId>thingsboard</artifactId>
+    </parent>
+    <groupId>org.thingsboard</groupId>
+    <artifactId>netty-mqtt</artifactId>
+    <version>2.0.0-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>
\ No newline at end of file
diff --git a/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttChannelHandler.java b/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttChannelHandler.java
new file mode 100644
index 0000000..ef5e7a5
--- /dev/null
+++ b/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttChannelHandler.java
@@ -0,0 +1,269 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.mqtt;
+
+import com.google.common.collect.ImmutableSet;
+import io.netty.channel.Channel;
+import io.netty.channel.ChannelHandlerContext;
+import io.netty.channel.SimpleChannelInboundHandler;
+import io.netty.handler.codec.mqtt.*;
+import io.netty.util.CharsetUtil;
+import io.netty.util.concurrent.Promise;
+
+final class MqttChannelHandler extends SimpleChannelInboundHandler<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();
+    }
+}
diff --git a/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttClient.java b/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttClient.java
new file mode 100644
index 0000000..6563525
--- /dev/null
+++ b/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttClient.java
@@ -0,0 +1,205 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.mqtt;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.channel.Channel;
+import io.netty.channel.EventLoopGroup;
+import io.netty.channel.nio.NioEventLoopGroup;
+import io.netty.handler.codec.mqtt.MqttQoS;
+import io.netty.util.concurrent.Future;
+
+public interface MqttClient {
+
+    /**
+     * Connect to the specified hostname/ip. By default uses port 1883.
+     * If you want to change the port number, see {@link #connect(String, int)}
+     *
+     * @param host The ip address or host to connect to
+     * @return A future which will be completed when the connection is opened and we received an CONNACK
+     */
+    Future<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);
+
+}
diff --git a/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttClientConfig.java b/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttClientConfig.java
new file mode 100644
index 0000000..a59d83b
--- /dev/null
+++ b/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttClientConfig.java
@@ -0,0 +1,149 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.mqtt;
+
+import io.netty.channel.Channel;
+import io.netty.channel.socket.nio.NioSocketChannel;
+import io.netty.handler.codec.mqtt.MqttVersion;
+import io.netty.handler.ssl.SslContext;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import java.util.Random;
+
+@SuppressWarnings({"WeakerAccess", "unused"})
+public final class MqttClientConfig {
+
+    private final SslContext sslContext;
+    private final String randomClientId;
+
+    private String clientId;
+    private int timeoutSeconds = 60;
+    private MqttVersion protocolVersion = MqttVersion.MQTT_3_1;
+    @Nullable private String username = null;
+    @Nullable private String password = null;
+    private boolean cleanSession = true;
+    @Nullable private MqttLastWill lastWill;
+    private Class<? 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;
+    }
+}
diff --git a/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttClientImpl.java b/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttClientImpl.java
new file mode 100644
index 0000000..3914105
--- /dev/null
+++ b/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttClientImpl.java
@@ -0,0 +1,484 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.mqtt;
+
+import com.google.common.collect.HashMultimap;
+import com.google.common.collect.ImmutableSet;
+import io.netty.bootstrap.Bootstrap;
+import io.netty.buffer.ByteBuf;
+import io.netty.channel.*;
+import io.netty.channel.nio.NioEventLoopGroup;
+import io.netty.channel.socket.SocketChannel;
+import io.netty.handler.codec.mqtt.*;
+import io.netty.handler.ssl.SslContext;
+import io.netty.handler.timeout.IdleStateHandler;
+import io.netty.util.collection.IntObjectHashMap;
+import io.netty.util.concurrent.DefaultPromise;
+import io.netty.util.concurrent.Future;
+import io.netty.util.concurrent.Promise;
+
+import java.util.*;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * Represents an MqttClientImpl connected to a single MQTT server. Will try to keep the connection going at all times
+ */
+@SuppressWarnings({"WeakerAccess", "unused"})
+final class MqttClientImpl implements MqttClient {
+
+    private final Set<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));
+        }
+    }
+}
diff --git a/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttConnectResult.java b/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttConnectResult.java
new file mode 100644
index 0000000..5fa0e6d
--- /dev/null
+++ b/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttConnectResult.java
@@ -0,0 +1,45 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.mqtt;
+
+import io.netty.channel.ChannelFuture;
+import io.netty.handler.codec.mqtt.MqttConnectReturnCode;
+
+@SuppressWarnings({"WeakerAccess", "unused"})
+public final class MqttConnectResult {
+
+    private final boolean success;
+    private final MqttConnectReturnCode returnCode;
+    private final ChannelFuture closeFuture;
+
+    MqttConnectResult(boolean success, MqttConnectReturnCode returnCode, ChannelFuture closeFuture) {
+        this.success = success;
+        this.returnCode = returnCode;
+        this.closeFuture = closeFuture;
+    }
+
+    public boolean isSuccess() {
+        return success;
+    }
+
+    public MqttConnectReturnCode getReturnCode() {
+        return returnCode;
+    }
+
+    public ChannelFuture getCloseFuture() {
+        return closeFuture;
+    }
+}
diff --git a/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttIncomingQos2Publish.java b/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttIncomingQos2Publish.java
new file mode 100644
index 0000000..af84cde
--- /dev/null
+++ b/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttIncomingQos2Publish.java
@@ -0,0 +1,48 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.mqtt;
+
+import io.netty.channel.EventLoop;
+import io.netty.handler.codec.mqtt.*;
+
+import java.util.function.Consumer;
+
+final class MqttIncomingQos2Publish {
+
+    private final MqttPublishMessage incomingPublish;
+
+    private final RetransmissionHandler<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();
+    }
+}
diff --git a/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttLastWill.java b/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttLastWill.java
new file mode 100644
index 0000000..1dadfcd
--- /dev/null
+++ b/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttLastWill.java
@@ -0,0 +1,154 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.mqtt;
+
+import io.netty.handler.codec.mqtt.MqttQoS;
+
+@SuppressWarnings({"WeakerAccess", "unused", "SimplifiableIfStatement", "StringBufferReplaceableByString"})
+public final class MqttLastWill {
+
+    private final String topic;
+    private final String message;
+    private final boolean retain;
+    private final MqttQoS qos;
+
+    public MqttLastWill(String topic, String message, boolean retain, MqttQoS qos) {
+        if(topic == null){
+            throw new NullPointerException("topic");
+        }
+        if(message == null){
+            throw new NullPointerException("message");
+        }
+        if(qos == null){
+            throw new NullPointerException("qos");
+        }
+        this.topic = topic;
+        this.message = message;
+        this.retain = retain;
+        this.qos = qos;
+    }
+
+    public String getTopic() {
+        return topic;
+    }
+
+    public String getMessage() {
+        return message;
+    }
+
+    public boolean isRetain() {
+        return retain;
+    }
+
+    public MqttQoS getQos() {
+        return qos;
+    }
+
+    public static MqttLastWill.Builder builder(){
+        return new MqttLastWill.Builder();
+    }
+
+    public static final class Builder {
+
+        private String topic;
+        private String message;
+        private boolean retain;
+        private MqttQoS qos;
+
+        public String getTopic() {
+            return topic;
+        }
+
+        public Builder setTopic(String topic) {
+            if(topic == null){
+                throw new NullPointerException("topic");
+            }
+            this.topic = topic;
+            return this;
+        }
+
+        public String getMessage() {
+            return message;
+        }
+
+        public Builder setMessage(String message) {
+            if(message == null){
+                throw new NullPointerException("message");
+            }
+            this.message = message;
+            return this;
+        }
+
+        public boolean isRetain() {
+            return retain;
+        }
+
+        public Builder setRetain(boolean retain) {
+            this.retain = retain;
+            return this;
+        }
+
+        public MqttQoS getQos() {
+            return qos;
+        }
+
+        public Builder setQos(MqttQoS qos) {
+            if(qos == null){
+                throw new NullPointerException("qos");
+            }
+            this.qos = qos;
+            return this;
+        }
+
+        public MqttLastWill build(){
+            return new MqttLastWill(topic, message, retain, qos);
+        }
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+
+        MqttLastWill that = (MqttLastWill) o;
+
+        if (retain != that.retain) return false;
+        if (!topic.equals(that.topic)) return false;
+        if (!message.equals(that.message)) return false;
+        return qos == that.qos;
+
+    }
+
+    @Override
+    public int hashCode() {
+        int result = topic.hashCode();
+        result = 31 * result + message.hashCode();
+        result = 31 * result + (retain ? 1 : 0);
+        result = 31 * result + qos.hashCode();
+        return result;
+    }
+
+    @Override
+    public String toString() {
+        final StringBuilder sb = new StringBuilder("MqttLastWill{");
+        sb.append("topic='").append(topic).append('\'');
+        sb.append(", message='").append(message).append('\'');
+        sb.append(", retain=").append(retain);
+        sb.append(", qos=").append(qos.name());
+        sb.append('}');
+        return sb.toString();
+    }
+}
diff --git a/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttPendingPublish.java b/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttPendingPublish.java
new file mode 100644
index 0000000..c656e84
--- /dev/null
+++ b/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttPendingPublish.java
@@ -0,0 +1,101 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.mqtt;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.channel.EventLoop;
+import io.netty.handler.codec.mqtt.MqttMessage;
+import io.netty.handler.codec.mqtt.MqttPublishMessage;
+import io.netty.handler.codec.mqtt.MqttQoS;
+import io.netty.util.concurrent.Promise;
+
+import java.util.function.Consumer;
+
+final class MqttPendingPublish {
+
+    private final int messageId;
+    private final Promise<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();
+    }
+}
diff --git a/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttPendingSubscribtion.java b/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttPendingSubscribtion.java
new file mode 100644
index 0000000..782aef1
--- /dev/null
+++ b/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttPendingSubscribtion.java
@@ -0,0 +1,102 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.mqtt;
+
+import io.netty.channel.EventLoop;
+import io.netty.handler.codec.mqtt.MqttSubscribeMessage;
+import io.netty.util.concurrent.Promise;
+
+import java.util.HashSet;
+import java.util.Set;
+import java.util.function.Consumer;
+
+final class MqttPendingSubscribtion {
+
+    private final Promise<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;
+        }
+    }
+}
diff --git a/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttPendingUnsubscribtion.java b/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttPendingUnsubscribtion.java
new file mode 100644
index 0000000..a626a81
--- /dev/null
+++ b/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttPendingUnsubscribtion.java
@@ -0,0 +1,55 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.mqtt;
+
+import io.netty.channel.EventLoop;
+import io.netty.handler.codec.mqtt.MqttUnsubscribeMessage;
+import io.netty.util.concurrent.Promise;
+
+import java.util.function.Consumer;
+
+final class MqttPendingUnsubscribtion {
+
+    private final Promise<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();
+    }
+}
diff --git a/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttPingHandler.java b/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttPingHandler.java
new file mode 100644
index 0000000..d0fd998
--- /dev/null
+++ b/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttPingHandler.java
@@ -0,0 +1,98 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.mqtt;
+
+import io.netty.channel.Channel;
+import io.netty.channel.ChannelFutureListener;
+import io.netty.channel.ChannelHandlerContext;
+import io.netty.channel.ChannelInboundHandlerAdapter;
+import io.netty.handler.codec.mqtt.MqttFixedHeader;
+import io.netty.handler.codec.mqtt.MqttMessage;
+import io.netty.handler.codec.mqtt.MqttMessageType;
+import io.netty.handler.codec.mqtt.MqttQoS;
+import io.netty.handler.timeout.IdleStateEvent;
+import io.netty.util.ReferenceCountUtil;
+import io.netty.util.concurrent.ScheduledFuture;
+
+import java.util.concurrent.TimeUnit;
+
+final class MqttPingHandler extends ChannelInboundHandlerAdapter {
+
+    private final int keepaliveSeconds;
+
+    private ScheduledFuture<?> pingRespTimeout;
+
+    MqttPingHandler(int keepaliveSeconds) {
+        this.keepaliveSeconds = keepaliveSeconds;
+    }
+
+    @Override
+    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
+        if (!(msg instanceof MqttMessage)) {
+            ctx.fireChannelRead(msg);
+            return;
+        }
+        MqttMessage message = (MqttMessage) msg;
+        if(message.fixedHeader().messageType() == MqttMessageType.PINGREQ){
+            this.handlePingReq(ctx.channel());
+        } else if(message.fixedHeader().messageType() == MqttMessageType.PINGRESP){
+            this.handlePingResp();
+        }else{
+            ctx.fireChannelRead(ReferenceCountUtil.retain(msg));
+        }
+    }
+
+    @Override
+    public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
+        super.userEventTriggered(ctx, evt);
+
+        if(evt instanceof IdleStateEvent){
+            IdleStateEvent event = (IdleStateEvent) evt;
+            switch(event.state()){
+                case READER_IDLE:
+                    break;
+                case WRITER_IDLE:
+                    this.sendPingReq(ctx.channel());
+                    break;
+            }
+        }
+    }
+
+    private void sendPingReq(Channel channel){
+        MqttFixedHeader fixedHeader = new MqttFixedHeader(MqttMessageType.PINGREQ, false, MqttQoS.AT_MOST_ONCE, false, 0);
+        channel.writeAndFlush(new MqttMessage(fixedHeader));
+
+        if(this.pingRespTimeout != null){
+            this.pingRespTimeout = channel.eventLoop().schedule(() -> {
+                MqttFixedHeader fixedHeader2 = new MqttFixedHeader(MqttMessageType.DISCONNECT, false, MqttQoS.AT_MOST_ONCE, false, 0);
+                channel.writeAndFlush(new MqttMessage(fixedHeader2)).addListener(ChannelFutureListener.CLOSE);
+                //TODO: what do when the connection is closed ?
+            }, this.keepaliveSeconds, TimeUnit.SECONDS);
+        }
+    }
+
+    private void handlePingReq(Channel channel){
+        MqttFixedHeader fixedHeader = new MqttFixedHeader(MqttMessageType.PINGRESP, false, MqttQoS.AT_MOST_ONCE, false, 0);
+        channel.writeAndFlush(new MqttMessage(fixedHeader));
+    }
+
+    private void handlePingResp(){
+        if(this.pingRespTimeout != null && !this.pingRespTimeout.isCancelled() && !this.pingRespTimeout.isDone()){
+            this.pingRespTimeout.cancel(true);
+            this.pingRespTimeout = null;
+        }
+    }
+}
diff --git a/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttSubscribtion.java b/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttSubscribtion.java
new file mode 100644
index 0000000..27f4cb9
--- /dev/null
+++ b/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttSubscribtion.java
@@ -0,0 +1,84 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.mqtt;
+
+import java.util.regex.Pattern;
+
+final class MqttSubscribtion {
+
+    private final String topic;
+    private final Pattern topicRegex;
+    private final MqttHandler handler;
+
+    private final boolean once;
+
+    private boolean called;
+
+    MqttSubscribtion(String topic, MqttHandler handler, boolean once) {
+        if(topic == null){
+            throw new NullPointerException("topic");
+        }
+        if(handler == null){
+            throw new NullPointerException("handler");
+        }
+        this.topic = topic;
+        this.handler = handler;
+        this.once = once;
+        this.topicRegex = Pattern.compile(topic.replace("+", "[^/]+").replace("#", ".+") + "$");
+    }
+
+    String getTopic() {
+        return topic;
+    }
+
+    public MqttHandler getHandler() {
+        return handler;
+    }
+
+    boolean isOnce() {
+        return once;
+    }
+
+    boolean isCalled() {
+        return called;
+    }
+
+    boolean matches(String topic){
+        return this.topicRegex.matcher(topic).matches();
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+
+        MqttSubscribtion that = (MqttSubscribtion) o;
+
+        return once == that.once && topic.equals(that.topic) && handler.equals(that.handler);
+    }
+
+    @Override
+    public int hashCode() {
+        int result = topic.hashCode();
+        result = 31 * result + handler.hashCode();
+        result = 31 * result + (once ? 1 : 0);
+        return result;
+    }
+
+    void setCalled(boolean called) {
+        this.called = called;
+    }
+}
diff --git a/netty-mqtt/src/main/java/org/thingsboard/mqtt/RetransmissionHandler.java b/netty-mqtt/src/main/java/org/thingsboard/mqtt/RetransmissionHandler.java
new file mode 100644
index 0000000..36e91e5
--- /dev/null
+++ b/netty-mqtt/src/main/java/org/thingsboard/mqtt/RetransmissionHandler.java
@@ -0,0 +1,66 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.mqtt;
+
+import io.netty.channel.EventLoop;
+import io.netty.handler.codec.mqtt.MqttFixedHeader;
+import io.netty.handler.codec.mqtt.MqttMessage;
+import io.netty.util.concurrent.ScheduledFuture;
+
+import java.util.concurrent.TimeUnit;
+import java.util.function.BiConsumer;
+
+final class RetransmissionHandler<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;
+    }
+}

pom.xml 97(+31 -66)

diff --git a/pom.xml b/pom.xml
index 15950ec..a767696 100755
--- a/pom.xml
+++ b/pom.xml
@@ -20,7 +20,7 @@
     <modelVersion>4.0.0</modelVersion>
     <groupId>org.thingsboard</groupId>
     <artifactId>thingsboard</artifactId>
-    <version>1.4.1-SNAPSHOT</version>
+    <version>2.0.0-SNAPSHOT</version>
     <packaging>pom</packaging>
 
     <name>Thingsboard</name>
@@ -35,16 +35,16 @@
         <spring-data-redis.version>1.8.10.RELEASE</spring-data-redis.version>
         <jedis.version>2.9.0</jedis.version>
         <jjwt.version>0.7.0</jjwt.version>
-         <json-path.version>2.2.0</json-path.version>
+        <json-path.version>2.2.0</json-path.version>
         <junit.version>4.12</junit.version>
         <slf4j.version>1.7.7</slf4j.version>
         <logback.version>1.2.3</logback.version>
         <mockito.version>1.9.5</mockito.version>
         <rat.version>0.10</rat.version>
-        <cassandra.version>3.0.7</cassandra.version>
-        <cassandra-unit.version>3.0.0.1</cassandra-unit.version>
+        <cassandra.version>3.5.0</cassandra.version>
+        <cassandra-unit.version>3.3.0.2</cassandra-unit.version>
         <takari-cpsuite.version>1.2.7</takari-cpsuite.version>
-        <guava.version>18.0</guava.version>
+        <guava.version>21.0</guava.version>
         <caffeine.version>2.6.1</caffeine.version>
         <commons-lang3.version>3.4</commons-lang3.version>
         <commons-validator.version>1.5.0</commons-validator.version>
@@ -59,17 +59,15 @@
         <velocity.version>1.7</velocity.version>
         <velocity-tools.version>2.0</velocity-tools.version>
         <mail.version>1.4.3</mail.version>
-        <curator.version>2.11.0</curator.version>
+        <curator.version>4.0.1</curator.version>
         <protobuf.version>3.0.2</protobuf.version>
-        <grpc.version>1.0.0</grpc.version>
+        <grpc.version>1.12.0</grpc.version>
         <lombok.version>1.16.18</lombok.version>
         <paho.client.version>1.1.0</paho.client.version>
         <netty.version>4.1.22.Final</netty.version>
         <os-maven-plugin.version>1.5.0</os-maven-plugin.version>
         <rabbitmq.version>3.6.5</rabbitmq.version>
         <kafka.version>0.9.0.0</kafka.version>
-        <hazelcast.version>3.6.6</hazelcast.version>
-        <hazelcast-zookeeper.version>3.6.1</hazelcast-zookeeper.version>
         <surfire.version>2.19.1</surfire.version>
         <jar-plugin.version>3.0.2</jar-plugin.version>
         <springfox-swagger.version>2.6.1</springfox-swagger.version>
@@ -79,16 +77,18 @@
         <dbunit.version>2.5.3</dbunit.version>
         <spring-test-dbunit.version>1.2.1</spring-test-dbunit.version>
         <postgresql.driver.version>9.4.1211</postgresql.driver.version>
-        <sonar.exclusions>org/thingsboard/server/gen/**/*, org/thingsboard/server/extensions/core/plugin/telemetry/gen/**/*</sonar.exclusions>
+        <sonar.exclusions>org/thingsboard/server/gen/**/*,
+            org/thingsboard/server/extensions/core/plugin/telemetry/gen/**/*
+        </sonar.exclusions>
         <elasticsearch.version>5.0.2</elasticsearch.version>
+        <delight-nashorn-sandbox.version>0.1.14</delight-nashorn-sandbox.version>
     </properties>
 
     <modules>
+        <module>netty-mqtt</module>
         <module>common</module>
+        <module>rule-engine</module>
         <module>dao</module>
-        <module>extensions-api</module>
-        <module>extensions-core</module>
-        <module>extensions</module>
         <module>transport</module>
         <module>ui</module>
         <module>tools</module>
@@ -281,6 +281,7 @@
                             <exclude>src/sh/**</exclude>
                             <exclude>src/main/scripts/control/**</exclude>
                             <exclude>src/main/scripts/windows/**</exclude>
+                            <exclude>src/main/resources/public/static/rulenode/**</exclude>
                         </excludes>
                         <mapping>
                             <proto>JAVADOC_STYLE</proto>
@@ -321,53 +322,22 @@
         <dependencies>
             <dependency>
                 <groupId>org.thingsboard</groupId>
-                <artifactId>extensions-api</artifactId>
+                <artifactId>netty-mqtt</artifactId>
                 <version>${project.version}</version>
             </dependency>
             <dependency>
-                <groupId>org.thingsboard</groupId>
-                <artifactId>extensions-core</artifactId>
-                <version>${project.version}</version>
-            </dependency>
-            <dependency>
-                <groupId>org.thingsboard.extensions</groupId>
-                <artifactId>extension-rabbitmq</artifactId>
-                <classifier>extension</classifier>
-                <version>${project.version}</version>
-            </dependency>
-            <dependency>
-                <groupId>org.thingsboard.extensions</groupId>
-                <artifactId>extension-rest-api-call</artifactId>
-                <classifier>extension</classifier>
-                <version>${project.version}</version>
-            </dependency>
-            <dependency>
-                <groupId>org.thingsboard.extensions</groupId>
-                <artifactId>extension-kafka</artifactId>
-                <classifier>extension</classifier>
-                <version>${project.version}</version>
-            </dependency>
-            <dependency>
-                <groupId>org.thingsboard.extensions</groupId>
-                <artifactId>extension-mqtt</artifactId>
-                <classifier>extension</classifier>
-                <version>${project.version}</version>
-            </dependency>
-            <dependency>
-                <groupId>org.thingsboard.extensions</groupId>
-                <artifactId>extension-sqs</artifactId>
-                <classifier>extension</classifier>
+                <groupId>org.thingsboard.common</groupId>
+                <artifactId>data</artifactId>
                 <version>${project.version}</version>
             </dependency>
             <dependency>
-                <groupId>org.thingsboard.extensions</groupId>
-                <artifactId>extension-sns</artifactId>
-                <classifier>extension</classifier>
+                <groupId>org.thingsboard.rule-engine</groupId>
+                <artifactId>rule-engine-api</artifactId>
                 <version>${project.version}</version>
             </dependency>
             <dependency>
-                <groupId>org.thingsboard.common</groupId>
-                <artifactId>data</artifactId>
+                <groupId>org.thingsboard.rule-engine</groupId>
+                <artifactId>rule-engine-components</artifactId>
                 <version>${project.version}</version>
             </dependency>
             <dependency>
@@ -554,6 +524,11 @@
                 <version>${netty.version}</version>
             </dependency>
             <dependency>
+                <groupId>io.netty</groupId>
+                <artifactId>netty-codec-mqtt</artifactId>
+                <version>${netty.version}</version>
+            </dependency>
+            <dependency>
                 <groupId>com.datastax.cassandra</groupId>
                 <artifactId>cassandra-driver-core</artifactId>
                 <version>${cassandra.version}</version>
@@ -736,26 +711,11 @@
                 <version>${paho.client.version}</version>
             </dependency>
             <dependency>
-                <groupId>com.hazelcast</groupId>
-                <artifactId>hazelcast-spring</artifactId>
-                <version>${hazelcast.version}</version>
-            </dependency>
-            <dependency>
                 <groupId>org.apache.curator</groupId>
                 <artifactId>curator-x-discovery</artifactId>
                 <version>${curator.version}</version>
             </dependency>
             <dependency>
-                <groupId>com.hazelcast</groupId>
-                <artifactId>hazelcast-zookeeper</artifactId>
-                <version>${hazelcast-zookeeper.version}</version>
-            </dependency>
-            <dependency>
-                <groupId>com.hazelcast</groupId>
-                <artifactId>hazelcast</artifactId>
-                <version>${hazelcast.version}</version>
-            </dependency>
-            <dependency>
                 <groupId>io.springfox</groupId>
                 <artifactId>springfox-swagger-ui</artifactId>
                 <version>${springfox-swagger.version}</version>
@@ -809,6 +769,11 @@
                 <artifactId>rest</artifactId>
                 <version>${elasticsearch.version}</version>
             </dependency>
+            <dependency>
+                <groupId>org.javadelight</groupId>
+                <artifactId>delight-nashorn-sandbox</artifactId>
+                <version>${delight-nashorn-sandbox.version}</version>
+            </dependency>
         </dependencies>
     </dependencyManagement>
 

rule-engine/pom.xml 41(+41 -0)

diff --git a/rule-engine/pom.xml b/rule-engine/pom.xml
new file mode 100644
index 0000000..0ee609c
--- /dev/null
+++ b/rule-engine/pom.xml
@@ -0,0 +1,41 @@
+<!--
+
+    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>2.0.0-SNAPSHOT</version>
+        <artifactId>thingsboard</artifactId>
+    </parent>
+    <artifactId>rule-engine</artifactId>
+    <packaging>pom</packaging>
+
+    <name>Thingsboard Extensions</name>
+    <url>https://thingsboard.io</url>
+
+    <properties>
+        <main.dir>${basedir}/..</main.dir>
+    </properties>
+
+    <modules>
+        <module>rule-engine-api</module>
+        <module>rule-engine-components</module>
+    </modules>
+
+</project>
diff --git a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/EmptyNodeConfiguration.java b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/EmptyNodeConfiguration.java
new file mode 100644
index 0000000..9553b13
--- /dev/null
+++ b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/EmptyNodeConfiguration.java
@@ -0,0 +1,35 @@
+/**
+ * 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.api;
+
+import lombok.Data;
+import org.thingsboard.rule.engine.api.NodeConfiguration;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+@Data
+public class EmptyNodeConfiguration implements NodeConfiguration<EmptyNodeConfiguration> {
+
+    private int version;
+
+    @Override
+    public EmptyNodeConfiguration defaultConfiguration() {
+        EmptyNodeConfiguration configuration = new EmptyNodeConfiguration();
+        return configuration;
+    }
+}
diff --git a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/NodeDefinition.java b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/NodeDefinition.java
new file mode 100644
index 0000000..7852715
--- /dev/null
+++ b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/NodeDefinition.java
@@ -0,0 +1,38 @@
+/**
+ * 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.api;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import lombok.Data;
+
+@Data
+public class NodeDefinition {
+
+    private String details;
+    private String description;
+    private boolean inEnabled;
+    private boolean outEnabled;
+    String[] relationTypes;
+    boolean customRelations;
+    JsonNode defaultConfiguration;
+    String[] uiResources;
+    String configDirective;
+    String icon;
+    String iconUrl;
+    String docUrl;
+
+}
diff --git a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleEngineDeviceRpcRequest.java b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleEngineDeviceRpcRequest.java
new file mode 100644
index 0000000..5ea481a
--- /dev/null
+++ b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleEngineDeviceRpcRequest.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.api;
+
+import lombok.Builder;
+import lombok.Data;
+import org.thingsboard.server.common.data.id.DeviceId;
+
+/**
+ * Created by ashvayka on 02.04.18.
+ */
+@Data
+@Builder
+public final class RuleEngineDeviceRpcRequest {
+
+    private final DeviceId deviceId;
+    private final int requestId;
+    private final boolean oneway;
+    private final String method;
+    private final String body;
+    private final long timeout;
+
+}
diff --git a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleEngineTelemetryService.java b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleEngineTelemetryService.java
new file mode 100644
index 0000000..aa57a02
--- /dev/null
+++ b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleEngineTelemetryService.java
@@ -0,0 +1,44 @@
+/**
+ * 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.api;
+
+import com.google.common.util.concurrent.FutureCallback;
+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 java.util.List;
+
+/**
+ * Created by ashvayka on 02.04.18.
+ */
+public interface RuleEngineTelemetryService {
+
+    void saveAndNotify(EntityId entityId, List<TsKvEntry> ts, FutureCallback<Void> callback);
+
+    void saveAndNotify(EntityId entityId, List<TsKvEntry> ts, long ttl, FutureCallback<Void> callback);
+
+    void saveAndNotify(EntityId entityId, String scope, List<AttributeKvEntry> attributes, FutureCallback<Void> callback);
+
+    void saveAttrAndNotify(EntityId entityId, String scope, String key, long value, FutureCallback<Void> callback);
+
+    void saveAttrAndNotify(EntityId entityId, String scope, String key, String value, FutureCallback<Void> callback);
+
+    void saveAttrAndNotify(EntityId entityId, String scope, String key, double value, FutureCallback<Void> callback);
+
+    void saveAttrAndNotify(EntityId entityId, String scope, String key, boolean value, FutureCallback<Void> callback);
+
+}
diff --git a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java
new file mode 100644
index 0000000..a3d6db4
--- /dev/null
+++ b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java
@@ -0,0 +1,95 @@
+/**
+ * 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.api;
+
+import org.thingsboard.server.common.data.id.EntityId;
+import org.thingsboard.server.common.data.id.RuleNodeId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.rule.RuleNode;
+import org.thingsboard.server.common.msg.TbMsg;
+import org.thingsboard.server.common.msg.TbMsgMetaData;
+import org.thingsboard.server.dao.alarm.AlarmService;
+import org.thingsboard.server.dao.asset.AssetService;
+import org.thingsboard.server.dao.attributes.AttributesService;
+import org.thingsboard.server.dao.customer.CustomerService;
+import org.thingsboard.server.dao.device.DeviceService;
+import org.thingsboard.server.dao.relation.RelationService;
+import org.thingsboard.server.dao.rule.RuleChainService;
+import org.thingsboard.server.dao.timeseries.TimeseriesService;
+import org.thingsboard.server.dao.user.UserService;
+
+import java.util.Set;
+
+/**
+ * Created by ashvayka on 13.01.18.
+ */
+public interface TbContext {
+
+    void tellNext(TbMsg msg, String relationType);
+
+    void tellNext(TbMsg msg, String relationType, Throwable th);
+
+    void tellNext(TbMsg msg, Set<String> relationTypes);
+
+    void tellSelf(TbMsg msg, long delayMs);
+
+    void tellFailure(TbMsg msg, Throwable th);
+
+    void updateSelf(RuleNode self);
+
+    TbMsg newMsg(String type, EntityId originator, TbMsgMetaData metaData, String data);
+
+    TbMsg transformMsg(TbMsg origMsg, String type, EntityId originator, TbMsgMetaData metaData, String data);
+
+    RuleNodeId getSelfId();
+
+    TenantId getTenantId();
+
+    AttributesService getAttributesService();
+
+    CustomerService getCustomerService();
+
+    UserService getUserService();
+
+    AssetService getAssetService();
+
+    DeviceService getDeviceService();
+
+    AlarmService getAlarmService();
+
+    RuleChainService getRuleChainService();
+
+    RuleEngineRpcService getRpcService();
+
+    RuleEngineTelemetryService getTelemetryService();
+
+    TimeseriesService getTimeseriesService();
+
+    RelationService getRelationService();
+
+    ListeningExecutor getJsExecutor();
+
+    ListeningExecutor getMailExecutor();
+
+    ListeningExecutor getDbCallbackExecutor();
+
+    ListeningExecutor getExternalCallExecutor();
+
+    MailService getMailService();
+
+    ScriptEngine createJsScriptEngine(String script, String... argNames);
+
+}
diff --git a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/util/DonAsynchron.java b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/util/DonAsynchron.java
new file mode 100644
index 0000000..81220f6
--- /dev/null
+++ b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/util/DonAsynchron.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.api.util;
+
+import com.google.common.util.concurrent.FutureCallback;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+
+import javax.annotation.Nullable;
+import java.util.concurrent.Executor;
+import java.util.function.Consumer;
+
+public class DonAsynchron {
+
+    public static  <T> void withCallback(ListenableFuture<T> future, Consumer<T> onSuccess,
+                                         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
+            public void onSuccess(@Nullable T result) {
+                try {
+                    onSuccess.accept(result);
+                } catch (Throwable th) {
+                    onFailure(th);
+                }
+            }
+
+            @Override
+            public void onFailure(Throwable t) {
+                onFailure.accept(t);
+            }
+        };
+        if (executor != null) {
+            Futures.addCallback(future, callback, executor);
+        } else {
+            Futures.addCallback(future, callback);
+        }
+    }
+}
diff --git a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/util/TbNodeUtils.java b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/util/TbNodeUtils.java
new file mode 100644
index 0000000..dff7cf4
--- /dev/null
+++ b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/util/TbNodeUtils.java
@@ -0,0 +1,57 @@
+/**
+ * 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.api.util;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.thingsboard.rule.engine.api.TbNodeConfiguration;
+import org.thingsboard.rule.engine.api.TbNodeException;
+import org.thingsboard.server.common.msg.TbMsgMetaData;
+
+import java.util.Map;
+
+/**
+ * Created by ashvayka on 19.01.18.
+ */
+public class TbNodeUtils {
+
+    private static final ObjectMapper mapper = new ObjectMapper();
+
+    private static final String VARIABLE_TEMPLATE = "${%s}";
+
+
+    public static <T> T convert(TbNodeConfiguration configuration, Class<T> clazz) throws TbNodeException {
+        try {
+            return mapper.treeToValue(configuration.getData(), clazz);
+        } catch (JsonProcessingException e) {
+            throw new TbNodeException(e);
+        }
+    }
+
+    public static String processPattern(String pattern, TbMsgMetaData metaData) {
+        String result = new String(pattern);
+        for (Map.Entry<String,String> keyVal  : metaData.values().entrySet()) {
+            result = processVar(result, keyVal.getKey(), keyVal.getValue());
+        }
+        return result;
+    }
+
+    private static String processVar(String pattern, String key, String val) {
+        String varPattern = String.format(VARIABLE_TEMPLATE, key);
+        return pattern.replace(varPattern, val);
+    }
+
+}
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAbstractAlarmNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAbstractAlarmNode.java
new file mode 100644
index 0000000..1fa2350
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAbstractAlarmNode.java
@@ -0,0 +1,117 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.rule.engine.action;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.google.common.util.concurrent.ListenableFuture;
+import lombok.extern.slf4j.Slf4j;
+import org.thingsboard.rule.engine.api.*;
+import org.thingsboard.server.common.data.alarm.Alarm;
+import org.thingsboard.server.common.msg.TbMsg;
+import org.thingsboard.server.common.msg.TbMsgMetaData;
+
+import static org.thingsboard.rule.engine.api.util.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());
+    }
+
+    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.tellFailure(msg, t), ctx.getDbCallbackExecutor());
+    }
+
+    protected abstract ListenableFuture<AlarmResult> processAlarm(TbContext ctx, TbMsg msg);
+
+    protected ListenableFuture<JsonNode> buildAlarmDetails(TbContext ctx, TbMsg msg, JsonNode previousDetails) {
+        return ctx.getJsExecutor().executeAsync(() -> {
+            TbMsg dummyMsg = msg;
+            if (previousDetails != null) {
+                TbMsgMetaData metaData = msg.getMetaData().copy();
+                metaData.putValue(PREV_ALARM_DETAILS, mapper.writeValueAsString(previousDetails));
+                dummyMsg = ctx.transformMsg(msg, msg.getType(), msg.getOriginator(), metaData, msg.getData());
+            }
+            return buildDetailsJsEngine.executeJson(dummyMsg);
+        });
+    }
+
+    private TbMsg toAlarmMsg(TbContext ctx, AlarmResult alarmResult, TbMsg originalMsg) {
+        JsonNode jsonNodes = mapper.valueToTree(alarmResult.alarm);
+        String data = jsonNodes.toString();
+        TbMsgMetaData metaData = originalMsg.getMetaData().copy();
+        if (alarmResult.isCreated) {
+            metaData.putValue(IS_NEW_ALARM, Boolean.TRUE.toString());
+        } else if (alarmResult.isUpdated) {
+            metaData.putValue(IS_EXISTING_ALARM, Boolean.TRUE.toString());
+        } else if (alarmResult.isCleared) {
+            metaData.putValue(IS_CLEARED_ALARM, Boolean.TRUE.toString());
+        }
+        return ctx.transformMsg(originalMsg, "ALARM", originalMsg.getOriginator(), metaData, data);
+    }
+
+
+    @Override
+    public void destroy() {
+        if (buildDetailsJsEngine != null) {
+            buildDetailsJsEngine.destroy();
+        }
+    }
+
+    protected static class AlarmResult {
+        boolean isCreated;
+        boolean isUpdated;
+        boolean isCleared;
+        Alarm alarm;
+
+        AlarmResult(boolean isCreated, boolean isUpdated, boolean isCleared, Alarm alarm) {
+            this.isCreated = isCreated;
+            this.isUpdated = isUpdated;
+            this.isCleared = isCleared;
+            this.alarm = alarm;
+        }
+    }
+}
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbClearAlarmNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbClearAlarmNode.java
new file mode 100644
index 0000000..2d722f5
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbClearAlarmNode.java
@@ -0,0 +1,79 @@
+/**
+ * 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.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import lombok.extern.slf4j.Slf4j;
+import org.thingsboard.rule.engine.api.util.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.transformAsync(latest, 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.transformAsync(asyncDetails, details -> {
+            ListenableFuture<Boolean> clearFuture = ctx.getAlarmService().clearAlarm(alarm.getId(), details, System.currentTimeMillis());
+            return Futures.transformAsync(clearFuture, cleared -> {
+                if (cleared && details != null) {
+                    alarm.setDetails(details);
+                }
+                alarm.setStatus(alarm.getStatus().isAck() ? AlarmStatus.CLEARED_ACK : AlarmStatus.CLEARED_UNACK);
+                return Futures.immediateFuture(new AlarmResult(false, false, true, alarm));
+            });
+        }, ctx.getDbCallbackExecutor());
+    }
+}
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbClearAlarmNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbClearAlarmNodeConfiguration.java
new file mode 100644
index 0000000..f3f3072
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbClearAlarmNodeConfiguration.java
@@ -0,0 +1,36 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.rule.engine.action;
+
+import lombok.Data;
+import org.thingsboard.rule.engine.api.NodeConfiguration;
+import org.thingsboard.server.common.data.alarm.AlarmSeverity;
+
+@Data
+public class TbClearAlarmNodeConfiguration extends TbAbstractAlarmNodeConfiguration implements NodeConfiguration<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;
+    }
+}
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbCreateAlarmNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbCreateAlarmNode.java
new file mode 100644
index 0000000..a660c93
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbCreateAlarmNode.java
@@ -0,0 +1,105 @@
+/**
+ * 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.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import lombok.extern.slf4j.Slf4j;
+import org.thingsboard.rule.engine.api.util.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.transformAsync(latest, 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),
+                details -> buildAlarm(msg, details, ctx.getTenantId()));
+        ListenableFuture<Alarm> asyncCreated = Futures.transform(asyncAlarm,
+                alarm -> ctx.getAlarmService().createOrUpdateAlarm(alarm), ctx.getDbCallbackExecutor());
+        return Futures.transform(asyncCreated, 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, a -> new AlarmResult(false, true, false, a));
+    }
+
+    private Alarm buildAlarm(TbMsg msg, JsonNode details, TenantId tenantId) {
+        return Alarm.builder()
+                .tenantId(tenantId)
+                .originator(msg.getOriginator())
+                .status(AlarmStatus.ACTIVE_UNACK)
+                .severity(config.getSeverity())
+                .propagate(config.isPropagate())
+                .type(config.getAlarmType())
+                //todo-vp: alarm date should be taken from Message or current Time should be used?
+//                .startTs(System.currentTimeMillis())
+//                .endTs(System.currentTimeMillis())
+                .details(details)
+                .build();
+    }
+
+}
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbCreateAlarmNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbCreateAlarmNodeConfiguration.java
new file mode 100644
index 0000000..b424794
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbCreateAlarmNodeConfiguration.java
@@ -0,0 +1,41 @@
+/**
+ * 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 TbCreateAlarmNodeConfiguration extends TbAbstractAlarmNodeConfiguration implements NodeConfiguration<TbCreateAlarmNodeConfiguration> {
+
+    private AlarmSeverity severity;
+    private boolean propagate;
+
+    @Override
+    public TbCreateAlarmNodeConfiguration defaultConfiguration() {
+        TbCreateAlarmNodeConfiguration configuration = new TbCreateAlarmNodeConfiguration();
+        configuration.setAlarmDetailsBuildJs("var details = {};\n" +
+                "if (metadata.prevAlarmDetails) {\n" +
+                "    details = JSON.parse(metadata.prevAlarmDetails);\n" +
+                "}\n"+
+                "return details;");
+        configuration.setAlarmType("General Alarm");
+        configuration.setSeverity(AlarmSeverity.CRITICAL);
+        configuration.setPropagate(false);
+        return configuration;
+    }
+}
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbLogNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbLogNode.java
new file mode 100644
index 0000000..7cab0c2
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbLogNode.java
@@ -0,0 +1,69 @@
+/**
+ * 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.extern.slf4j.Slf4j;
+import org.thingsboard.rule.engine.api.util.TbNodeUtils;
+import org.thingsboard.rule.engine.api.*;
+import org.thingsboard.server.common.data.plugin.ComponentType;
+import org.thingsboard.server.common.msg.TbMsg;
+
+import static org.thingsboard.rule.engine.api.util.DonAsynchron.withCallback;
+import static org.thingsboard.rule.engine.api.TbRelationTypes.SUCCESS;
+
+@Slf4j
+@RuleNode(
+        type = ComponentType.ACTION,
+        name = "log",
+        configClazz = TbLogNodeConfiguration.class,
+        nodeDescription = "Log incoming messages using JS script for transformation Message into String",
+        nodeDetails = "Transform incoming Message with configured JS function to String and log final value into Thingsboard log file. " +
+                "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 = "tbActionNodeLogConfig",
+        icon = "menu"
+)
+
+public class TbLogNode implements TbNode {
+
+    private TbLogNodeConfiguration config;
+    private ScriptEngine jsEngine;
+
+    @Override
+    public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
+        this.config = TbNodeUtils.convert(configuration, TbLogNodeConfiguration.class);
+        this.jsEngine = ctx.createJsScriptEngine(config.getJsScript());
+    }
+
+    @Override
+    public void onMsg(TbContext ctx, TbMsg msg) {
+        ListeningExecutor jsExecutor = ctx.getJsExecutor();
+        withCallback(jsExecutor.executeAsync(() -> jsEngine.executeToString(msg)),
+                toString -> {
+                    log.info(toString);
+                    ctx.tellNext(msg, SUCCESS);
+                },
+                t -> ctx.tellFailure(msg, t));
+    }
+
+    @Override
+    public void destroy() {
+        if (jsEngine != null) {
+            jsEngine.destroy();
+        }
+    }
+}
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/aws/sns/TbSnsNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/aws/sns/TbSnsNode.java
new file mode 100644
index 0000000..7393099
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/aws/sns/TbSnsNode.java
@@ -0,0 +1,120 @@
+/**
+ * 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.aws.sns;
+
+import com.amazonaws.auth.AWSCredentials;
+import com.amazonaws.auth.AWSStaticCredentialsProvider;
+import com.amazonaws.auth.BasicAWSCredentials;
+import com.amazonaws.services.sns.AmazonSNS;
+import com.amazonaws.services.sns.AmazonSNSClient;
+import com.amazonaws.services.sns.model.PublishRequest;
+import com.amazonaws.services.sns.model.PublishResult;
+import com.google.common.util.concurrent.ListenableFuture;
+import lombok.extern.slf4j.Slf4j;
+import org.thingsboard.rule.engine.api.util.TbNodeUtils;
+import org.thingsboard.rule.engine.api.*;
+import org.thingsboard.server.common.data.plugin.ComponentType;
+import org.thingsboard.server.common.msg.TbMsg;
+import org.thingsboard.server.common.msg.TbMsgMetaData;
+
+import java.util.concurrent.ExecutionException;
+
+import static org.thingsboard.rule.engine.api.util.DonAsynchron.withCallback;
+
+@Slf4j
+@RuleNode(
+        type = ComponentType.EXTERNAL,
+        name = "aws sns",
+        configClazz = TbSnsNodeConfiguration.class,
+        nodeDescription = "Publish message to the AWS SNS",
+        nodeDetails = "Will publish message payload to the AWS SNS topic. Outbound message will contain response fields " +
+                "(<code>messageId</code>, <code>requestId</code>) in the Message Metadata from the AWS SNS. " +
+                "For example <b>requestId</b> field can be accessed with <code>metadata.requestId</code>.",
+        uiResources = {"static/rulenode/rulenode-core-config.js"},
+        configDirective = "tbActionNodeSnsConfig",
+        iconUrl = "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgd2lkdGg9IjQ4IiBoZWlnaHQ9IjQ4Ij48cGF0aCBkPSJNMTMuMjMgMTAuNTZWMTBjLTEuOTQgMC0zLjk5LjM5LTMuOTkgMi42NyAwIDEuMTYuNjEgMS45NSAxLjYzIDEuOTUuNzYgMCAxLjQzLS40NyAxLjg2LTEuMjIuNTItLjkzLjUtMS44LjUtMi44NG0yLjcgNi41M2MtLjE4LjE2LS40My4xNy0uNjMuMDYtLjg5LS43NC0xLjA1LTEuMDgtMS41NC0xLjc5LTEuNDcgMS41LTIuNTEgMS45NS00LjQyIDEuOTUtMi4yNSAwLTQuMDEtMS4zOS00LjAxLTQuMTcgMC0yLjE4IDEuMTctMy42NCAyLjg2LTQuMzggMS40Ni0uNjQgMy40OS0uNzYgNS4wNC0uOTNWNy41YzAtLjY2LjA1LTEuNDEtLjMzLTEuOTYtLjMyLS40OS0uOTUtLjctMS41LS43LTEuMDIgMC0xLjkzLjUzLTIuMTUgMS42MS0uMDUuMjQtLjI1LjQ4LS40Ny40OWwtMi42LS4yOGMtLjIyLS4wNS0uNDYtLjIyLS40LS41Ni42LTMuMTUgMy40NS00LjEgNi00LjEgMS4zIDAgMyAuMzUgNC4wMyAxLjMzQzE3LjExIDQuNTUgMTcgNi4xOCAxNyA3Ljk1djQuMTdjMCAxLjI1LjUgMS44MSAxIDIuNDguMTcuMjUuMjEuNTQgMCAuNzFsLTIuMDYgMS43OGgtLjAxIj48L3BhdGg+PHBhdGggZD0iTTIwLjE2IDE5LjU0QzE4IDIxLjE0IDE0LjgyIDIyIDEyLjEgMjJjLTMuODEgMC03LjI1LTEuNDEtOS44NS0zLjc2LS4yLS4xOC0uMDItLjQzLjI1LS4yOSAyLjc4IDEuNjMgNi4yNSAyLjYxIDkuODMgMi42MSAyLjQxIDAgNS4wNy0uNSA3LjUxLTEuNTMuMzctLjE2LjY2LjI0LjMyLjUxIj48L3BhdGg+PHBhdGggZD0iTTIxLjA3IDE4LjVjLS4yOC0uMzYtMS44NS0uMTctMi41Ny0uMDgtLjE5LjAyLS4yMi0uMTYtLjAzLS4zIDEuMjQtLjg4IDMuMjktLjYyIDMuNTMtLjMzLjI0LjMtLjA3IDIuMzUtMS4yNCAzLjMyLS4xOC4xNi0uMzUuMDctLjI2LS4xMS4yNi0uNjcuODUtMi4xNC41Ny0yLjV6Ij48L3BhdGg+PC9zdmc+"
+)
+public class TbSnsNode implements TbNode {
+
+    private static final String MESSAGE_ID = "messageId";
+    private static final String REQUEST_ID = "requestId";
+    private static final String ERROR = "error";
+
+    private TbSnsNodeConfiguration config;
+    private AmazonSNS snsClient;
+
+    @Override
+    public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
+        this.config = TbNodeUtils.convert(configuration, TbSnsNodeConfiguration.class);
+        AWSCredentials awsCredentials = new BasicAWSCredentials(this.config.getAccessKeyId(), this.config.getSecretAccessKey());
+        AWSStaticCredentialsProvider credProvider = new AWSStaticCredentialsProvider(awsCredentials);
+        try {
+            this.snsClient = AmazonSNSClient.builder()
+                    .withCredentials(credProvider)
+                    .withRegion(this.config.getRegion())
+                    .build();
+        } catch (Exception e) {
+            throw new TbNodeException(e);
+        }
+    }
+
+    @Override
+    public void onMsg(TbContext ctx, TbMsg msg) throws ExecutionException, InterruptedException, TbNodeException {
+        withCallback(publishMessageAsync(ctx, msg),
+                m -> ctx.tellNext(m, TbRelationTypes.SUCCESS),
+                t -> {
+                    TbMsg next = processException(ctx, msg, t);
+                    ctx.tellFailure(next, t);
+                });
+    }
+
+    private ListenableFuture<TbMsg> publishMessageAsync(TbContext ctx, TbMsg msg) {
+        return ctx.getExternalCallExecutor().executeAsync(() -> publishMessage(ctx, msg));
+    }
+
+    private TbMsg publishMessage(TbContext ctx, TbMsg msg) {
+        String topicArn = TbNodeUtils.processPattern(this.config.getTopicArnPattern(), msg.getMetaData());
+        PublishRequest publishRequest = new PublishRequest()
+                .withTopicArn(topicArn)
+                .withMessage(msg.getData());
+        PublishResult result = this.snsClient.publish(publishRequest);
+        return processPublishResult(ctx, msg, result);
+    }
+
+    private TbMsg processPublishResult(TbContext ctx, TbMsg origMsg, PublishResult result) {
+        TbMsgMetaData metaData = origMsg.getMetaData().copy();
+        metaData.putValue(MESSAGE_ID, result.getMessageId());
+        metaData.putValue(REQUEST_ID, result.getSdkResponseMetadata().getRequestId());
+        return ctx.transformMsg(origMsg, origMsg.getType(), origMsg.getOriginator(), metaData, origMsg.getData());
+    }
+
+    private TbMsg processException(TbContext ctx, TbMsg origMsg, Throwable t) {
+        TbMsgMetaData metaData = origMsg.getMetaData().copy();
+        metaData.putValue(ERROR, t.getClass() + ": " + t.getMessage());
+        return ctx.transformMsg(origMsg, origMsg.getType(), origMsg.getOriginator(), metaData, origMsg.getData());
+    }
+
+    @Override
+    public void destroy() {
+        if (this.snsClient != null) {
+            try {
+                this.snsClient.shutdown();
+            } catch (Exception e) {
+                log.error("Failed to shutdown SNS client during destroy()", e);
+            }
+        }
+    }
+}
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/aws/sns/TbSnsNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/aws/sns/TbSnsNodeConfiguration.java
new file mode 100644
index 0000000..a124778
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/aws/sns/TbSnsNodeConfiguration.java
@@ -0,0 +1,37 @@
+/**
+ * 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.aws.sns;
+
+import lombok.Data;
+import org.thingsboard.rule.engine.api.NodeConfiguration;
+
+@Data
+public class TbSnsNodeConfiguration implements NodeConfiguration<TbSnsNodeConfiguration> {
+
+    private String topicArnPattern;
+    private String accessKeyId;
+    private String secretAccessKey;
+    private String region;
+
+    @Override
+    public TbSnsNodeConfiguration defaultConfiguration() {
+        TbSnsNodeConfiguration configuration = new TbSnsNodeConfiguration();
+        configuration.setTopicArnPattern("arn:aws:sns:us-east-1:123456789012:MyNewTopic");
+        configuration.setRegion("us-east-1");
+        return configuration;
+    }
+}
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/aws/sqs/TbSqsNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/aws/sqs/TbSqsNode.java
new file mode 100644
index 0000000..fd67605
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/aws/sqs/TbSqsNode.java
@@ -0,0 +1,151 @@
+/**
+ * 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.aws.sqs;
+
+import com.amazonaws.auth.AWSCredentials;
+import com.amazonaws.auth.AWSStaticCredentialsProvider;
+import com.amazonaws.auth.BasicAWSCredentials;
+import com.amazonaws.services.sqs.AmazonSQS;
+import com.amazonaws.services.sqs.AmazonSQSClientBuilder;
+import com.amazonaws.services.sqs.model.MessageAttributeValue;
+import com.amazonaws.services.sqs.model.SendMessageRequest;
+import com.amazonaws.services.sqs.model.SendMessageResult;
+import com.google.common.util.concurrent.ListenableFuture;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
+import org.thingsboard.rule.engine.api.util.TbNodeUtils;
+import org.thingsboard.rule.engine.api.*;
+import org.thingsboard.server.common.data.plugin.ComponentType;
+import org.thingsboard.server.common.msg.TbMsg;
+import org.thingsboard.server.common.msg.TbMsgMetaData;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.ExecutionException;
+
+import static org.thingsboard.rule.engine.api.util.DonAsynchron.withCallback;
+
+@Slf4j
+@RuleNode(
+        type = ComponentType.EXTERNAL,
+        name = "aws sqs",
+        configClazz = TbSqsNodeConfiguration.class,
+        nodeDescription = "Publish messages to the AWS SQS",
+        nodeDetails = "Will publish message payload and metadata attributes to the AWS SQS queue. Outbound message will contain " +
+                "response fields (<code>messageId</code>, <code>requestId</code>, <code>messageBodyMd5</code>, <code>messageAttributesMd5</code>" +
+                ", <code>sequenceNumber</code>) in the Message Metadata from the AWS SQS." +
+                " For example <b>requestId</b> field can be accessed with <code>metadata.requestId</code>.",
+        uiResources = {"static/rulenode/rulenode-core-config.js"},
+        configDirective = "tbActionNodeSqsConfig",
+        iconUrl = "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgd2lkdGg9IjQ4IiBoZWlnaHQ9IjQ4Ij48cGF0aCBkPSJNMTMuMjMgMTAuNTZWMTBjLTEuOTQgMC0zLjk5LjM5LTMuOTkgMi42NyAwIDEuMTYuNjEgMS45NSAxLjYzIDEuOTUuNzYgMCAxLjQzLS40NyAxLjg2LTEuMjIuNTItLjkzLjUtMS44LjUtMi44NG0yLjcgNi41M2MtLjE4LjE2LS40My4xNy0uNjMuMDYtLjg5LS43NC0xLjA1LTEuMDgtMS41NC0xLjc5LTEuNDcgMS41LTIuNTEgMS45NS00LjQyIDEuOTUtMi4yNSAwLTQuMDEtMS4zOS00LjAxLTQuMTcgMC0yLjE4IDEuMTctMy42NCAyLjg2LTQuMzggMS40Ni0uNjQgMy40OS0uNzYgNS4wNC0uOTNWNy41YzAtLjY2LjA1LTEuNDEtLjMzLTEuOTYtLjMyLS40OS0uOTUtLjctMS41LS43LTEuMDIgMC0xLjkzLjUzLTIuMTUgMS42MS0uMDUuMjQtLjI1LjQ4LS40Ny40OWwtMi42LS4yOGMtLjIyLS4wNS0uNDYtLjIyLS40LS41Ni42LTMuMTUgMy40NS00LjEgNi00LjEgMS4zIDAgMyAuMzUgNC4wMyAxLjMzQzE3LjExIDQuNTUgMTcgNi4xOCAxNyA3Ljk1djQuMTdjMCAxLjI1LjUgMS44MSAxIDIuNDguMTcuMjUuMjEuNTQgMCAuNzFsLTIuMDYgMS43OGgtLjAxIj48L3BhdGg+PHBhdGggZD0iTTIwLjE2IDE5LjU0QzE4IDIxLjE0IDE0LjgyIDIyIDEyLjEgMjJjLTMuODEgMC03LjI1LTEuNDEtOS44NS0zLjc2LS4yLS4xOC0uMDItLjQzLjI1LS4yOSAyLjc4IDEuNjMgNi4yNSAyLjYxIDkuODMgMi42MSAyLjQxIDAgNS4wNy0uNSA3LjUxLTEuNTMuMzctLjE2LjY2LjI0LjMyLjUxIj48L3BhdGg+PHBhdGggZD0iTTIxLjA3IDE4LjVjLS4yOC0uMzYtMS44NS0uMTctMi41Ny0uMDgtLjE5LjAyLS4yMi0uMTYtLjAzLS4zIDEuMjQtLjg4IDMuMjktLjYyIDMuNTMtLjMzLjI0LjMtLjA3IDIuMzUtMS4yNCAzLjMyLS4xOC4xNi0uMzUuMDctLjI2LS4xMS4yNi0uNjcuODUtMi4xNC41Ny0yLjV6Ij48L3BhdGg+PC9zdmc+"
+)
+public class TbSqsNode implements TbNode {
+
+    private static final String MESSAGE_ID = "messageId";
+    private static final String REQUEST_ID = "requestId";
+    private static final String MESSAGE_BODY_MD5 = "messageBodyMd5";
+    private static final String MESSAGE_ATTRIBUTES_MD5 = "messageAttributesMd5";
+    private static final String SEQUENCE_NUMBER = "sequenceNumber";
+    private static final String ERROR = "error";
+
+    private TbSqsNodeConfiguration config;
+    private AmazonSQS sqsClient;
+
+    @Override
+    public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
+        this.config = TbNodeUtils.convert(configuration, TbSqsNodeConfiguration.class);
+        AWSCredentials awsCredentials = new BasicAWSCredentials(this.config.getAccessKeyId(), this.config.getSecretAccessKey());
+        AWSStaticCredentialsProvider credProvider = new AWSStaticCredentialsProvider(awsCredentials);
+        try {
+            this.sqsClient = AmazonSQSClientBuilder.standard()
+                    .withCredentials(credProvider)
+                    .withRegion(this.config.getRegion())
+                    .build();
+        } catch (Exception e) {
+            throw new TbNodeException(e);
+        }
+    }
+
+    @Override
+    public void onMsg(TbContext ctx, TbMsg msg) throws ExecutionException, InterruptedException, TbNodeException {
+        withCallback(publishMessageAsync(ctx, msg),
+                m -> ctx.tellNext(m, TbRelationTypes.SUCCESS),
+                t -> {
+                    TbMsg next = processException(ctx, msg, t);
+                    ctx.tellFailure(next, t);
+                });
+    }
+
+    private ListenableFuture<TbMsg> publishMessageAsync(TbContext ctx, TbMsg msg) {
+        return ctx.getExternalCallExecutor().executeAsync(() -> publishMessage(ctx, msg));
+    }
+
+    private TbMsg publishMessage(TbContext ctx, TbMsg msg) {
+        String queueUrl = TbNodeUtils.processPattern(this.config.getQueueUrlPattern(), msg.getMetaData());
+        SendMessageRequest sendMsgRequest =  new SendMessageRequest();
+        sendMsgRequest.withQueueUrl(queueUrl);
+        sendMsgRequest.withMessageBody(msg.getData());
+        Map<String, MessageAttributeValue> messageAttributes = new HashMap<>();
+        this.config.getMessageAttributes().forEach((k,v) -> {
+            String name = TbNodeUtils.processPattern(k, msg.getMetaData());
+            String val = TbNodeUtils.processPattern(v, msg.getMetaData());
+            messageAttributes.put(name, new MessageAttributeValue().withDataType("String").withStringValue(val));
+        });
+        sendMsgRequest.setMessageAttributes(messageAttributes);
+        if (this.config.getQueueType() == TbSqsNodeConfiguration.QueueType.STANDARD) {
+            sendMsgRequest.withDelaySeconds(this.config.getDelaySeconds());
+        } else {
+            sendMsgRequest.withMessageDeduplicationId(msg.getId().toString());
+            sendMsgRequest.withMessageGroupId(msg.getOriginator().toString());
+        }
+        SendMessageResult result = this.sqsClient.sendMessage(sendMsgRequest);
+        return processSendMessageResult(ctx, msg, result);
+    }
+
+    private TbMsg processSendMessageResult(TbContext ctx, TbMsg origMsg, SendMessageResult result) {
+        TbMsgMetaData metaData = origMsg.getMetaData().copy();
+        metaData.putValue(MESSAGE_ID, result.getMessageId());
+        metaData.putValue(REQUEST_ID, result.getSdkResponseMetadata().getRequestId());
+        if (!StringUtils.isEmpty(result.getMD5OfMessageBody())) {
+            metaData.putValue(MESSAGE_BODY_MD5, result.getMD5OfMessageBody());
+        }
+        if (!StringUtils.isEmpty(result.getMD5OfMessageAttributes())) {
+            metaData.putValue(MESSAGE_ATTRIBUTES_MD5, result.getMD5OfMessageAttributes());
+        }
+        if (!StringUtils.isEmpty(result.getSequenceNumber())) {
+            metaData.putValue(SEQUENCE_NUMBER, result.getSequenceNumber());
+        }
+        return ctx.transformMsg(origMsg, origMsg.getType(), origMsg.getOriginator(), metaData, origMsg.getData());
+    }
+
+    private TbMsg processException(TbContext ctx, TbMsg origMsg, Throwable t) {
+        TbMsgMetaData metaData = origMsg.getMetaData().copy();
+        metaData.putValue(ERROR, t.getClass() + ": " + t.getMessage());
+        return ctx.transformMsg(origMsg, origMsg.getType(), origMsg.getOriginator(), metaData, origMsg.getData());
+    }
+
+    @Override
+    public void destroy() {
+        if (this.sqsClient != null) {
+            try {
+                this.sqsClient.shutdown();
+            } catch (Exception e) {
+                log.error("Failed to shutdown SQS client during destroy()", e);
+            }
+        }
+    }
+}
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/aws/sqs/TbSqsNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/aws/sqs/TbSqsNodeConfiguration.java
new file mode 100644
index 0000000..d27af36
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/aws/sqs/TbSqsNodeConfiguration.java
@@ -0,0 +1,51 @@
+/**
+ * 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.aws.sqs;
+
+import lombok.Data;
+import org.thingsboard.rule.engine.api.NodeConfiguration;
+
+import java.util.Collections;
+import java.util.Map;
+
+@Data
+public class TbSqsNodeConfiguration implements NodeConfiguration<TbSqsNodeConfiguration> {
+
+    private QueueType queueType;
+    private String queueUrlPattern;
+    private int delaySeconds;
+    private Map<String, String> messageAttributes;
+    private String accessKeyId;
+    private String secretAccessKey;
+    private String region;
+
+    @Override
+    public TbSqsNodeConfiguration defaultConfiguration() {
+        TbSqsNodeConfiguration configuration = new TbSqsNodeConfiguration();
+        configuration.setQueueType(QueueType.STANDARD);
+        configuration.setQueueUrlPattern("https://sqs.us-east-1.amazonaws.com/123456789012/my-queue-name");
+        configuration.setDelaySeconds(0);
+        configuration.setMessageAttributes(Collections.emptyMap());
+        configuration.setRegion("us-east-1");
+        return configuration;
+    }
+
+    public enum QueueType {
+        STANDARD,
+        FIFO
+    }
+}
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/debug/TbMsgGeneratorNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/debug/TbMsgGeneratorNode.java
new file mode 100644
index 0000000..5a30dcb
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/debug/TbMsgGeneratorNode.java
@@ -0,0 +1,105 @@
+/**
+ * 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.debug;
+
+import com.google.common.util.concurrent.ListenableFuture;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.util.StringUtils;
+import org.thingsboard.rule.engine.api.util.TbNodeUtils;
+import org.thingsboard.rule.engine.api.*;
+import org.thingsboard.server.common.data.id.EntityId;
+import org.thingsboard.server.common.data.id.EntityIdFactory;
+import org.thingsboard.server.common.data.plugin.ComponentType;
+import org.thingsboard.server.common.msg.TbMsg;
+import org.thingsboard.server.common.msg.TbMsgMetaData;
+
+import java.util.UUID;
+import java.util.concurrent.TimeUnit;
+
+import static org.thingsboard.rule.engine.api.util.DonAsynchron.withCallback;
+import static org.thingsboard.rule.engine.api.TbRelationTypes.SUCCESS;
+
+@Slf4j
+@RuleNode(
+        type = ComponentType.ACTION,
+        name = "generator",
+        configClazz = TbMsgGeneratorNodeConfiguration.class,
+        nodeDescription = "Periodically generates messages",
+        nodeDetails = "Generates messages with configurable period. Javascript function used for message generation.",
+        inEnabled = false,
+        uiResources = {"static/rulenode/rulenode-core-config.js", "static/rulenode/rulenode-core-config.css"},
+        configDirective = "tbActionNodeGeneratorConfig",
+        icon = "repeat"
+)
+
+public class TbMsgGeneratorNode implements TbNode {
+
+    public static final String TB_MSG_GENERATOR_NODE_MSG = "TbMsgGeneratorNodeMsg";
+
+    private TbMsgGeneratorNodeConfiguration config;
+    private ScriptEngine jsEngine;
+    private long delay;
+    private EntityId originatorId;
+    private UUID nextTickId;
+    private TbMsg prevMsg;
+
+    @Override
+    public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
+        this.config = TbNodeUtils.convert(configuration, TbMsgGeneratorNodeConfiguration.class);
+        this.delay = TimeUnit.SECONDS.toMillis(config.getPeriodInSeconds());
+        if (!StringUtils.isEmpty(config.getOriginatorId())) {
+            originatorId = EntityIdFactory.getByTypeAndUuid(config.getOriginatorType(), config.getOriginatorId());
+        } else {
+            originatorId = ctx.getSelfId();
+        }
+        this.jsEngine = ctx.createJsScriptEngine(config.getJsScript(), "prevMsg", "prevMetadata", "prevMsgType");
+        sentTickMsg(ctx);
+    }
+
+    @Override
+    public void onMsg(TbContext ctx, TbMsg msg) {
+        if (msg.getType().equals(TB_MSG_GENERATOR_NODE_MSG) && msg.getId().equals(nextTickId)) {
+            withCallback(generate(ctx),
+                    m -> {ctx.tellNext(m, SUCCESS); sentTickMsg(ctx);},
+                    t -> {ctx.tellFailure(msg, t); sentTickMsg(ctx);});
+        }
+    }
+
+    private void sentTickMsg(TbContext ctx) {
+        TbMsg tickMsg = ctx.newMsg(TB_MSG_GENERATOR_NODE_MSG, ctx.getSelfId(), new TbMsgMetaData(), "");
+        nextTickId = tickMsg.getId();
+        ctx.tellSelf(tickMsg, delay);
+    }
+
+    private ListenableFuture<TbMsg> generate(TbContext ctx) {
+        return ctx.getJsExecutor().executeAsync(() -> {
+            if (prevMsg == null) {
+                prevMsg = ctx.newMsg( "", originatorId, new TbMsgMetaData(), "{}");
+            }
+            TbMsg generated = jsEngine.executeGenerate(prevMsg);
+            prevMsg = ctx.newMsg(generated.getType(), originatorId, generated.getMetaData(), generated.getData());
+            return prevMsg;
+        });
+    }
+
+    @Override
+    public void destroy() {
+        prevMsg = null;
+        if (jsEngine != null) {
+            jsEngine.destroy();
+        }
+    }
+}
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/debug/TbMsgGeneratorNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/debug/TbMsgGeneratorNodeConfiguration.java
new file mode 100644
index 0000000..c568e3d
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/debug/TbMsgGeneratorNodeConfiguration.java
@@ -0,0 +1,44 @@
+/**
+ * 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.debug;
+
+import lombok.Data;
+import org.thingsboard.rule.engine.api.NodeConfiguration;
+import org.thingsboard.server.common.data.EntityType;
+
+import java.util.Map;
+
+@Data
+public class TbMsgGeneratorNodeConfiguration implements NodeConfiguration<TbMsgGeneratorNodeConfiguration> {
+
+    private int msgCount;
+    private int periodInSeconds;
+    private String originatorId;
+    private EntityType originatorType;
+    private String jsScript;
+
+    @Override
+    public TbMsgGeneratorNodeConfiguration defaultConfiguration() {
+        TbMsgGeneratorNodeConfiguration configuration = new TbMsgGeneratorNodeConfiguration();
+        configuration.setMsgCount(0);
+        configuration.setPeriodInSeconds(1);
+        configuration.setJsScript("var msg = { temp: 42, humidity: 77 };\n" +
+                "var metadata = { data: 40 };\n" +
+                "var msgType = \"DebugMsg\";\n\n" +
+                "return { msg: msg, metadata: metadata, msgType: msgType };");
+        return configuration;
+    }
+}
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsFilterNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsFilterNode.java
new file mode 100644
index 0000000..73a9aad
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsFilterNode.java
@@ -0,0 +1,65 @@
+/**
+ * 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.filter;
+
+import lombok.extern.slf4j.Slf4j;
+import org.thingsboard.rule.engine.api.util.TbNodeUtils;
+import org.thingsboard.rule.engine.api.*;
+import org.thingsboard.server.common.data.plugin.ComponentType;
+import org.thingsboard.server.common.msg.TbMsg;
+
+import static org.thingsboard.rule.engine.api.util.DonAsynchron.withCallback;
+
+@Slf4j
+@RuleNode(
+        type = ComponentType.FILTER,
+        name = "script", relationTypes = {"True", "False"},
+        configClazz = TbJsFilterNodeConfiguration.class,
+        nodeDescription = "Filter incoming messages using JS script",
+        nodeDetails = "Evaluate incoming Message with configured JS condition. " +
+                "If <b>True</b> - send Message via <b>True</b> chain, otherwise <b>False</b> chain is used." +
+                "Message payload can be accessed via <code>msg</code> property. For example <code>msg.temperature < 10;</code><br/>" +
+                "Message metadata can be accessed via <code>metadata</code> property. For example <code>metadata.customerName === 'John';</code><br/>" +
+                "Message type can be accessed via <code>msgType</code> property.",
+        uiResources = {"static/rulenode/rulenode-core-config.js"},
+        configDirective = "tbFilterNodeScriptConfig")
+
+public class TbJsFilterNode implements TbNode {
+
+    private TbJsFilterNodeConfiguration config;
+    private ScriptEngine jsEngine;
+
+    @Override
+    public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
+        this.config = TbNodeUtils.convert(configuration, TbJsFilterNodeConfiguration.class);
+        this.jsEngine = ctx.createJsScriptEngine(config.getJsScript());
+    }
+
+    @Override
+    public void onMsg(TbContext ctx, TbMsg msg) {
+        ListeningExecutor jsExecutor = ctx.getJsExecutor();
+        withCallback(jsExecutor.executeAsync(() -> jsEngine.executeFilter(msg)),
+                filterResult -> ctx.tellNext(msg, filterResult ? "True" : "False"),
+                t -> ctx.tellFailure(msg, t));
+    }
+
+    @Override
+    public void destroy() {
+        if (jsEngine != null) {
+            jsEngine.destroy();
+        }
+    }
+}
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsSwitchNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsSwitchNode.java
new file mode 100644
index 0000000..c8f8c06
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsSwitchNode.java
@@ -0,0 +1,71 @@
+/**
+ * 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.filter;
+
+import lombok.extern.slf4j.Slf4j;
+import org.thingsboard.rule.engine.api.util.TbNodeUtils;
+import org.thingsboard.rule.engine.api.*;
+import org.thingsboard.server.common.data.plugin.ComponentType;
+import org.thingsboard.server.common.msg.TbMsg;
+
+import java.util.Set;
+
+import static org.thingsboard.rule.engine.api.util.DonAsynchron.withCallback;
+
+@Slf4j
+@RuleNode(
+        type = ComponentType.FILTER,
+        name = "switch", customRelations = true,
+        relationTypes = {},
+        configClazz = TbJsSwitchNodeConfiguration.class,
+        nodeDescription = "Route incoming Message to one or multiple output chains",
+        nodeDetails = "Node executes configured JS script. Script should return array of next Chain names where Message should be routed. " +
+                "If Array is empty - message not routed to next Node. " +
+                "Message payload can be accessed via <code>msg</code> property. For example <code>msg.temperature < 10;</code><br/>" +
+                "Message metadata can be accessed via <code>metadata</code> property. For example <code>metadata.customerName === 'John';</code><br/>" +
+                "Message type can be accessed via <code>msgType</code> property.",
+        uiResources = {"static/rulenode/rulenode-core-config.js"},
+        configDirective = "tbFilterNodeSwitchConfig")
+public class TbJsSwitchNode implements TbNode {
+
+    private TbJsSwitchNodeConfiguration config;
+    private ScriptEngine jsEngine;
+
+    @Override
+    public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
+        this.config = TbNodeUtils.convert(configuration, TbJsSwitchNodeConfiguration.class);
+        this.jsEngine = ctx.createJsScriptEngine(config.getJsScript());
+    }
+
+    @Override
+    public void onMsg(TbContext ctx, TbMsg msg) {
+        ListeningExecutor jsExecutor = ctx.getJsExecutor();
+        withCallback(jsExecutor.executeAsync(() -> jsEngine.executeSwitch(msg)),
+                result -> processSwitch(ctx, msg, result),
+                t -> ctx.tellFailure(msg, t));
+    }
+
+    private void processSwitch(TbContext ctx, TbMsg msg, Set<String> nextRelations) {
+        ctx.tellNext(msg, nextRelations);
+    }
+
+    @Override
+    public void destroy() {
+        if (jsEngine != null) {
+            jsEngine.destroy();
+        }
+    }
+}
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsSwitchNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsSwitchNodeConfiguration.java
new file mode 100644
index 0000000..d1dba1b
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsSwitchNodeConfiguration.java
@@ -0,0 +1,41 @@
+/**
+ * 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.filter;
+
+import com.google.common.collect.Sets;
+import lombok.Data;
+import org.thingsboard.rule.engine.api.NodeConfiguration;
+
+import java.util.Set;
+
+@Data
+public class TbJsSwitchNodeConfiguration implements NodeConfiguration<TbJsSwitchNodeConfiguration> {
+
+    private String jsScript;
+
+    @Override
+    public TbJsSwitchNodeConfiguration defaultConfiguration() {
+        TbJsSwitchNodeConfiguration configuration = new TbJsSwitchNodeConfiguration();
+        configuration.setJsScript("function nextRelation(metadata, msg) {\n" +
+                "    return ['one','nine'];\n" +
+                "}\n" +
+                "if(msgType === 'POST_TELEMETRY_REQUEST') {\n" +
+                "    return ['two'];\n" +
+                "}\n" +
+                "return nextRelation(metadata, msg);");
+        return configuration;
+    }
+}
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbMsgTypeFilterNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbMsgTypeFilterNode.java
new file mode 100644
index 0000000..161abb1
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbMsgTypeFilterNode.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.rule.engine.filter;
+
+import lombok.extern.slf4j.Slf4j;
+import org.thingsboard.rule.engine.api.util.TbNodeUtils;
+import org.thingsboard.rule.engine.api.*;
+import org.thingsboard.server.common.data.plugin.ComponentType;
+import org.thingsboard.server.common.msg.TbMsg;
+
+/**
+ * Created by ashvayka on 19.01.18.
+ */
+@Slf4j
+@RuleNode(
+        type = ComponentType.FILTER,
+        name = "message type",
+        configClazz = TbMsgTypeFilterNodeConfiguration.class,
+        relationTypes = {"True", "False"},
+        nodeDescription = "Filter incoming messages by Message Type",
+        nodeDetails = "If incoming MessageType is expected - send Message via <b>True</b> chain, otherwise <b>False</b> chain is used.",
+        uiResources = {"static/rulenode/rulenode-core-config.js", "static/rulenode/rulenode-core-config.css"},
+        configDirective = "tbFilterNodeMessageTypeConfig")
+public class TbMsgTypeFilterNode implements TbNode {
+
+    TbMsgTypeFilterNodeConfiguration config;
+
+    @Override
+    public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
+        this.config = TbNodeUtils.convert(configuration, TbMsgTypeFilterNodeConfiguration.class);
+    }
+
+    @Override
+    public void onMsg(TbContext ctx, TbMsg msg) throws TbNodeException {
+        ctx.tellNext(msg, config.getMessageTypes().contains(msg.getType()) ? "True" : "False");
+    }
+
+    @Override
+    public void destroy() {
+
+    }
+}
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbMsgTypeFilterNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbMsgTypeFilterNodeConfiguration.java
new file mode 100644
index 0000000..100d876
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbMsgTypeFilterNodeConfiguration.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.rule.engine.filter;
+
+import lombok.Data;
+import org.thingsboard.rule.engine.api.NodeConfiguration;
+import org.thingsboard.server.common.msg.session.SessionMsgType;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Created by ashvayka on 19.01.18.
+ */
+@Data
+public class TbMsgTypeFilterNodeConfiguration implements NodeConfiguration<TbMsgTypeFilterNodeConfiguration> {
+
+    private List<String> messageTypes;
+
+    @Override
+    public TbMsgTypeFilterNodeConfiguration defaultConfiguration() {
+        TbMsgTypeFilterNodeConfiguration configuration = new TbMsgTypeFilterNodeConfiguration();
+        configuration.setMessageTypes(Arrays.asList(
+                SessionMsgType.POST_ATTRIBUTES_REQUEST.name(),
+                SessionMsgType.POST_TELEMETRY_REQUEST.name(),
+                SessionMsgType.TO_SERVER_RPC_REQUEST.name()));
+        return configuration;
+    }
+}
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbMsgTypeSwitchNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbMsgTypeSwitchNode.java
new file mode 100644
index 0000000..5426278
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbMsgTypeSwitchNode.java
@@ -0,0 +1,88 @@
+/**
+ * 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.filter;
+
+import lombok.extern.slf4j.Slf4j;
+import org.thingsboard.rule.engine.api.util.TbNodeUtils;
+import org.thingsboard.rule.engine.api.*;
+import org.thingsboard.server.common.data.DataConstants;
+import org.thingsboard.server.common.data.plugin.ComponentType;
+import org.thingsboard.server.common.msg.TbMsg;
+import org.thingsboard.server.common.msg.session.SessionMsgType;
+
+@Slf4j
+@RuleNode(
+        type = ComponentType.FILTER,
+        name = "message type switch",
+        configClazz = EmptyNodeConfiguration.class,
+        relationTypes = {"Post attributes", "Post telemetry", "RPC Request", "Activity Event", "Inactivity Event",
+                "Connect Event", "Disconnect Event", "Entity Created", "Entity Updated", "Entity Deleted", "Entity Assigned",
+                "Entity Unassigned", "Attributes Updated", "Attributes Deleted", "Other"},
+        nodeDescription = "Route incoming messages by Message Type",
+        nodeDetails = "Sends messages with message types <b>\"Post attributes\", \"Post telemetry\", \"RPC Request\"</b> etc. via corresponding chain, otherwise <b>Other</b> chain is used.",
+        uiResources = {"static/rulenode/rulenode-core-config.js"},
+        configDirective = "tbNodeEmptyConfig")
+public class TbMsgTypeSwitchNode implements TbNode {
+
+    EmptyNodeConfiguration config;
+
+    @Override
+    public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
+        this.config = TbNodeUtils.convert(configuration, EmptyNodeConfiguration.class);
+    }
+
+    @Override
+    public void onMsg(TbContext ctx, TbMsg msg) throws TbNodeException {
+        String relationType;
+        if (msg.getType().equals(SessionMsgType.POST_ATTRIBUTES_REQUEST.name())) {
+            relationType = "Post attributes";
+        } else if (msg.getType().equals(SessionMsgType.POST_TELEMETRY_REQUEST.name())) {
+            relationType = "Post telemetry";
+        } else if (msg.getType().equals(SessionMsgType.TO_SERVER_RPC_REQUEST.name())) {
+            relationType = "RPC Request";
+        } else if (msg.getType().equals(DataConstants.ACTIVITY_EVENT)) {
+            relationType = "Activity Event";
+        } else if (msg.getType().equals(DataConstants.INACTIVITY_EVENT)) {
+            relationType = "Inactivity Event";
+        } else if (msg.getType().equals(DataConstants.CONNECT_EVENT)) {
+            relationType = "Connect Event";
+        } else if (msg.getType().equals(DataConstants.DISCONNECT_EVENT)) {
+            relationType = "Disconnect Event";
+        } else if (msg.getType().equals(DataConstants.ENTITY_CREATED)) {
+            relationType = "Entity Created";
+        } else if (msg.getType().equals(DataConstants.ENTITY_UPDATED)) {
+            relationType = "Entity Updated";
+        } else if (msg.getType().equals(DataConstants.ENTITY_DELETED)) {
+            relationType = "Entity Deleted";
+        } else if (msg.getType().equals(DataConstants.ENTITY_ASSIGNED)) {
+            relationType = "Entity Assigned";
+        } else if (msg.getType().equals(DataConstants.ENTITY_UNASSIGNED)) {
+            relationType = "Entity Unassigned";
+        } else if (msg.getType().equals(DataConstants.ATTRIBUTES_UPDATED)) {
+            relationType = "Attributes Updated";
+        } else if (msg.getType().equals(DataConstants.ATTRIBUTES_DELETED)) {
+            relationType = "Attributes Deleted";
+        } else {
+            relationType = "Other";
+        }
+        ctx.tellNext(msg, relationType);
+    }
+
+    @Override
+    public void destroy() {
+
+    }
+}
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbOriginatorTypeSwitchNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbOriginatorTypeSwitchNode.java
new file mode 100644
index 0000000..e4a54bd
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbOriginatorTypeSwitchNode.java
@@ -0,0 +1,83 @@
+/**
+ * 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.filter;
+
+import lombok.extern.slf4j.Slf4j;
+import org.thingsboard.rule.engine.api.util.TbNodeUtils;
+import org.thingsboard.rule.engine.api.*;
+import org.thingsboard.server.common.data.EntityType;
+import org.thingsboard.server.common.data.plugin.ComponentType;
+import org.thingsboard.server.common.msg.TbMsg;
+
+@Slf4j
+@RuleNode(
+        type = ComponentType.FILTER,
+        name = "originator type switch",
+        configClazz = EmptyNodeConfiguration.class,
+        relationTypes = {"Device", "Asset", "Tenant", "Customer", "User", "Dashboard", "Rule chain", "Rule node"},
+        nodeDescription = "Route incoming messages by Message Originator Type",
+        nodeDetails = "Routes messages to chain according to the originator type ('Device', 'Asset', etc.).",
+        uiResources = {"static/rulenode/rulenode-core-config.js"},
+        configDirective = "tbNodeEmptyConfig")
+public class TbOriginatorTypeSwitchNode implements TbNode {
+
+    EmptyNodeConfiguration config;
+
+    @Override
+    public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
+        this.config = TbNodeUtils.convert(configuration, EmptyNodeConfiguration.class);
+    }
+
+    @Override
+    public void onMsg(TbContext ctx, TbMsg msg) throws TbNodeException {
+        String relationType;
+        EntityType originatorType = msg.getOriginator().getEntityType();
+        switch (originatorType) {
+            case TENANT:
+                relationType = "Tenant";
+                break;
+            case CUSTOMER:
+                relationType = "Customer";
+                break;
+            case USER:
+                relationType = "User";
+                break;
+            case DASHBOARD:
+                relationType = "Dashboard";
+                break;
+            case ASSET:
+                relationType = "Asset";
+                break;
+            case DEVICE:
+                relationType = "Device";
+                break;
+            case RULE_CHAIN:
+                relationType = "Rule chain";
+                break;
+            case RULE_NODE:
+                relationType = "Rule node";
+                break;
+            default:
+                throw new TbNodeException("Unsupported originator type: " + originatorType);
+        }
+        ctx.tellNext(msg, relationType);
+    }
+
+    @Override
+    public void destroy() {
+
+    }
+}
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/kafka/TbKafkaNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/kafka/TbKafkaNode.java
new file mode 100644
index 0000000..aa8bf5f
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/kafka/TbKafkaNode.java
@@ -0,0 +1,120 @@
+/**
+ * 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.kafka;
+
+import lombok.extern.slf4j.Slf4j;
+import org.apache.kafka.clients.producer.*;
+import org.thingsboard.rule.engine.api.util.TbNodeUtils;
+import org.thingsboard.rule.engine.api.*;
+import org.thingsboard.server.common.data.plugin.ComponentType;
+import org.thingsboard.server.common.msg.TbMsg;
+import org.thingsboard.server.common.msg.TbMsgMetaData;
+
+import java.util.Properties;
+import java.util.concurrent.ExecutionException;
+
+@Slf4j
+@RuleNode(
+        type = ComponentType.EXTERNAL,
+        name = "kafka",
+        configClazz = TbKafkaNodeConfiguration.class,
+        nodeDescription = "Publish messages to Kafka server",
+        nodeDetails = "Will send record via Kafka producer to Kafka server. " +
+                "Outbound message will contain response fields (<code>offset</code>, <code>partition</code>, <code>topic</code>)" +
+                " from the Kafka in the Message Metadata. For example <b>partition</b> field can be accessed with <code>metadata.partition</code>.",
+        uiResources = {"static/rulenode/rulenode-core-config.js"},
+        configDirective = "tbActionNodeKafkaConfig",
+        iconUrl = "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTUzOCIgaGVpZ2h0PSIyNTAwIiB2aWV3Qm94PSIwIDAgMjU2IDQxNiIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiBwcmVzZXJ2ZUFzcGVjdFJhdGlvPSJ4TWlkWU1pZCI+PHBhdGggZD0iTTIwMS44MTYgMjMwLjIxNmMtMTYuMTg2IDAtMzAuNjk3IDcuMTcxLTQwLjYzNCAxOC40NjFsLTI1LjQ2My0xOC4wMjZjMi43MDMtNy40NDIgNC4yNTUtMTUuNDMzIDQuMjU1LTIzLjc5NyAwLTguMjE5LTEuNDk4LTE2LjA3Ni00LjExMi0yMy40MDhsMjUuNDA2LTE3LjgzNWM5LjkzNiAxMS4yMzMgMjQuNDA5IDE4LjM2NSA0MC41NDggMTguMzY1IDI5Ljg3NSAwIDU0LjE4NC0yNC4zMDUgNTQuMTg0LTU0LjE4NCAwLTI5Ljg3OS0yNC4zMDktNTQuMTg0LTU0LjE4NC01NC4xODQtMjkuODc1IDAtNTQuMTg0IDI0LjMwNS01NC4xODQgNTQuMTg0IDAgNS4zNDguODA4IDEwLjUwNSAyLjI1OCAxNS4zODlsLTI1LjQyMyAxNy44NDRjLTEwLjYyLTEzLjE3NS0yNS45MTEtMjIuMzc0LTQzLjMzMy0yNS4xODJ2LTMwLjY0YzI0LjU0NC01LjE1NSA0My4wMzctMjYuOTYyIDQzLjAzNy01My4wMTlDMTI0LjE3MSAyNC4zMDUgOTkuODYyIDAgNjkuOTg3IDAgNDAuMTEyIDAgMTUuODAzIDI0LjMwNSAxNS44MDMgNTQuMTg0YzAgMjUuNzA4IDE4LjAxNCA0Ny4yNDYgNDIuMDY3IDUyLjc2OXYzMS4wMzhDMjUuMDQ0IDE0My43NTMgMCAxNzIuNDAxIDAgMjA2Ljg1NGMwIDM0LjYyMSAyNS4yOTIgNjMuMzc0IDU4LjM1NSA2OC45NHYzMi43NzRjLTI0LjI5OSA1LjM0MS00Mi41NTIgMjcuMDExLTQyLjU1MiA1Mi44OTQgMCAyOS44NzkgMjQuMzA5IDU0LjE4NCA1NC4xODQgNTQuMTg0IDI5Ljg3NSAwIDU0LjE4NC0yNC4zMDUgNTQuMTg0LTU0LjE4NCAwLTI1Ljg4My0xOC4yNTMtNDcuNTUzLTQyLjU1Mi01Mi44OTR2LTMyLjc3NWE2OS45NjUgNjkuOTY1IDAgMCAwIDQyLjYtMjQuNzc2bDI1LjYzMyAxOC4xNDNjLTEuNDIzIDQuODQtMi4yMiA5Ljk0Ni0yLjIyIDE1LjI0IDAgMjkuODc5IDI0LjMwOSA1NC4xODQgNTQuMTg0IDU0LjE4NCAyOS44NzUgMCA1NC4xODQtMjQuMzA1IDU0LjE4NC01NC4xODQgMC0yOS44NzktMjQuMzA5LTU0LjE4NC01NC4xODQtNTQuMTg0em0wLTEyNi42OTVjMTQuNDg3IDAgMjYuMjcgMTEuNzg4IDI2LjI3IDI2LjI3MXMtMTEuNzgzIDI2LjI3LTI2LjI3IDI2LjI3LTI2LjI3LTExLjc4Ny0yNi4yNy0yNi4yN2MwLTE0LjQ4MyAxMS43ODMtMjYuMjcxIDI2LjI3LTI2LjI3MXptLTE1OC4xLTQ5LjMzN2MwLTE0LjQ4MyAxMS43ODQtMjYuMjcgMjYuMjcxLTI2LjI3czI2LjI3IDExLjc4NyAyNi4yNyAyNi4yN2MwIDE0LjQ4My0xMS43ODMgMjYuMjctMjYuMjcgMjYuMjdzLTI2LjI3MS0xMS43ODctMjYuMjcxLTI2LjI3em01Mi41NDEgMzA3LjI3OGMwIDE0LjQ4My0xMS43ODMgMjYuMjctMjYuMjcgMjYuMjdzLTI2LjI3MS0xMS43ODctMjYuMjcxLTI2LjI3YzAtMTQuNDgzIDExLjc4NC0yNi4yNyAyNi4yNzEtMjYuMjdzMjYuMjcgMTEuNzg3IDI2LjI3IDI2LjI3em0tMjYuMjcyLTExNy45N2MtMjAuMjA1IDAtMzYuNjQyLTE2LjQzNC0zNi42NDItMzYuNjM4IDAtMjAuMjA1IDE2LjQzNy0zNi42NDIgMzYuNjQyLTM2LjY0MiAyMC4yMDQgMCAzNi42NDEgMTYuNDM3IDM2LjY0MSAzNi42NDIgMCAyMC4yMDQtMTYuNDM3IDM2LjYzOC0zNi42NDEgMzYuNjM4em0xMzEuODMxIDY3LjE3OWMtMTQuNDg3IDAtMjYuMjctMTEuNzg4LTI2LjI3LTI2LjI3MXMxMS43ODMtMjYuMjcgMjYuMjctMjYuMjcgMjYuMjcgMTEuNzg3IDI2LjI3IDI2LjI3YzAgMTQuNDgzLTExLjc4MyAyNi4yNzEtMjYuMjcgMjYuMjcxeiIvPjwvc3ZnPg=="
+)
+public class TbKafkaNode implements TbNode {
+
+    private static final String OFFSET = "offset";
+    private static final String PARTITION = "partition";
+    private static final String TOPIC = "topic";
+    private static final String ERROR = "error";
+
+    private TbKafkaNodeConfiguration config;
+
+    private Producer<?, String> producer;
+
+    @Override
+    public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
+        this.config = TbNodeUtils.convert(configuration, TbKafkaNodeConfiguration.class);
+        Properties properties = new Properties();
+        properties.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, config.getBootstrapServers());
+        properties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, config.getValueSerializer());
+        properties.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, config.getKeySerializer());
+        properties.put(ProducerConfig.ACKS_CONFIG, config.getAcks());
+        properties.put(ProducerConfig.RETRIES_CONFIG, config.getRetries());
+        properties.put(ProducerConfig.BATCH_SIZE_CONFIG, config.getBatchSize());
+        properties.put(ProducerConfig.LINGER_MS_CONFIG, config.getLinger());
+        properties.put(ProducerConfig.BUFFER_MEMORY_CONFIG, config.getBufferMemory());
+        if (config.getOtherProperties() != null) {
+            config.getOtherProperties()
+                    .forEach((k,v) -> properties.put(k, v));
+        }
+        try {
+            this.producer = new KafkaProducer<>(properties);
+        } catch (Exception e) {
+            throw new TbNodeException(e);
+        }
+    }
+
+    @Override
+    public void onMsg(TbContext ctx, TbMsg msg) throws ExecutionException, InterruptedException, TbNodeException {
+        String topic = TbNodeUtils.processPattern(config.getTopicPattern(), msg.getMetaData());
+        try {
+            producer.send(new ProducerRecord<>(topic, msg.getData()),
+                    (metadata, e) -> {
+                        if (metadata != null) {
+                            TbMsg next = processResponse(ctx, msg, metadata);
+                            ctx.tellNext(next, TbRelationTypes.SUCCESS);
+                        } else {
+                            TbMsg next = processException(ctx, msg, e);
+                            ctx.tellFailure(next, e);
+                        }
+                    });
+        } catch (Exception e) {
+            ctx.tellFailure(msg, e);
+        }
+    }
+
+    @Override
+    public void destroy() {
+        if (this.producer != null) {
+            try {
+                this.producer.close();
+            } catch (Exception e) {
+                log.error("Failed to close producer during destroy()", e);
+            }
+        }
+    }
+
+    private TbMsg processResponse(TbContext ctx, TbMsg origMsg, RecordMetadata recordMetadata) {
+        TbMsgMetaData metaData = origMsg.getMetaData().copy();
+        metaData.putValue(OFFSET, String.valueOf(recordMetadata.offset()));
+        metaData.putValue(PARTITION, String.valueOf(recordMetadata.partition()));
+        metaData.putValue(TOPIC, recordMetadata.topic());
+        return ctx.transformMsg(origMsg, origMsg.getType(), origMsg.getOriginator(), metaData, origMsg.getData());
+    }
+
+    private TbMsg processException(TbContext ctx, TbMsg origMsg, Exception e) {
+        TbMsgMetaData metaData = origMsg.getMetaData().copy();
+        metaData.putValue(ERROR, e.getClass() + ": " + e.getMessage());
+        return ctx.transformMsg(origMsg, origMsg.getType(), origMsg.getOriginator(), metaData, origMsg.getData());
+    }
+
+}
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/kafka/TbKafkaNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/kafka/TbKafkaNodeConfiguration.java
new file mode 100644
index 0000000..2c16e56
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/kafka/TbKafkaNodeConfiguration.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.rule.engine.kafka;
+
+import lombok.Data;
+import org.apache.kafka.common.serialization.StringSerializer;
+import org.thingsboard.rule.engine.api.NodeConfiguration;
+
+import java.util.Collections;
+import java.util.Map;
+
+@Data
+public class TbKafkaNodeConfiguration implements NodeConfiguration<TbKafkaNodeConfiguration> {
+
+    private String topicPattern;
+    private String bootstrapServers;
+    private int retries;
+    private int batchSize;
+    private int linger;
+    private int bufferMemory;
+    private String acks;
+    private String keySerializer;
+    private String valueSerializer;
+    private Map<String, String> otherProperties;
+
+    @Override
+    public TbKafkaNodeConfiguration defaultConfiguration() {
+        TbKafkaNodeConfiguration configuration = new TbKafkaNodeConfiguration();
+        configuration.setTopicPattern("my-topic");
+        configuration.setBootstrapServers("localhost:9092");
+        configuration.setRetries(0);
+        configuration.setBatchSize(16384);
+        configuration.setLinger(0);
+        configuration.setBufferMemory(33554432);
+        configuration.setAcks("-1");
+        configuration.setKeySerializer(StringSerializer.class.getName());
+        configuration.setValueSerializer(StringSerializer.class.getName());
+        configuration.setOtherProperties(Collections.emptyMap());
+        return configuration;
+    }
+}
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mail/TbMsgToEmailNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mail/TbMsgToEmailNode.java
new file mode 100644
index 0000000..7dece25
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mail/TbMsgToEmailNode.java
@@ -0,0 +1,96 @@
+/**
+ * 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.mail;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.util.StringUtils;
+import org.thingsboard.rule.engine.api.util.TbNodeUtils;
+import org.thingsboard.rule.engine.api.*;
+import org.thingsboard.server.common.data.plugin.ComponentType;
+import org.thingsboard.server.common.msg.TbMsg;
+import org.thingsboard.server.common.msg.TbMsgMetaData;
+
+import java.io.IOException;
+
+import static org.thingsboard.rule.engine.api.TbRelationTypes.SUCCESS;
+import static org.thingsboard.rule.engine.mail.TbSendEmailNode.SEND_EMAIL_TYPE;
+
+@Slf4j
+@RuleNode(
+        type = ComponentType.TRANSFORMATION,
+        name = "to email",
+        configClazz = TbMsgToEmailNodeConfiguration.class,
+        nodeDescription = "Transforms message to email message",
+        nodeDetails = "Transforms message to email message by populating email fields using values derived from message metadata. " +
+                      "Set 'SEND_EMAIL' output message type.",
+        uiResources = {"static/rulenode/rulenode-core-config.js"},
+        configDirective = "tbTransformationNodeToEmailConfig",
+        icon = "email"
+)
+public class TbMsgToEmailNode implements TbNode {
+
+    private static final ObjectMapper MAPPER = new ObjectMapper();
+
+    private TbMsgToEmailNodeConfiguration config;
+
+    @Override
+    public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
+        this.config = TbNodeUtils.convert(configuration, TbMsgToEmailNodeConfiguration.class);
+    }
+
+    @Override
+    public void onMsg(TbContext ctx, TbMsg msg) {
+        try {
+            EmailPojo email = convert(msg);
+            TbMsg emailMsg = buildEmailMsg(ctx, msg, email);
+            ctx.tellNext(emailMsg, SUCCESS);
+        } catch (Exception ex) {
+            log.warn("Can not convert message to email " + ex.getMessage());
+            ctx.tellFailure(msg, ex);
+        }
+    }
+
+    private TbMsg buildEmailMsg(TbContext ctx, TbMsg msg, EmailPojo email) throws JsonProcessingException {
+        String emailJson = MAPPER.writeValueAsString(email);
+        return ctx.transformMsg(msg, SEND_EMAIL_TYPE, msg.getOriginator(), msg.getMetaData().copy(), emailJson);
+    }
+
+    private EmailPojo convert(TbMsg msg) throws IOException {
+        EmailPojo.EmailPojoBuilder builder = EmailPojo.builder();
+        builder.from(fromTemplate(this.config.getFromTemplate(), msg.getMetaData()));
+        builder.to(fromTemplate(this.config.getToTemplate(), msg.getMetaData()));
+        builder.cc(fromTemplate(this.config.getCcTemplate(), msg.getMetaData()));
+        builder.bcc(fromTemplate(this.config.getBccTemplate(), msg.getMetaData()));
+        builder.subject(fromTemplate(this.config.getSubjectTemplate(), msg.getMetaData()));
+        builder.body(fromTemplate(this.config.getBodyTemplate(), msg.getMetaData()));
+        return builder.build();
+    }
+
+    private String fromTemplate(String template, TbMsgMetaData metaData) {
+        if (!StringUtils.isEmpty(template)) {
+            return TbNodeUtils.processPattern(template, metaData);
+        } else {
+            return null;
+        }
+    }
+
+    @Override
+    public void destroy() {
+
+    }
+}
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mail/TbMsgToEmailNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mail/TbMsgToEmailNodeConfiguration.java
new file mode 100644
index 0000000..f99259f
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mail/TbMsgToEmailNodeConfiguration.java
@@ -0,0 +1,40 @@
+/**
+ * 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.mail;
+
+import lombok.Data;
+import org.thingsboard.rule.engine.api.NodeConfiguration;
+
+@Data
+public class TbMsgToEmailNodeConfiguration implements NodeConfiguration {
+
+    private String fromTemplate;
+    private String toTemplate;
+    private String ccTemplate;
+    private String bccTemplate;
+    private String subjectTemplate;
+    private String bodyTemplate;
+
+    @Override
+    public TbMsgToEmailNodeConfiguration defaultConfiguration() {
+        TbMsgToEmailNodeConfiguration configuration = new TbMsgToEmailNodeConfiguration();
+        configuration.fromTemplate = "info@testmail.org";
+        configuration.toTemplate = "${userEmail}";
+        configuration.subjectTemplate = "Device ${deviceType} temperature high";
+        configuration.bodyTemplate = "Device ${deviceName} has high temperature ${temp}";
+        return configuration;
+    }
+}
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mail/TbSendEmailNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mail/TbSendEmailNode.java
new file mode 100644
index 0000000..9862d02
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mail/TbSendEmailNode.java
@@ -0,0 +1,146 @@
+/**
+ * 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.mail;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.mail.javamail.JavaMailSenderImpl;
+import org.springframework.mail.javamail.MimeMessageHelper;
+import org.thingsboard.rule.engine.api.util.TbNodeUtils;
+import org.thingsboard.rule.engine.api.*;
+import org.thingsboard.server.common.data.plugin.ComponentType;
+import org.thingsboard.server.common.msg.TbMsg;
+
+import javax.mail.internet.MimeMessage;
+import java.io.IOException;
+import java.util.Properties;
+
+import static org.thingsboard.rule.engine.api.util.DonAsynchron.withCallback;
+import static org.thingsboard.rule.engine.api.TbRelationTypes.SUCCESS;
+
+@Slf4j
+@RuleNode(
+        type = ComponentType.EXTERNAL,
+        name = "send email",
+        configClazz = TbSendEmailNodeConfiguration.class,
+        nodeDescription = "Sends email message via SMTP server.",
+        nodeDetails = "Expects messages with <b>SEND_EMAIL</b> type. Node works only with messages that " +
+                " where created using <code>to Email</code> transformation Node, please connect this Node " +
+                "with <code>to Email</code> Node using <code>Successful</code> chain.",
+        uiResources = {"static/rulenode/rulenode-core-config.js"},
+        configDirective = "tbActionNodeSendEmailConfig",
+        icon = "send"
+)
+public class TbSendEmailNode implements TbNode {
+
+    private static final String MAIL_PROP = "mail.";
+    static final String SEND_EMAIL_TYPE = "SEND_EMAIL";
+    private static final ObjectMapper MAPPER = new ObjectMapper();
+
+    private TbSendEmailNodeConfiguration config;
+    private JavaMailSenderImpl mailSender;
+
+    @Override
+    public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
+        try {
+            this.config = TbNodeUtils.convert(configuration, TbSendEmailNodeConfiguration.class);
+            if (!this.config.isUseSystemSmtpSettings()) {
+                mailSender = createMailSender();
+            }
+        } catch (Exception e) {
+            throw new TbNodeException(e);
+        }
+    }
+
+    @Override
+    public void onMsg(TbContext ctx, TbMsg msg) {
+        try {
+            validateType(msg.getType());
+            EmailPojo email = getEmail(msg);
+            withCallback(ctx.getMailExecutor().executeAsync(() -> {
+                        sendEmail(ctx, email);
+                        return null;
+                    }),
+                    ok -> ctx.tellNext(msg, SUCCESS),
+                    fail -> ctx.tellFailure(msg, fail));
+        } catch (Exception ex) {
+            ctx.tellFailure(msg, ex);
+        }
+    }
+
+    private void sendEmail(TbContext ctx, EmailPojo email) throws Exception {
+        if (this.config.isUseSystemSmtpSettings()) {
+            ctx.getMailService().send(email.getFrom(), email.getTo(), email.getCc(),
+                    email.getBcc(), email.getSubject(), email.getBody());
+        } else {
+            MimeMessage mailMsg = mailSender.createMimeMessage();
+            MimeMessageHelper helper = new MimeMessageHelper(mailMsg, "UTF-8");
+            helper.setFrom(email.getFrom());
+            helper.setTo(email.getTo().split("\\s*,\\s*"));
+            if (!StringUtils.isBlank(email.getCc())) {
+                helper.setCc(email.getCc().split("\\s*,\\s*"));
+            }
+            if (!StringUtils.isBlank(email.getBcc())) {
+                helper.setBcc(email.getBcc().split("\\s*,\\s*"));
+            }
+            helper.setSubject(email.getSubject());
+            helper.setText(email.getBody());
+            mailSender.send(helper.getMimeMessage());
+        }
+    }
+
+    private EmailPojo getEmail(TbMsg msg) throws IOException {
+        EmailPojo email = MAPPER.readValue(msg.getData(), EmailPojo.class);
+        if (StringUtils.isBlank(email.getTo())) {
+            throw new IllegalStateException("Email destination can not be blank [" + email.getTo() + "]");
+        }
+        return email;
+    }
+
+    private void validateType(String type) {
+        if (!SEND_EMAIL_TYPE.equals(type)) {
+            log.warn("Not expected msg type [{}] for SendEmail Node", type);
+            throw new IllegalStateException("Not expected msg type " + type + " for SendEmail Node");
+        }
+    }
+
+    @Override
+    public void destroy() {
+    }
+
+    private JavaMailSenderImpl createMailSender() {
+        JavaMailSenderImpl mailSender = new JavaMailSenderImpl();
+        mailSender.setHost(this.config.getSmtpHost());
+        mailSender.setPort(this.config.getSmtpPort());
+        mailSender.setUsername(this.config.getUsername());
+        mailSender.setPassword(this.config.getPassword());
+        mailSender.setJavaMailProperties(createJavaMailProperties());
+        return mailSender;
+    }
+
+    private Properties createJavaMailProperties() {
+        Properties javaMailProperties = new Properties();
+        String protocol = this.config.getSmtpProtocol();
+        javaMailProperties.put("mail.transport.protocol", protocol);
+        javaMailProperties.put(MAIL_PROP + protocol + ".host", this.config.getSmtpHost());
+        javaMailProperties.put(MAIL_PROP + protocol + ".port", this.config.getSmtpPort()+"");
+        javaMailProperties.put(MAIL_PROP + protocol + ".timeout", this.config.getTimeout()+"");
+        javaMailProperties.put(MAIL_PROP + protocol + ".auth", String.valueOf(StringUtils.isNotEmpty(this.config.getUsername())));
+        javaMailProperties.put(MAIL_PROP + protocol + ".starttls.enable", Boolean.valueOf(this.config.isEnableTls()).toString());
+        return javaMailProperties;
+    }
+}
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mail/TbSendEmailNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mail/TbSendEmailNodeConfiguration.java
new file mode 100644
index 0000000..ea8a9a3
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mail/TbSendEmailNodeConfiguration.java
@@ -0,0 +1,44 @@
+/**
+ * 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.mail;
+
+import lombok.Data;
+import org.thingsboard.rule.engine.api.NodeConfiguration;
+
+@Data
+public class TbSendEmailNodeConfiguration implements NodeConfiguration {
+
+    private boolean useSystemSmtpSettings;
+    private String smtpHost;
+    private int smtpPort;
+    private String username;
+    private String password;
+    private String smtpProtocol;
+    private int timeout;
+    private boolean enableTls;
+
+    @Override
+    public TbSendEmailNodeConfiguration defaultConfiguration() {
+        TbSendEmailNodeConfiguration configuration = new TbSendEmailNodeConfiguration();
+        configuration.setUseSystemSmtpSettings(true);
+        configuration.setSmtpHost("localhost");
+        configuration.setSmtpProtocol("smtp");
+        configuration.setSmtpPort(25);
+        configuration.setTimeout(10000);
+        configuration.setEnableTls(false);
+        return configuration;
+    }
+}
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbAbstractGetAttributesNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbAbstractGetAttributesNode.java
new file mode 100644
index 0000000..0f50eee
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbAbstractGetAttributesNode.java
@@ -0,0 +1,116 @@
+/**
+ * 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.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.api.util.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.CLIENT_SCOPE;
+import static org.thingsboard.server.common.data.DataConstants.SERVER_SCOPE;
+import static org.thingsboard.server.common.data.DataConstants.SHARED_SCOPE;
+
+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.tellFailure(msg, t), ctx.getDbCallbackExecutor());
+        } catch (Throwable th) {
+            ctx.tellFailure(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.tellFailure(msg, t), ctx.getDbCallbackExecutor());
+    }
+
+    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, l -> {
+            l.forEach(r -> {
+                if (r.getValue() != null) {
+                    msg.getMetaData().putValue(prefix + r.getKey(), r.getValueAsString());
+                } else {
+                    throw new RuntimeException("[" + scope + "][" + r.getKey() + "] attribute value is not present in the DB!");
+                }
+            });
+            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, l -> {
+            l.forEach(r -> {
+                if (r.getValue() != null) {
+                    msg.getMetaData().putValue(r.getKey(), r.getValueAsString());
+                } else {
+                    throw new RuntimeException("[" + r.getKey() + "] telemetry value is not present in the DB!");
+                }
+            });
+            return null;
+        });
+    }
+
+    @Override
+    public void destroy() {
+
+    }
+
+    protected abstract ListenableFuture<T> findEntityAsync(TbContext ctx, EntityId originator);
+}
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbEntityGetAttrNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbEntityGetAttrNode.java
new file mode 100644
index 0000000..6f651f1
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbEntityGetAttrNode.java
@@ -0,0 +1,105 @@
+/**
+ * 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.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import lombok.extern.slf4j.Slf4j;
+import org.thingsboard.rule.engine.api.util.TbNodeUtils;
+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.KvEntry;
+import org.thingsboard.server.common.data.kv.TsKvEntry;
+import org.thingsboard.server.common.msg.TbMsg;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+import static org.thingsboard.rule.engine.api.util.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.SERVER_SCOPE;
+
+@Slf4j
+public abstract class TbEntityGetAttrNode<T extends EntityId> implements TbNode {
+
+    private TbGetEntityAttrNodeConfiguration config;
+
+    @Override
+    public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
+        this.config = TbNodeUtils.convert(configuration, TbGetEntityAttrNodeConfiguration.class);
+    }
+
+    @Override
+    public void onMsg(TbContext ctx, TbMsg msg) {
+        try {
+            withCallback(
+                    findEntityAsync(ctx, msg.getOriginator()),
+                    entityId -> safeGetAttributes(ctx, msg, entityId),
+                    t -> ctx.tellFailure(msg, t), ctx.getDbCallbackExecutor());
+        } catch (Throwable th) {
+            ctx.tellFailure(msg, th);
+        }
+    }
+
+    private void safeGetAttributes(TbContext ctx, TbMsg msg, T entityId) {
+        if(entityId == null || entityId.isNullUid()) {
+            ctx.tellNext(msg, FAILURE);
+            return;
+        }
+
+        withCallback(config.isTelemetry() ? getLatestTelemetry(ctx, entityId) : getAttributesAsync(ctx, entityId),
+                attributes -> putAttributesAndTell(ctx, msg, attributes),
+                t -> ctx.tellFailure(msg, t), ctx.getDbCallbackExecutor());
+    }
+
+    private ListenableFuture<List<KvEntry>> getAttributesAsync(TbContext ctx, EntityId entityId) {
+        ListenableFuture<List<AttributeKvEntry>> latest = ctx.getAttributesService().find(entityId, SERVER_SCOPE, config.getAttrMapping().keySet());
+        return Futures.transform(latest, l ->
+                l.stream().map(i -> (KvEntry) i).collect(Collectors.toList()));
+    }
+
+    private ListenableFuture<List<KvEntry>> getLatestTelemetry(TbContext ctx, EntityId entityId) {
+        ListenableFuture<List<TsKvEntry>> latest = ctx.getTimeseriesService().findLatest(entityId, config.getAttrMapping().keySet());
+        return Futures.transform(latest, l ->
+                l.stream().map(i -> (KvEntry) i).collect(Collectors.toList()));
+    }
+
+
+    private void putAttributesAndTell(TbContext ctx, TbMsg msg, List<? extends KvEntry> attributes) {
+        attributes.forEach(r -> {
+            String attrName = config.getAttrMapping().get(r.getKey());
+            msg.getMetaData().putValue(attrName, r.getValueAsString());
+        });
+        ctx.tellNext(msg, SUCCESS);
+    }
+
+    @Override
+    public void destroy() {
+
+    }
+
+    protected abstract ListenableFuture<T> findEntityAsync(TbContext ctx, EntityId originator);
+
+    public void setConfig(TbGetEntityAttrNodeConfiguration config) {
+        this.config = config;
+    }
+
+}
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetAttributesNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetAttributesNode.java
new file mode 100644
index 0000000..4908b1c
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetAttributesNode.java
@@ -0,0 +1,54 @@
+/**
+ * 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.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import lombok.extern.slf4j.Slf4j;
+import org.thingsboard.rule.engine.api.util.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.id.EntityId;
+import org.thingsboard.server.common.data.plugin.ComponentType;
+
+/**
+ * Created by ashvayka on 19.01.18.
+ */
+@Slf4j
+@RuleNode(type = ComponentType.ENRICHMENT,
+          name = "originator attributes",
+          configClazz = TbGetAttributesNodeConfiguration.class,
+          nodeDescription = "Add Message Originator 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 = "tbEnrichmentNodeOriginatorAttributesConfig")
+public class TbGetAttributesNode extends TbAbstractGetAttributesNode<TbGetAttributesNodeConfiguration, EntityId> {
+
+    @Override
+    protected TbGetAttributesNodeConfiguration loadGetAttributesNodeConfig(TbNodeConfiguration configuration) throws TbNodeException {
+        return TbNodeUtils.convert(configuration, TbGetAttributesNodeConfiguration.class);
+    }
+
+    @Override
+    protected ListenableFuture<EntityId> findEntityAsync(TbContext ctx, EntityId originator) {
+        return Futures.immediateFuture(originator);
+    }
+}
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetAttributesNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetAttributesNodeConfiguration.java
new file mode 100644
index 0000000..6cd2247
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetAttributesNodeConfiguration.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.rule.engine.metadata;
+
+import lombok.Data;
+import org.thingsboard.rule.engine.api.NodeConfiguration;
+
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Created by ashvayka on 19.01.18.
+ */
+@Data
+public class TbGetAttributesNodeConfiguration implements NodeConfiguration<TbGetAttributesNodeConfiguration> {
+
+    private List<String> clientAttributeNames;
+    private List<String> sharedAttributeNames;
+    private List<String> serverAttributeNames;
+
+    private List<String> latestTsKeyNames;
+
+    @Override
+    public TbGetAttributesNodeConfiguration defaultConfiguration() {
+        TbGetAttributesNodeConfiguration configuration = new TbGetAttributesNodeConfiguration();
+        configuration.setClientAttributeNames(Collections.emptyList());
+        configuration.setSharedAttributeNames(Collections.emptyList());
+        configuration.setServerAttributeNames(Collections.emptyList());
+        configuration.setLatestTsKeyNames(Collections.emptyList());
+        return configuration;
+    }
+}
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetCustomerAttributeNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetCustomerAttributeNode.java
new file mode 100644
index 0000000..60b6b84
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetCustomerAttributeNode.java
@@ -0,0 +1,44 @@
+/**
+ * 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 org.thingsboard.rule.engine.api.RuleNode;
+import org.thingsboard.rule.engine.api.TbContext;
+import org.thingsboard.rule.engine.util.EntitiesCustomerIdAsyncLoader;
+import org.thingsboard.server.common.data.id.CustomerId;
+import org.thingsboard.server.common.data.id.EntityId;
+import org.thingsboard.server.common.data.plugin.ComponentType;
+
+@RuleNode(
+        type = ComponentType.ENRICHMENT,
+        name="customer attributes",
+        configClazz = TbGetEntityAttrNodeConfiguration.class,
+        nodeDescription = "Add Originators Customer Attributes or Latest Telemetry into Message Metadata",
+        nodeDetails = "If Attributes enrichment configured, server scope attributes are added into Message metadata. " +
+                "If Latest Telemetry enrichment configured, latest telemetry added into metadata. " +
+                "To access those attributes in other nodes this template can be used " +
+                "<code>metadata.temperature</code>.",
+        uiResources = {"static/rulenode/rulenode-core-config.js", "static/rulenode/rulenode-core-config.css"},
+        configDirective = "tbEnrichmentNodeCustomerAttributesConfig")
+public class TbGetCustomerAttributeNode extends TbEntityGetAttrNode<CustomerId> {
+
+    @Override
+    protected ListenableFuture<CustomerId> findEntityAsync(TbContext ctx, EntityId originator) {
+        return EntitiesCustomerIdAsyncLoader.findEntityIdAsync(ctx, originator);
+    }
+
+}
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetDeviceAttrNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetDeviceAttrNode.java
new file mode 100644
index 0000000..327d91a
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetDeviceAttrNode.java
@@ -0,0 +1,52 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.rule.engine.metadata;
+
+import com.google.common.util.concurrent.ListenableFuture;
+import lombok.extern.slf4j.Slf4j;
+import org.thingsboard.rule.engine.api.util.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 and Latest Telemetry value 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());
+    }
+}
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetDeviceAttrNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetDeviceAttrNodeConfiguration.java
new file mode 100644
index 0000000..4d8307c
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetDeviceAttrNodeConfiguration.java
@@ -0,0 +1,48 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.rule.engine.metadata;
+
+import lombok.Data;
+import org.thingsboard.rule.engine.data.DeviceRelationsQuery;
+import org.thingsboard.server.common.data.relation.EntityRelation;
+import org.thingsboard.server.common.data.relation.EntitySearchDirection;
+
+import java.util.Collections;
+
+@Data
+public class TbGetDeviceAttrNodeConfiguration extends TbGetAttributesNodeConfiguration {
+
+    private DeviceRelationsQuery deviceRelationsQuery;
+
+    @Override
+    public TbGetDeviceAttrNodeConfiguration defaultConfiguration() {
+        TbGetDeviceAttrNodeConfiguration configuration = new TbGetDeviceAttrNodeConfiguration();
+        configuration.setClientAttributeNames(Collections.emptyList());
+        configuration.setSharedAttributeNames(Collections.emptyList());
+        configuration.setServerAttributeNames(Collections.emptyList());
+        configuration.setLatestTsKeyNames(Collections.emptyList());
+
+        DeviceRelationsQuery deviceRelationsQuery = new DeviceRelationsQuery();
+        deviceRelationsQuery.setDirection(EntitySearchDirection.FROM);
+        deviceRelationsQuery.setMaxLevel(1);
+        deviceRelationsQuery.setRelationType(EntityRelation.CONTAINS_TYPE);
+        deviceRelationsQuery.setDeviceTypes(Collections.singletonList("default"));
+
+        configuration.setDeviceRelationsQuery(deviceRelationsQuery);
+
+        return configuration;
+    }
+}
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetRelatedAttributeNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetRelatedAttributeNode.java
new file mode 100644
index 0000000..66a1648
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetRelatedAttributeNode.java
@@ -0,0 +1,54 @@
+/**
+ * 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 org.thingsboard.rule.engine.api.util.TbNodeUtils;
+import org.thingsboard.rule.engine.api.*;
+import org.thingsboard.rule.engine.util.EntitiesRelatedEntityIdAsyncLoader;
+
+import org.thingsboard.server.common.data.id.EntityId;
+import org.thingsboard.server.common.data.plugin.ComponentType;
+
+@RuleNode(
+        type = ComponentType.ENRICHMENT,
+        name="related attributes",
+        configClazz = TbGetRelatedAttrNodeConfiguration.class,
+        nodeDescription = "Add Originators Related Entity Attributes or Latest Telemetry into Message Metadata",
+        nodeDetails = "Related Entity found using configured relation direction and Relation Type. " +
+                "If multiple Related Entities are found, only first Entity is used for attributes enrichment, other entities are discarded. " +
+                "If Attributes enrichment configured, server scope attributes are added into Message metadata. " +
+                "If Latest Telemetry enrichment configured, latest telemetry added into metadata. " +
+                "To access those attributes in other nodes this template can be used " +
+                "<code>metadata.temperature</code>.",
+        uiResources = {"static/rulenode/rulenode-core-config.js", "static/rulenode/rulenode-core-config.css"},
+        configDirective = "tbEnrichmentNodeRelatedAttributesConfig")
+
+public class TbGetRelatedAttributeNode extends TbEntityGetAttrNode<EntityId> {
+
+    private TbGetRelatedAttrNodeConfiguration config;
+
+    @Override
+    public void init(TbContext context, TbNodeConfiguration configuration) throws TbNodeException {
+        this.config = TbNodeUtils.convert(configuration, TbGetRelatedAttrNodeConfiguration.class);
+        setConfig(config);
+    }
+
+    @Override
+    protected ListenableFuture<EntityId> findEntityAsync(TbContext ctx, EntityId originator) {
+        return EntitiesRelatedEntityIdAsyncLoader.findEntityAsync(ctx, originator, config.getRelationsQuery());
+    }
+}
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetRelatedAttrNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetRelatedAttrNodeConfiguration.java
new file mode 100644
index 0000000..63186ca
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetRelatedAttrNodeConfiguration.java
@@ -0,0 +1,50 @@
+/**
+ * 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.RelationsQuery;
+import org.thingsboard.server.common.data.relation.EntityRelation;
+import org.thingsboard.server.common.data.relation.EntitySearchDirection;
+import org.thingsboard.server.common.data.relation.EntityTypeFilter;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+@Data
+public class TbGetRelatedAttrNodeConfiguration extends TbGetEntityAttrNodeConfiguration {
+
+    private RelationsQuery relationsQuery;
+
+    @Override
+    public TbGetRelatedAttrNodeConfiguration defaultConfiguration() {
+        TbGetRelatedAttrNodeConfiguration configuration = new TbGetRelatedAttrNodeConfiguration();
+        Map<String, String> attrMapping = new HashMap<>();
+        attrMapping.putIfAbsent("temperature", "tempo");
+        configuration.setAttrMapping(attrMapping);
+        configuration.setTelemetry(false);
+
+        RelationsQuery relationsQuery = new RelationsQuery();
+        relationsQuery.setDirection(EntitySearchDirection.FROM);
+        relationsQuery.setMaxLevel(1);
+        EntityTypeFilter entityTypeFilter = new EntityTypeFilter(EntityRelation.CONTAINS_TYPE, Collections.emptyList());
+        relationsQuery.setFilters(Collections.singletonList(entityTypeFilter));
+        configuration.setRelationsQuery(relationsQuery);
+
+        return configuration;
+    }
+}
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetTenantAttributeNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetTenantAttributeNode.java
new file mode 100644
index 0000000..1ae6c68
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetTenantAttributeNode.java
@@ -0,0 +1,46 @@
+/**
+ * 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.api.RuleNode;
+import org.thingsboard.rule.engine.api.TbContext;
+import org.thingsboard.rule.engine.util.EntitiesTenantIdAsyncLoader;
+import org.thingsboard.server.common.data.id.EntityId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.plugin.ComponentType;
+
+@Slf4j
+@RuleNode(
+        type = ComponentType.ENRICHMENT,
+        name="tenant attributes",
+        configClazz = TbGetEntityAttrNodeConfiguration.class,
+        nodeDescription = "Add Originators Tenant Attributes or Latest Telemetry into Message Metadata",
+        nodeDetails = "If Attributes enrichment configured, server scope attributes are added into Message metadata. " +
+                "If Latest Telemetry enrichment configured, latest telemetry added into metadata. " +
+                "To access those attributes in other nodes this template can be used " +
+                "<code>metadata.temperature</code>.",
+        uiResources = {"static/rulenode/rulenode-core-config.js", "static/rulenode/rulenode-core-config.css"},
+        configDirective = "tbEnrichmentNodeTenantAttributesConfig")
+public class TbGetTenantAttributeNode extends TbEntityGetAttrNode<TenantId> {
+
+    @Override
+    protected ListenableFuture<TenantId> findEntityAsync(TbContext ctx, EntityId originator) {
+        return EntitiesTenantIdAsyncLoader.findEntityIdAsync(ctx, originator);
+    }
+
+}
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/credentials/CertPemClientCredentials.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/credentials/CertPemClientCredentials.java
new file mode 100644
index 0000000..a462839
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/credentials/CertPemClientCredentials.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.rule.engine.mqtt.credentials;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import io.netty.handler.ssl.ClientAuth;
+import io.netty.handler.ssl.SslContext;
+import io.netty.handler.ssl.SslContextBuilder;
+import lombok.Data;
+import lombok.extern.slf4j.Slf4j;
+import org.thingsboard.mqtt.MqttClientConfig;
+import org.apache.commons.codec.binary.Base64;
+import org.bouncycastle.jce.provider.BouncyCastleProvider;
+import org.bouncycastle.openssl.PEMDecryptorProvider;
+import org.bouncycastle.openssl.PEMEncryptedKeyPair;
+import org.bouncycastle.openssl.PEMKeyPair;
+import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter;
+import org.bouncycastle.openssl.jcajce.JcePEMDecryptorProviderBuilder;
+import org.springframework.util.StringUtils;
+
+import javax.net.ssl.KeyManagerFactory;
+import javax.net.ssl.TrustManagerFactory;
+import java.io.ByteArrayInputStream;
+import java.security.*;
+import java.security.cert.Certificate;
+import java.security.cert.CertificateFactory;
+import java.security.cert.X509Certificate;
+import java.security.interfaces.RSAPrivateKey;
+import java.security.spec.PKCS8EncodedKeySpec;
+import java.util.Optional;
+
+@Data
+@Slf4j
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class CertPemClientCredentials implements MqttClientCredentials {
+
+    private static final String TLS_VERSION = "TLSv1.2";
+
+    private String caCert;
+    private String cert;
+    private String privateKey;
+    private String password;
+
+    @Override
+    public Optional<SslContext> initSslContext() {
+        try {
+            Security.addProvider(new BouncyCastleProvider());
+            return Optional.of(SslContextBuilder.forClient()
+                    .keyManager(createAndInitKeyManagerFactory())
+                    .trustManager(createAndInitTrustManagerFactory())
+                    .clientAuth(ClientAuth.REQUIRE)
+                    .build());
+        } catch (Exception e) {
+            log.error("[{}:{}] Creating TLS factory failed!", caCert, cert, e);
+            throw new RuntimeException("Creating TLS factory failed!", e);
+        }
+    }
+
+    @Override
+    public void configure(MqttClientConfig config) {
+
+    }
+
+    private KeyManagerFactory createAndInitKeyManagerFactory() throws Exception {
+        X509Certificate certHolder = readCertFile(cert);
+        Object keyObject = readPrivateKeyFile(privateKey);
+        char[] passwordCharArray = "".toCharArray();
+        if (!StringUtils.isEmpty(password)) {
+            passwordCharArray = password.toCharArray();
+        }
+
+        JcaPEMKeyConverter keyConverter = new JcaPEMKeyConverter().setProvider("BC");
+
+        PrivateKey privateKey;
+        if (keyObject instanceof PEMEncryptedKeyPair) {
+            PEMDecryptorProvider provider = new JcePEMDecryptorProviderBuilder().build(passwordCharArray);
+            KeyPair key = keyConverter.getKeyPair(((PEMEncryptedKeyPair) keyObject).decryptKeyPair(provider));
+            privateKey = key.getPrivate();
+        } else if (keyObject instanceof PEMKeyPair) {
+            KeyPair key = keyConverter.getKeyPair((PEMKeyPair) keyObject);
+            privateKey = key.getPrivate();
+        } else if (keyObject instanceof PrivateKey) {
+            privateKey = (PrivateKey)keyObject;
+        } else {
+            throw new RuntimeException("Unable to get private key from object: " + keyObject.getClass());
+        }
+
+        KeyStore clientKeyStore = KeyStore.getInstance(KeyStore.getDefaultType());
+        clientKeyStore.load(null, null);
+        clientKeyStore.setCertificateEntry("cert", certHolder);
+        clientKeyStore.setKeyEntry("private-key",
+                privateKey,
+                passwordCharArray,
+                new Certificate[]{certHolder});
+
+        KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
+        keyManagerFactory.init(clientKeyStore, passwordCharArray);
+        return keyManagerFactory;
+    }
+
+    private TrustManagerFactory createAndInitTrustManagerFactory() throws Exception {
+        X509Certificate caCertHolder;
+        caCertHolder = readCertFile(caCert);
+
+        KeyStore caKeyStore = KeyStore.getInstance(KeyStore.getDefaultType());
+        caKeyStore.load(null, null);
+        caKeyStore.setCertificateEntry("caCert-cert", caCertHolder);
+
+        TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
+        trustManagerFactory.init(caKeyStore);
+        return trustManagerFactory;
+    }
+
+    private X509Certificate readCertFile(String fileContent) throws Exception {
+        X509Certificate certificate = null;
+        if (fileContent != null && !fileContent.trim().isEmpty()) {
+            fileContent = fileContent.replace("-----BEGIN CERTIFICATE-----", "")
+                    .replace("-----END CERTIFICATE-----", "")
+                    .replaceAll("\\s", "");
+            byte[] decoded = Base64.decodeBase64(fileContent);
+            CertificateFactory certFactory = CertificateFactory.getInstance("X.509");
+            certificate = (X509Certificate) certFactory.generateCertificate(new ByteArrayInputStream(decoded));
+        }
+        return certificate;
+    }
+
+    private PrivateKey readPrivateKeyFile(String fileContent) throws Exception {
+        RSAPrivateKey privateKey = null;
+        if (fileContent != null && !fileContent.isEmpty()) {
+            fileContent = fileContent.replaceAll(".*BEGIN.*PRIVATE KEY.*", "")
+                    .replaceAll(".*END.*PRIVATE KEY.*", "")
+                    .replaceAll("\\s", "");
+            byte[] decoded = Base64.decodeBase64(fileContent);
+            KeyFactory keyFactory = KeyFactory.getInstance("RSA");
+            privateKey = (RSAPrivateKey) keyFactory.generatePrivate(new PKCS8EncodedKeySpec(decoded));
+        }
+        return privateKey;
+    }
+
+}
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/TbMqttNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/TbMqttNode.java
new file mode 100644
index 0000000..dc16a1e
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/TbMqttNode.java
@@ -0,0 +1,144 @@
+/**
+ * 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.mqtt;
+
+import io.netty.buffer.Unpooled;
+import io.netty.channel.EventLoopGroup;
+import io.netty.channel.nio.NioEventLoopGroup;
+import io.netty.handler.codec.mqtt.MqttQoS;
+import io.netty.handler.ssl.SslContext;
+import io.netty.handler.ssl.SslContextBuilder;
+import io.netty.util.concurrent.Future;
+import lombok.extern.slf4j.Slf4j;
+import org.thingsboard.mqtt.MqttClient;
+import org.thingsboard.mqtt.MqttClientConfig;
+import org.thingsboard.mqtt.MqttConnectResult;
+import org.springframework.util.StringUtils;
+import org.thingsboard.rule.engine.api.util.TbNodeUtils;
+import org.thingsboard.rule.engine.api.*;
+import org.thingsboard.server.common.data.plugin.ComponentType;
+import org.thingsboard.server.common.msg.TbMsg;
+import org.thingsboard.server.common.msg.TbMsgMetaData;
+
+import javax.net.ssl.SSLException;
+import java.nio.charset.Charset;
+import java.util.Optional;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+@Slf4j
+@RuleNode(
+        type = ComponentType.EXTERNAL,
+        name = "mqtt",
+        configClazz = TbMqttNodeConfiguration.class,
+        nodeDescription = "Publish messages to the MQTT broker",
+        nodeDetails = "Will publish message payload to the MQTT broker with QoS <b>AT_LEAST_ONCE</b>.",
+        uiResources = {"static/rulenode/rulenode-core-config.js", "static/rulenode/rulenode-core-config.css"},
+        configDirective = "tbActionNodeMqttConfig",
+        icon = "call_split"
+)
+public class TbMqttNode implements TbNode {
+
+    private static final Charset UTF8 = Charset.forName("UTF-8");
+
+    private static final String ERROR = "error";
+
+    private TbMqttNodeConfiguration config;
+
+    private EventLoopGroup eventLoopGroup;
+    private MqttClient mqttClient;
+
+    @Override
+    public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
+        try {
+            this.config = TbNodeUtils.convert(configuration, TbMqttNodeConfiguration.class);
+            this.eventLoopGroup = new NioEventLoopGroup();
+            this.mqttClient = initClient();
+        } catch (Exception e) {
+            throw new TbNodeException(e);
+        }
+    }
+
+    @Override
+    public void onMsg(TbContext ctx, TbMsg msg) throws ExecutionException, InterruptedException, TbNodeException {
+        String topic = TbNodeUtils.processPattern(this.config.getTopicPattern(), msg.getMetaData());
+        this.mqttClient.publish(topic, Unpooled.wrappedBuffer(msg.getData().getBytes(UTF8)), MqttQoS.AT_LEAST_ONCE)
+                .addListener(future -> {
+                    if (future.isSuccess()) {
+                        ctx.tellNext(msg, TbRelationTypes.SUCCESS);
+                    } else {
+                        TbMsg next = processException(ctx, msg, future.cause());
+                        ctx.tellFailure(next, future.cause());
+                    }
+                }
+        );
+    }
+
+    private TbMsg processException(TbContext ctx, TbMsg origMsg, Throwable e) {
+        TbMsgMetaData metaData = origMsg.getMetaData().copy();
+        metaData.putValue(ERROR, e.getClass() + ": " + e.getMessage());
+        return ctx.transformMsg(origMsg, origMsg.getType(), origMsg.getOriginator(), metaData, origMsg.getData());
+    }
+
+    @Override
+    public void destroy() {
+        if (this.mqttClient != null) {
+            this.mqttClient.disconnect();
+        }
+        if (this.eventLoopGroup != null) {
+            this.eventLoopGroup.shutdownGracefully(0, 5, TimeUnit.SECONDS);
+        }
+    }
+
+    private MqttClient initClient() throws Exception {
+        Optional<SslContext> sslContextOpt = initSslContext();
+        MqttClientConfig config = sslContextOpt.isPresent() ? new MqttClientConfig(sslContextOpt.get()) : new MqttClientConfig();
+        if (!StringUtils.isEmpty(this.config.getClientId())) {
+            config.setClientId(this.config.getClientId());
+        }
+        this.config.getCredentials().configure(config);
+        MqttClient client = MqttClient.create(config);
+        client.setEventLoop(this.eventLoopGroup);
+        Future<MqttConnectResult> connectFuture = client.connect(this.config.getHost(), this.config.getPort());
+        MqttConnectResult result;
+        try {
+            result = connectFuture.get(this.config.getConnectTimeoutSec(), TimeUnit.SECONDS);
+        } catch (TimeoutException ex) {
+            connectFuture.cancel(true);
+            client.disconnect();
+            String hostPort = this.config.getHost() + ":" + this.config.getPort();
+            throw new RuntimeException(String.format("Failed to connect to MQTT broker at %s.", hostPort));
+        }
+        if (!result.isSuccess()) {
+            connectFuture.cancel(true);
+            client.disconnect();
+            String hostPort = this.config.getHost() + ":" + this.config.getPort();
+            throw new RuntimeException(String.format("Failed to connect to MQTT broker at %s. Result code is: %s", hostPort, result.getReturnCode()));
+        }
+        return client;
+    }
+
+    private Optional<SslContext> initSslContext() throws SSLException {
+        Optional<SslContext> result = this.config.getCredentials().initSslContext();
+        if (this.config.isSsl() && !result.isPresent()) {
+            result = Optional.of(SslContextBuilder.forClient().build());
+        }
+        return result;
+    }
+
+}
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/TbMqttNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/TbMqttNodeConfiguration.java
new file mode 100644
index 0000000..f63e201
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/TbMqttNodeConfiguration.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.mqtt;
+
+import lombok.Data;
+import org.thingsboard.rule.engine.api.NodeConfiguration;
+import org.thingsboard.rule.engine.mqtt.credentials.AnonymousCredentials;
+import org.thingsboard.rule.engine.mqtt.credentials.MqttClientCredentials;
+
+@Data
+public class TbMqttNodeConfiguration implements NodeConfiguration<TbMqttNodeConfiguration> {
+
+    private String topicPattern;
+    private String host;
+    private int port;
+    private int connectTimeoutSec;
+    private String clientId;
+
+    private boolean ssl;
+    private MqttClientCredentials credentials;
+
+    @Override
+    public TbMqttNodeConfiguration defaultConfiguration() {
+        TbMqttNodeConfiguration configuration = new TbMqttNodeConfiguration();
+        configuration.setTopicPattern("my-topic");
+        configuration.setHost("localhost");
+        configuration.setPort(1883);
+        configuration.setConnectTimeoutSec(10);
+        configuration.setSsl(false);
+        configuration.setCredentials(new AnonymousCredentials());
+        return configuration;
+    }
+
+}
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rabbitmq/TbRabbitMqNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rabbitmq/TbRabbitMqNode.java
new file mode 100644
index 0000000..839eec7
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rabbitmq/TbRabbitMqNode.java
@@ -0,0 +1,148 @@
+/**
+ * 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.rabbitmq;
+
+import com.google.common.util.concurrent.ListenableFuture;
+import com.rabbitmq.client.*;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
+import org.thingsboard.rule.engine.api.util.TbNodeUtils;
+import org.thingsboard.rule.engine.api.*;
+import org.thingsboard.server.common.data.plugin.ComponentType;
+import org.thingsboard.server.common.msg.TbMsg;
+import org.thingsboard.server.common.msg.TbMsgMetaData;
+
+import java.nio.charset.Charset;
+import java.util.concurrent.ExecutionException;
+
+import static org.thingsboard.rule.engine.api.util.DonAsynchron.withCallback;
+
+@Slf4j
+@RuleNode(
+        type = ComponentType.EXTERNAL,
+        name = "rabbitmq",
+        configClazz = TbRabbitMqNodeConfiguration.class,
+        nodeDescription = "Publish messages to the RabbitMQ",
+        nodeDetails = "Will publish message payload to RabbitMQ queue.",
+        uiResources = {"static/rulenode/rulenode-core-config.js"},
+        configDirective = "tbActionNodeRabbitMqConfig",
+        iconUrl = "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbDpzcGFjZT0icHJlc2VydmUiIHZlcnNpb249IjEuMSIgeT0iMHB4IiB4PSIwcHgiIHZpZXdCb3g9IjAgMCAxMDAwIDEwMDAiPjxwYXRoIHN0cm9rZS13aWR0aD0iLjg0OTU2IiBkPSJtODYwLjQ3IDQxNi4zMmgtMjYyLjAxYy0xMi45MTMgMC0yMy42MTgtMTAuNzA0LTIzLjYxOC0yMy42MTh2LTI3Mi43MWMwLTIwLjMwNS0xNi4yMjctMzYuMjc2LTM2LjI3Ni0zNi4yNzZoLTkzLjc5MmMtMjAuMzA1IDAtMzYuMjc2IDE2LjIyNy0zNi4yNzYgMzYuMjc2djI3MC44NGMtMC4yNTQ4NyAxNC4xMDMtMTEuNDY5IDI1LjU3Mi0yNS43NDIgMjUuNTcybC04NS42MzYgMC42Nzk2NWMtMTQuMTAzIDAtMjUuNTcyLTExLjQ2OS0yNS41NzItMjUuNTcybDAuNjc5NjUtMjcxLjUyYzAtMjAuMzA1LTE2LjIyNy0zNi4yNzYtMzYuMjc2LTM2LjI3NmgtOTMuNTM3Yy0yMC4zMDUgMC0zNi4yNzYgMTYuMjI3LTM2LjI3NiAzNi4yNzZ2NzYzLjg0YzAgMTguMDk2IDE0Ljc4MiAzMi40NTMgMzIuNDUzIDMyLjQ1M2g3MjIuODFjMTguMDk2IDAgMzIuNDUzLTE0Ljc4MiAzMi40NTMtMzIuNDUzdi00MzUuMzFjLTEuMTg5NC0xOC4xODEtMTUuMjkyLTMyLjE5OC0zMy4zODgtMzIuMTk4em0tMTIyLjY4IDI4Ny4wN2MwIDIzLjYxOC0xOC44NiA0Mi40NzgtNDIuNDc4IDQyLjQ3OGgtNzMuOTk3Yy0yMy42MTggMC00Mi40NzgtMTguODYtNDIuNDc4LTQyLjQ3OHYtNzQuMjUyYzAtMjMuNjE4IDE4Ljg2LTQyLjQ3OCA0Mi40NzgtNDIuNDc4aDczLjk5N2MyMy42MTggMCA0Mi40NzggMTguODYgNDIuNDc4IDQyLjQ3OHoiLz48L3N2Zz4="
+)
+public class TbRabbitMqNode implements TbNode {
+
+    private static final Charset UTF8 = Charset.forName("UTF-8");
+
+    private static final String ERROR = "error";
+
+    private TbRabbitMqNodeConfiguration config;
+
+    private Connection connection;
+    private Channel channel;
+
+    @Override
+    public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
+        this.config = TbNodeUtils.convert(configuration, TbRabbitMqNodeConfiguration.class);
+        ConnectionFactory factory = new ConnectionFactory();
+        factory.setHost(this.config.getHost());
+        factory.setPort(this.config.getPort());
+        factory.setVirtualHost(this.config.getVirtualHost());
+        factory.setUsername(this.config.getUsername());
+        factory.setPassword(this.config.getPassword());
+        factory.setAutomaticRecoveryEnabled(this.config.isAutomaticRecoveryEnabled());
+        factory.setConnectionTimeout(this.config.getConnectionTimeout());
+        factory.setHandshakeTimeout(this.config.getHandshakeTimeout());
+        this.config.getClientProperties().forEach((k,v) -> factory.getClientProperties().put(k,v));
+        try {
+            this.connection = factory.newConnection();
+            this.channel = this.connection.createChannel();
+        } catch (Exception e) {
+            throw new TbNodeException(e);
+        }
+    }
+
+    @Override
+    public void onMsg(TbContext ctx, TbMsg msg) throws ExecutionException, InterruptedException, TbNodeException {
+        withCallback(publishMessageAsync(ctx, msg),
+                m -> ctx.tellNext(m, TbRelationTypes.SUCCESS),
+                t -> {
+                    TbMsg next = processException(ctx, msg, t);
+                    ctx.tellFailure(next, t);
+                });
+    }
+
+    private ListenableFuture<TbMsg> publishMessageAsync(TbContext ctx, TbMsg msg) {
+        return ctx.getExternalCallExecutor().executeAsync(() -> publishMessage(ctx, msg));
+    }
+
+    private TbMsg publishMessage(TbContext ctx, TbMsg msg) throws Exception {
+        String exchangeName = "";
+        if (!StringUtils.isEmpty(this.config.getExchangeNamePattern())) {
+            exchangeName = TbNodeUtils.processPattern(this.config.getExchangeNamePattern(), msg.getMetaData());
+        }
+        String routingKey = "";
+        if (!StringUtils.isEmpty(this.config.getRoutingKeyPattern())) {
+            routingKey = TbNodeUtils.processPattern(this.config.getRoutingKeyPattern(), msg.getMetaData());
+        }
+        AMQP.BasicProperties properties = null;
+        if (!StringUtils.isEmpty(this.config.getMessageProperties())) {
+            properties = convert(this.config.getMessageProperties());
+        }
+        channel.basicPublish(
+                exchangeName,
+                routingKey,
+                properties,
+                msg.getData().getBytes(UTF8));
+        return msg;
+    }
+
+    private TbMsg processException(TbContext ctx, TbMsg origMsg, Throwable t) {
+        TbMsgMetaData metaData = origMsg.getMetaData().copy();
+        metaData.putValue(ERROR, t.getClass() + ": " + t.getMessage());
+        return ctx.transformMsg(origMsg, origMsg.getType(), origMsg.getOriginator(), metaData, origMsg.getData());
+    }
+
+    @Override
+    public void destroy() {
+        if (this.connection != null) {
+            try {
+                this.connection.close();
+            } catch (Exception e) {
+                log.error("Failed to close connection during destroy()", e);
+            }
+        }
+    }
+
+    private static AMQP.BasicProperties convert(String name) throws TbNodeException {
+        switch (name) {
+            case "BASIC":
+                return MessageProperties.BASIC;
+            case "TEXT_PLAIN":
+                return MessageProperties.TEXT_PLAIN;
+            case "MINIMAL_BASIC":
+                return MessageProperties.MINIMAL_BASIC;
+            case "MINIMAL_PERSISTENT_BASIC":
+                return MessageProperties.MINIMAL_PERSISTENT_BASIC;
+            case "PERSISTENT_BASIC":
+                return MessageProperties.PERSISTENT_BASIC;
+            case "PERSISTENT_TEXT_PLAIN":
+                return MessageProperties.PERSISTENT_TEXT_PLAIN;
+            default:
+                throw new TbNodeException("Message Properties: '" + name + "' is undefined!");
+        }
+    }
+}
+
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rabbitmq/TbRabbitMqNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rabbitmq/TbRabbitMqNodeConfiguration.java
new file mode 100644
index 0000000..1a0a00a
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rabbitmq/TbRabbitMqNodeConfiguration.java
@@ -0,0 +1,59 @@
+/**
+ * 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.rabbitmq;
+
+import com.rabbitmq.client.ConnectionFactory;
+import lombok.Data;
+import org.thingsboard.rule.engine.api.NodeConfiguration;
+
+import java.util.Collections;
+import java.util.Map;
+
+@Data
+public class TbRabbitMqNodeConfiguration implements NodeConfiguration<TbRabbitMqNodeConfiguration> {
+
+    private String exchangeNamePattern;
+    private String routingKeyPattern;
+    private String messageProperties;
+    private String host;
+    private int port;
+    private String virtualHost;
+    private String username;
+    private String password;
+    private boolean automaticRecoveryEnabled;
+    private int connectionTimeout;
+    private int handshakeTimeout;
+    private Map<String, String> clientProperties;
+
+    @Override
+    public TbRabbitMqNodeConfiguration defaultConfiguration() {
+        TbRabbitMqNodeConfiguration configuration = new TbRabbitMqNodeConfiguration();
+        configuration.setExchangeNamePattern("");
+        configuration.setRoutingKeyPattern("");
+        configuration.setMessageProperties(null);
+        configuration.setHost(ConnectionFactory.DEFAULT_HOST);
+        configuration.setPort(ConnectionFactory.DEFAULT_AMQP_PORT);
+        configuration.setVirtualHost(ConnectionFactory.DEFAULT_VHOST);
+        configuration.setUsername(ConnectionFactory.DEFAULT_USER);
+        configuration.setPassword(ConnectionFactory.DEFAULT_PASS);
+        configuration.setAutomaticRecoveryEnabled(false);
+        configuration.setConnectionTimeout(ConnectionFactory.DEFAULT_CONNECTION_TIMEOUT);
+        configuration.setHandshakeTimeout(ConnectionFactory.DEFAULT_HANDSHAKE_TIMEOUT);
+        configuration.setClientProperties(Collections.emptyMap());
+        return configuration;
+    }
+}
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbRestApiCallNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbRestApiCallNode.java
new file mode 100644
index 0000000..f7f2d2d
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbRestApiCallNode.java
@@ -0,0 +1,158 @@
+/**
+ * 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.rest;
+
+import io.netty.channel.EventLoopGroup;
+import io.netty.channel.nio.NioEventLoopGroup;
+import io.netty.handler.ssl.SslContextBuilder;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.http.HttpEntity;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpMethod;
+import org.springframework.http.ResponseEntity;
+import org.springframework.http.client.Netty4ClientHttpRequestFactory;
+import org.springframework.util.concurrent.ListenableFuture;
+import org.springframework.util.concurrent.ListenableFutureCallback;
+import org.springframework.web.client.AsyncRestTemplate;
+import org.springframework.web.client.HttpClientErrorException;
+import org.thingsboard.rule.engine.api.util.TbNodeUtils;
+import org.thingsboard.rule.engine.api.*;
+import org.thingsboard.server.common.data.plugin.ComponentType;
+import org.thingsboard.server.common.msg.TbMsg;
+import org.thingsboard.server.common.msg.TbMsgMetaData;
+
+import javax.net.ssl.SSLException;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+
+@Slf4j
+@RuleNode(
+        type = ComponentType.EXTERNAL,
+        name = "rest api call",
+        configClazz = TbRestApiCallNodeConfiguration.class,
+        nodeDescription = "Invoke REST API calls to external REST server",
+        nodeDetails = "Will invoke REST API call <code>GET | POST | PUT | DELETE</code> to external REST server. " +
+                "Message payload added into Request body. Configured attributes can be added into Headers from Message Metadata." +
+                " Outbound message will contain response fields " +
+                "(<code>status</code>, <code>statusCode</code>, <code>statusReason</code> and response <code>headers</code>) in the Message Metadata." +
+                " Response body saved in outbound Message payload. " +
+                "For example <b>statusCode</b> field can be accessed with <code>metadata.statusCode</code>.",
+        uiResources = {"static/rulenode/rulenode-core-config.js"},
+        configDirective = "tbActionNodeRestApiCallConfig",
+        iconUrl = "data:image/svg+xml;base64,PHN2ZyBzdHlsZT0iZW5hYmxlLWJhY2tncm91bmQ6bmV3IDAgMCA1MTIgNTEyIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbDpzcGFjZT0icHJlc2VydmUiIHZpZXdCb3g9IjAgMCA1MTIgNTEyIiB2ZXJzaW9uPSIxLjEiIHk9IjBweCIgeD0iMHB4Ij48ZyB0cmFuc2Zvcm09Im1hdHJpeCguOTQ5NzUgMCAwIC45NDk3NSAxNy4xMiAyNi40OTIpIj48cGF0aCBkPSJtMTY5LjExIDEwOC41NGMtOS45MDY2IDAuMDczNC0xOS4wMTQgNi41NzI0LTIyLjAxNCAxNi40NjlsLTY5Ljk5MyAyMzEuMDhjLTMuNjkwNCAxMi4xODEgMy4yODkyIDI1LjIyIDE1LjQ2OSAyOC45MSAyLjIyNTkgMC42NzQ4MSA0LjQ5NjkgMSA2LjcyODUgMSA5Ljk3MjEgMCAxOS4xNjUtNi41MTUzIDIyLjE4Mi0xNi40NjdhNi41MjI0IDYuNTIyNCAwIDAgMCAwLjAwMiAtMC4wMDJsNjkuOTktMjMxLjA3YTYuNTIyNCA2LjUyMjQgMCAwIDAgMCAtMC4wMDJjMy42ODU1LTEyLjE4MS0zLjI4Ny0yNS4yMjUtMTUuNDcxLTI4LjkxMi0yLjI4MjUtMC42OTE0NS00LjYxMTYtMS4wMTY5LTYuODk4NC0xem04NC45ODggMGMtOS45MDQ4IDAuMDczNC0xOS4wMTggNi41Njc1LTIyLjAxOCAxNi40NjlsLTY5Ljk4NiAyMzEuMDhjLTMuNjg5OCAxMi4xNzkgMy4yODUzIDI1LjIxNyAxNS40NjUgMjguOTA4IDIuMjI5NyAwLjY3NjQ3IDQuNTAwOCAxLjAwMiA2LjczMjQgMS4wMDIgOS45NzIxIDAgMTkuMTY1LTYuNTE1MyAyMi4xODItMTYuNDY3YTYuNTIyNCA2LjUyMjQgMCAwIDAgMC4wMDIgLTAuMDAybDY5Ljk4OC0yMzEuMDdjMy42OTA4LTEyLjE4MS0zLjI4NTItMjUuMjIzLTE1LjQ2Ny0yOC45MTItMi4yODE0LTAuNjkyMzEtNC42MTA4LTEuMDE4OS02Ljg5ODQtMS4wMDJ6bS0yMTcuMjkgNDIuMjNjLTEyLjcyOS0wLjAwMDg3LTIzLjE4OCAxMC40NTYtMjMuMTg4IDIzLjE4NiAwLjAwMSAxMi43MjggMTAuNDU5IDIzLjE4NiAyMy4xODggMjMuMTg2IDEyLjcyNy0wLjAwMSAyMy4xODMtMTAuNDU5IDIzLjE4NC0yMy4xODYgMC4wMDA4NzYtMTIuNzI4LTEwLjQ1Ni0yMy4xODUtMjMuMTg0LTIzLjE4NnptMCAxNDYuNjRjLTEyLjcyNy0wLjAwMDg3LTIzLjE4NiAxMC40NTUtMjMuMTg4IDIzLjE4NC0wLjAwMDg3MyAxMi43MjkgMTAuNDU4IDIzLjE4OCAyMy4xODggMjMuMTg4IDEyLjcyOC0wLjAwMSAyMy4xODQtMTAuNDYgMjMuMTg0LTIzLjE4OC0wLjAwMS0xMi43MjYtMTAuNDU3LTIzLjE4My0yMy4xODQtMjMuMTg0em0yNzAuNzkgNDIuMjExYy0xMi43MjcgMC0yMy4xODQgMTAuNDU3LTIzLjE4NCAyMy4xODRzMTAuNDU1IDIzLjE4OCAyMy4xODQgMjMuMTg4aDE1NC45OGMxMi43MjkgMCAyMy4xODYtMTAuNDYgMjMuMTg2LTIzLjE4OCAwLjAwMS0xMi43MjgtMTAuNDU4LTIzLjE4NC0yMy4xODYtMjMuMTg0eiIgdHJhbnNmb3JtPSJtYXRyaXgoMS4wMzc2IDAgMCAxLjAzNzYgLTcuNTY3NiAtMTQuOTI1KSIgc3Ryb2tlLXdpZHRoPSIxLjI2OTMiLz48L2c+PC9zdmc+"
+)
+public class TbRestApiCallNode implements TbNode {
+
+    private static final String STATUS = "status";
+    private static final String STATUS_CODE = "statusCode";
+    private static final String STATUS_REASON = "statusReason";
+    private static final String ERROR = "error";
+    private static final String ERROR_BODY = "error_body";
+
+    private TbRestApiCallNodeConfiguration config;
+
+    private EventLoopGroup eventLoopGroup;
+    private AsyncRestTemplate httpClient;
+
+    @Override
+    public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
+        try {
+            this.config = TbNodeUtils.convert(configuration, TbRestApiCallNodeConfiguration.class);
+            this.eventLoopGroup = new NioEventLoopGroup();
+            Netty4ClientHttpRequestFactory nettyFactory = new Netty4ClientHttpRequestFactory(this.eventLoopGroup);
+            nettyFactory.setSslContext(SslContextBuilder.forClient().build());
+            httpClient = new AsyncRestTemplate(nettyFactory);
+        } catch (SSLException e) {
+            throw new TbNodeException(e);
+        }
+    }
+
+    @Override
+    public void onMsg(TbContext ctx, TbMsg msg) throws ExecutionException, InterruptedException, TbNodeException {
+        String endpointUrl = TbNodeUtils.processPattern(config.getRestEndpointUrlPattern(), msg.getMetaData());
+        HttpHeaders headers = prepareHeaders(msg.getMetaData());
+        HttpMethod method = HttpMethod.valueOf(config.getRequestMethod());
+        HttpEntity<String> entity = new HttpEntity<>(msg.getData(), headers);
+
+        ListenableFuture<ResponseEntity<String>> future = httpClient.exchange(
+                endpointUrl, method, entity, String.class);
+
+        future.addCallback(new ListenableFutureCallback<ResponseEntity<String>>() {
+            @Override
+            public void onFailure(Throwable throwable) {
+                TbMsg next = processException(ctx, msg, throwable);
+                ctx.tellFailure(next, throwable);
+            }
+
+            @Override
+            public void onSuccess(ResponseEntity<String> responseEntity) {
+                if (responseEntity.getStatusCode().is2xxSuccessful()) {
+                    TbMsg next = processResponse(ctx, msg, responseEntity);
+                    ctx.tellNext(next, TbRelationTypes.SUCCESS);
+                } else {
+                    TbMsg next = processFailureResponse(ctx, msg, responseEntity);
+                    ctx.tellNext(next, TbRelationTypes.FAILURE);
+                }
+            }
+        });
+    }
+
+    @Override
+    public void destroy() {
+        if (this.eventLoopGroup != null) {
+            this.eventLoopGroup.shutdownGracefully(0, 5, TimeUnit.SECONDS);
+        }
+    }
+
+    private TbMsg processResponse(TbContext ctx, TbMsg origMsg, ResponseEntity<String> response) {
+        TbMsgMetaData metaData = new TbMsgMetaData();
+        metaData.putValue(STATUS, response.getStatusCode().name());
+        metaData.putValue(STATUS_CODE, response.getStatusCode().value()+"");
+        metaData.putValue(STATUS_REASON, response.getStatusCode().getReasonPhrase());
+        response.getHeaders().toSingleValueMap().forEach((k,v) -> metaData.putValue(k,v) );
+        return ctx.transformMsg(origMsg, origMsg.getType(), origMsg.getOriginator(), metaData, response.getBody());
+    }
+
+    private TbMsg processFailureResponse(TbContext ctx, TbMsg origMsg, ResponseEntity<String> response) {
+        TbMsgMetaData metaData = origMsg.getMetaData().copy();
+        metaData.putValue(STATUS, response.getStatusCode().name());
+        metaData.putValue(STATUS_CODE, response.getStatusCode().value()+"");
+        metaData.putValue(STATUS_REASON, response.getStatusCode().getReasonPhrase());
+        metaData.putValue(ERROR_BODY, response.getBody());
+        return ctx.transformMsg(origMsg, origMsg.getType(), origMsg.getOriginator(), metaData, origMsg.getData());
+    }
+
+    private TbMsg processException(TbContext ctx, TbMsg origMsg, Throwable e) {
+        TbMsgMetaData metaData = origMsg.getMetaData().copy();
+        metaData.putValue(ERROR, e.getClass() + ": " + e.getMessage());
+        if (e instanceof HttpClientErrorException) {
+            HttpClientErrorException httpClientErrorException = (HttpClientErrorException)e;
+            metaData.putValue(STATUS, httpClientErrorException.getStatusText());
+            metaData.putValue(STATUS_CODE, httpClientErrorException.getRawStatusCode()+"");
+            metaData.putValue(ERROR_BODY, httpClientErrorException.getResponseBodyAsString());
+        }
+        return ctx.transformMsg(origMsg, origMsg.getType(), origMsg.getOriginator(), metaData, origMsg.getData());
+    }
+
+    private HttpHeaders prepareHeaders(TbMsgMetaData metaData) {
+        HttpHeaders headers = new HttpHeaders();
+        config.getHeaders().forEach((k,v) -> {
+            headers.add(TbNodeUtils.processPattern(k, metaData), TbNodeUtils.processPattern(v, metaData));
+        });
+        return headers;
+    }
+
+}
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbRestApiCallNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbRestApiCallNodeConfiguration.java
new file mode 100644
index 0000000..812eb77
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbRestApiCallNodeConfiguration.java
@@ -0,0 +1,39 @@
+/**
+ * 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.rest;
+
+import lombok.Data;
+import org.thingsboard.rule.engine.api.NodeConfiguration;
+
+import java.util.Collections;
+import java.util.Map;
+
+@Data
+public class TbRestApiCallNodeConfiguration implements NodeConfiguration<TbRestApiCallNodeConfiguration> {
+
+    private String restEndpointUrlPattern;
+    private String requestMethod;
+    private Map<String, String> headers;
+
+    @Override
+    public TbRestApiCallNodeConfiguration defaultConfiguration() {
+        TbRestApiCallNodeConfiguration configuration = new TbRestApiCallNodeConfiguration();
+        configuration.setRestEndpointUrlPattern("http://localhost/api");
+        configuration.setRequestMethod("POST");
+        configuration.setHeaders(Collections.emptyMap());
+        return configuration;
+    }
+}
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rpc/TbSendRPCReplyNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rpc/TbSendRPCReplyNode.java
new file mode 100644
index 0000000..288c2ea
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rpc/TbSendRPCReplyNode.java
@@ -0,0 +1,69 @@
+/**
+ * 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.rpc;
+
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.util.StringUtils;
+import org.thingsboard.rule.engine.api.util.TbNodeUtils;
+import org.thingsboard.rule.engine.api.RuleNode;
+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.EntityType;
+import org.thingsboard.server.common.data.id.DeviceId;
+import org.thingsboard.server.common.data.plugin.ComponentType;
+import org.thingsboard.server.common.msg.TbMsg;
+
+@Slf4j
+@RuleNode(
+        type = ComponentType.ACTION,
+        name = "rpc call reply",
+        configClazz = TbSendRpcReplyNodeConfiguration.class,
+        nodeDescription = "Sends one-way RPC call to device",
+        nodeDetails = "Expects messages with any message type. Will forward message body to the device.",
+        uiResources = {"static/rulenode/rulenode-core-config.js"},
+        configDirective = "tbActionNodeRpcReplyConfig",
+        icon = "call_merge"
+)
+public class TbSendRPCReplyNode implements TbNode {
+
+    private TbSendRpcReplyNodeConfiguration config;
+
+    @Override
+    public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
+        this.config = TbNodeUtils.convert(configuration, TbSendRpcReplyNodeConfiguration.class);
+    }
+
+    @Override
+    public void onMsg(TbContext ctx, TbMsg msg) {
+        String requestIdStr = msg.getMetaData().getValue(config.getRequestIdMetaDataAttribute());
+        if (msg.getOriginator().getEntityType() != EntityType.DEVICE) {
+            ctx.tellFailure(msg, new RuntimeException("Message originator is not a device entity!"));
+        } else if (StringUtils.isEmpty(requestIdStr)) {
+            ctx.tellFailure(msg, new RuntimeException("Request id is not present in the metadata!"));
+        } else if (StringUtils.isEmpty(msg.getData())) {
+            ctx.tellFailure(msg, new RuntimeException("Request body is empty!"));
+        } else {
+            ctx.getRpcService().sendRpcReply(new DeviceId(msg.getOriginator().getId()), Integer.parseInt(requestIdStr), msg.getData());
+        }
+    }
+
+    @Override
+    public void destroy() {
+    }
+
+}
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rpc/TbSendRpcReplyNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rpc/TbSendRpcReplyNodeConfiguration.java
new file mode 100644
index 0000000..402a33b
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rpc/TbSendRpcReplyNodeConfiguration.java
@@ -0,0 +1,33 @@
+/**
+ * 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.rpc;
+
+import lombok.Data;
+import org.thingsboard.rule.engine.api.NodeConfiguration;
+import org.thingsboard.server.common.data.DataConstants;
+
+@Data
+public class TbSendRpcReplyNodeConfiguration implements NodeConfiguration<TbSendRpcReplyNodeConfiguration> {
+
+    private String requestIdMetaDataAttribute;
+
+    @Override
+    public TbSendRpcReplyNodeConfiguration defaultConfiguration() {
+        TbSendRpcReplyNodeConfiguration configuration = new TbSendRpcReplyNodeConfiguration();
+        configuration.setRequestIdMetaDataAttribute("requestId");
+        return configuration;
+    }
+}
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rpc/TbSendRPCRequestNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rpc/TbSendRPCRequestNode.java
new file mode 100644
index 0000000..c1165ca
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rpc/TbSendRPCRequestNode.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.rpc;
+
+import com.google.gson.Gson;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParser;
+import lombok.extern.slf4j.Slf4j;
+import org.thingsboard.rule.engine.api.util.TbNodeUtils;
+import org.thingsboard.rule.engine.api.RuleEngineDeviceRpcRequest;
+import org.thingsboard.rule.engine.api.RuleNode;
+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.rule.engine.api.TbRelationTypes;
+import org.thingsboard.server.common.data.EntityType;
+import org.thingsboard.server.common.data.id.DeviceId;
+import org.thingsboard.server.common.data.plugin.ComponentType;
+import org.thingsboard.server.common.msg.TbMsg;
+
+import java.util.Random;
+import java.util.concurrent.TimeUnit;
+
+@Slf4j
+@RuleNode(
+        type = ComponentType.ACTION,
+        name = "rpc call request",
+        configClazz = TbSendRpcRequestNodeConfiguration.class,
+        nodeDescription = "Sends two-way RPC call to device",
+        nodeDetails = "Expects messages with \"method\" and \"params\". Will forward response from device to next nodes.",
+        uiResources = {"static/rulenode/rulenode-core-config.js"},
+        configDirective = "tbActionNodeRpcRequestConfig",
+        icon = "call_made"
+)
+public class TbSendRPCRequestNode implements TbNode {
+
+    private Random random = new Random();
+    private Gson gson = new Gson();
+    private JsonParser jsonParser = new JsonParser();
+    private TbSendRpcRequestNodeConfiguration config;
+
+    @Override
+    public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
+        this.config = TbNodeUtils.convert(configuration, TbSendRpcRequestNodeConfiguration.class);
+    }
+
+    @Override
+    public void onMsg(TbContext ctx, TbMsg msg) {
+        JsonObject json = jsonParser.parse(msg.getData()).getAsJsonObject();
+
+        if (msg.getOriginator().getEntityType() != EntityType.DEVICE) {
+            ctx.tellFailure(msg, new RuntimeException("Message originator is not a device entity!"));
+        } else if (!json.has("method")) {
+            ctx.tellFailure(msg, new RuntimeException("Method is not present in the message!"));
+        } else if (!json.has("params")) {
+            ctx.tellFailure(msg, new RuntimeException("Params are not present in the message!"));
+        } else {
+            int requestId = json.has("requestId") ? json.get("requestId").getAsInt() : random.nextInt();
+            RuleEngineDeviceRpcRequest request = RuleEngineDeviceRpcRequest.builder()
+                    .method(json.get("method").getAsString())
+                    .body(gson.toJson(json.get("params")))
+                    .deviceId(new DeviceId(msg.getOriginator().getId()))
+                    .requestId(requestId)
+                    .timeout(TimeUnit.SECONDS.toMillis(config.getTimeoutInSeconds()))
+                    .build();
+
+            ctx.getRpcService().sendRpcRequest(request, ruleEngineDeviceRpcResponse -> {
+                if (!ruleEngineDeviceRpcResponse.getError().isPresent()) {
+                    TbMsg next = ctx.transformMsg(msg, msg.getType(), msg.getOriginator(), msg.getMetaData(), ruleEngineDeviceRpcResponse.getResponse().get());
+                    ctx.tellNext(next, TbRelationTypes.SUCCESS);
+                } else {
+                    TbMsg next = ctx.transformMsg(msg, msg.getType(), msg.getOriginator(), msg.getMetaData(), wrap("error", ruleEngineDeviceRpcResponse.getError().get().name()));
+                    ctx.tellFailure(next, new RuntimeException(ruleEngineDeviceRpcResponse.getError().get().name()));
+                }
+            });
+        }
+    }
+
+    @Override
+    public void destroy() {
+    }
+
+    private String wrap(String name, String body) {
+        JsonObject json = new JsonObject();
+        json.addProperty(name, body);
+        return gson.toJson(json);
+    }
+
+}
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgAttributesNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgAttributesNode.java
new file mode 100644
index 0000000..4f82884
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgAttributesNode.java
@@ -0,0 +1,71 @@
+/**
+ * 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.telemetry;
+
+import com.google.gson.JsonParser;
+import lombok.extern.slf4j.Slf4j;
+import org.thingsboard.rule.engine.api.util.TbNodeUtils;
+import org.thingsboard.rule.engine.api.RuleNode;
+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.kv.AttributeKvEntry;
+import org.thingsboard.server.common.data.plugin.ComponentType;
+import org.thingsboard.server.common.msg.TbMsg;
+import org.thingsboard.server.common.msg.session.SessionMsgType;
+import org.thingsboard.server.common.transport.adaptor.JsonConverter;
+
+import java.util.ArrayList;
+import java.util.Set;
+
+@Slf4j
+@RuleNode(
+        type = ComponentType.ACTION,
+        name = "save attributes",
+        configClazz = TbMsgAttributesNodeConfiguration.class,
+        nodeDescription = "Saves attributes data",
+        nodeDetails = "Saves entity attributes based on configurable scope parameter. Expects messages with 'POST_ATTRIBUTES_REQUEST' message type",
+        uiResources = {"static/rulenode/rulenode-core-config.js", "static/rulenode/rulenode-core-config.css"},
+        configDirective = "tbActionNodeAttributesConfig",
+        icon = "file_upload"
+)
+public class TbMsgAttributesNode implements TbNode {
+
+    private TbMsgAttributesNodeConfiguration config;
+
+    @Override
+    public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
+        this.config = TbNodeUtils.convert(configuration, TbMsgAttributesNodeConfiguration.class);
+    }
+
+    @Override
+    public void onMsg(TbContext ctx, TbMsg msg) {
+        if (!msg.getType().equals(SessionMsgType.POST_ATTRIBUTES_REQUEST.name())) {
+            ctx.tellFailure(msg, new IllegalArgumentException("Unsupported msg type: " + msg.getType()));
+            return;
+        }
+
+        String src = msg.getData();
+        Set<AttributeKvEntry> attributes = JsonConverter.convertToAttributes(new JsonParser().parse(src)).getAttributes();
+        ctx.getTelemetryService().saveAndNotify(msg.getOriginator(), config.getScope(), new ArrayList<>(attributes), new TelemetryNodeCallback(ctx, msg));
+    }
+
+    @Override
+    public void destroy() {
+    }
+
+}
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgAttributesNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgAttributesNodeConfiguration.java
new file mode 100644
index 0000000..c633917
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgAttributesNodeConfiguration.java
@@ -0,0 +1,33 @@
+/**
+ * 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.telemetry;
+
+import lombok.Data;
+import org.thingsboard.rule.engine.api.NodeConfiguration;
+import org.thingsboard.server.common.data.DataConstants;
+
+@Data
+public class TbMsgAttributesNodeConfiguration implements NodeConfiguration<TbMsgAttributesNodeConfiguration> {
+
+    private String scope;
+
+    @Override
+    public TbMsgAttributesNodeConfiguration defaultConfiguration() {
+        TbMsgAttributesNodeConfiguration configuration = new TbMsgAttributesNodeConfiguration();
+        configuration.setScope(DataConstants.SERVER_SCOPE);
+        return configuration;
+    }
+}
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgTimeseriesNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgTimeseriesNode.java
new file mode 100644
index 0000000..efdf4af
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgTimeseriesNode.java
@@ -0,0 +1,97 @@
+/**
+ * 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.telemetry;
+
+import com.google.gson.JsonParser;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.util.StringUtils;
+import org.thingsboard.rule.engine.api.util.TbNodeUtils;
+import org.thingsboard.rule.engine.api.RuleNode;
+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.kv.BasicTsKvEntry;
+import org.thingsboard.server.common.data.kv.KvEntry;
+import org.thingsboard.server.common.data.kv.TsKvEntry;
+import org.thingsboard.server.common.data.plugin.ComponentType;
+import org.thingsboard.server.common.msg.TbMsg;
+import org.thingsboard.server.common.msg.core.TelemetryUploadRequest;
+import org.thingsboard.server.common.msg.session.SessionMsgType;
+import org.thingsboard.server.common.transport.adaptor.JsonConverter;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+@Slf4j
+@RuleNode(
+        type = ComponentType.ACTION,
+        name = "save timeseries",
+        configClazz = TbMsgTimeseriesNodeConfiguration.class,
+        nodeDescription = "Saves timeseries data",
+        nodeDetails = "Saves timeseries telemetry data based on configurable TTL parameter. Expects messages with 'POST_TELEMETRY_REQUEST' message type",
+        uiResources = {"static/rulenode/rulenode-core-config.js", "static/rulenode/rulenode-core-config.css"},
+        configDirective = "tbActionNodeTimeseriesConfig",
+        icon = "file_upload"
+)
+public class TbMsgTimeseriesNode implements TbNode {
+
+    private TbMsgTimeseriesNodeConfiguration config;
+
+    @Override
+    public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
+        this.config = TbNodeUtils.convert(configuration, TbMsgTimeseriesNodeConfiguration.class);
+    }
+
+    @Override
+    public void onMsg(TbContext ctx, TbMsg msg) {
+        if (!msg.getType().equals(SessionMsgType.POST_TELEMETRY_REQUEST.name())) {
+            ctx.tellFailure(msg, new IllegalArgumentException("Unsupported msg type: " + msg.getType()));
+            return;
+        }
+        long ts = -1;
+        String tsStr = msg.getMetaData().getValue("ts");
+        if (!StringUtils.isEmpty(tsStr)) {
+            try {
+                ts = Long.parseLong(tsStr);
+            } catch (NumberFormatException e) {}
+        } else {
+            ts = System.currentTimeMillis();
+        }
+        String src = msg.getData();
+        TelemetryUploadRequest telemetryUploadRequest = JsonConverter.convertToTelemetry(new JsonParser().parse(src), ts);
+        Map<Long, List<KvEntry>> tsKvMap = telemetryUploadRequest.getData();
+        if (tsKvMap == null) {
+            ctx.tellFailure(msg, new IllegalArgumentException("Msg body is empty: " + src));
+            return;
+        }
+        List<TsKvEntry> tsKvEntryList = new ArrayList<>();
+        for (Map.Entry<Long, List<KvEntry>> tsKvEntry : tsKvMap.entrySet()) {
+            for (KvEntry kvEntry : tsKvEntry.getValue()) {
+                tsKvEntryList.add(new BasicTsKvEntry(tsKvEntry.getKey(), kvEntry));
+            }
+        }
+        String ttlValue = msg.getMetaData().getValue("TTL");
+        long ttl = !StringUtils.isEmpty(ttlValue) ? Long.valueOf(ttlValue) : config.getDefaultTTL();
+        ctx.getTelemetryService().saveAndNotify(msg.getOriginator(), tsKvEntryList, ttl, new TelemetryNodeCallback(ctx, msg));
+    }
+
+    @Override
+    public void destroy() {
+    }
+
+}
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbAbstractTransformNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbAbstractTransformNode.java
new file mode 100644
index 0000000..679745c
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbAbstractTransformNode.java
@@ -0,0 +1,62 @@
+/**
+ * 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.transform;
+
+import com.google.common.util.concurrent.ListenableFuture;
+import lombok.extern.slf4j.Slf4j;
+import org.thingsboard.rule.engine.api.util.TbNodeUtils;
+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.msg.TbMsg;
+
+import static org.thingsboard.rule.engine.api.util.DonAsynchron.withCallback;
+import static org.thingsboard.rule.engine.api.TbRelationTypes.FAILURE;
+import static org.thingsboard.rule.engine.api.TbRelationTypes.SUCCESS;
+
+/**
+ * Created by ashvayka on 19.01.18.
+ */
+@Slf4j
+public abstract class TbAbstractTransformNode implements TbNode {
+
+    private TbTransformNodeConfiguration config;
+
+    @Override
+    public void init(TbContext context, TbNodeConfiguration configuration) throws TbNodeException {
+        this.config = TbNodeUtils.convert(configuration, TbTransformNodeConfiguration.class);
+    }
+
+    @Override
+    public void onMsg(TbContext ctx, TbMsg msg) {
+        withCallback(transform(ctx, msg),
+                m -> {
+                    if (m != null) {
+                        ctx.tellNext(m, SUCCESS);
+                    } else {
+                        ctx.tellNext(msg, FAILURE);
+                    }
+                },
+                t -> ctx.tellFailure(msg, t));
+    }
+
+    protected abstract ListenableFuture<TbMsg> transform(TbContext ctx, TbMsg msg);
+
+    public void setConfig(TbTransformNodeConfiguration config) {
+        this.config = config;
+    }
+}
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbChangeOriginatorNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbChangeOriginatorNode.java
new file mode 100644
index 0000000..a68cc82
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbChangeOriginatorNode.java
@@ -0,0 +1,109 @@
+/**
+ * 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.transform;
+
+import com.google.common.base.Function;
+import com.google.common.collect.Sets;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import lombok.extern.slf4j.Slf4j;
+import org.thingsboard.rule.engine.api.util.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.EntitiesCustomerIdAsyncLoader;
+import org.thingsboard.rule.engine.util.EntitiesRelatedEntityIdAsyncLoader;
+import org.thingsboard.rule.engine.util.EntitiesTenantIdAsyncLoader;
+import org.thingsboard.server.common.data.id.EntityId;
+import org.thingsboard.server.common.data.plugin.ComponentType;
+import org.thingsboard.server.common.msg.TbMsg;
+
+import java.util.HashSet;
+
+@Slf4j
+@RuleNode(
+        type = ComponentType.TRANSFORMATION,
+        name = "change originator",
+        configClazz = TbChangeOriginatorNodeConfiguration.class,
+        nodeDescription = "Change Message Originator To Tenant/Customer/Related Entity",
+        nodeDetails = "Related Entity found using configured relation direction and Relation Type. " +
+                "If multiple Related Entities are found, only first Entity is used as new Originator, other entities are discarded. ",
+        uiResources = {"static/rulenode/rulenode-core-config.js", "static/rulenode/rulenode-core-config.css"},
+        configDirective = "tbTransformationNodeChangeOriginatorConfig",
+        icon = "find_replace"
+)
+public class TbChangeOriginatorNode extends TbAbstractTransformNode {
+
+    protected static final String CUSTOMER_SOURCE = "CUSTOMER";
+    protected static final String TENANT_SOURCE = "TENANT";
+    protected static final String RELATED_SOURCE = "RELATED";
+
+    private TbChangeOriginatorNodeConfiguration config;
+
+    @Override
+    public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
+        this.config = TbNodeUtils.convert(configuration, TbChangeOriginatorNodeConfiguration.class);
+        validateConfig(config);
+        setConfig(config);
+    }
+
+    @Override
+    protected ListenableFuture<TbMsg> transform(TbContext ctx, TbMsg msg) {
+        ListenableFuture<? extends EntityId> newOriginator = getNewOriginator(ctx, msg.getOriginator());
+        return Futures.transform(newOriginator, (Function<EntityId, TbMsg>) n -> {
+            if (n == null || n.isNullUid()) {
+                return null;
+            }
+            return ctx.transformMsg(msg, msg.getType(), n, msg.getMetaData(), msg.getData());
+        }, ctx.getDbCallbackExecutor());
+    }
+
+    private ListenableFuture<? extends EntityId> getNewOriginator(TbContext ctx, EntityId original) {
+        switch (config.getOriginatorSource()) {
+            case CUSTOMER_SOURCE:
+                return EntitiesCustomerIdAsyncLoader.findEntityIdAsync(ctx, original);
+            case TENANT_SOURCE:
+                return EntitiesTenantIdAsyncLoader.findEntityIdAsync(ctx, original);
+            case RELATED_SOURCE:
+                return EntitiesRelatedEntityIdAsyncLoader.findEntityAsync(ctx, original, config.getRelationsQuery());
+            default:
+                return Futures.immediateFailedFuture(new IllegalStateException("Unexpected originator source " + config.getOriginatorSource()));
+        }
+    }
+
+    private void validateConfig(TbChangeOriginatorNodeConfiguration conf) {
+        HashSet<String> knownSources = Sets.newHashSet(CUSTOMER_SOURCE, TENANT_SOURCE, RELATED_SOURCE);
+        if (!knownSources.contains(conf.getOriginatorSource())) {
+            log.error("Unsupported source [{}] for TbChangeOriginatorNode", conf.getOriginatorSource());
+            throw new IllegalArgumentException("Unsupported source TbChangeOriginatorNode" + conf.getOriginatorSource());
+        }
+
+        if (conf.getOriginatorSource().equals(RELATED_SOURCE)) {
+            if (conf.getRelationsQuery() == null) {
+                log.error("Related source for TbChangeOriginatorNode should have relations query. Actual [{}]",
+                        conf.getRelationsQuery());
+                throw new IllegalArgumentException("Wrong config for RElated Source in TbChangeOriginatorNode" + conf.getOriginatorSource());
+            }
+        }
+
+    }
+
+    @Override
+    public void destroy() {
+
+    }
+}
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbChangeOriginatorNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbChangeOriginatorNodeConfiguration.java
new file mode 100644
index 0000000..32e9119
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbChangeOriginatorNodeConfiguration.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.transform;
+
+import lombok.Data;
+import org.thingsboard.rule.engine.api.NodeConfiguration;
+import org.thingsboard.rule.engine.data.RelationsQuery;
+import org.thingsboard.server.common.data.relation.EntityRelation;
+import org.thingsboard.server.common.data.relation.EntitySearchDirection;
+import org.thingsboard.server.common.data.relation.EntityTypeFilter;
+
+import java.util.Collections;
+
+@Data
+public class TbChangeOriginatorNodeConfiguration extends TbTransformNodeConfiguration implements NodeConfiguration {
+
+    private String originatorSource;
+
+    private RelationsQuery relationsQuery;
+
+    @Override
+    public TbChangeOriginatorNodeConfiguration defaultConfiguration() {
+        TbChangeOriginatorNodeConfiguration configuration = new TbChangeOriginatorNodeConfiguration();
+        configuration.setOriginatorSource(TbChangeOriginatorNode.CUSTOMER_SOURCE);
+
+        RelationsQuery relationsQuery = new RelationsQuery();
+        relationsQuery.setDirection(EntitySearchDirection.FROM);
+        relationsQuery.setMaxLevel(1);
+        EntityTypeFilter entityTypeFilter = new EntityTypeFilter(EntityRelation.CONTAINS_TYPE, Collections.emptyList());
+        relationsQuery.setFilters(Collections.singletonList(entityTypeFilter));
+        configuration.setRelationsQuery(relationsQuery);
+
+        return configuration;
+    }
+}
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbTransformMsgNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbTransformMsgNode.java
new file mode 100644
index 0000000..ab73d7c
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbTransformMsgNode.java
@@ -0,0 +1,61 @@
+/**
+ * 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.transform;
+
+import com.google.common.util.concurrent.ListenableFuture;
+import org.thingsboard.rule.engine.api.util.TbNodeUtils;
+import org.thingsboard.rule.engine.api.*;
+import org.thingsboard.server.common.data.plugin.ComponentType;
+import org.thingsboard.server.common.msg.TbMsg;
+
+@RuleNode(
+        type = ComponentType.TRANSFORMATION,
+        name = "script",
+        configClazz = TbTransformMsgNodeConfiguration.class,
+        nodeDescription = "Change Message payload, Metadata or Message type using JavaScript",
+        nodeDetails = "JavaScript function receive 3 input parameters <br/> " +
+                "<code>metadata</code> - is a Message metadata.<br/>" +
+                "<code>msg</code> - is a Message payload.<br/>" +
+                "<code>msgType</code> - is a Message type.<br/>" +
+                "Should return the following structure:<br/>" +
+                "<code>{ msg: <i style=\"color: #666;\">new payload</i>,<br/>&nbsp&nbsp&nbspmetadata: <i style=\"color: #666;\">new metadata</i>,<br/>&nbsp&nbsp&nbspmsgType: <i style=\"color: #666;\">new msgType</i> }</code><br/>" +
+                "All fields in resulting object are optional and will be taken from original message if not specified.",
+        uiResources = {"static/rulenode/rulenode-core-config.js", "static/rulenode/rulenode-core-config.css"},
+        configDirective = "tbTransformationNodeScriptConfig")
+public class TbTransformMsgNode extends TbAbstractTransformNode {
+
+    private TbTransformMsgNodeConfiguration config;
+    private ScriptEngine jsEngine;
+
+    @Override
+    public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
+        this.config = TbNodeUtils.convert(configuration, TbTransformMsgNodeConfiguration.class);
+        this.jsEngine = ctx.createJsScriptEngine(config.getJsScript());
+        setConfig(config);
+    }
+
+    @Override
+    protected ListenableFuture<TbMsg> transform(TbContext ctx, TbMsg msg) {
+        return ctx.getJsExecutor().executeAsync(() -> jsEngine.executeUpdate(msg));
+    }
+
+    @Override
+    public void destroy() {
+        if (jsEngine != null) {
+            jsEngine.destroy();
+        }
+    }
+}
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbTransformMsgNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbTransformMsgNodeConfiguration.java
new file mode 100644
index 0000000..3e112ad
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbTransformMsgNodeConfiguration.java
@@ -0,0 +1,32 @@
+/**
+ * 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.transform;
+
+import lombok.Data;
+import org.thingsboard.rule.engine.api.NodeConfiguration;
+
+@Data
+public class TbTransformMsgNodeConfiguration extends TbTransformNodeConfiguration implements NodeConfiguration {
+
+    private String jsScript;
+
+    @Override
+    public TbTransformMsgNodeConfiguration defaultConfiguration() {
+        TbTransformMsgNodeConfiguration configuration = new TbTransformMsgNodeConfiguration();
+        configuration.setJsScript("return {msg: msg, metadata: metadata, msgType: msgType};");
+        return configuration;
+    }
+}
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/EntitiesCustomerIdAsyncLoader.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/EntitiesCustomerIdAsyncLoader.java
new file mode 100644
index 0000000..be72833
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/EntitiesCustomerIdAsyncLoader.java
@@ -0,0 +1,49 @@
+/**
+ * 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.thingsboard.rule.engine.api.TbContext;
+import org.thingsboard.rule.engine.api.TbNodeException;
+import org.thingsboard.server.common.data.HasCustomerId;
+import org.thingsboard.server.common.data.id.*;
+
+public class EntitiesCustomerIdAsyncLoader {
+
+
+    public static ListenableFuture<CustomerId> findEntityIdAsync(TbContext ctx, EntityId original) {
+
+        switch (original.getEntityType()) {
+            case CUSTOMER:
+                return Futures.immediateFuture((CustomerId) original);
+            case USER:
+                return getCustomerAsync(ctx.getUserService().findUserByIdAsync((UserId) original));
+            case ASSET:
+                return getCustomerAsync(ctx.getAssetService().findAssetByIdAsync((AssetId) original));
+            case DEVICE:
+                return getCustomerAsync(ctx.getDeviceService().findDeviceByIdAsync((DeviceId) original));
+            default:
+                return Futures.immediateFailedFuture(new TbNodeException("Unexpected original EntityType " + original));
+        }
+    }
+
+    private static <T extends HasCustomerId> ListenableFuture<CustomerId> getCustomerAsync(ListenableFuture<T> future) {
+        return Futures.transformAsync(future, in -> in != null ? Futures.immediateFuture(in.getCustomerId())
+                : Futures.immediateFuture(null));
+    }
+}
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/EntitiesRelatedDeviceIdAsyncLoader.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/EntitiesRelatedDeviceIdAsyncLoader.java
new file mode 100644
index 0000000..9e3a639
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/EntitiesRelatedDeviceIdAsyncLoader.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.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.transformAsync(asyncDevices, d -> CollectionUtils.isNotEmpty(d) ? Futures.immediateFuture(d.get(0).getId())
+                : Futures.immediateFuture(null));
+    }
+
+    private static DeviceSearchQuery buildQuery(EntityId originator, DeviceRelationsQuery deviceRelationsQuery) {
+        DeviceSearchQuery query = new DeviceSearchQuery();
+        RelationsSearchParameters parameters = new RelationsSearchParameters(originator,
+                deviceRelationsQuery.getDirection(), deviceRelationsQuery.getMaxLevel());
+        query.setParameters(parameters);
+        query.setRelationType(deviceRelationsQuery.getRelationType());
+        query.setDeviceTypes(deviceRelationsQuery.getDeviceTypes());
+        return query;
+    }
+}
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/EntitiesRelatedEntityIdAsyncLoader.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/EntitiesRelatedEntityIdAsyncLoader.java
new file mode 100644
index 0000000..f4de8fc
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/EntitiesRelatedEntityIdAsyncLoader.java
@@ -0,0 +1,58 @@
+/**
+ * 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.RelationsQuery;
+import org.thingsboard.server.common.data.id.EntityId;
+import org.thingsboard.server.common.data.relation.EntityRelation;
+import org.thingsboard.server.common.data.relation.EntityRelationsQuery;
+import org.thingsboard.server.common.data.relation.EntitySearchDirection;
+import org.thingsboard.server.common.data.relation.RelationsSearchParameters;
+import org.thingsboard.server.dao.relation.RelationService;
+
+import java.util.List;
+
+public class EntitiesRelatedEntityIdAsyncLoader {
+
+    public static ListenableFuture<EntityId> findEntityAsync(TbContext ctx, EntityId originator,
+                                                             RelationsQuery relationsQuery) {
+        RelationService relationService = ctx.getRelationService();
+        EntityRelationsQuery query = buildQuery(originator, relationsQuery);
+        ListenableFuture<List<EntityRelation>> asyncRelation = relationService.findByQuery(query);
+        if (relationsQuery.getDirection() == EntitySearchDirection.FROM) {
+            return Futures.transformAsync(asyncRelation, r -> CollectionUtils.isNotEmpty(r) ? Futures.immediateFuture(r.get(0).getTo())
+                    : Futures.immediateFuture(null));
+        } else if (relationsQuery.getDirection() == EntitySearchDirection.TO) {
+            return Futures.transformAsync(asyncRelation, r -> CollectionUtils.isNotEmpty(r) ? Futures.immediateFuture(r.get(0).getFrom())
+                    : Futures.immediateFuture(null));
+        }
+        return Futures.immediateFailedFuture(new IllegalStateException("Unknown direction"));
+    }
+
+    private static EntityRelationsQuery buildQuery(EntityId originator, RelationsQuery relationsQuery) {
+        EntityRelationsQuery query = new EntityRelationsQuery();
+        RelationsSearchParameters parameters = new RelationsSearchParameters(originator,
+                relationsQuery.getDirection(), relationsQuery.getMaxLevel());
+        query.setParameters(parameters);
+        query.setFilters(relationsQuery.getFilters());
+        return query;
+    }
+}
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/EntitiesTenantIdAsyncLoader.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/EntitiesTenantIdAsyncLoader.java
new file mode 100644
index 0000000..774c269
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/EntitiesTenantIdAsyncLoader.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.thingsboard.rule.engine.api.TbContext;
+import org.thingsboard.rule.engine.api.TbNodeException;
+import org.thingsboard.server.common.data.HasTenantId;
+import org.thingsboard.server.common.data.alarm.AlarmId;
+import org.thingsboard.server.common.data.id.*;
+
+public class EntitiesTenantIdAsyncLoader {
+
+    public static ListenableFuture<TenantId> findEntityIdAsync(TbContext ctx, EntityId original) {
+
+        switch (original.getEntityType()) {
+            case TENANT:
+                return Futures.immediateFuture((TenantId) original);
+            case CUSTOMER:
+                return getTenantAsync(ctx.getCustomerService().findCustomerByIdAsync((CustomerId) original));
+            case USER:
+                return getTenantAsync(ctx.getUserService().findUserByIdAsync((UserId) original));
+            case ASSET:
+                return getTenantAsync(ctx.getAssetService().findAssetByIdAsync((AssetId) original));
+            case DEVICE:
+                return getTenantAsync(ctx.getDeviceService().findDeviceByIdAsync((DeviceId) original));
+            case ALARM:
+                return getTenantAsync(ctx.getAlarmService().findAlarmByIdAsync((AlarmId) original));
+            case RULE_CHAIN:
+                return getTenantAsync(ctx.getRuleChainService().findRuleChainByIdAsync((RuleChainId) original));
+            default:
+                return Futures.immediateFailedFuture(new TbNodeException("Unexpected original EntityType " + original));
+        }
+    }
+
+    private static <T extends HasTenantId> ListenableFuture<TenantId> getTenantAsync(ListenableFuture<T> future) {
+        return Futures.transformAsync(future, in -> {
+            return in != null ? Futures.immediateFuture(in.getTenantId())
+                    : Futures.immediateFuture(null);});
+    }
+}
diff --git a/rule-engine/rule-engine-components/src/main/resources/public/static/rulenode/rulenode-core-config.css b/rule-engine/rule-engine-components/src/main/resources/public/static/rulenode/rulenode-core-config.css
new file mode 100644
index 0000000..b5109b5
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/main/resources/public/static/rulenode/rulenode-core-config.css
@@ -0,0 +1,2 @@
+.tb-message-type-autocomplete .tb-not-found{display:block;line-height:1.5;height:48px}.tb-message-type-autocomplete .tb-not-found .tb-no-entries{line-height:48px}.tb-message-type-autocomplete li{height:auto!important;white-space:normal!important}.tb-generator-config tb-json-content.tb-message-body,.tb-generator-config tb-json-object-edit.tb-metadata-json{height:200px;display:block}.tb-mqtt-config .tb-credentials-panel-group .tb-panel-title{-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;min-width:90px}@media (min-width:960px){.tb-mqtt-config .tb-credentials-panel-group .tb-panel-title{min-width:180px}}.tb-mqtt-config .tb-credentials-panel-group .tb-panel-prompt{font-size:14px;color:rgba(0,0,0,.87);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.tb-mqtt-config .tb-credentials-panel-group.disabled .tb-panel-prompt,.tb-mqtt-config .tb-credentials-panel-group.disabled .tb-panel-title{color:rgba(0,0,0,.38)}.tb-mqtt-config .tb-credentials-panel-group md-icon.md-expansion-panel-icon{margin-right:0}.tb-mqtt-config .tb-container{width:100%}.tb-mqtt-config .dropdown-messages .tb-error-message{padding:5px 0 0}.tb-kv-map-config .header{padding-left:5px;padding-right:5px;padding-bottom:5px}.tb-kv-map-config .header .cell{padding-left:5px;padding-right:5px;color:rgba(0,0,0,.54);font-size:12px;font-weight:700;white-space:nowrap}.tb-kv-map-config .body{padding-left:5px;padding-right:5px;padding-bottom:20px;max-height:300px;overflow:auto}.tb-kv-map-config .body .row{padding-top:5px;max-height:40px}.tb-kv-map-config .body .cell{padding-left:5px;padding-right:5px}.tb-kv-map-config .body md-input-container.cell{margin:0;max-height:40px}.tb-kv-map-config .body .md-button{margin:0}
+/*# sourceMappingURL=rulenode-core-config.css.map*/
\ No newline at end of file
diff --git a/rule-engine/rule-engine-components/src/main/resources/public/static/rulenode/rulenode-core-config.js b/rule-engine/rule-engine-components/src/main/resources/public/static/rulenode/rulenode-core-config.js
new file mode 100644
index 0000000..11dc000
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/main/resources/public/static/rulenode/rulenode-core-config.js
@@ -0,0 +1,4 @@
+!function(e){function t(a){if(n[a])return n[a].exports;var r=n[a]={exports:{},id:a,loaded:!1};return e[a].call(r.exports,r,r.exports,t),r.loaded=!0,r.exports}var n={};return t.m=e,t.c=n,t.p="/static/",t(0)}(function(e){for(var t in e)if(Object.prototype.hasOwnProperty.call(e,t))switch(typeof e[t]){case"function":break;case"object":e[t]=function(t){var n=t.slice(1),a=e[t[0]];return function(e,t,r){a.apply(this,[e,t,r].concat(n))}}(e[t]);break;default:e[t]=e[e[t]]}return e}([function(e,t,n){e.exports=n(68)},function(e,t){},1,1,1,function(e,t){e.exports=' <section ng-form name=attributesConfigForm layout=column> <md-input-container class=md-block> <label translate>attribute.attributes-scope</label> <md-select ng-model=configuration.scope ng-disabled=$root.loading> <md-option ng-repeat="scope in types.attributesScope" ng-value=scope.value> {{scope.name | translate}} </md-option> </md-select> </md-input-container> </section> '},function(e,t){e.exports=" <section class=tb-alarm-config ng-form name=alarmConfigForm layout=column> <label translate class=\"tb-title no-padding\">tb.rulenode.alarm-details-builder</label> <tb-js-func ng-model=configuration.alarmDetailsBuildJs function-name=Details function-args=\"{{ ['msg', 'metadata', 'msgType'] }}\" no-validate=true> </tb-js-func> <div layout=row style=padding-bottom:15px> <md-button ng-click=testDetailsBuildJs($event) class=\"md-primary md-raised\"> {{ 'tb.rulenode.test-details-function' | translate }} </md-button> </div> <md-input-container class=md-block> <label translate>tb.rulenode.alarm-type</label> <input ng-required=true name=alarmType ng-model=configuration.alarmType> <div ng-messages=alarmConfigForm.alarmType.$error> <div ng-message=required translate>tb.rulenode.alarm-type-required</div> </div> </md-input-container> </section> "},function(e,t){e.exports=" <section class=tb-alarm-config ng-form name=alarmConfigForm layout=column> <label translate class=\"tb-title no-padding\">tb.rulenode.alarm-details-builder</label> <tb-js-func ng-model=configuration.alarmDetailsBuildJs function-name=Details function-args=\"{{ ['msg', 'metadata', 'msgType'] }}\" no-validate=true> </tb-js-func> <div layout=row style=padding-bottom:15px> <md-button ng-click=testDetailsBuildJs($event) class=\"md-primary md-raised\"> {{ 'tb.rulenode.test-details-function' | translate }} </md-button> </div> <section layout=column layout-gt-sm=row> <md-input-container flex class=md-block> <label translate>tb.rulenode.alarm-type</label> <input ng-required=true name=alarmType ng-model=configuration.alarmType> <div ng-messages=alarmConfigForm.alarmType.$error> <div ng-message=required translate>tb.rulenode.alarm-type-required</div> </div> </md-input-container> <md-input-container flex class=md-block> <label translate>tb.rulenode.alarm-severity</label> <md-select required name=severity ng-model=configuration.severity> <md-option ng-repeat=\"(severityKey, severity) in types.alarmSeverity\" ng-value=severityKey> {{ severity.name | translate}} </md-option> </md-select> <div ng-messages=alarmConfigForm.severity.$error> <div ng-message=required translate>tb.rulenode.alarm-severity-required</div> </div> </md-input-container> </section> <md-checkbox aria-label=\"{{ 'tb.rulenode.propagate' | translate }}\" ng-model=configuration.propagate>{{ 'tb.rulenode.propagate' | translate }} </md-checkbox> </section> "},function(e,t){e.exports=" <section class=tb-generator-config ng-form name=generatorConfigForm layout=column> <md-input-container class=md-block> <label translate>tb.rulenode.message-count</label> <input ng-required=true type=number step=1 name=messageCount ng-model=configuration.msgCount min=0> <div ng-messages=generatorConfigForm.messageCount.$error multiple=multiple md-auto-hide=false> <div ng-message=required translate>tb.rulenode.message-count-required</div> <div ng-message=min translate>tb.rulenode.min-message-count-message</div> </div> </md-input-container> <md-input-container class=md-block> <label translate>tb.rulenode.period-seconds</label> <input ng-required=true type=number step=1 name=periodInSeconds ng-model=configuration.periodInSeconds min=1> <div ng-messages=generatorConfigForm.periodInSeconds.$error multiple=multiple md-auto-hide=false> <div ng-message=required translate>tb.rulenode.period-seconds-required</div> <div ng-message=min translate>tb.rulenode.min-period-seconds-message</div> </div> </md-input-container> <div layout=column> <label class=tb-small>{{ 'tb.rulenode.originator' | translate }}</label> <tb-entity-select the-form=generatorConfigForm tb-required=false ng-model=originator> </tb-entity-select> </div> <label translate class=\"tb-title no-padding\">tb.rulenode.generate</label> <tb-js-func ng-model=configuration.jsScript function-name=Generate function-args=\"{{ ['prevMsg', 'prevMetadata', 'prevMsgType'] }}\" no-validate=true> </tb-js-func> <div layout=row> <md-button ng-click=testScript($event) class=\"md-primary md-raised\"> {{ 'tb.rulenode.test-generator-function' | translate }} </md-button> </div> </section> "},function(e,t){e.exports=' <section ng-form name=kafkaConfigForm layout=column> <md-input-container class=md-block> <label translate>tb.rulenode.topic-pattern</label> <input ng-required=true name=topicPattern ng-model=configuration.topicPattern> <div ng-messages=kafkaConfigForm.topicPattern.$error> <div ng-message=required translate>tb.rulenode.topic-pattern-required</div> </div> </md-input-container> <md-input-container class=md-block> <label translate>tb.rulenode.bootstrap-servers</label> <input ng-required=true name=bootstrapServers ng-model=configuration.bootstrapServers> <div ng-messages=kafkaConfigForm.bootstrapServers.$error> <div ng-message=required translate>tb.rulenode.bootstrap-servers-required</div> </div> </md-input-container> <md-input-container class=md-block> <label translate>tb.rulenode.retries</label> <input type=number step=1 name=retries ng-model=configuration.retries min=0> <div ng-messages=kafkaConfigForm.retries.$error> <div ng-message=min translate>tb.rulenode.min-retries-message</div> </div> </md-input-container> <md-input-container class=md-block> <label translate>tb.rulenode.batch-size-bytes</label> <input type=number step=1 name=batchSize ng-model=configuration.batchSize min=0> <div ng-messages=kafkaConfigForm.batchSize.$error> <div ng-message=min translate>tb.rulenode.min-batch-size-bytes-message</div> </div> </md-input-container> <md-input-container class=md-block> <label translate>tb.rulenode.linger-ms</label> <input type=number step=1 name=linger ng-model=configuration.linger min=0> <div ng-messages=kafkaConfigForm.linger.$error> <div ng-message=min translate>tb.rulenode.min-linger-ms-message</div> </div> </md-input-container> <md-input-container class=md-block> <label translate>tb.rulenode.buffer-memory-bytes</label> <input type=number step=1 name=bufferMemory ng-model=configuration.bufferMemory min=0> <div ng-messages=kafkaConfigForm.bufferMemory.$error> <div ng-message=min translate>tb.rulenode.min-buffer-memory-bytes-message</div> </div> </md-input-container> <md-input-container class=md-block> <label translate>tb.rulenode.acks</label> <md-select ng-model=configuration.acks ng-disabled=$root.loading> <md-option ng-repeat="ackValue in ackValues" ng-value=ackValue> {{ ackValue }} </md-option> </md-select> </md-input-container> <md-input-container class=md-block> <label translate>tb.rulenode.key-serializer</label> <input ng-required=true name=keySerializer ng-model=configuration.keySerializer> <div ng-messages=kafkaConfigForm.keySerializer.$error> <div ng-message=required translate>tb.rulenode.key-serializer-required</div> </div> </md-input-container> <md-input-container class=md-block> <label translate>tb.rulenode.value-serializer</label> <input ng-required=true name=valueSerializer ng-model=configuration.valueSerializer> <div ng-messages=kafkaConfigForm.valueSerializer.$error> <div ng-message=required translate>tb.rulenode.value-serializer-required</div> </div> </md-input-container> <label translate class=tb-title>tb.rulenode.other-properties</label> <tb-kv-map-config ng-model=configuration.otherProperties ng-required=false key-text="\'tb.rulenode.key\'" key-required-text="\'tb.rulenode.key-required\'" val-text="\'tb.rulenode.value\'" val-required-text="\'tb.rulenode.value-required\'"> </tb-kv-map-config> </section> '},function(e,t){e.exports=" <section layout=column> <label translate class=\"tb-title no-padding\">tb.rulenode.to-string</label> <tb-js-func ng-model=configuration.jsScript function-name=ToString function-args=\"{{ ['msg', 'metadata', 'msgType'] }}\" no-validate=true> </tb-js-func> <div layout=row> <md-button ng-click=testScript($event) class=\"md-primary md-raised\"> {{ 'tb.rulenode.test-to-string-function' | translate }} </md-button> </div> </section> "},function(e,t){e.exports=' <section class=tb-mqtt-config ng-form name=mqttConfigForm layout=column> <md-input-container class=md-block> <label translate>tb.rulenode.topic-pattern</label> <input ng-required=true name=topicPattern ng-model=configuration.topicPattern> <div ng-messages=mqttConfigForm.topicPattern.$error> <div translate ng-message=required>tb.rulenode.topic-pattern-required</div> </div> <div class=tb-hint translate>tb.rulenode.mqtt-topic-pattern-hint</div> </md-input-container> <div flex layout=column layout-gt-sm=row> <md-input-container flex=60 class=md-block> <label translate>tb.rulenode.host</label> <input ng-required=true name=host ng-model=configuration.host> <div ng-messages=mqttConfigForm.host.$error> <div translate ng-message=required>tb.rulenode.host-required</div> </div> </md-input-container> <md-input-container flex=40 class=md-block> <label translate>tb.rulenode.port</label> <input type=number step=1 min=1 max=65535 ng-required=true name=port ng-model=configuration.port> <div ng-messages=mqttConfigForm.port.$error> <div translate ng-message=required>tb.rulenode.port-required</div> <div translate ng-message=min>tb.rulenode.port-range</div> <div translate ng-message=max>tb.rulenode.port-range</div> </div> </md-input-container> <md-input-container flex=40 class=md-block> <label translate>tb.rulenode.connect-timeout</label> <input type=number step=1 min=1 max=200 ng-required=true name=connectTimeoutSec ng-model=configuration.connectTimeoutSec> <div ng-messages=mqttConfigForm.connectTimeoutSec.$error> <div translate ng-message=required>tb.rulenode.connect-timeout-required</div> <div translate ng-message=min>tb.rulenode.connect-timeout-range</div> <div translate ng-message=max>tb.rulenode.connect-timeout-range</div> </div> </md-input-container> </div> <md-input-container class=md-block> <label translate>tb.rulenode.client-id</label> <input name=clientId ng-model=configuration.clientId> </md-input-container> <md-checkbox ng-disabled="$root.loading || readonly" aria-label="{{ \'tb.rulenode.enable-ssl\' | translate }}" ng-model=configuration.ssl> {{ \'tb.rulenode.enable-ssl\' | translate }} </md-checkbox> <md-expansion-panel-group class=tb-credentials-panel-group ng-class="{\'disabled\': $root.loading || readonly}" md-component-id=credentialsPanelGroup> <md-expansion-panel md-component-id=credentialsPanel> <md-expansion-panel-collapsed> <div class=tb-panel-title>{{ \'tb.rulenode.credentials\' | translate }}</div> <div class=tb-panel-prompt>{{ ruleNodeTypes.mqttCredentialTypes[configuration.credentials.type].name | translate }}</div> <span flex></span> <md-expansion-panel-icon></md-expansion-panel-icon> </md-expansion-panel-collapsed> <md-expansion-panel-expanded> <md-expansion-panel-header ng-click="$mdExpansionPanel(\'credentialsPanel\').collapse()"> <div class=tb-panel-title>{{ \'tb.rulenode.credentials\' | translate }}</div> <div class=tb-panel-prompt>{{ ruleNodeTypes.mqttCredentialTypes[configuration.credentials.type].name | translate }}</div> <span flex></span> <md-expansion-panel-icon></md-expansion-panel-icon> </md-expansion-panel-header> <md-expansion-panel-content> <div layout=column> <md-input-container class=md-block> <label translate>tb.rulenode.credentials-type</label> <md-select ng-required=true name=credentialsType ng-model=configuration.credentials.type ng-disabled="$root.loading || readonly" ng-change=credentialsTypeChanged()> <md-option ng-repeat="(credentialsType, credentialsValue) in ruleNodeTypes.mqttCredentialTypes" ng-value=credentialsValue.value> {{credentialsValue.name | translate}} </md-option> </md-select> <div ng-messages=mqttConfigForm.credentialsType.$error> <div translate ng-message=required>tb.rulenode.credentials-type-required</div> </div> </md-input-container> <section flex layout=column ng-if="configuration.credentials.type == ruleNodeTypes.mqttCredentialTypes.basic.value"> <md-input-container class=md-block> <label translate>tb.rulenode.username</label> <input ng-required=true name=mqttUsername ng-model=configuration.credentials.username> <div ng-messages=mqttConfigForm.mqttUsername.$error> <div translate ng-message=required>tb.rulenode.username-required</div> </div> </md-input-container> <md-input-container class=md-block> <label translate>tb.rulenode.password</label> <input type=password ng-required=true name=mqttPassword ng-model=configuration.credentials.password> <div ng-messages=mqttConfigForm.mqttPassword.$error> <div translate ng-message=required>tb.rulenode.password-required</div> </div> </md-input-container> </section> <section flex layout=column ng-if="configuration.credentials.type == ruleNodeTypes.mqttCredentialTypes[\'cert.PEM\'].value" class=dropdown-section> <div class=tb-container ng-class="configuration.credentials.caCertFileName ? \'ng-valid\' : \'ng-invalid\'"> <label class=tb-label translate>tb.rulenode.ca-cert</label> <div flow-init={singleFile:true} flow-file-added="certFileAdded($file, \'caCert\')" class=tb-file-select-container> <div class=tb-file-clear-container> <md-button ng-click="clearCertFile(\'caCert\')" class="tb-file-clear-btn md-icon-button md-primary" aria-label="{{ \'action.remove\' | translate }}"> <md-tooltip md-direction=top> {{ \'action.remove\' | translate }} </md-tooltip> <md-icon aria-label="{{ \'action.remove\' | translate }}" class=material-icons>close</md-icon> </md-button> </div> <div class="alert tb-flow-drop" flow-drop> <label for=caCertSelect translate>tb.rulenode.drop-file</label> <input class=file-input flow-btn id=caCertSelect> </div> </div> </div> <div class=dropdown-messages> <div ng-if=!configuration.credentials.caCertFileName class=tb-error-message translate>tb.rulenode.no-file</div> <div ng-if=configuration.credentials.caCertFileName>{{configuration.credentials.caCertFileName}}</div> </div> <div class=tb-container ng-class="configuration.credentials.certFileName ? \'ng-valid\' : \'ng-invalid\'"> <label class=tb-label translate>tb.rulenode.cert</label> <div flow-init={singleFile:true} flow-file-added="certFileAdded($file, \'Cert\')" class=tb-file-select-container> <div class=tb-file-clear-container> <md-button ng-click="clearCertFile(\'Cert\')" class="tb-file-clear-btn md-icon-button md-primary" aria-label="{{ \'action.remove\' | translate }}"> <md-tooltip md-direction=top> {{ \'action.remove\' | translate }} </md-tooltip> <md-icon aria-label="{{ \'action.remove\' | translate }}" class=material-icons>close</md-icon> </md-button> </div> <div class="alert tb-flow-drop" flow-drop> <label for=CertSelect translate>tb.rulenode.drop-file</label> <input class=file-input flow-btn id=CertSelect> </div> </div> </div> <div class=dropdown-messages> <div ng-if=!configuration.credentials.certFileName class=tb-error-message translate>tb.rulenode.no-file</div> <div ng-if=configuration.credentials.certFileName>{{configuration.credentials.certFileName}}</div> </div> <div class=tb-container ng-class="configuration.credentials.privateKeyFileName ? \'ng-valid\' : \'ng-invalid\'"> <label class=tb-label translate>tb.rulenode.private-key</label> <div flow-init={singleFile:true} flow-file-added="certFileAdded($file, \'privateKey\')" class=tb-file-select-container> <div class=tb-file-clear-container> <md-button ng-click="clearCertFile(\'privateKey\')" class="tb-file-clear-btn md-icon-button md-primary" aria-label="{{ \'action.remove\' | translate }}"> <md-tooltip md-direction=top> {{ \'action.remove\' | translate }} </md-tooltip> <md-icon aria-label="{{ \'action.remove\' | translate }}" class=material-icons>close</md-icon> </md-button> </div> <div class="alert tb-flow-drop" flow-drop> <label for=privateKeySelect translate>tb.rulenode.drop-file</label> <input class=file-input flow-btn id=privateKeySelect> </div> </div> </div> <div class=dropdown-messages> <div ng-if=!configuration.credentials.privateKeyFileName class=tb-error-message translate>tb.rulenode.no-file</div> <div ng-if=configuration.credentials.privateKeyFileName>{{configuration.credentials.privateKeyFileName}}</div> </div> <md-input-container class=md-block> <label translate>tb.rulenode.private-key-password</label> <input type=password name=privateKeyPassword ng-model=configuration.credentials.password> </md-input-container> </section> </div> </md-expansion-panel-content> </md-expansion-panel-expanded> </md-expansion-panel> </md-expansion-panel-group> </section>'},function(e,t){e.exports=' <section ng-form name=rabbitMqConfigForm layout=column> <md-input-container class=md-block> <label translate>tb.rulenode.exchange-name-pattern</label> <input name=exchangeNamePattern ng-model=configuration.exchangeNamePattern> </md-input-container> <md-input-container class=md-block> <label translate>tb.rulenode.routing-key-pattern</label> <input name=routingKeyPattern ng-model=configuration.routingKeyPattern> </md-input-container> <md-input-container class=md-block> <label translate>tb.rulenode.message-properties</label> <md-select ng-model=configuration.messageProperties ng-disabled="$root.loading || readonly"> <md-option ng-repeat="property in messageProperties" ng-value=property> {{ property }} </md-option> </md-select> </md-input-container> <div layout-gt-sm=row> <md-input-container class=md-block flex=100 flex-gt-sm=60> <label translate>tb.rulenode.host</label> <input ng-required=true name=host ng-model=configuration.host> <div ng-messages=rabbitMqConfigForm.host.$error> <div ng-message=required translate>tb.rulenode.host-required</div> </div> </md-input-container> <md-input-container class=md-block flex=100 flex-gt-sm=40> <label translate>tb.rulenode.port</label> <input ng-required=true type=number step=1 name=port ng-model=configuration.port min=0 max=65535> <div ng-messages=rabbitMqConfigForm.port.$error> <div ng-message=required translate>tb.rulenode.port-required</div> <div ng-message=min translate>tb.rulenode.port-range</div> <div ng-message=max translate>tb.rulenode.port-range</div> </div> </md-input-container> </div> <md-input-container class=md-block> <label translate>tb.rulenode.virtual-host</label> <input name=virtualHost ng-model=configuration.virtualHost> </md-input-container> <md-input-container class=md-block> <label translate>tb.rulenode.username</label> <input name=virtualHost ng-model=configuration.username> </md-input-container> <md-input-container class=md-block> <label translate>tb.rulenode.password</label> <input name=virtualHost type=password ng-model=configuration.password> </md-input-container> <md-input-container class=md-block> <md-checkbox ng-disabled="$root.loading || readonly" aria-label="{{ \'tb.rulenode.automatic-recovery\' | translate }}" ng-model=ruleNode.automaticRecoveryEnabled>{{ \'tb.rulenode.automatic-recovery\' | translate }} </md-checkbox> </md-input-container> <md-input-container class=md-block> <label translate>tb.rulenode.connection-timeout-ms</label> <input type=number step=1 name=connectionTimeout ng-model=configuration.connectionTimeout min=0> <div ng-messages=rabbitMqConfigForm.connectionTimeout.$error> <div ng-message=min translate>tb.rulenode.min-connection-timeout-ms-message</div> </div> </md-input-container> <md-input-container class=md-block> <label translate>tb.rulenode.handshake-timeout-ms</label> <input type=number step=1 name=handshakeTimeout ng-model=configuration.handshakeTimeout min=0> <div ng-messages=rabbitMqConfigForm.handshakeTimeout.$error> <div ng-message=min translate>tb.rulenode.min-handshake-timeout-ms-message</div> </div> </md-input-container> <label translate class=tb-title>tb.rulenode.client-properties</label> <tb-kv-map-config ng-model=configuration.clientProperties ng-required=false key-text="\'tb.rulenode.key\'" key-required-text="\'tb.rulenode.key-required\'" val-text="\'tb.rulenode.value\'" val-required-text="\'tb.rulenode.value-required\'"> </tb-kv-map-config> </section> '},function(e,t){e.exports=' <section ng-form name=restApiCallConfigForm layout=column> <md-input-container class=md-block> <label translate>tb.rulenode.endpoint-url-pattern</label> <input ng-required=true name=endpointUrlPattern ng-model=configuration.restEndpointUrlPattern> <div ng-messages=restApiCallConfigForm.endpointUrlPattern.$error> <div ng-message=required translate>tb.rulenode.endpoint-url-pattern-required</div> </div> <div class=tb-hint translate>tb.rulenode.endpoint-url-pattern-hint</div> </md-input-container> <md-input-container class=md-block> <label translate>tb.rulenode.request-method</label> <md-select ng-model=configuration.requestMethod ng-disabled=$root.loading> <md-option ng-repeat="type in ruleNodeTypes.httpRequestType" ng-value=type> {{ type }} </md-option> </md-select> </md-input-container> <label translate class=tb-title>tb.rulenode.headers</label> <div class=tb-hint translate>tb.rulenode.headers-hint</div> <tb-kv-map-config ng-model=configuration.headers ng-required=false key-text="\'tb.rulenode.header\'" key-required-text="\'tb.rulenode.header-required\'" val-text="\'tb.rulenode.value\'" val-required-text="\'tb.rulenode.value-required\'"> </tb-kv-map-config> </section> '},function(e,t){e.exports=" <section ng-form name=rpcReplyConfigForm layout=column> <md-input-container class=md-block> <label translate>tb.rulenode.request-id-metadata-attribute</label> <input name=requestIdMetaDataAttribute ng-model=configuration.requestIdMetaDataAttribute> </md-input-container> </section> "},function(e,t){e.exports=" <section ng-form name=rpcRequestConfigForm layout=column> <md-input-container class=md-block> <label translate>tb.rulenode.timeout-sec</label> <input ng-required=true type=number step=1 name=timeoutInSeconds ng-model=configuration.timeoutInSeconds min=0> <div ng-messages=rpcRequestConfigForm.timeoutInSeconds.$error multiple=multiple md-auto-hide=false> <div ng-message=required translate>tb.rulenode.timeout-required</div> <div ng-message=min translate>tb.rulenode.min-timeout-message</div> </div> </md-input-container> </section> "},function(e,t){e.exports=' <section ng-form name=sendEmailConfigForm layout=column> <md-checkbox ng-disabled="$root.loading || readonly" aria-label="{{ \'tb.rulenode.use-system-smtp-settings\' | translate }}" ng-model=configuration.useSystemSmtpSettings> {{ \'tb.rulenode.use-system-smtp-settings\' | translate }} </md-checkbox> <section layout=column ng-if=!configuration.useSystemSmtpSettings> <md-input-container class=md-block> <label translate>tb.rulenode.smtp-protocol</label> <md-select ng-disabled="$root.loading || readonly" ng-model=configuration.smtpProtocol> <md-option ng-repeat="smtpProtocol in smtpProtocols" value={{smtpProtocol}}> {{smtpProtocol.toUpperCase()}} </md-option> </md-select> </md-input-container> <div layout-gt-sm=row> <md-input-container class=md-block flex=100 flex-gt-sm=60> <label translate>tb.rulenode.smtp-host</label> <input ng-required=true name=smtpHost ng-model=configuration.smtpHost> <div ng-messages=sendEmailConfigForm.smtpHost.$error> <div translate ng-message=required>tb.rulenode.smtp-host-required</div> </div> </md-input-container> <md-input-container class=md-block flex=100 flex-gt-sm=40> <label translate>tb.rulenode.smtp-port</label> <input type=number step=1 min=1 max=65535 ng-required=true name=port ng-model=configuration.smtpPort> <div ng-messages=sendEmailConfigForm.port.$error> <div translate ng-message=required>tb.rulenode.smtp-port-required</div> <div translate ng-message=min>tb.rulenode.smtp-port-range</div> <div translate ng-message=max>tb.rulenode.smtp-port-range</div> </div> </md-input-container> </div> <md-input-container class=md-block> <label translate>tb.rulenode.timeout-msec</label> <input type=number step=1 min=0 ng-required=true name=timeout ng-model=configuration.timeout> <div ng-messages=sendEmailConfigForm.timeout.$error> <div translate ng-message=required>tb.rulenode.timeout-required</div> <div translate ng-message=min>tb.rulenode.min-timeout-msec-message</div> </div> </md-input-container> <md-checkbox ng-disabled="$root.loading || readonly" aria-label="{{ \'tb.rulenode.enable-tls\' | translate }}" ng-model=configuration.enableTls>{{ \'tb.rulenode.enable-tls\' | translate }}</md-checkbox> <md-input-container class=md-block> <label translate>tb.rulenode.username</label> <input name=username placeholder="{{ \'tb.rulenode.enter-username\' | translate }}" ng-model=configuration.username> </md-input-container> <md-input-container class=md-block> <label translate>tb.rulenode.password</label> <input name=password placeholder="{{ \'tb.rulenode.enter-password\' | translate }}" type=password ng-model=configuration.password> </md-input-container> </section> </section> '},function(e,t){e.exports=" <section ng-form name=snsConfigForm layout=column> <md-input-container class=md-block> <label translate>tb.rulenode.topic-arn-pattern</label> <input ng-required=true name=topicArnPattern ng-model=configuration.topicArnPattern> <div ng-messages=snsConfigForm.topicArnPattern.$error> <div ng-message=required translate>tb.rulenode.topic-arn-pattern-required</div> </div> <div class=tb-hint translate>tb.rulenode.topic-arn-pattern-hint</div> </md-input-container> <md-input-container class=md-block> <label translate>tb.rulenode.aws-access-key-id</label> <input ng-required=true name=accessKeyId ng-model=configuration.accessKeyId> <div ng-messages=snsConfigForm.accessKeyId.$error> <div ng-message=required translate>tb.rulenode.aws-access-key-id-required</div> </div> </md-input-container> <md-input-container class=md-block> <label translate>tb.rulenode.aws-secret-access-key</label> <input ng-required=true name=secretAccessKey ng-model=configuration.secretAccessKey> <div ng-messages=snsConfigForm.secretAccessKey.$error> <div ng-message=required translate>tb.rulenode.aws-secret-access-key-required</div> </div> </md-input-container> <md-input-container class=md-block> <label translate>tb.rulenode.aws-region</label> <input ng-required=true name=region ng-model=configuration.region> <div ng-messages=snsConfigForm.region.$error> <div ng-message=required translate>tb.rulenode.aws-region-required</div> </div> </md-input-container> </section> "},function(e,t){e.exports=' <section ng-form name=sqsConfigForm layout=column> <md-input-container class=md-block> <label translate>tb.rulenode.queue-type</label> <md-select ng-model=configuration.queueType ng-disabled="$root.loading || readonly"> <md-option ng-repeat="type in ruleNodeTypes.sqsQueueType" ng-value=type.value> {{ type.name | translate }} </md-option> </md-select> </md-input-container> <md-input-container class=md-block> <label translate>tb.rulenode.queue-url-pattern</label> <input ng-required=true name=queueUrlPattern ng-model=configuration.queueUrlPattern> <div ng-messages=sqsConfigForm.queueUrlPattern.$error> <div ng-message=required translate>tb.rulenode.queue-url-pattern-required</div> </div> <div class=tb-hint translate>tb.rulenode.queue-url-pattern-hint</div> </md-input-container> <md-input-container class=md-block ng-if="configuration.queueType == ruleNodeTypes.sqsQueueType.STANDARD.value"> <label translate>tb.rulenode.delay-seconds</label> <input type=number step=1 name=delaySeconds ng-model=configuration.delaySeconds min=0 max=900> <div ng-messages=sqsConfigForm.delaySeconds.$error> <div ng-message=min translate>tb.rulenode.min-delay-seconds-message</div> <div ng-message=max translate>tb.rulenode.max-delay-seconds-message</div> </div> </md-input-container> <label translate class=tb-title>tb.rulenode.message-attributes</label> <div class=tb-hint translate>tb.rulenode.message-attributes-hint</div> <tb-kv-map-config ng-model=configuration.messageAttributes ng-required=false key-text="\'tb.rulenode.name\'" key-required-text="\'tb.rulenode.name-required\'" val-text="\'tb.rulenode.value\'" val-required-text="\'tb.rulenode.value-required\'"> </tb-kv-map-config> <md-input-container class=md-block> <label translate>tb.rulenode.aws-access-key-id</label> <input ng-required=true name=accessKeyId ng-model=configuration.accessKeyId> <div ng-messages=snsConfigForm.accessKeyId.$error> <div ng-message=required translate>tb.rulenode.aws-access-key-id-required</div> </div> </md-input-container> <md-input-container class=md-block> <label translate>tb.rulenode.aws-secret-access-key</label> <input ng-required=true name=secretAccessKey ng-model=configuration.secretAccessKey> <div ng-messages=snsConfigForm.secretAccessKey.$error> <div ng-message=required translate>tb.rulenode.aws-secret-access-key-required</div> </div> </md-input-container> <md-input-container class=md-block> <label translate>tb.rulenode.aws-region</label> <input ng-required=true name=region ng-model=configuration.region> <div ng-messages=snsConfigForm.region.$error> <div ng-message=required translate>tb.rulenode.aws-region-required</div> </div> </md-input-container> </section> '},function(e,t){e.exports=" <section ng-form name=timeseriesConfigForm layout=column> <md-input-container class=md-block> <label translate>tb.rulenode.default-ttl</label> <input ng-required=true type=number step=1 name=defaultTTL ng-model=configuration.defaultTTL min=0> <div ng-messages=timeseriesConfigForm.defaultTTL.$error multiple=multiple md-auto-hide=false> <div ng-message=required translate>tb.rulenode.default-ttl-required</div> <div ng-message=min translate>tb.rulenode.min-default-ttl-message</div> </div> </md-input-container> </section> "},function(e,t){e.exports=' <section layout=column> <div layout=row> <md-input-container class=md-block style=min-width:100px> <label translate>relation.direction</label> <md-select required ng-model=query.direction> <md-option ng-repeat="direction in types.entitySearchDirection" ng-value=direction> {{ (\'relation.search-direction.\' + direction) | translate}} </md-option> </md-select> </md-input-container> <md-input-container class=md-block> <label translate>tb.rulenode.max-relation-level</label> <input name=maxRelationLevel type=number min=1 step=1 placeholder="{{ \'tb.rulenode.unlimited-level\' | translate }}" ng-model=query.maxLevel aria-label="{{ \'tb.rulenode.max-relation-level\' | translate }}"> </md-input-container> </div> <div class=md-caption style=color:rgba(0,0,0,.57) translate>relation.relation-type</div> <tb-relation-type-autocomplete flex hide-label ng-model=query.relationType tb-required=false> </tb-relation-type-autocomplete> <div class="md-caption tb-required" style=color:rgba(0,0,0,.57) translate>device.device-types</div> <tb-entity-subtype-list tb-required=true entity-type=types.entityType.device ng-model=query.deviceTypes> </tb-entity-subtype-list> </section> '},function(e,t){e.exports=" <section layout=column> <label translate class=\"tb-title tb-required\">tb.rulenode.attr-mapping</label> <md-checkbox aria-label=\"{{ 'tb.rulenode.latest-telemetry' | translate }}\" ng-model=configuration.telemetry>{{ 'tb.rulenode.latest-telemetry' | translate }} </md-checkbox> <tb-kv-map-config ng-model=configuration.attrMapping ng-required=true required-text=\"'tb.rulenode.attr-mapping-required'\" key-text=\"configuration.telemetry ? 'tb.rulenode.source-telemetry' : 'tb.rulenode.source-attribute'\" key-required-text=\"configuration.telemetry ? 'tb.rulenode.source-telemetry-required' : 'tb.rulenode.source-attribute-required'\" val-text=\"'tb.rulenode.target-attribute'\" val-required-text=\"'tb.rulenode.target-attribute-required'\"> </tb-kv-map-config> </section> ";
+},function(e,t){e.exports=' <section layout=column> <label translate class="tb-title tb-required">tb.rulenode.device-relations-query</label> <tb-device-relations-query-config style=padding-bottom:15px ng-model=configuration.deviceRelationsQuery> </tb-device-relations-query-config> <label translate class="tb-title no-padding">tb.rulenode.client-attributes</label> <md-chips style=padding-bottom:15px ng-required=false readonly=readonly ng-model=configuration.clientAttributeNames placeholder="{{\'tb.rulenode.client-attributes\' | translate}}" md-separator-keys=separatorKeys> </md-chips> <label translate class="tb-title no-padding">tb.rulenode.shared-attributes</label> <md-chips style=padding-bottom:15px ng-required=false readonly=readonly ng-model=configuration.sharedAttributeNames placeholder="{{\'tb.rulenode.shared-attributes\' | translate}}" md-separator-keys=separatorKeys> </md-chips> <label translate class="tb-title no-padding">tb.rulenode.server-attributes</label> <md-chips style=padding-bottom:15px ng-required=false readonly=readonly ng-model=configuration.serverAttributeNames placeholder="{{\'tb.rulenode.server-attributes\' | translate}}" md-separator-keys=separatorKeys> </md-chips> <label translate class="tb-title no-padding">tb.rulenode.latest-timeseries</label> <md-chips ng-required=false readonly=readonly ng-model=configuration.latestTsKeyNames placeholder="{{\'tb.rulenode.latest-timeseries\' | translate}}" md-separator-keys=separatorKeys> </md-chips> </section> '},function(e,t){e.exports=' <section layout=column> <label translate class="tb-title no-padding">tb.rulenode.client-attributes</label> <md-chips style=padding-bottom:15px ng-required=false readonly=readonly ng-model=configuration.clientAttributeNames placeholder="{{\'tb.rulenode.client-attributes\' | translate}}" md-separator-keys=separatorKeys> </md-chips> <label translate class="tb-title no-padding">tb.rulenode.shared-attributes</label> <md-chips style=padding-bottom:15px ng-required=false readonly=readonly ng-model=configuration.sharedAttributeNames placeholder="{{\'tb.rulenode.shared-attributes\' | translate}}" md-separator-keys=separatorKeys> </md-chips> <label translate class="tb-title no-padding">tb.rulenode.server-attributes</label> <md-chips style=padding-bottom:15px ng-required=false readonly=readonly ng-model=configuration.serverAttributeNames placeholder="{{\'tb.rulenode.server-attributes\' | translate}}" md-separator-keys=separatorKeys> </md-chips> <label translate class="tb-title no-padding">tb.rulenode.latest-timeseries</label> <md-chips ng-required=false readonly=readonly ng-model=configuration.latestTsKeyNames placeholder="{{\'tb.rulenode.latest-timeseries\' | translate}}" md-separator-keys=separatorKeys> </md-chips> </section> '},function(e,t){e.exports=" <section layout=column> <label translate class=\"tb-title tb-required\">tb.rulenode.relations-query</label> <tb-relations-query-config style=padding-bottom:15px ng-model=configuration.relationsQuery> </tb-relations-query-config> <label translate class=\"tb-title tb-required\">tb.rulenode.attr-mapping</label> <md-checkbox aria-label=\"{{ 'tb.rulenode.latest-telemetry' | translate }}\" ng-model=configuration.telemetry>{{ 'tb.rulenode.latest-telemetry' | translate }} </md-checkbox> <tb-kv-map-config ng-model=configuration.attrMapping ng-required=true required-text=\"'tb.rulenode.attr-mapping-required'\" key-text=\"configuration.telemetry ? 'tb.rulenode.source-telemetry' : 'tb.rulenode.source-attribute'\" key-required-text=\"configuration.telemetry ? 'tb.rulenode.source-telemetry-required' : 'tb.rulenode.source-attribute-required'\" val-text=\"'tb.rulenode.target-attribute'\" val-required-text=\"'tb.rulenode.target-attribute-required'\"> </tb-kv-map-config> </section> "},21,function(e,t){e.exports=' <section layout=column> <label translate class="tb-title no-padding" ng-class="{\'tb-required\': required}">tb.rulenode.message-types-filter</label> <md-chips id=message_type_chips ng-required=required readonly=readonly ng-model=messageTypes md-autocomplete-snap md-transform-chip=transformMessageTypeChip($chip) md-require-match=false> <md-autocomplete id=message_type md-no-cache=true md-selected-item=selectedMessageType md-search-text=messageTypeSearchText md-items="item in messageTypesSearch(messageTypeSearchText)" md-item-text=item.name md-min-length=0 placeholder="{{\'tb.rulenode.message-type\' | translate }}" md-menu-class=tb-message-type-autocomplete> <span md-highlight-text=messageTypeSearchText md-highlight-flags=^i>{{item}}</span> <md-not-found> <div class=tb-not-found> <div class=tb-no-entries ng-if="!messageTypeSearchText || !messageTypeSearchText.length"> <span translate>tb.rulenode.no-message-types-found</span> </div> <div ng-if="messageTypeSearchText && messageTypeSearchText.length"> <span translate translate-values=\'{ messageType: "{{messageTypeSearchText | truncate:true:6:&apos;...&apos;}}" }\'>tb.rulenode.no-message-type-matching</span> <span> <a translate ng-click="createMessageType($event, \'#message_type_chips\')">tb.rulenode.create-new-message-type</a> </span> </div> </div> </md-not-found> </md-autocomplete> <md-chip-template> <span>{{$chip.name}}</span> </md-chip-template> </md-chips> <div class=tb-error-messages ng-messages=ngModelCtrl.$error role=alert> <div translate ng-message=messageTypes class=tb-error-message>tb.rulenode.message-types-required</div> </div> </section>'},function(e,t){e.exports=" <section layout=column> <label translate class=\"tb-title no-padding\">tb.rulenode.filter</label> <tb-js-func ng-model=configuration.jsScript function-name=Filter function-args=\"{{ ['msg', 'metadata', 'msgType'] }}\" no-validate=true> </tb-js-func> <div layout=row> <md-button ng-click=testScript($event) class=\"md-primary md-raised\"> {{ 'tb.rulenode.test-filter-function' | translate }} </md-button> </div> </section> "},function(e,t){e.exports=" <section layout=column> <label translate class=\"tb-title no-padding\">tb.rulenode.switch</label> <tb-js-func ng-model=configuration.jsScript function-name=Switch function-args=\"{{ ['msg', 'metadata', 'msgType'] }}\" no-validate=true> </tb-js-func> <div layout=row> <md-button ng-click=testScript($event) class=\"md-primary md-raised\"> {{ 'tb.rulenode.test-switch-function' | translate }} </md-button> </div> </section> "},function(e,t){e.exports=' <section class=tb-kv-map-config layout=column> <div class=header flex layout=row> <span class=cell flex translate>{{ keyText }}</span> <span class=cell flex translate>{{ valText }}</span> <span ng-show=!disabled style=width:52px>&nbsp</span> </div> <div class=body> <div class=row ng-form name=kvForm flex layout=row layout-align="start center" ng-repeat="keyVal in kvList track by $index"> <md-input-container class="cell md-block" flex md-no-float> <input placeholder="{{ keyText | translate }}" ng-required=true name=key ng-model=keyVal.key> <div ng-messages=kvForm.key.$error> <div translate ng-message=required>{{keyRequiredText}}</div> </div> </md-input-container> <md-input-container class="cell md-block" flex md-no-float> <input placeholder="{{ valText | translate }}" ng-required=true name=value ng-model=keyVal.value> <div ng-messages=kvForm.value.$error> <div translate ng-message=required>{{valRequiredText}}</div> </div> </md-input-container> <md-button ng-show=!disabled ng-disabled=loading class="md-icon-button md-primary" ng-click=removeKeyVal($index) aria-label="{{ \'action.remove\' | translate }}"> <md-tooltip md-direction=top> {{ \'tb.key-val.remove-entry\' | translate }} </md-tooltip> <md-icon aria-label="{{ \'action.delete\' | translate }}" class=material-icons> close </md-icon> </md-button> </div> </div> <div class=tb-error-messages ng-messages=ngModelCtrl.$error role=alert> <div translate ng-message=kvMap class=tb-error-message>{{requiredText}}</div> </div> <div> <md-button ng-show=!disabled ng-disabled=loading class="md-primary md-raised" ng-click=addKeyVal() aria-label="{{ \'action.add\' | translate }}"> <md-tooltip md-direction=top> {{ \'tb.key-val.add-entry\' | translate }} </md-tooltip> <md-icon aria-label="{{ \'action.add\' | translate }}" class=material-icons> add </md-icon> {{ \'action.add\' | translate }} </md-button> </div> </section> '},function(e,t){e.exports=" <section layout=column> <div layout=row> <md-input-container class=md-block style=min-width:100px> <label translate>relation.direction</label> <md-select required ng-model=query.direction> <md-option ng-repeat=\"direction in types.entitySearchDirection\" ng-value=direction> {{ ('relation.search-direction.' + direction) | translate}} </md-option> </md-select> </md-input-container> <md-input-container class=md-block> <label translate>tb.rulenode.max-relation-level</label> <input name=maxRelationLevel type=number min=1 step=1 placeholder=\"{{ 'tb.rulenode.unlimited-level' | translate }}\" ng-model=query.maxLevel aria-label=\"{{ 'tb.rulenode.max-relation-level' | translate }}\"> </md-input-container> </div> <div class=md-caption style=padding-bottom:10px;color:rgba(0,0,0,.57) translate>relation.relation-filters</div> <tb-relation-filters ng-model=query.filters> </tb-relation-filters> </section> "},function(e,t){e.exports=' <section layout=column> <md-input-container class=md-block> <label translate>tb.rulenode.originator-source</label> <md-select required ng-model=configuration.originatorSource> <md-option ng-repeat="source in ruleNodeTypes.originatorSource" ng-value=source.value> {{ source.name | translate}} </md-option> </md-select> </md-input-container> <section layout=column ng-if="configuration.originatorSource == ruleNodeTypes.originatorSource.RELATED.value"> <label translate class="tb-title tb-required">tb.rulenode.relations-query</label> <tb-relations-query-config style=padding-bottom:15px ng-model=configuration.relationsQuery> </tb-relations-query-config> </section> </section> '},function(e,t){e.exports=" <section layout=column> <label translate class=\"tb-title no-padding\">tb.rulenode.transform</label> <tb-js-func ng-model=configuration.jsScript function-name=Transform function-args=\"{{ ['msg', 'metadata', 'msgType'] }}\" no-validate=true> </tb-js-func> <div layout=row style=padding-bottom:15px> <md-button ng-click=testScript($event) class=\"md-primary md-raised\"> {{ 'tb.rulenode.test-transformer-function' | translate }} </md-button> </div> </section> "},function(e,t){e.exports=" <section ng-form name=toEmailConfigForm layout=column> <md-input-container class=md-block> <label translate>tb.rulenode.from-template</label> <textarea ng-required=true name=fromTemplate ng-model=configuration.fromTemplate rows=2></textarea> <div ng-messages=toEmailConfigForm.fromTemplate.$error> <div ng-message=required translate>tb.rulenode.from-template-required</div> </div> <div class=tb-hint translate>tb.rulenode.from-template-hint</div> </md-input-container> <md-input-container class=md-block> <label translate>tb.rulenode.to-template</label> <textarea ng-required=true name=toTemplate ng-model=configuration.toTemplate rows=2></textarea> <div ng-messages=toEmailConfigForm.toTemplate.$error> <div ng-message=required translate>tb.rulenode.to-template-required</div> </div> <div class=tb-hint translate>tb.rulenode.mail-address-list-template-hint</div> </md-input-container> <md-input-container class=md-block> <label translate>tb.rulenode.cc-template</label> <textarea name=ccTemplate ng-model=configuration.ccTemplate rows=2></textarea> <div class=tb-hint translate>tb.rulenode.mail-address-list-template-hint</div> </md-input-container> <md-input-container class=md-block> <label translate>tb.rulenode.bcc-template</label> <textarea name=ccTemplate ng-model=configuration.bccTemplate rows=2></textarea> <div class=tb-hint translate>tb.rulenode.mail-address-list-template-hint</div> </md-input-container> <md-input-container class=md-block> <label translate>tb.rulenode.subject-template</label> <textarea ng-required=true name=subjectTemplate ng-model=configuration.subjectTemplate rows=2></textarea> <div ng-messages=toEmailConfigForm.subjectTemplate.$error> <div ng-message=required translate>tb.rulenode.subject-template-required</div> </div> <div class=tb-hint translate>tb.rulenode.subject-template-hint</div> </md-input-container> <md-input-container class=md-block> <label translate>tb.rulenode.body-template</label> <textarea ng-required=true name=bodyTemplate ng-model=configuration.bodyTemplate rows=6></textarea> <div ng-messages=toEmailConfigForm.bodyTemplate.$error> <div ng-message=required translate>tb.rulenode.body-template-required</div> </div> <div class=tb-hint translate>tb.rulenode.body-template-hint</div> </md-input-container> </section> "},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function r(e,t){var n=function(n,a,r,i){var l=o.default;a.html(l),n.types=t,n.$watch("configuration",function(e,t){angular.equals(e,t)||i.$setViewValue(n.configuration)}),i.$render=function(){n.configuration=i.$viewValue},e(a.contents())(n)};return{restrict:"E",require:"^ngModel",scope:{},link:n}}r.$inject=["$compile","types"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=r;var i=n(5),o=a(i)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function r(e,t,n,a){var r=function(r,i,l,s){var u=o.default;i.html(u),r.types=n,r.$watch("configuration",function(e,t){angular.equals(e,t)||s.$setViewValue(r.configuration)}),s.$render=function(){r.configuration=s.$viewValue},r.testDetailsBuildJs=function(e){var n=angular.copy(r.configuration.alarmDetailsBuildJs);a.testNodeScript(e,n,"json",t.instant("tb.rulenode.details")+"","Details",["msg","metadata","msgType"],r.ruleNodeId).then(function(e){r.configuration.alarmDetailsBuildJs=e,s.$setDirty()})},e(i.contents())(r)};return{restrict:"E",require:"^ngModel",scope:{ruleNodeId:"="},link:r}}r.$inject=["$compile","$translate","types","ruleNodeScriptTest"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=r;var i=n(6),o=a(i)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function r(e,t,n,a){var r=function(r,i,l,s){var u=o.default;i.html(u),r.types=n,r.$watch("configuration",function(e,t){angular.equals(e,t)||s.$setViewValue(r.configuration)}),s.$render=function(){r.configuration=s.$viewValue},r.testDetailsBuildJs=function(e){var n=angular.copy(r.configuration.alarmDetailsBuildJs);a.testNodeScript(e,n,"json",t.instant("tb.rulenode.details")+"","Details",["msg","metadata","msgType"],r.ruleNodeId).then(function(e){r.configuration.alarmDetailsBuildJs=e,s.$setDirty()})},e(i.contents())(r)};return{restrict:"E",require:"^ngModel",scope:{ruleNodeId:"="},link:r}}r.$inject=["$compile","$translate","types","ruleNodeScriptTest"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=r;var i=n(7),o=a(i)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function r(e,t,n,a){var r=function(r,i,l,s){var u=o.default;i.html(u),r.types=n,r.originator=null,r.$watch("configuration",function(e,t){angular.equals(e,t)||s.$setViewValue(r.configuration)}),s.$render=function(){r.configuration=s.$viewValue,r.configuration.originatorId&&r.configuration.originatorType?r.originator={id:r.configuration.originatorId,entityType:r.configuration.originatorType}:r.originator=null,r.$watch("originator",function(e,t){angular.equals(e,t)||(r.originator?(s.$viewValue.originatorId=r.originator.id,s.$viewValue.originatorType=r.originator.entityType):(s.$viewValue.originatorId=null,s.$viewValue.originatorType=null))},!0)},r.testScript=function(e){var n=angular.copy(r.configuration.jsScript);a.testNodeScript(e,n,"generate",t.instant("tb.rulenode.generator")+"","Generate",["prevMsg","prevMetadata","prevMsgType"],r.ruleNodeId).then(function(e){r.configuration.jsScript=e,s.$setDirty()})},e(i.contents())(r)};return{restrict:"E",require:"^ngModel",scope:{ruleNodeId:"="},link:r}}r.$inject=["$compile","$translate","types","ruleNodeScriptTest"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=r,n(1);var i=n(8),o=a(i)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}Object.defineProperty(t,"__esModule",{value:!0});var r=n(49),i=a(r),o=n(34),l=a(o),s=n(37),u=a(s),d=n(36),c=a(d),m=n(35),g=a(m),p=n(40),f=a(p),b=n(44),v=a(b),y=n(45),q=a(y),T=n(43),h=a(T),$=n(39),k=a($),w=n(47),C=a(w),x=n(48),_=a(x),E=n(42),S=a(E),M=n(41),N=a(M),V=n(46),P=a(V);t.default=angular.module("thingsboard.ruleChain.config.action",[]).directive("tbActionNodeTimeseriesConfig",i.default).directive("tbActionNodeAttributesConfig",l.default).directive("tbActionNodeGeneratorConfig",u.default).directive("tbActionNodeCreateAlarmConfig",c.default).directive("tbActionNodeClearAlarmConfig",g.default).directive("tbActionNodeLogConfig",f.default).directive("tbActionNodeRpcReplyConfig",v.default).directive("tbActionNodeRpcRequestConfig",q.default).directive("tbActionNodeRestApiCallConfig",h.default).directive("tbActionNodeKafkaConfig",k.default).directive("tbActionNodeSnsConfig",C.default).directive("tbActionNodeSqsConfig",_.default).directive("tbActionNodeRabbitMqConfig",S.default).directive("tbActionNodeMqttConfig",N.default).directive("tbActionNodeSendEmailConfig",P.default).name},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function r(e){var t=function(t,n,a,r){var i=o.default;n.html(i),t.ackValues=["all","-1","0","1"],t.$watch("configuration",function(e,n){angular.equals(e,n)||r.$setViewValue(t.configuration)}),r.$render=function(){t.configuration=r.$viewValue},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{},link:t}}r.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=r;var i=n(9),o=a(i)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function r(e,t,n){var a=function(a,r,i,l){var s=o.default;r.html(s),a.$watch("configuration",function(e,t){angular.equals(e,t)||l.$setViewValue(a.configuration)}),l.$render=function(){a.configuration=l.$viewValue},a.testScript=function(e){var r=angular.copy(a.configuration.jsScript);n.testNodeScript(e,r,"string",t.instant("tb.rulenode.to-string")+"","ToString",["msg","metadata","msgType"],a.ruleNodeId).then(function(e){a.configuration.jsScript=e,l.$setDirty()})},e(r.contents())(a)};return{restrict:"E",require:"^ngModel",scope:{ruleNodeId:"="},link:a}}r.$inject=["$compile","$translate","ruleNodeScriptTest"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=r;var i=n(10),o=a(i)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function r(e,t,n){var a=function(a,r,i,l){var s=o.default;r.html(s),a.$mdExpansionPanel=t,a.ruleNodeTypes=n,a.credentialsTypeChanged=function(){var e=a.configuration.credentials.type;a.configuration.credentials={},a.configuration.credentials.type=e,a.updateValidity()},a.certFileAdded=function(e,t){var n=new FileReader;n.onload=function(n){a.$apply(function(){if(n.target.result){l.$setDirty();var r=n.target.result;r&&r.length>0&&("caCert"==t&&(a.configuration.credentials.caCertFileName=e.name,a.configuration.credentials.caCert=r),"privateKey"==t&&(a.configuration.credentials.privateKeyFileName=e.name,a.configuration.credentials.privateKey=r),"Cert"==t&&(a.configuration.credentials.certFileName=e.name,a.configuration.credentials.cert=r)),a.updateValidity()}})},n.readAsText(e.file)},a.clearCertFile=function(e){l.$setDirty(),"caCert"==e&&(a.configuration.credentials.caCertFileName=null,a.configuration.credentials.caCert=null),"privateKey"==e&&(a.configuration.credentials.privateKeyFileName=null,a.configuration.credentials.privateKey=null),"Cert"==e&&(a.configuration.credentials.certFileName=null,a.configuration.credentials.cert=null),a.updateValidity()},a.updateValidity=function(){var e=!0,t=a.configuration.credentials;t.type==n.mqttCredentialTypes["cert.PEM"].value&&(t.caCert&&t.cert&&t.privateKey||(e=!1)),l.$setValidity("Certs",e)},a.$watch("configuration",function(e,t){angular.equals(e,t)||l.$setViewValue(a.configuration)}),l.$render=function(){a.configuration=l.$viewValue},e(r.contents())(a)};return{restrict:"E",require:"^ngModel",scope:{readonly:"=ngReadonly"},link:a}}r.$inject=["$compile","$mdExpansionPanel","ruleNodeTypes"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=r,n(2);var i=n(11),o=a(i)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function r(e){var t=function(t,n,a,r){var i=o.default;n.html(i),t.messageProperties=[null,"BASIC","TEXT_PLAIN","MINIMAL_BASIC","MINIMAL_PERSISTENT_BASIC","PERSISTENT_BASIC","PERSISTENT_TEXT_PLAIN"],t.$watch("configuration",function(e,n){angular.equals(e,n)||r.$setViewValue(t.configuration)}),r.$render=function(){t.configuration=r.$viewValue},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{readonly:"=ngReadonly"},link:t}}r.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=r;var i=n(12),o=a(i)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function r(e,t){var n=function(n,a,r,i){var l=o.default;a.html(l),n.ruleNodeTypes=t,n.$watch("configuration",function(e,t){angular.equals(e,t)||i.$setViewValue(n.configuration)}),i.$render=function(){n.configuration=i.$viewValue},e(a.contents())(n)};return{restrict:"E",require:"^ngModel",scope:{},link:n}}r.$inject=["$compile","ruleNodeTypes"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=r;var i=n(13),o=a(i)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function r(e){var t=function(t,n,a,r){var i=o.default;n.html(i),t.$watch("configuration",function(e,n){angular.equals(e,n)||r.$setViewValue(t.configuration)}),r.$render=function(){t.configuration=r.$viewValue},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{},link:t}}r.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=r;var i=n(14),o=a(i)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function r(e){var t=function(t,n,a,r){var i=o.default;n.html(i),t.$watch("configuration",function(e,n){angular.equals(e,n)||r.$setViewValue(t.configuration)}),r.$render=function(){t.configuration=r.$viewValue},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{},link:t}}r.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=r;var i=n(15),o=a(i)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function r(e){var t=function(t,n,a,r){var i=o.default;n.html(i),t.smtpProtocols=["smtp","smtps"],t.$watch("configuration",function(e,n){angular.equals(e,n)||r.$setViewValue(t.configuration)}),r.$render=function(){t.configuration=r.$viewValue},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{readonly:"=ngReadonly"},link:t}}r.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=r;var i=n(16),o=a(i)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function r(e){var t=function(t,n,a,r){var i=o.default;n.html(i),t.$watch("configuration",function(e,n){angular.equals(e,n)||r.$setViewValue(t.configuration)}),r.$render=function(){t.configuration=r.$viewValue},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{},link:t}}r.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=r;var i=n(17),o=a(i)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function r(e,t){var n=function(n,a,r,i){var l=o.default;a.html(l),n.ruleNodeTypes=t,n.$watch("configuration",function(e,t){angular.equals(e,t)||i.$setViewValue(n.configuration)}),i.$render=function(){n.configuration=i.$viewValue},e(a.contents())(n)};return{restrict:"E",require:"^ngModel",scope:{readonly:"=ngReadonly"},link:n}}r.$inject=["$compile","ruleNodeTypes"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=r;var i=n(18),o=a(i)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function r(e){var t=function(t,n,a,r){var i=o.default;n.html(i),t.$watch("configuration",function(e,n){angular.equals(e,n)||r.$setViewValue(t.configuration)}),r.$render=function(){t.configuration=r.$viewValue},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{},link:t}}r.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=r;var i=n(19),o=a(i)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function r(e,t){var n=function(n,a,r,i){var l=o.default;a.html(l),n.types=t,n.$watch("query",function(e,t){angular.equals(e,t)||i.$setViewValue(n.query)}),i.$render=function(){n.query=i.$viewValue},e(a.contents())(n)};return{restrict:"E",require:"^ngModel",scope:{},link:n}}r.$inject=["$compile","types"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=r;var i=n(20),o=a(i)},function(e,t){"use strict";function n(e){var t=function(t,n,a,r){n.html("<div></div>"),t.$watch("configuration",function(e,n){angular.equals(e,n)||r.$setViewValue(t.configuration)}),r.$render=function(){t.configuration=r.$viewValue},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{},link:t}}n.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=n},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function r(e){var t=function(t,n,a,r){var i=o.default;n.html(i),t.$watch("configuration",function(e,n){angular.equals(e,n)||r.$setViewValue(t.configuration)}),r.$render=function(){t.configuration=r.$viewValue},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{},link:t}}r.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=r;var i=n(21),o=a(i)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function r(e,t){var n=function(n,a,r,i){var l=o.default;a.html(l);var s=186;n.separatorKeys=[t.KEY_CODE.ENTER,t.KEY_CODE.COMMA,s],n.$watch("configuration",function(e,t){angular.equals(e,t)||i.$setViewValue(n.configuration)}),i.$render=function(){n.configuration=i.$viewValue},e(a.contents())(n)};return{restrict:"E",require:"^ngModel",scope:{},link:n}}r.$inject=["$compile","$mdConstant"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=r;var i=n(22),o=a(i)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}Object.defineProperty(t,"__esModule",{value:!0});var r=n(55),i=a(r),o=n(53),l=a(o),s=n(56),u=a(s),d=n(52),c=a(d),m=n(57),g=a(m);t.default=angular.module("thingsboard.ruleChain.config.enrichment",[]).directive("tbEnrichmentNodeOriginatorAttributesConfig",i.default).directive("tbEnrichmentNodeDeviceAttributesConfig",l.default).directive("tbEnrichmentNodeRelatedAttributesConfig",u.default).directive("tbEnrichmentNodeCustomerAttributesConfig",c.default).directive("tbEnrichmentNodeTenantAttributesConfig",g.default).name},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function r(e,t){var n=function(n,a,r,i){var l=o.default;a.html(l);var s=186;n.separatorKeys=[t.KEY_CODE.ENTER,t.KEY_CODE.COMMA,s],n.$watch("configuration",function(e,t){angular.equals(e,t)||i.$setViewValue(n.configuration)}),i.$render=function(){n.configuration=i.$viewValue},e(a.contents())(n)};return{restrict:"E",require:"^ngModel",scope:{},link:n}}r.$inject=["$compile","$mdConstant"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=r;var i=n(23),o=a(i)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function r(e){var t=function(t,n,a,r){var i=o.default;n.html(i),t.$watch("configuration",function(e,n){angular.equals(e,n)||r.$setViewValue(t.configuration)}),r.$render=function(){t.configuration=r.$viewValue},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{},link:t}}r.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=r;var i=n(24),o=a(i)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function r(e){var t=function(t,n,a,r){var i=o.default;n.html(i),t.$watch("configuration",function(e,n){angular.equals(e,n)||r.$setViewValue(t.configuration)}),r.$render=function(){t.configuration=r.$viewValue},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{},link:t}}r.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=r;var i=n(25),o=a(i)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}Object.defineProperty(t,"__esModule",{value:!0});var r=n(60),i=a(r),o=n(59),l=a(o),s=n(61),u=a(s);t.default=angular.module("thingsboard.ruleChain.config.filter",[]).directive("tbFilterNodeScriptConfig",i.default).directive("tbFilterNodeMessageTypeConfig",l.default).directive("tbFilterNodeSwitchConfig",u.default).name},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function r(e,t,n){var a=function(a,r,i,l){function s(){if(l.$viewValue){for(var e=[],t=0;t<a.messageTypes.length;t++)e.push(a.messageTypes[t].value);l.$viewValue.messageTypes=e,u()}}function u(){if(a.required){var e=!(!l.$viewValue.messageTypes||!l.$viewValue.messageTypes.length);l.$setValidity("messageTypes",e)}else l.$setValidity("messageTypes",!0)}var d=o.default;r.html(d),a.selectedMessageType=null,a.messageTypeSearchText=null,a.ngModelCtrl=l;var c=[];for(var m in n.messageType){var g={name:n.messageType[m].name,value:n.messageType[m].value};c.push(g)}a.transformMessageTypeChip=function(e){var n,a=t("filter")(c,{name:e},!0);return n=a&&a.length?angular.copy(a[0]):{name:e,value:e}},a.messageTypesSearch=function(e){var n=e?t("filter")(c,{name:e}):c;return n.map(function(e){return e.name})},a.createMessageType=function(e,t){var n=angular.element(t,r)[0].firstElementChild,a=angular.element(n),i=a.scope().$mdChipsCtrl.getChipBuffer();e.preventDefault(),e.stopPropagation(),a.scope().$mdChipsCtrl.appendChip(i.trim()),a.scope().$mdChipsCtrl.resetChipBuffer()},l.$render=function(){var e=l.$viewValue,t=[];if(e&&e.messageTypes)for(var r=0;r<e.messageTypes.length;r++){var i=e.messageTypes[r];n.messageType[i]?t.push(angular.copy(n.messageType[i])):t.push({name:i,value:i})}a.messageTypes=t,a.$watch("messageTypes",function(e,t){angular.equals(e,t)||s()},!0)},e(r.contents())(a)};return{restrict:"E",require:"^ngModel",scope:{required:"=ngRequired",readonly:"=ngReadonly"},link:a}}r.$inject=["$compile","$filter","ruleNodeTypes"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=r,n(3);var i=n(26),o=a(i)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function r(e,t,n){var a=function(a,r,i,l){var s=o.default;r.html(s),a.$watch("configuration",function(e,t){angular.equals(e,t)||l.$setViewValue(a.configuration)}),l.$render=function(){a.configuration=l.$viewValue},a.testScript=function(e){var r=angular.copy(a.configuration.jsScript);n.testNodeScript(e,r,"filter",t.instant("tb.rulenode.filter")+"","Filter",["msg","metadata","msgType"],a.ruleNodeId).then(function(e){a.configuration.jsScript=e,l.$setDirty()})},e(r.contents())(a)};return{restrict:"E",require:"^ngModel",scope:{ruleNodeId:"="},link:a}}r.$inject=["$compile","$translate","ruleNodeScriptTest"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=r;var i=n(27),o=a(i)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function r(e,t,n){var a=function(a,r,i,l){var s=o.default;r.html(s),a.$watch("configuration",function(e,t){angular.equals(e,t)||l.$setViewValue(a.configuration)}),l.$render=function(){a.configuration=l.$viewValue},a.testScript=function(e){var r=angular.copy(a.configuration.jsScript);n.testNodeScript(e,r,"switch",t.instant("tb.rulenode.switch")+"","Switch",["msg","metadata","msgType"],a.ruleNodeId).then(function(e){a.configuration.jsScript=e,l.$setDirty()})},e(r.contents())(a)};return{restrict:"E",require:"^ngModel",scope:{ruleNodeId:"="},link:a}}r.$inject=["$compile","$translate","ruleNodeScriptTest"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=r;var i=n(28),o=a(i)},function(e,t,n){
+"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function r(e){var t=function(t,n,a,r){function i(e){e>-1&&t.kvList.splice(e,1)}function l(){t.kvList||(t.kvList=[]),t.kvList.push({key:"",value:""})}function s(){var e={};t.kvList.forEach(function(t){t.key&&(e[t.key]=t.value)}),r.$setViewValue(e),u()}function u(){var e=!0;t.required&&!t.kvList.length&&(e=!1),r.$setValidity("kvMap",e)}var d=o.default;n.html(d),t.ngModelCtrl=r,t.removeKeyVal=i,t.addKeyVal=l,t.kvList=[],t.$watch("query",function(e,n){angular.equals(e,n)||r.$setViewValue(t.query)}),r.$render=function(){if(r.$viewValue){var e=r.$viewValue;t.kvList.length=0;for(var n in e)t.kvList.push({key:n,value:e[n]})}t.$watch("kvList",function(e,t){angular.equals(e,t)||s()},!0),u()},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{required:"=ngRequired",disabled:"=ngDisabled",requiredText:"=",keyText:"=",keyRequiredText:"=",valText:"=",valRequiredText:"="},link:t}}r.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=r;var i=n(29),o=a(i);n(4)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function r(e,t){var n=function(n,a,r,i){var l=o.default;a.html(l),n.types=t,n.$watch("query",function(e,t){angular.equals(e,t)||i.$setViewValue(n.query)}),i.$render=function(){n.query=i.$viewValue},e(a.contents())(n)};return{restrict:"E",require:"^ngModel",scope:{},link:n}}r.$inject=["$compile","types"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=r;var i=n(30),o=a(i)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function r(e,t){var n=function(n,a,r,i){var l=o.default;a.html(l),n.ruleNodeTypes=t,n.$watch("configuration",function(e,t){angular.equals(e,t)||i.$setViewValue(n.configuration)}),i.$render=function(){n.configuration=i.$viewValue},e(a.contents())(n)};return{restrict:"E",require:"^ngModel",scope:{},link:n}}r.$inject=["$compile","ruleNodeTypes"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=r;var i=n(31),o=a(i)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}Object.defineProperty(t,"__esModule",{value:!0});var r=n(64),i=a(r),o=n(66),l=a(o),s=n(67),u=a(s);t.default=angular.module("thingsboard.ruleChain.config.transform",[]).directive("tbTransformationNodeChangeOriginatorConfig",i.default).directive("tbTransformationNodeScriptConfig",l.default).directive("tbTransformationNodeToEmailConfig",u.default).name},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function r(e,t,n){var a=function(a,r,i,l){var s=o.default;r.html(s),a.$watch("configuration",function(e,t){angular.equals(e,t)||l.$setViewValue(a.configuration)}),l.$render=function(){a.configuration=l.$viewValue},a.testScript=function(e){var r=angular.copy(a.configuration.jsScript);n.testNodeScript(e,r,"update",t.instant("tb.rulenode.transformer")+"","Transform",["msg","metadata","msgType"],a.ruleNodeId).then(function(e){a.configuration.jsScript=e,l.$setDirty()})},e(r.contents())(a)};return{restrict:"E",require:"^ngModel",scope:{ruleNodeId:"="},link:a}}r.$inject=["$compile","$translate","ruleNodeScriptTest"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=r;var i=n(32),o=a(i)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function r(e){var t=function(t,n,a,r){var i=o.default;n.html(i),t.$watch("configuration",function(e,n){angular.equals(e,n)||r.$setViewValue(t.configuration)}),r.$render=function(){t.configuration=r.$viewValue},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{},link:t}}r.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=r;var i=n(33),o=a(i)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}Object.defineProperty(t,"__esModule",{value:!0});var r=n(71),i=a(r),o=n(58),l=a(o),s=n(54),u=a(s),d=n(65),c=a(d),m=n(38),g=a(m),p=n(51),f=a(p),b=n(63),v=a(b),y=n(50),q=a(y),T=n(62),h=a(T),$=n(70),k=a($);t.default=angular.module("thingsboard.ruleChain.config",[i.default,l.default,u.default,c.default,g.default]).directive("tbNodeEmptyConfig",f.default).directive("tbRelationsQueryConfig",v.default).directive("tbDeviceRelationsQueryConfig",q.default).directive("tbKvMapConfig",h.default).config(k.default).name},function(e,t){"use strict";function n(e){var t={tb:{rulenode:{filter:"Filter",switch:"Switch","message-type":"Message type","message-type-required":"Message type is required.","message-types-filter":"Message types filter","no-message-types-found":"No message types found","no-message-type-matching":"'{{messageType}}' not found.","create-new-message-type":"Create a new one!","message-types-required":"Message types are required.","client-attributes":"Client attributes","shared-attributes":"Shared attributes","server-attributes":"Server attributes","latest-timeseries":"Latest timeseries","relations-query":"Relations query","device-relations-query":"Device relations query","max-relation-level":"Max relation level","unlimited-level":"Unlimited level","latest-telemetry":"Latest telemetry","attr-mapping":"Attributes mapping","source-attribute":"Source attribute","source-attribute-required":"Source attribute is required.","source-telemetry":"Source telemetry","source-telemetry-required":"Source telemetry is required.","target-attribute":"Target attribute","target-attribute-required":"Target attribute is required.","attr-mapping-required":"At least one attribute mapping should be specified.","originator-source":"Originator source","originator-customer":"Customer","originator-tenant":"Tenant","originator-related":"Related","clone-message":"Clone message",transform:"Transform","default-ttl":"Default TTL in seconds","default-ttl-required":"Default TTL is required.","min-default-ttl-message":"Only 0 minimum TTL is allowed.","message-count":"Message count (0 - unlimited)","message-count-required":"Message count is required.","min-message-count-message":"Only 0 minimum message count is allowed.","period-seconds":"Period in seconds","period-seconds-required":"Period is required.","min-period-seconds-message":"Only 1 second minimum period is allowed.",originator:"Originator","message-body":"Message body","message-metadata":"Message metadata",generate:"Generate","test-generator-function":"Test generator function",generator:"Generator","test-filter-function":"Test filter function","test-switch-function":"Test switch function","test-transformer-function":"Test transformer function",transformer:"Transformer","alarm-create-condition":"Alarm create condition","test-condition-function":"Test condition function","alarm-clear-condition":"Alarm clear condition","alarm-details-builder":"Alarm details builder","test-details-function":"Test details function","alarm-type":"Alarm type","alarm-type-required":"Alarm type is required.","alarm-severity":"Alarm severity","alarm-severity-required":"Alarm severity is required",propagate:"Propagate",condition:"Condition",details:"Details","to-string":"To string","test-to-string-function":"Test to string function","from-template":"From Template","from-template-required":"From Template is required","from-template-hint":"From address template, use <code>${metaKeyName}</code> to substitute variables from metadata","to-template":"To Template","to-template-required":"To Template is required","mail-address-list-template-hint":"Comma separated address list, use <code>${metaKeyName}</code> to substitute variables from metadata","cc-template":"Cc Template","bcc-template":"Bcc Template","subject-template":"Subject Template","subject-template-required":"Subject Template is required","subject-template-hint":"Mail subject template, use <code>${metaKeyName}</code> to substitute variables from metadata","body-template":"Body Template","body-template-required":"Body Template is required","body-template-hint":"Mail body template, use <code>${metaKeyName}</code> to substitute variables from metadata","request-id-metadata-attribute":"Request Id Metadata attribute name","timeout-sec":"Timeout in seconds","timeout-required":"Timeout is required","min-timeout-message":"Only 0 minimum timeout value is allowed.","endpoint-url-pattern":"Endpoint URL pattern","endpoint-url-pattern-required":"Endpoint URL pattern is required","endpoint-url-pattern-hint":"HTTP URL address pattern, use <code>${metaKeyName}</code> to substitute variables from metadata","request-method":"Request method",headers:"Headers","headers-hint":"Use <code>${metaKeyName}</code> in header/value fields to substitute variables from metadata",header:"Header","header-required":"Header is required",value:"Value","value-required":"Value is required","topic-pattern":"Topic pattern","topic-pattern-required":"Topic pattern is required","mqtt-topic-pattern-hint":"MQTT topic pattern, use <code>${metaKeyName}</code> to substitute variables from metadata","bootstrap-servers":"Bootstrap servers","bootstrap-servers-required":"Bootstrap servers value is required","other-properties":"Other properties",key:"Key","key-required":"Key is required",retries:"Automatically retry times if fails","min-retries-message":"Only 0 minimum retries is allowed.","batch-size-bytes":"Produces batch size in bytes","min-batch-size-bytes-message":"Only 0 minimum batch size is allowed.","linger-ms":"Time to buffer locally (ms)","min-linger-ms-message":"Only 0 ms minimum value is allowed.","buffer-memory-bytes":"Client buffer max size in bytes","min-buffer-memory-message":"Only 0 minimum buffer size is allowed.",acks:"Number of acknowledgments","key-serializer":"Key serializer","key-serializer-required":"Key serializer is required","value-serializer":"Value serializer","value-serializer-required":"Value serializer is required","topic-arn-pattern":"Topic ARN pattern","topic-arn-pattern-required":"Topic ARN pattern is required","topic-arn-pattern-hint":"Topic ARN pattern, use <code>${metaKeyName}</code> to substitute variables from metadata","aws-access-key-id":"AWS Access Key ID","aws-access-key-id-required":"AWS Access Key ID is required","aws-secret-access-key":"AWS Secret Access Key","aws-secret-access-key-required":"AWS Secret Access Key is required","aws-region":"AWS Region","aws-region-required":"AWS Region is required","exchange-name-pattern":"Exchange name pattern","routing-key-pattern":"Routing key pattern","message-properties":"Message properties",host:"Host","host-required":"Host is required",port:"Port","port-required":"Port is required","port-range":"Port should be in a range from 1 to 65535.","virtual-host":"Virtual host",username:"Username",password:"Password","automatic-recovery":"Automatic recovery","connection-timeout-ms":"Connection timeout (ms)","min-connection-timeout-ms-message":"Only 0 ms minimum value is allowed.","handshake-timeout-ms":"Handshake timeout (ms)","min-handshake-timeout-ms-message":"Only 0 ms minimum value is allowed.","client-properties":"Client properties","queue-url-pattern":"Queue URL pattern","queue-url-pattern-required":"Queue URL pattern is required","queue-url-pattern-hint":"Queue URL pattern, use <code>${metaKeyName}</code> to substitute variables from metadata","delay-seconds":"Delay (seconds)","min-delay-seconds-message":"Only 0 seconds minimum value is allowed.","max-delay-seconds-message":"Only 900 seconds maximum value is allowed.",name:"Name","name-required":"Name is required","queue-type":"Queue type","sqs-queue-standard":"Standard","sqs-queue-fifo":"FIFO","message-attributes":"Message attributes","message-attributes-hint":"Use <code>${metaKeyName}</code> in name/value fields to substitute variables from metadata","connect-timeout":"Connection timeout (sec)","connect-timeout-required":"Connection timeout is required.","connect-timeout-range":"Connection timeout should be in a range from 1 to 200.","client-id":"Client ID","enable-ssl":"Enable SSL",credentials:"Credentials","credentials-type":"Credentials type","credentials-type-required":"Credentials type is required.","credentials-anonymous":"Anonymous","credentials-basic":"Basic","credentials-pem":"PEM","username-required":"Username is required.","password-required":"Password is required.","ca-cert":"CA certificate file *","private-key":"Private key file *",cert:"Certificate file *","no-file":"No file selected.","drop-file":"Drop a file or click to select a file to upload.","private-key-password":"Private key password","use-system-smtp-settings":"Use system SMTP settings","smtp-protocol":"Protocol","smtp-host":"SMTP host","smtp-host-required":"SMTP host is required.","smtp-port":"SMTP port","smtp-port-required":"You must supply a smtp port.","smtp-port-range":"SMTP port should be in a range from 1 to 65535.","timeout-msec":"Timeout ms","min-timeout-msec-message":"Only 0 ms minimum value is allowed.","enter-username":"Enter username","enter-password":"Enter password","enable-tls":"Enable TLS"},"key-val":{key:"Key",value:"Value","remove-entry":"Remove entry","add-entry":"Add entry"}}};angular.merge(e.en_US,t)}Object.defineProperty(t,"__esModule",{value:!0}),t.default=n},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function r(e,t){(0,o.default)(t);for(var n in t){var a=t[n];e.translations(n,a)}}r.$inject=["$translateProvider","locales"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=r;var i=n(69),o=a(i)},function(e,t){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.default=angular.module("thingsboard.ruleChain.config.types",[]).constant("ruleNodeTypes",{messageType:{POST_ATTRIBUTES_REQUEST:{name:"Post attributes",value:"POST_ATTRIBUTES_REQUEST"},POST_TELEMETRY_REQUEST:{name:"Post telemetry",value:"POST_TELEMETRY_REQUEST"},TO_SERVER_RPC_REQUEST:{name:"RPC Request",value:"TO_SERVER_RPC_REQUEST"},ACTIVITY_EVENT:{name:"Activity Event",value:"ACTIVITY_EVENT"},INACTIVITY_EVENT:{name:"Inactivity Event",value:"INACTIVITY_EVENT"},CONNECT_EVENT:{name:"Connect Event",value:"CONNECT_EVENT"},DISCONNECT_EVENT:{name:"Disconnect Event",value:"DISCONNECT_EVENT"},ENTITY_CREATED:{name:"Entity Created",value:"ENTITY_CREATED"},ENTITY_UPDATED:{name:"Entity Updated",value:"ENTITY_UPDATED"},ENTITY_DELETED:{name:"Entity Deleted",value:"ENTITY_DELETED"},ENTITY_ASSIGNED:{name:"Entity Assigned",value:"ENTITY_ASSIGNED"},ENTITY_UNASSIGNED:{name:"Entity Unassigned",value:"ENTITY_UNASSIGNED"},ATTRIBUTES_UPDATED:{name:"Attributes Updated",value:"ATTRIBUTES_UPDATED"},ATTRIBUTES_DELETED:{name:"Attributes Deleted",value:"ATTRIBUTES_DELETED"}},originatorSource:{CUSTOMER:{name:"tb.rulenode.originator-customer",value:"CUSTOMER"},TENANT:{name:"tb.rulenode.originator-tenant",value:"TENANT"},RELATED:{name:"tb.rulenode.originator-related",value:"RELATED"}},httpRequestType:["GET","POST","PUT","DELETE"],sqsQueueType:{STANDARD:{name:"tb.rulenode.sqs-queue-standard",value:"STANDARD"},FIFO:{name:"tb.rulenode.sqs-queue-fifo",value:"FIFO"}},mqttCredentialTypes:{anonymous:{value:"anonymous",name:"tb.rulenode.credentials-anonymous"},basic:{value:"basic",name:"tb.rulenode.credentials-basic"},"cert.PEM":{value:"cert.PEM",name:"tb.rulenode.credentials-pem"}}}).name}]));
+//# sourceMappingURL=rulenode-core-config.js.map
\ No newline at end of file
diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbAlarmNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbAlarmNodeTest.java
new file mode 100644
index 0000000..aeda2de
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbAlarmNodeTest.java
@@ -0,0 +1,378 @@
+/**
+ * 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.datastax.driver.core.utils.UUIDs;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import org.apache.commons.lang3.NotImplementedException;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.runners.MockitoJUnitRunner;
+import org.mockito.stubbing.Answer;
+import org.thingsboard.rule.engine.api.*;
+import org.thingsboard.server.common.data.alarm.Alarm;
+import org.thingsboard.server.common.data.id.DeviceId;
+import org.thingsboard.server.common.data.id.EntityId;
+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.msg.TbMsg;
+import org.thingsboard.server.common.msg.TbMsgMetaData;
+import org.thingsboard.server.dao.alarm.AlarmService;
+
+import javax.script.ScriptException;
+import java.io.IOException;
+import java.util.concurrent.Callable;
+
+import static org.junit.Assert.*;
+import static org.mockito.Matchers.any;
+import static org.mockito.Mockito.*;
+import static org.thingsboard.rule.engine.action.TbAbstractAlarmNode.*;
+import static org.thingsboard.server.common.data.alarm.AlarmSeverity.CRITICAL;
+import static org.thingsboard.server.common.data.alarm.AlarmSeverity.WARNING;
+import static org.thingsboard.server.common.data.alarm.AlarmStatus.*;
+
+@RunWith(MockitoJUnitRunner.class)
+public class TbAlarmNodeTest {
+
+    private TbAbstractAlarmNode node;
+
+    @Mock
+    private TbContext ctx;
+    @Mock
+    private ListeningExecutor executor;
+    @Mock
+    private AlarmService alarmService;
+
+    @Mock
+    private ScriptEngine detailsJs;
+
+    private RuleChainId ruleChainId = new RuleChainId(UUIDs.timeBased());
+    private RuleNodeId ruleNodeId = new RuleNodeId(UUIDs.timeBased());
+
+    private ListeningExecutor dbExecutor;
+
+    private EntityId originator = new DeviceId(UUIDs.timeBased());
+    private TenantId tenantId = new TenantId(UUIDs.timeBased());
+    private TbMsgMetaData metaData = new TbMsgMetaData();
+    private String rawJson = "{\"name\": \"Vit\", \"passed\": 5}";
+
+    @Before
+    public void before() {
+        dbExecutor = new ListeningExecutor() {
+            @Override
+            public <T> ListenableFuture<T> executeAsync(Callable<T> task) {
+                try {
+                    return Futures.immediateFuture(task.call());
+                } catch (Exception e) {
+                    throw new RuntimeException(e);
+                }
+            }
+
+            @Override
+            public void execute(Runnable command) {
+                command.run();
+            }
+        };
+    }
+
+    @Test
+    public void newAlarmCanBeCreated() throws ScriptException, IOException {
+        initWithCreateAlarmScript();
+        metaData.putValue("key", "value");
+        TbMsg msg = new TbMsg(UUIDs.timeBased(), "USER", originator, metaData, rawJson, ruleChainId, ruleNodeId, 0L);
+
+        when(detailsJs.executeJson(msg)).thenReturn(null);
+        when(alarmService.findLatestByOriginatorAndType(tenantId, originator, "SomeType")).thenReturn(Futures.immediateFuture(null));
+
+        doAnswer((Answer<Alarm>) invocationOnMock -> (Alarm) (invocationOnMock.getArguments())[0]).when(alarmService).createOrUpdateAlarm(any(Alarm.class));
+
+        node.onMsg(ctx, msg);
+
+        verify(ctx).tellNext(any(), eq("Created"));
+
+        ArgumentCaptor<TbMsg> msgCaptor = ArgumentCaptor.forClass(TbMsg.class);
+        ArgumentCaptor<String> typeCaptor = ArgumentCaptor.forClass(String.class);
+        ArgumentCaptor<EntityId> originatorCaptor = ArgumentCaptor.forClass(EntityId.class);
+        ArgumentCaptor<TbMsgMetaData> metadataCaptor = ArgumentCaptor.forClass(TbMsgMetaData.class);
+        ArgumentCaptor<String> dataCaptor = ArgumentCaptor.forClass(String.class);
+        verify(ctx).transformMsg(msgCaptor.capture(), typeCaptor.capture(), originatorCaptor.capture(), metadataCaptor.capture(), dataCaptor.capture());
+
+        assertEquals("ALARM", typeCaptor.getValue());
+        assertEquals(originator, originatorCaptor.getValue());
+        assertEquals("value", metadataCaptor.getValue().getValue("key"));
+        assertEquals(Boolean.TRUE.toString(), metadataCaptor.getValue().getValue(IS_NEW_ALARM));
+        assertNotSame(metaData, metadataCaptor.getValue());
+
+        Alarm actualAlarm = new ObjectMapper().readValue(dataCaptor.getValue().getBytes(), Alarm.class);
+        Alarm expectedAlarm = Alarm.builder()
+                .tenantId(tenantId)
+                .originator(originator)
+                .status(ACTIVE_UNACK)
+                .severity(CRITICAL)
+                .propagate(true)
+                .type("SomeType")
+                .details(null)
+                .build();
+
+        assertEquals(expectedAlarm, actualAlarm);
+
+        verify(executor, times(1)).executeAsync(any(Callable.class));
+    }
+
+    @Test
+    public void buildDetailsThrowsException() throws ScriptException, IOException {
+        initWithCreateAlarmScript();
+        metaData.putValue("key", "value");
+        TbMsg msg = new TbMsg(UUIDs.timeBased(), "USER", originator, metaData, rawJson, ruleChainId, ruleNodeId, 0L);
+
+        when(detailsJs.executeJson(msg)).thenThrow(new NotImplementedException("message"));
+        when(alarmService.findLatestByOriginatorAndType(tenantId, originator, "SomeType")).thenReturn(Futures.immediateFuture(null));
+
+        node.onMsg(ctx, msg);
+
+        verifyError(msg, "message", NotImplementedException.class);
+
+        verify(ctx).createJsScriptEngine("DETAILS");
+        verify(ctx, times(1)).getJsExecutor();
+        verify(ctx).getAlarmService();
+        verify(ctx, times(3)).getDbCallbackExecutor();
+        verify(ctx).getTenantId();
+        verify(alarmService).findLatestByOriginatorAndType(tenantId, originator, "SomeType");
+
+        verifyNoMoreInteractions(ctx, alarmService);
+    }
+
+    @Test
+    public void ifAlarmClearedCreateNew() throws ScriptException, IOException {
+        initWithCreateAlarmScript();
+        metaData.putValue("key", "value");
+        TbMsg msg = new TbMsg(UUIDs.timeBased(), "USER", originator, metaData, rawJson, ruleChainId, ruleNodeId, 0L);
+
+        Alarm clearedAlarm = Alarm.builder().status(CLEARED_ACK).build();
+
+        when(detailsJs.executeJson(msg)).thenReturn(null);
+        when(alarmService.findLatestByOriginatorAndType(tenantId, originator, "SomeType")).thenReturn(Futures.immediateFuture(clearedAlarm));
+
+        doAnswer((Answer<Alarm>) invocationOnMock -> (Alarm) (invocationOnMock.getArguments())[0]).when(alarmService).createOrUpdateAlarm(any(Alarm.class));
+
+        node.onMsg(ctx, msg);
+
+        verify(ctx).tellNext(any(), eq("Created"));
+
+        ArgumentCaptor<TbMsg> msgCaptor = ArgumentCaptor.forClass(TbMsg.class);
+        ArgumentCaptor<String> typeCaptor = ArgumentCaptor.forClass(String.class);
+        ArgumentCaptor<EntityId> originatorCaptor = ArgumentCaptor.forClass(EntityId.class);
+        ArgumentCaptor<TbMsgMetaData> metadataCaptor = ArgumentCaptor.forClass(TbMsgMetaData.class);
+        ArgumentCaptor<String> dataCaptor = ArgumentCaptor.forClass(String.class);
+        verify(ctx).transformMsg(msgCaptor.capture(), typeCaptor.capture(), originatorCaptor.capture(), metadataCaptor.capture(), dataCaptor.capture());
+
+        assertEquals("ALARM", typeCaptor.getValue());
+        assertEquals(originator, originatorCaptor.getValue());
+        assertEquals("value", metadataCaptor.getValue().getValue("key"));
+        assertEquals(Boolean.TRUE.toString(), metadataCaptor.getValue().getValue(IS_NEW_ALARM));
+        assertNotSame(metaData, metadataCaptor.getValue());
+
+
+        Alarm actualAlarm = new ObjectMapper().readValue(dataCaptor.getValue().getBytes(), Alarm.class);
+        Alarm expectedAlarm = Alarm.builder()
+                .tenantId(tenantId)
+                .originator(originator)
+                .status(ACTIVE_UNACK)
+                .severity(CRITICAL)
+                .propagate(true)
+                .type("SomeType")
+                .details(null)
+                .build();
+
+        assertEquals(expectedAlarm, actualAlarm);
+
+        verify(executor, times(1)).executeAsync(any(Callable.class));
+    }
+
+    @Test
+    public void alarmCanBeUpdated() throws ScriptException, IOException {
+        initWithCreateAlarmScript();
+        metaData.putValue("key", "value");
+        TbMsg msg = new TbMsg(UUIDs.timeBased(), "USER", originator, metaData, rawJson, ruleChainId, ruleNodeId, 0L);
+
+        long oldEndDate = System.currentTimeMillis();
+        Alarm activeAlarm = Alarm.builder().type("SomeType").tenantId(tenantId).originator(originator).status(ACTIVE_UNACK).severity(WARNING).endTs(oldEndDate).build();
+
+        when(detailsJs.executeJson(msg)).thenReturn(null);
+        when(alarmService.findLatestByOriginatorAndType(tenantId, originator, "SomeType")).thenReturn(Futures.immediateFuture(activeAlarm));
+
+        doAnswer((Answer<Alarm>) invocationOnMock -> (Alarm) (invocationOnMock.getArguments())[0]).when(alarmService).createOrUpdateAlarm(activeAlarm);
+
+        node.onMsg(ctx, msg);
+
+        verify(ctx).tellNext(any(), eq("Updated"));
+
+        ArgumentCaptor<TbMsg> msgCaptor = ArgumentCaptor.forClass(TbMsg.class);
+        ArgumentCaptor<String> typeCaptor = ArgumentCaptor.forClass(String.class);
+        ArgumentCaptor<EntityId> originatorCaptor = ArgumentCaptor.forClass(EntityId.class);
+        ArgumentCaptor<TbMsgMetaData> metadataCaptor = ArgumentCaptor.forClass(TbMsgMetaData.class);
+        ArgumentCaptor<String> dataCaptor = ArgumentCaptor.forClass(String.class);
+        verify(ctx).transformMsg(msgCaptor.capture(), typeCaptor.capture(), originatorCaptor.capture(), metadataCaptor.capture(), dataCaptor.capture());
+
+        assertEquals("ALARM", typeCaptor.getValue());
+        assertEquals(originator, originatorCaptor.getValue());
+        assertEquals("value", metadataCaptor.getValue().getValue("key"));
+        assertEquals(Boolean.TRUE.toString(), metadataCaptor.getValue().getValue(IS_EXISTING_ALARM));
+        assertNotSame(metaData, metadataCaptor.getValue());
+
+        Alarm actualAlarm = new ObjectMapper().readValue(dataCaptor.getValue().getBytes(), Alarm.class);
+        assertTrue(activeAlarm.getEndTs() > oldEndDate);
+        Alarm expectedAlarm = Alarm.builder()
+                .tenantId(tenantId)
+                .originator(originator)
+                .status(ACTIVE_UNACK)
+                .severity(CRITICAL)
+                .propagate(true)
+                .type("SomeType")
+                .details(null)
+                .endTs(activeAlarm.getEndTs())
+                .build();
+
+        assertEquals(expectedAlarm, actualAlarm);
+
+        verify(executor, times(1)).executeAsync(any(Callable.class));
+    }
+
+    @Test
+    public void alarmCanBeCleared() throws ScriptException, IOException {
+        initWithClearAlarmScript();
+        metaData.putValue("key", "value");
+        TbMsg msg = new TbMsg(UUIDs.timeBased(), "USER", originator, metaData, rawJson, ruleChainId, ruleNodeId, 0L);
+
+        long oldEndDate = System.currentTimeMillis();
+        Alarm activeAlarm = Alarm.builder().type("SomeType").tenantId(tenantId).originator(originator).status(ACTIVE_UNACK).severity(WARNING).endTs(oldEndDate).build();
+
+//        when(detailsJs.executeJson(msg)).thenReturn(null);
+        when(alarmService.findLatestByOriginatorAndType(tenantId, originator, "SomeType")).thenReturn(Futures.immediateFuture(activeAlarm));
+        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);
+
+        node.onMsg(ctx, msg);
+
+        verify(ctx).tellNext(any(), eq("Cleared"));
+
+        ArgumentCaptor<TbMsg> msgCaptor = ArgumentCaptor.forClass(TbMsg.class);
+        ArgumentCaptor<String> typeCaptor = ArgumentCaptor.forClass(String.class);
+        ArgumentCaptor<EntityId> originatorCaptor = ArgumentCaptor.forClass(EntityId.class);
+        ArgumentCaptor<TbMsgMetaData> metadataCaptor = ArgumentCaptor.forClass(TbMsgMetaData.class);
+        ArgumentCaptor<String> dataCaptor = ArgumentCaptor.forClass(String.class);
+        verify(ctx).transformMsg(msgCaptor.capture(), typeCaptor.capture(), originatorCaptor.capture(), metadataCaptor.capture(), dataCaptor.capture());
+
+        assertEquals("ALARM", typeCaptor.getValue());
+        assertEquals(originator, originatorCaptor.getValue());
+        assertEquals("value", metadataCaptor.getValue().getValue("key"));
+        assertEquals(Boolean.TRUE.toString(), metadataCaptor.getValue().getValue(IS_CLEARED_ALARM));
+        assertNotSame(metaData, metadataCaptor.getValue());
+
+        Alarm actualAlarm = new ObjectMapper().readValue(dataCaptor.getValue().getBytes(), Alarm.class);
+        Alarm expectedAlarm = Alarm.builder()
+                .tenantId(tenantId)
+                .originator(originator)
+                .status(CLEARED_UNACK)
+                .severity(WARNING)
+                .propagate(false)
+                .type("SomeType")
+                .details(null)
+                .endTs(oldEndDate)
+                .build();
+
+        assertEquals(expectedAlarm, actualAlarm);
+    }
+
+    private void initWithCreateAlarmScript() {
+        try {
+            TbCreateAlarmNodeConfiguration config = new TbCreateAlarmNodeConfiguration();
+            config.setPropagate(true);
+            config.setSeverity(CRITICAL);
+            config.setAlarmType("SomeType");
+            config.setAlarmDetailsBuildJs("DETAILS");
+            ObjectMapper mapper = new ObjectMapper();
+            TbNodeConfiguration nodeConfiguration = new TbNodeConfiguration(mapper.valueToTree(config));
+
+            when(ctx.createJsScriptEngine("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 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")).thenReturn(detailsJs);
+
+            when(ctx.getTenantId()).thenReturn(tenantId);
+            when(ctx.getJsExecutor()).thenReturn(executor);
+            when(ctx.getAlarmService()).thenReturn(alarmService);
+            when(ctx.getDbCallbackExecutor()).thenReturn(dbExecutor);
+
+            mockJsExecutor();
+
+            node = new TbClearAlarmNode();
+            node.init(ctx, nodeConfiguration);
+        } catch (TbNodeException ex) {
+            throw new IllegalStateException(ex);
+        }
+    }
+
+    private void mockJsExecutor() {
+        when(ctx.getJsExecutor()).thenReturn(executor);
+        doAnswer((Answer<ListenableFuture<Boolean>>) invocationOnMock -> {
+            try {
+                Callable task = (Callable) (invocationOnMock.getArguments())[0];
+                return Futures.immediateFuture((Boolean) task.call());
+            } catch (Throwable th) {
+                return Futures.immediateFailedFuture(th);
+            }
+        }).when(executor).executeAsync(any(Callable.class));
+    }
+
+    private void verifyError(TbMsg msg, String message, Class expectedClass) {
+        ArgumentCaptor<Throwable> captor = ArgumentCaptor.forClass(Throwable.class);
+        verify(ctx).tellFailure(same(msg), captor.capture());
+
+        Throwable value = captor.getValue();
+        assertEquals(expectedClass, value.getClass());
+        assertEquals(message, value.getMessage());
+    }
+
+}
\ No newline at end of file
diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/filter/TbJsFilterNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/filter/TbJsFilterNodeTest.java
new file mode 100644
index 0000000..b3e097b
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/filter/TbJsFilterNodeTest.java
@@ -0,0 +1,126 @@
+/**
+ * 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.filter;
+
+import com.datastax.driver.core.utils.UUIDs;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Matchers;
+import org.mockito.Mock;
+import org.mockito.runners.MockitoJUnitRunner;
+import org.mockito.stubbing.Answer;
+import org.thingsboard.rule.engine.api.*;
+import org.thingsboard.server.common.data.id.RuleChainId;
+import org.thingsboard.server.common.data.id.RuleNodeId;
+import org.thingsboard.server.common.msg.TbMsg;
+import org.thingsboard.server.common.msg.TbMsgMetaData;
+
+import javax.script.ScriptException;
+import java.util.concurrent.Callable;
+
+import static org.junit.Assert.assertEquals;
+import static org.mockito.Mockito.*;
+
+@RunWith(MockitoJUnitRunner.class)
+public class TbJsFilterNodeTest {
+
+    private TbJsFilterNode node;
+
+    @Mock
+    private TbContext ctx;
+    @Mock
+    private ListeningExecutor executor;
+    @Mock
+    private ScriptEngine scriptEngine;
+
+    private RuleChainId ruleChainId = new RuleChainId(UUIDs.timeBased());
+    private RuleNodeId ruleNodeId = new RuleNodeId(UUIDs.timeBased());
+
+    @Test
+    public void falseEvaluationDoNotSendMsg() throws TbNodeException, ScriptException {
+        initWithScript();
+        TbMsg msg = new TbMsg(UUIDs.timeBased(), "USER", null, new TbMsgMetaData(), "{}", ruleChainId, ruleNodeId, 0L);
+        mockJsExecutor();
+        when(scriptEngine.executeFilter(msg)).thenReturn(false);
+
+        node.onMsg(ctx, msg);
+        verify(ctx).getJsExecutor();
+        verify(ctx).tellNext(msg, "False");
+    }
+
+    @Test
+    public void exceptionInJsThrowsException() throws TbNodeException, ScriptException {
+        initWithScript();
+        TbMsgMetaData metaData = new TbMsgMetaData();
+        TbMsg msg = new TbMsg(UUIDs.timeBased(), "USER", null, metaData, "{}", ruleChainId, ruleNodeId, 0L);
+        mockJsExecutor();
+        when(scriptEngine.executeFilter(msg)).thenThrow(new ScriptException("error"));
+
+
+        node.onMsg(ctx, msg);
+        verifyError(msg, "error", ScriptException.class);
+    }
+
+    @Test
+    public void metadataConditionCanBeTrue() throws TbNodeException, ScriptException {
+        initWithScript();
+        TbMsgMetaData metaData = new TbMsgMetaData();
+        TbMsg msg = new TbMsg(UUIDs.timeBased(), "USER", null, metaData, "{}", ruleChainId, ruleNodeId, 0L);
+        mockJsExecutor();
+        when(scriptEngine.executeFilter(msg)).thenReturn(true);
+
+        node.onMsg(ctx, msg);
+        verify(ctx).getJsExecutor();
+        verify(ctx).tellNext(msg, "True");
+    }
+
+    private void initWithScript() throws TbNodeException {
+        TbJsFilterNodeConfiguration config = new TbJsFilterNodeConfiguration();
+        config.setJsScript("scr");
+        ObjectMapper mapper = new ObjectMapper();
+        TbNodeConfiguration nodeConfiguration = new TbNodeConfiguration(mapper.valueToTree(config));
+
+        when(ctx.createJsScriptEngine("scr")).thenReturn(scriptEngine);
+
+        node = new TbJsFilterNode();
+        node.init(ctx, nodeConfiguration);
+    }
+
+    private void mockJsExecutor() {
+        when(ctx.getJsExecutor()).thenReturn(executor);
+        doAnswer((Answer<ListenableFuture<Boolean>>) invocationOnMock -> {
+            try {
+                Callable task = (Callable) (invocationOnMock.getArguments())[0];
+                return Futures.immediateFuture((Boolean) task.call());
+            } catch (Throwable th) {
+                return Futures.immediateFailedFuture(th);
+            }
+        }).when(executor).executeAsync(Matchers.any(Callable.class));
+    }
+
+    private void verifyError(TbMsg msg, String message, Class expectedClass) {
+        ArgumentCaptor<Throwable> captor = ArgumentCaptor.forClass(Throwable.class);
+        verify(ctx).tellFailure(same(msg), captor.capture());
+
+        Throwable value = captor.getValue();
+        assertEquals(expectedClass, value.getClass());
+        assertEquals(message, value.getMessage());
+    }
+}
\ No newline at end of file
diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/filter/TbJsSwitchNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/filter/TbJsSwitchNodeTest.java
new file mode 100644
index 0000000..f547807
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/filter/TbJsSwitchNodeTest.java
@@ -0,0 +1,108 @@
+/**
+ * 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.filter;
+
+import com.datastax.driver.core.utils.UUIDs;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.google.common.collect.Sets;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Matchers;
+import org.mockito.Mock;
+import org.mockito.runners.MockitoJUnitRunner;
+import org.mockito.stubbing.Answer;
+import org.thingsboard.rule.engine.api.*;
+import org.thingsboard.server.common.data.id.RuleChainId;
+import org.thingsboard.server.common.data.id.RuleNodeId;
+import org.thingsboard.server.common.msg.TbMsg;
+import org.thingsboard.server.common.msg.TbMsgMetaData;
+
+import javax.script.ScriptException;
+import java.util.Set;
+import java.util.concurrent.Callable;
+
+import static org.junit.Assert.assertEquals;
+import static org.mockito.Matchers.same;
+import static org.mockito.Mockito.*;
+
+@RunWith(MockitoJUnitRunner.class)
+public class TbJsSwitchNodeTest {
+
+    private TbJsSwitchNode node;
+
+    @Mock
+    private TbContext ctx;
+    @Mock
+    private ListeningExecutor executor;
+    @Mock
+    private ScriptEngine scriptEngine;
+
+    private RuleChainId ruleChainId = new RuleChainId(UUIDs.timeBased());
+    private RuleNodeId ruleNodeId = new RuleNodeId(UUIDs.timeBased());
+
+    @Test
+    public void multipleRoutesAreAllowed() throws TbNodeException, ScriptException {
+        initWithScript();
+        TbMsgMetaData metaData = new TbMsgMetaData();
+        metaData.putValue("temp", "10");
+        metaData.putValue("humidity", "99");
+        String rawJson = "{\"name\": \"Vit\", \"passed\": 5}";
+
+        TbMsg msg = new TbMsg(UUIDs.timeBased(), "USER", null, metaData, rawJson, ruleChainId, ruleNodeId, 0L);
+        mockJsExecutor();
+        when(scriptEngine.executeSwitch(msg)).thenReturn(Sets.newHashSet("one", "three"));
+
+        node.onMsg(ctx, msg);
+        verify(ctx).getJsExecutor();
+        verify(ctx).tellNext(msg, Sets.newHashSet("one", "three"));
+    }
+
+    private void initWithScript() throws TbNodeException {
+        TbJsSwitchNodeConfiguration config = new TbJsSwitchNodeConfiguration();
+        config.setJsScript("scr");
+        ObjectMapper mapper = new ObjectMapper();
+        TbNodeConfiguration nodeConfiguration = new TbNodeConfiguration(mapper.valueToTree(config));
+
+        when(ctx.createJsScriptEngine("scr")).thenReturn(scriptEngine);
+
+        node = new TbJsSwitchNode();
+        node.init(ctx, nodeConfiguration);
+    }
+
+    private void mockJsExecutor() {
+        when(ctx.getJsExecutor()).thenReturn(executor);
+        doAnswer((Answer<ListenableFuture<Set<String>>>) invocationOnMock -> {
+            try {
+                Callable task = (Callable) (invocationOnMock.getArguments())[0];
+                return Futures.immediateFuture((Set<String>) task.call());
+            } catch (Throwable th) {
+                return Futures.immediateFailedFuture(th);
+            }
+        }).when(executor).executeAsync(Matchers.any(Callable.class));
+    }
+
+    private void verifyError(TbMsg msg, String message, Class expectedClass) {
+        ArgumentCaptor<Throwable> captor = ArgumentCaptor.forClass(Throwable.class);
+        verify(ctx).tellFailure(same(msg), captor.capture());
+
+        Throwable value = captor.getValue();
+        assertEquals(expectedClass, value.getClass());
+        assertEquals(message, value.getMessage());
+    }
+}
\ No newline at end of file
diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/mail/TbMsgToEmailNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/mail/TbMsgToEmailNodeTest.java
new file mode 100644
index 0000000..19d4a49
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/mail/TbMsgToEmailNodeTest.java
@@ -0,0 +1,110 @@
+/**
+ * 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.mail;
+
+import com.datastax.driver.core.utils.UUIDs;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.runners.MockitoJUnitRunner;
+import org.thingsboard.rule.engine.api.TbContext;
+import org.thingsboard.rule.engine.api.TbNodeConfiguration;
+import org.thingsboard.rule.engine.api.TbNodeException;
+import org.thingsboard.server.common.data.id.DeviceId;
+import org.thingsboard.server.common.data.id.EntityId;
+import org.thingsboard.server.common.data.id.RuleChainId;
+import org.thingsboard.server.common.data.id.RuleNodeId;
+import org.thingsboard.server.common.msg.TbMsg;
+import org.thingsboard.server.common.msg.TbMsgMetaData;
+
+import java.io.IOException;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotSame;
+import static org.mockito.Matchers.any;
+import static org.mockito.Mockito.verify;
+
+@RunWith(MockitoJUnitRunner.class)
+public class TbMsgToEmailNodeTest {
+
+    private TbMsgToEmailNode emailNode;
+
+    @Mock
+    private TbContext ctx;
+
+    private EntityId originator = new DeviceId(UUIDs.timeBased());
+    private TbMsgMetaData metaData = new TbMsgMetaData();
+    private String rawJson = "{\"name\": \"temp\", \"passed\": 5 , \"complex\": {\"val\":12, \"count\":100}}";
+
+    private RuleChainId ruleChainId = new RuleChainId(UUIDs.timeBased());
+    private RuleNodeId ruleNodeId = new RuleNodeId(UUIDs.timeBased());
+
+    @Test
+    public void msgCanBeConverted() throws IOException {
+        initWithScript();
+        metaData.putValue("username", "oreo");
+        metaData.putValue("userEmail", "user@email.io");
+        metaData.putValue("name", "temp");
+        metaData.putValue("passed", "5");
+        metaData.putValue("count", "100");
+        TbMsg msg = new TbMsg(UUIDs.timeBased(), "USER", originator, metaData, rawJson, ruleChainId, ruleNodeId, 0L);
+
+        emailNode.onMsg(ctx, msg);
+
+        ArgumentCaptor<TbMsg> msgCaptor = ArgumentCaptor.forClass(TbMsg.class);
+        ArgumentCaptor<String> typeCaptor = ArgumentCaptor.forClass(String.class);
+        ArgumentCaptor<EntityId> originatorCaptor = ArgumentCaptor.forClass(EntityId.class);
+        ArgumentCaptor<TbMsgMetaData> metadataCaptor = ArgumentCaptor.forClass(TbMsgMetaData.class);
+        ArgumentCaptor<String> dataCaptor = ArgumentCaptor.forClass(String.class);
+        verify(ctx).transformMsg(msgCaptor.capture(), typeCaptor.capture(), originatorCaptor.capture(), metadataCaptor.capture(), dataCaptor.capture());
+
+
+        assertEquals("SEND_EMAIL", typeCaptor.getValue());
+        assertEquals(originator, originatorCaptor.getValue());
+        assertEquals("oreo", metadataCaptor.getValue().getValue("username"));
+        assertNotSame(metaData, metadataCaptor.getValue());
+
+        EmailPojo actual = new ObjectMapper().readValue(dataCaptor.getValue().getBytes(), EmailPojo.class);
+
+        EmailPojo expected = new EmailPojo.EmailPojoBuilder()
+                .from("test@mail.org")
+                .to("user@email.io")
+                .subject("Hi oreo there")
+                .body("temp is to high. Current 5 and 100")
+                .build();
+        assertEquals(expected, actual);
+    }
+
+    private void initWithScript() {
+        try {
+            TbMsgToEmailNodeConfiguration config = new TbMsgToEmailNodeConfiguration();
+            config.setFromTemplate("test@mail.org");
+            config.setToTemplate("${userEmail}");
+            config.setSubjectTemplate("Hi ${username} there");
+            config.setBodyTemplate("${name} is to high. Current ${passed} and ${count}");
+            ObjectMapper mapper = new ObjectMapper();
+            TbNodeConfiguration nodeConfiguration = new TbNodeConfiguration(mapper.valueToTree(config));
+
+            emailNode = new TbMsgToEmailNode();
+            emailNode.init(ctx, nodeConfiguration);
+        } catch (TbNodeException ex) {
+            throw new IllegalStateException(ex);
+        }
+    }
+
+}
\ No newline at end of file
diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/TbGetCustomerAttributeNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/TbGetCustomerAttributeNodeTest.java
new file mode 100644
index 0000000..4a855ae
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/TbGetCustomerAttributeNodeTest.java
@@ -0,0 +1,262 @@
+/**
+ * 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.datastax.driver.core.utils.UUIDs;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.google.common.collect.Lists;
+import com.google.common.util.concurrent.Futures;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.runners.MockitoJUnitRunner;
+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.Device;
+import org.thingsboard.server.common.data.User;
+import org.thingsboard.server.common.data.asset.Asset;
+import org.thingsboard.server.common.data.id.*;
+import org.thingsboard.server.common.data.kv.*;
+import org.thingsboard.server.common.msg.TbMsg;
+import org.thingsboard.server.common.msg.TbMsgMetaData;
+import org.thingsboard.server.dao.asset.AssetService;
+import org.thingsboard.server.dao.attributes.AttributesService;
+import org.thingsboard.server.dao.device.DeviceService;
+import org.thingsboard.server.dao.timeseries.TimeseriesService;
+import org.thingsboard.server.dao.user.UserService;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Matchers.same;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+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.SERVER_SCOPE;
+
+@RunWith(MockitoJUnitRunner.class)
+public class TbGetCustomerAttributeNodeTest {
+
+    private TbGetCustomerAttributeNode node;
+
+    @Mock
+    private TbContext ctx;
+
+    @Mock
+    private AttributesService attributesService;
+    @Mock
+    private TimeseriesService timeseriesService;
+    @Mock
+    private UserService userService;
+    @Mock
+    private AssetService assetService;
+    @Mock
+    private DeviceService deviceService;
+
+    private TbMsg msg;
+
+    private RuleChainId ruleChainId = new RuleChainId(UUIDs.timeBased());
+    private RuleNodeId ruleNodeId = new RuleNodeId(UUIDs.timeBased());
+
+    @Before
+    public void init() throws TbNodeException {
+        TbGetEntityAttrNodeConfiguration config = new TbGetEntityAttrNodeConfiguration();
+        Map<String, String> attrMapping = new HashMap<>();
+        attrMapping.putIfAbsent("temperature", "tempo");
+        config.setAttrMapping(attrMapping);
+        config.setTelemetry(false);
+        ObjectMapper mapper = new ObjectMapper();
+        TbNodeConfiguration nodeConfiguration = new TbNodeConfiguration(mapper.valueToTree(config));
+
+        node = new TbGetCustomerAttributeNode();
+        node.init(null, nodeConfiguration);
+    }
+
+    @Test
+    public void errorThrownIfCannotLoadAttributes() {
+        UserId userId = new UserId(UUIDs.timeBased());
+        CustomerId customerId = new CustomerId(UUIDs.timeBased());
+        User user = new User();
+        user.setCustomerId(customerId);
+
+        msg = new TbMsg(UUIDs.timeBased(), "USER", userId, new TbMsgMetaData(), "{}", ruleChainId, ruleNodeId, 0L);
+
+        when(ctx.getUserService()).thenReturn(userService);
+        when(userService.findUserByIdAsync(userId)).thenReturn(Futures.immediateFuture(user));
+
+        when(ctx.getAttributesService()).thenReturn(attributesService);
+        when(attributesService.find(customerId, SERVER_SCOPE, Collections.singleton("temperature")))
+                .thenThrow(new IllegalStateException("something wrong"));
+
+        node.onMsg(ctx, msg);
+        final ArgumentCaptor<Throwable> captor = ArgumentCaptor.forClass(Throwable.class);
+        verify(ctx).tellFailure(same(msg), captor.capture());
+
+        Throwable value = captor.getValue();
+        assertEquals("something wrong", value.getMessage());
+        assertTrue(msg.getMetaData().getData().isEmpty());
+    }
+
+    @Test
+    public void errorThrownIfCannotLoadAttributesAsync() {
+        UserId userId = new UserId(UUIDs.timeBased());
+        CustomerId customerId = new CustomerId(UUIDs.timeBased());
+        User user = new User();
+        user.setCustomerId(customerId);
+
+        msg = new TbMsg(UUIDs.timeBased(), "USER", userId, new TbMsgMetaData(), "{}", ruleChainId, ruleNodeId, 0L);
+
+        when(ctx.getUserService()).thenReturn(userService);
+        when(userService.findUserByIdAsync(userId)).thenReturn(Futures.immediateFuture(user));
+
+        when(ctx.getAttributesService()).thenReturn(attributesService);
+        when(attributesService.find(customerId, SERVER_SCOPE, Collections.singleton("temperature")))
+                .thenReturn(Futures.immediateFailedFuture(new IllegalStateException("something wrong")));
+
+        node.onMsg(ctx, msg);
+        final ArgumentCaptor<Throwable> captor = ArgumentCaptor.forClass(Throwable.class);
+        verify(ctx).tellFailure(same(msg), captor.capture());
+
+        Throwable value = captor.getValue();
+        assertEquals("something wrong", value.getMessage());
+        assertTrue(msg.getMetaData().getData().isEmpty());
+    }
+
+    @Test
+    public void failedChainUsedIfCustomerCannotBeFound() {
+        UserId userId = new UserId(UUIDs.timeBased());
+        CustomerId customerId = new CustomerId(UUIDs.timeBased());
+        User user = new User();
+        user.setCustomerId(customerId);
+
+        msg = new TbMsg(UUIDs.timeBased(), "USER", userId, new TbMsgMetaData(), "{}", ruleChainId, ruleNodeId, 0L);
+
+        when(ctx.getUserService()).thenReturn(userService);
+        when(userService.findUserByIdAsync(userId)).thenReturn(Futures.immediateFuture(null));
+
+
+        node.onMsg(ctx, msg);
+        verify(ctx).tellNext(msg, FAILURE);
+        assertTrue(msg.getMetaData().getData().isEmpty());
+    }
+
+    @Test
+    public void customerAttributeAddedInMetadata() {
+        CustomerId customerId = new CustomerId(UUIDs.timeBased());
+        msg = new TbMsg(UUIDs.timeBased(), "CUSTOMER", customerId, new TbMsgMetaData(), "{}", ruleChainId, ruleNodeId, 0L);
+        entityAttributeFetched(customerId);
+    }
+
+    @Test
+    public void usersCustomerAttributesFetched() {
+        UserId userId = new UserId(UUIDs.timeBased());
+        CustomerId customerId = new CustomerId(UUIDs.timeBased());
+        User user = new User();
+        user.setCustomerId(customerId);
+
+        msg = new TbMsg(UUIDs.timeBased(), "USER", userId, new TbMsgMetaData(), "{}", ruleChainId, ruleNodeId, 0L);
+
+        when(ctx.getUserService()).thenReturn(userService);
+        when(userService.findUserByIdAsync(userId)).thenReturn(Futures.immediateFuture(user));
+
+        entityAttributeFetched(customerId);
+    }
+
+    @Test
+    public void assetsCustomerAttributesFetched() {
+        AssetId assetId = new AssetId(UUIDs.timeBased());
+        CustomerId customerId = new CustomerId(UUIDs.timeBased());
+        Asset asset = new Asset();
+        asset.setCustomerId(customerId);
+
+        msg = new TbMsg(UUIDs.timeBased(), "USER", assetId, new TbMsgMetaData(), "{}", ruleChainId, ruleNodeId, 0L);
+
+        when(ctx.getAssetService()).thenReturn(assetService);
+        when(assetService.findAssetByIdAsync(assetId)).thenReturn(Futures.immediateFuture(asset));
+
+        entityAttributeFetched(customerId);
+    }
+
+    @Test
+    public void deviceCustomerAttributesFetched() {
+        DeviceId deviceId = new DeviceId(UUIDs.timeBased());
+        CustomerId customerId = new CustomerId(UUIDs.timeBased());
+        Device device = new Device();
+        device.setCustomerId(customerId);
+
+        msg = new TbMsg(UUIDs.timeBased(), "USER", deviceId, new TbMsgMetaData(), "{}", ruleChainId, ruleNodeId, 0L);
+
+        when(ctx.getDeviceService()).thenReturn(deviceService);
+        when(deviceService.findDeviceByIdAsync(deviceId)).thenReturn(Futures.immediateFuture(device));
+
+        entityAttributeFetched(customerId);
+    }
+
+    @Test
+    public void deviceCustomerTelemetryFetched() throws TbNodeException {
+        TbGetEntityAttrNodeConfiguration config = new TbGetEntityAttrNodeConfiguration();
+        Map<String, String> attrMapping = new HashMap<>();
+        attrMapping.putIfAbsent("temperature", "tempo");
+        config.setAttrMapping(attrMapping);
+        config.setTelemetry(true);
+        ObjectMapper mapper = new ObjectMapper();
+        TbNodeConfiguration nodeConfiguration = new TbNodeConfiguration(mapper.valueToTree(config));
+
+        node = new TbGetCustomerAttributeNode();
+        node.init(null, nodeConfiguration);
+
+
+        DeviceId deviceId = new DeviceId(UUIDs.timeBased());
+        CustomerId customerId = new CustomerId(UUIDs.timeBased());
+        Device device = new Device();
+        device.setCustomerId(customerId);
+
+        msg = new TbMsg(UUIDs.timeBased(), "USER", deviceId, new TbMsgMetaData(), "{}", ruleChainId, ruleNodeId, 0L);
+
+        when(ctx.getDeviceService()).thenReturn(deviceService);
+        when(deviceService.findDeviceByIdAsync(deviceId)).thenReturn(Futures.immediateFuture(device));
+
+        List<TsKvEntry> timeseries = Lists.newArrayList(new BasicTsKvEntry(1L, new StringDataEntry("temperature", "highest")));
+
+        when(ctx.getTimeseriesService()).thenReturn(timeseriesService);
+        when(timeseriesService.findLatest(customerId, Collections.singleton("temperature")))
+                .thenReturn(Futures.immediateFuture(timeseries));
+
+        node.onMsg(ctx, msg);
+        verify(ctx).tellNext(msg, SUCCESS);
+        assertEquals(msg.getMetaData().getValue("tempo"), "highest");
+    }
+
+    private void entityAttributeFetched(CustomerId customerId) {
+        List<AttributeKvEntry> attributes = Lists.newArrayList(new BaseAttributeKvEntry(new StringDataEntry("temperature", "high"), 1L));
+
+        when(ctx.getAttributesService()).thenReturn(attributesService);
+        when(attributesService.find(customerId, SERVER_SCOPE, Collections.singleton("temperature")))
+                .thenReturn(Futures.immediateFuture(attributes));
+
+        node.onMsg(ctx, msg);
+        verify(ctx).tellNext(msg, SUCCESS);
+        assertEquals(msg.getMetaData().getValue("tempo"), "high");
+    }
+}
\ No newline at end of file
diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/transform/TbChangeOriginatorNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/transform/TbChangeOriginatorNodeTest.java
new file mode 100644
index 0000000..ce0afc9
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/transform/TbChangeOriginatorNodeTest.java
@@ -0,0 +1,167 @@
+/**
+ * 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.transform;
+
+import com.datastax.driver.core.utils.UUIDs;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.runners.MockitoJUnitRunner;
+import org.thingsboard.rule.engine.api.ListeningExecutor;
+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.asset.Asset;
+import org.thingsboard.server.common.data.id.AssetId;
+import org.thingsboard.server.common.data.id.CustomerId;
+import org.thingsboard.server.common.data.id.EntityId;
+import org.thingsboard.server.common.data.id.RuleChainId;
+import org.thingsboard.server.common.data.id.RuleNodeId;
+import org.thingsboard.server.common.msg.TbMsg;
+import org.thingsboard.server.common.msg.TbMsgMetaData;
+import org.thingsboard.server.dao.asset.AssetService;
+
+import java.util.concurrent.Callable;
+
+import static org.junit.Assert.assertEquals;
+import static org.mockito.Matchers.same;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+import static org.thingsboard.rule.engine.api.TbRelationTypes.FAILURE;
+
+@RunWith(MockitoJUnitRunner.class)
+public class TbChangeOriginatorNodeTest {
+
+    private TbChangeOriginatorNode node;
+
+    @Mock
+    private TbContext ctx;
+    @Mock
+    private AssetService assetService;
+
+    private ListeningExecutor dbExecutor;
+
+    @Before
+    public void before() {
+        dbExecutor = new ListeningExecutor() {
+            @Override
+            public <T> ListenableFuture<T> executeAsync(Callable<T> task) {
+                try {
+                    return Futures.immediateFuture(task.call());
+                } catch (Exception e) {
+                    throw new RuntimeException(e);
+                }
+            }
+
+            @Override
+            public void execute(Runnable command) {
+                command.run();
+            }
+        };
+    }
+
+    @Test
+    public void originatorCanBeChangedToCustomerId() throws TbNodeException {
+        init();
+        AssetId assetId = new AssetId(UUIDs.timeBased());
+        CustomerId customerId = new CustomerId(UUIDs.timeBased());
+        Asset asset = new Asset();
+        asset.setCustomerId(customerId);
+
+        RuleChainId ruleChainId = new RuleChainId(UUIDs.timeBased());
+        RuleNodeId ruleNodeId = new RuleNodeId(UUIDs.timeBased());
+
+        TbMsg msg = new TbMsg(UUIDs.timeBased(), "ASSET", assetId, new TbMsgMetaData(), "{}", ruleChainId, ruleNodeId, 0L);
+
+        when(ctx.getAssetService()).thenReturn(assetService);
+        when(assetService.findAssetByIdAsync(assetId)).thenReturn(Futures.immediateFuture(asset));
+
+        node.onMsg(ctx, msg);
+
+        ArgumentCaptor<TbMsg> msgCaptor = ArgumentCaptor.forClass(TbMsg.class);
+        ArgumentCaptor<String> typeCaptor = ArgumentCaptor.forClass(String.class);
+        ArgumentCaptor<EntityId> originatorCaptor = ArgumentCaptor.forClass(EntityId.class);
+        ArgumentCaptor<TbMsgMetaData> metadataCaptor = ArgumentCaptor.forClass(TbMsgMetaData.class);
+        ArgumentCaptor<String> dataCaptor = ArgumentCaptor.forClass(String.class);
+        verify(ctx).transformMsg(msgCaptor.capture(), typeCaptor.capture(), originatorCaptor.capture(), metadataCaptor.capture(), dataCaptor.capture());
+
+        assertEquals(customerId, originatorCaptor.getValue());
+    }
+
+    @Test
+    public void newChainCanBeStarted() throws TbNodeException {
+        init();
+        AssetId assetId = new AssetId(UUIDs.timeBased());
+        CustomerId customerId = new CustomerId(UUIDs.timeBased());
+        Asset asset = new Asset();
+        asset.setCustomerId(customerId);
+
+        RuleChainId ruleChainId = new RuleChainId(UUIDs.timeBased());
+        RuleNodeId ruleNodeId = new RuleNodeId(UUIDs.timeBased());
+
+        TbMsg msg = new TbMsg(UUIDs.timeBased(), "ASSET", assetId, new TbMsgMetaData(), "{}", ruleChainId, ruleNodeId, 0L);
+
+        when(ctx.getAssetService()).thenReturn(assetService);
+        when(assetService.findAssetByIdAsync(assetId)).thenReturn(Futures.immediateFuture(asset));
+
+        node.onMsg(ctx, msg);
+        ArgumentCaptor<TbMsg> msgCaptor = ArgumentCaptor.forClass(TbMsg.class);
+        ArgumentCaptor<String> typeCaptor = ArgumentCaptor.forClass(String.class);
+        ArgumentCaptor<EntityId> originatorCaptor = ArgumentCaptor.forClass(EntityId.class);
+        ArgumentCaptor<TbMsgMetaData> metadataCaptor = ArgumentCaptor.forClass(TbMsgMetaData.class);
+        ArgumentCaptor<String> dataCaptor = ArgumentCaptor.forClass(String.class);
+        verify(ctx).transformMsg(msgCaptor.capture(), typeCaptor.capture(), originatorCaptor.capture(), metadataCaptor.capture(), dataCaptor.capture());
+
+        assertEquals(customerId, originatorCaptor.getValue());
+    }
+
+    @Test
+    public void exceptionThrownIfCannotFindNewOriginator() throws TbNodeException {
+        init();
+        AssetId assetId = new AssetId(UUIDs.timeBased());
+        CustomerId customerId = new CustomerId(UUIDs.timeBased());
+        Asset asset = new Asset();
+        asset.setCustomerId(customerId);
+
+        RuleChainId ruleChainId = new RuleChainId(UUIDs.timeBased());
+        RuleNodeId ruleNodeId = new RuleNodeId(UUIDs.timeBased());
+
+        TbMsg msg = new TbMsg(UUIDs.timeBased(), "ASSET", assetId, new TbMsgMetaData(), "{}", ruleChainId, ruleNodeId, 0L);
+
+        when(ctx.getAssetService()).thenReturn(assetService);
+        when(assetService.findAssetByIdAsync(assetId)).thenReturn(Futures.immediateFuture(null));
+
+        node.onMsg(ctx, msg);
+        verify(ctx).tellNext(same(msg), same(FAILURE));
+    }
+
+    public void init() throws TbNodeException {
+        TbChangeOriginatorNodeConfiguration config = new TbChangeOriginatorNodeConfiguration();
+        config.setOriginatorSource(TbChangeOriginatorNode.CUSTOMER_SOURCE);
+        ObjectMapper mapper = new ObjectMapper();
+        TbNodeConfiguration nodeConfiguration = new TbNodeConfiguration(mapper.valueToTree(config));
+
+        when(ctx.getDbCallbackExecutor()).thenReturn(dbExecutor);
+
+        node = new TbChangeOriginatorNode();
+        node.init(null, nodeConfiguration);
+    }
+}
\ No newline at end of file
diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/transform/TbTransformMsgNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/transform/TbTransformMsgNodeTest.java
new file mode 100644
index 0000000..1daeb19
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/transform/TbTransformMsgNodeTest.java
@@ -0,0 +1,126 @@
+/**
+ * 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.transform;
+
+import com.datastax.driver.core.utils.UUIDs;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Matchers;
+import org.mockito.Mock;
+import org.mockito.runners.MockitoJUnitRunner;
+import org.mockito.stubbing.Answer;
+import org.thingsboard.rule.engine.api.*;
+import org.thingsboard.server.common.data.id.RuleChainId;
+import org.thingsboard.server.common.data.id.RuleNodeId;
+import org.thingsboard.server.common.msg.TbMsg;
+import org.thingsboard.server.common.msg.TbMsgMetaData;
+
+import javax.script.ScriptException;
+import java.util.concurrent.Callable;
+
+import static org.junit.Assert.assertEquals;
+import static org.mockito.Matchers.same;
+import static org.mockito.Mockito.*;
+import static org.thingsboard.rule.engine.api.TbRelationTypes.SUCCESS;
+
+@RunWith(MockitoJUnitRunner.class)
+public class TbTransformMsgNodeTest {
+
+    private TbTransformMsgNode node;
+
+    @Mock
+    private TbContext ctx;
+    @Mock
+    private ListeningExecutor executor;
+    @Mock
+    private ScriptEngine scriptEngine;
+
+    @Test
+    public void metadataCanBeUpdated() throws TbNodeException, ScriptException {
+        initWithScript();
+        TbMsgMetaData metaData = new TbMsgMetaData();
+        metaData.putValue("temp", "7");
+        String rawJson = "{\"passed\": 5}";
+
+        RuleChainId ruleChainId = new RuleChainId(UUIDs.timeBased());
+        RuleNodeId ruleNodeId = new RuleNodeId(UUIDs.timeBased());
+        TbMsg msg = new TbMsg(UUIDs.timeBased(), "USER", null, metaData, rawJson, ruleChainId, ruleNodeId, 0L);
+        TbMsg transformedMsg = new TbMsg(UUIDs.timeBased(), "USER", null, metaData, "{new}", ruleChainId, ruleNodeId, 0L);
+        mockJsExecutor();
+        when(scriptEngine.executeUpdate(msg)).thenReturn(transformedMsg);
+
+        node.onMsg(ctx, msg);
+        verify(ctx).getJsExecutor();
+        ArgumentCaptor<TbMsg> captor = ArgumentCaptor.forClass(TbMsg.class);
+        verify(ctx).tellNext(captor.capture(), eq(SUCCESS));
+        TbMsg actualMsg = captor.getValue();
+        assertEquals(transformedMsg, actualMsg);
+    }
+
+    @Test
+    public void exceptionHandledCorrectly() throws TbNodeException, ScriptException {
+        initWithScript();
+        TbMsgMetaData metaData = new TbMsgMetaData();
+        metaData.putValue("temp", "7");
+        String rawJson = "{\"passed\": 5";
+
+        RuleChainId ruleChainId = new RuleChainId(UUIDs.timeBased());
+        RuleNodeId ruleNodeId = new RuleNodeId(UUIDs.timeBased());
+        TbMsg msg = new TbMsg(UUIDs.timeBased(), "USER", null, metaData, rawJson, ruleChainId, ruleNodeId, 0L);
+        mockJsExecutor();
+        when(scriptEngine.executeUpdate(msg)).thenThrow(new IllegalStateException("error"));
+
+        node.onMsg(ctx, msg);
+        verifyError(msg, "error", IllegalStateException.class);
+    }
+
+    private void initWithScript() throws TbNodeException {
+        TbTransformMsgNodeConfiguration config = new TbTransformMsgNodeConfiguration();
+        config.setJsScript("scr");
+        ObjectMapper mapper = new ObjectMapper();
+        TbNodeConfiguration nodeConfiguration = new TbNodeConfiguration(mapper.valueToTree(config));
+
+        when(ctx.createJsScriptEngine("scr")).thenReturn(scriptEngine);
+
+        node = new TbTransformMsgNode();
+        node.init(ctx, nodeConfiguration);
+    }
+
+    private void mockJsExecutor() {
+        when(ctx.getJsExecutor()).thenReturn(executor);
+        doAnswer((Answer<ListenableFuture<TbMsg>>) invocationOnMock -> {
+            try {
+                Callable task = (Callable) (invocationOnMock.getArguments())[0];
+                return Futures.immediateFuture((TbMsg) task.call());
+            } catch (Throwable th) {
+                return Futures.immediateFailedFuture(th);
+            }
+        }).when(executor).executeAsync(Matchers.any(Callable.class));
+    }
+
+    private void verifyError(TbMsg msg, String message, Class expectedClass) {
+        ArgumentCaptor<Throwable> captor = ArgumentCaptor.forClass(Throwable.class);
+        verify(ctx).tellFailure(same(msg), captor.capture());
+
+        Throwable value = captor.getValue();
+        assertEquals(expectedClass, value.getClass());
+        assertEquals(message, value.getMessage());
+    }
+}
\ No newline at end of file

tools/pom.xml 2(+1 -1)

diff --git a/tools/pom.xml b/tools/pom.xml
index 112d8f1..c936287 100644
--- a/tools/pom.xml
+++ b/tools/pom.xml
@@ -20,7 +20,7 @@
     <modelVersion>4.0.0</modelVersion>
     <parent>
         <groupId>org.thingsboard</groupId>
-        <version>1.4.1-SNAPSHOT</version>
+        <version>2.0.0-SNAPSHOT</version>
         <artifactId>thingsboard</artifactId>
     </parent>
     <groupId>org.thingsboard</groupId>
diff --git a/transport/coap/pom.xml b/transport/coap/pom.xml
index 52f0357..18e8377 100644
--- a/transport/coap/pom.xml
+++ b/transport/coap/pom.xml
@@ -20,7 +20,7 @@
     <modelVersion>4.0.0</modelVersion>
     <parent>
         <groupId>org.thingsboard</groupId>
-        <version>1.4.1-SNAPSHOT</version>
+        <version>2.0.0-SNAPSHOT</version>
         <artifactId>transport</artifactId>
     </parent>
     <groupId>org.thingsboard.transport</groupId>
diff --git a/transport/coap/src/main/java/org/thingsboard/server/transport/coap/adaptors/JsonCoapAdaptor.java b/transport/coap/src/main/java/org/thingsboard/server/transport/coap/adaptors/JsonCoapAdaptor.java
index e3ef2cc..1c96311 100644
--- a/transport/coap/src/main/java/org/thingsboard/server/transport/coap/adaptors/JsonCoapAdaptor.java
+++ b/transport/coap/src/main/java/org/thingsboard/server/transport/coap/adaptors/JsonCoapAdaptor.java
@@ -28,7 +28,7 @@ import org.thingsboard.server.common.msg.kv.AttributesKVMsg;
 import org.thingsboard.server.common.msg.session.AdaptorToSessionActorMsg;
 import org.thingsboard.server.common.msg.session.BasicAdaptorToSessionActorMsg;
 import org.thingsboard.server.common.msg.session.FromDeviceMsg;
-import org.thingsboard.server.common.msg.session.MsgType;
+import org.thingsboard.server.common.msg.session.SessionMsgType;
 import org.thingsboard.server.common.msg.session.SessionActorToAdaptorMsg;
 import org.thingsboard.server.common.msg.session.SessionContext;
 import org.thingsboard.server.common.msg.session.ToDeviceMsg;
@@ -48,7 +48,7 @@ import org.thingsboard.server.transport.coap.session.CoapSessionCtx;
 public class JsonCoapAdaptor implements CoapTransportAdaptor {
 
     @Override
-    public AdaptorToSessionActorMsg convertToActorMsg(CoapSessionCtx ctx, MsgType type, Request inbound) throws AdaptorException {
+    public AdaptorToSessionActorMsg convertToActorMsg(CoapSessionCtx ctx, SessionMsgType type, Request inbound) throws AdaptorException {
         FromDeviceMsg msg = null;
         switch (type) {
             case POST_TELEMETRY_REQUEST:
@@ -104,7 +104,7 @@ public class JsonCoapAdaptor implements CoapTransportAdaptor {
     @Override
     public Optional<Response> convertToAdaptorMsg(CoapSessionCtx ctx, SessionActorToAdaptorMsg source) throws AdaptorException {
         ToDeviceMsg msg = source.getMsg();
-        switch (msg.getMsgType()) {
+        switch (msg.getSessionMsgType()) {
             case STATUS_CODE_RESPONSE:
             case TO_DEVICE_RPC_RESPONSE_ACK:
                 return Optional.of(convertStatusCodeResponse((StatusCodeResponse) msg));
@@ -119,19 +119,19 @@ public class JsonCoapAdaptor implements CoapTransportAdaptor {
             case RULE_ENGINE_ERROR:
                 return Optional.of(convertToRuleEngineErrorResponse(ctx, (RuleEngineErrorMsg) msg));
             default:
-                log.warn("[{}] Unsupported msg type: {}!", source.getSessionId(), msg.getMsgType());
-                throw new AdaptorException(new IllegalArgumentException("Unsupported msg type: " + msg.getMsgType() + "!"));
+                log.warn("[{}] Unsupported msg type: {}!", source.getSessionId(), msg.getSessionMsgType());
+                throw new AdaptorException(new IllegalArgumentException("Unsupported msg type: " + msg.getSessionMsgType() + "!"));
         }
     }
 
     private Response convertToRuleEngineErrorResponse(CoapSessionCtx ctx, RuleEngineErrorMsg msg) {
         ResponseCode status = ResponseCode.INTERNAL_SERVER_ERROR;
         switch (msg.getError()) {
-            case PLUGIN_TIMEOUT:
+            case QUEUE_PUT_TIMEOUT:
                 status = ResponseCode.GATEWAY_TIMEOUT;
                 break;
             default:
-                if (msg.getInMsgType() == MsgType.TO_SERVER_RPC_REQUEST) {
+                if (msg.getInSessionMsgType() == SessionMsgType.TO_SERVER_RPC_REQUEST) {
                     status = ResponseCode.BAD_REQUEST;
                 }
                 break;
@@ -156,7 +156,7 @@ public class JsonCoapAdaptor implements CoapTransportAdaptor {
         return response;
     }
 
-    private UpdateAttributesRequest convertToUpdateAttributesRequest(SessionContext ctx, Request inbound) throws AdaptorException {
+    private AttributesUpdateRequest convertToUpdateAttributesRequest(SessionContext ctx, Request inbound) throws AdaptorException {
         String payload = validatePayload(ctx, inbound);
         try {
             return JsonConverter.convertToAttributes(new JsonParser().parse(payload));
diff --git a/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportResource.java b/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportResource.java
index 7ba5e36..bac68ff 100644
--- a/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportResource.java
+++ b/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportResource.java
@@ -38,8 +38,6 @@ import org.thingsboard.server.common.transport.quota.QuotaService;
 import org.thingsboard.server.transport.coap.adaptors.CoapTransportAdaptor;
 import org.thingsboard.server.transport.coap.session.CoapExchangeObserverProxy;
 import org.thingsboard.server.transport.coap.session.CoapSessionCtx;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 import org.springframework.util.ReflectionUtils;
 
 @Slf4j
@@ -90,7 +88,7 @@ public class CoapTransportResource extends CoapResource {
         } else if (exchange.getRequestOptions().hasObserve()) {
             processExchangeGetRequest(exchange, featureType.get());
         } else if (featureType.get() == FeatureType.ATTRIBUTES) {
-            processRequest(exchange, MsgType.GET_ATTRIBUTES_REQUEST);
+            processRequest(exchange, SessionMsgType.GET_ATTRIBUTES_REQUEST);
         } else {
             log.trace("Invalid feature type parameter");
             exchange.respond(ResponseCode.BAD_REQUEST);
@@ -99,13 +97,13 @@ public class CoapTransportResource extends CoapResource {
 
     private void processExchangeGetRequest(CoapExchange exchange, FeatureType featureType) {
         boolean unsubscribe = exchange.getRequestOptions().getObserve() == 1;
-        MsgType msgType;
+        SessionMsgType sessionMsgType;
         if (featureType == FeatureType.RPC) {
-            msgType = unsubscribe ? MsgType.UNSUBSCRIBE_RPC_COMMANDS_REQUEST : MsgType.SUBSCRIBE_RPC_COMMANDS_REQUEST;
+            sessionMsgType = unsubscribe ? SessionMsgType.UNSUBSCRIBE_RPC_COMMANDS_REQUEST : SessionMsgType.SUBSCRIBE_RPC_COMMANDS_REQUEST;
         } else {
-            msgType = unsubscribe ? MsgType.UNSUBSCRIBE_ATTRIBUTES_REQUEST : MsgType.SUBSCRIBE_ATTRIBUTES_REQUEST;
+            sessionMsgType = unsubscribe ? SessionMsgType.UNSUBSCRIBE_ATTRIBUTES_REQUEST : SessionMsgType.SUBSCRIBE_ATTRIBUTES_REQUEST;
         }
-        Optional<SessionId> sessionId = processRequest(exchange, msgType);
+        Optional<SessionId> sessionId = processRequest(exchange, sessionMsgType);
         if (sessionId.isPresent()) {
             if (exchange.getRequestOptions().getObserve() == 1) {
                 exchange.respond(ResponseCode.VALID);
@@ -122,24 +120,24 @@ public class CoapTransportResource extends CoapResource {
         } else {
             switch (featureType.get()) {
                 case ATTRIBUTES:
-                    processRequest(exchange, MsgType.POST_ATTRIBUTES_REQUEST);
+                    processRequest(exchange, SessionMsgType.POST_ATTRIBUTES_REQUEST);
                     break;
                 case TELEMETRY:
-                    processRequest(exchange, MsgType.POST_TELEMETRY_REQUEST);
+                    processRequest(exchange, SessionMsgType.POST_TELEMETRY_REQUEST);
                     break;
                 case RPC:
                     Optional<Integer> requestId = getRequestId(exchange.advanced().getRequest());
                     if (requestId.isPresent()) {
-                        processRequest(exchange, MsgType.TO_DEVICE_RPC_RESPONSE);
+                        processRequest(exchange, SessionMsgType.TO_DEVICE_RPC_RESPONSE);
                     } else {
-                        processRequest(exchange, MsgType.TO_SERVER_RPC_REQUEST);
+                        processRequest(exchange, SessionMsgType.TO_SERVER_RPC_REQUEST);
                     }
                     break;
             }
         }
     }
 
-    private Optional<SessionId> processRequest(CoapExchange exchange, MsgType type) {
+    private Optional<SessionId> processRequest(CoapExchange exchange, SessionMsgType type) {
         log.trace("Processing {}", exchange.advanced().getRequest());
         exchange.accept();
         Exchange advanced = exchange.advanced();
@@ -186,7 +184,7 @@ public class CoapTransportResource extends CoapResource {
                     throw new IllegalArgumentException("Unsupported msg type: " + type);
             }
             log.trace("Processing msg: {}", msg);
-            processor.process(new BasicToDeviceActorSessionMsg(ctx.getDevice(), msg));
+            processor.process(new BasicTransportToDeviceSessionActorMsg(ctx.getDevice(), msg));
         } catch (AdaptorException e) {
             log.debug("Failed to decode payload {}", e);
             exchange.respond(ResponseCode.BAD_REQUEST, e.getMessage());
diff --git a/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportService.java b/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportService.java
index 15706d4..6c8437c 100644
--- a/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportService.java
+++ b/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportService.java
@@ -27,6 +27,7 @@ import org.springframework.stereotype.Service;
 import org.thingsboard.server.common.transport.SessionMsgProcessor;
 import org.thingsboard.server.common.transport.auth.DeviceAuthService;
 import org.thingsboard.server.common.transport.quota.QuotaService;
+import org.thingsboard.server.common.transport.quota.host.HostRequestsQuotaService;
 import org.thingsboard.server.transport.coap.adaptors.CoapTransportAdaptor;
 
 import javax.annotation.PostConstruct;
@@ -55,7 +56,7 @@ public class CoapTransportService {
     private DeviceAuthService authService;
 
     @Autowired(required = false)
-    private QuotaService quotaService;
+    private HostRequestsQuotaService quotaService;
 
 
     @Value("${coap.bind_address}")
diff --git a/transport/coap/src/test/java/org/thingsboard/server/transport/coap/CoapServerTest.java b/transport/coap/src/test/java/org/thingsboard/server/transport/coap/CoapServerTest.java
index 072c735..4815056 100644
--- a/transport/coap/src/test/java/org/thingsboard/server/transport/coap/CoapServerTest.java
+++ b/transport/coap/src/test/java/org/thingsboard/server/transport/coap/CoapServerTest.java
@@ -50,7 +50,7 @@ import org.thingsboard.server.common.msg.session.*;
 import org.thingsboard.server.common.transport.SessionMsgProcessor;
 import org.thingsboard.server.common.transport.auth.DeviceAuthResult;
 import org.thingsboard.server.common.transport.auth.DeviceAuthService;
-import org.thingsboard.server.common.transport.quota.QuotaService;
+import org.thingsboard.server.common.transport.quota.host.HostRequestsQuotaService;
 
 import java.util.ArrayList;
 import java.util.List;
@@ -108,14 +108,14 @@ public class CoapServerTest {
 
                 @Override
                 public void process(SessionAwareMsg toActorMsg) {
-                    if (toActorMsg instanceof ToDeviceActorSessionMsg) {
-                        AdaptorToSessionActorMsg sessionMsg = ((ToDeviceActorSessionMsg) toActorMsg).getSessionMsg();
+                    if (toActorMsg instanceof TransportToDeviceSessionActorMsg) {
+                        AdaptorToSessionActorMsg sessionMsg = ((TransportToDeviceSessionActorMsg) toActorMsg).getSessionMsg();
                         try {
                             FromDeviceMsg deviceMsg = sessionMsg.getMsg();
                             ToDeviceMsg toDeviceMsg = null;
-                            if (deviceMsg.getMsgType() == MsgType.POST_TELEMETRY_REQUEST) {
+                            if (deviceMsg.getMsgType() == SessionMsgType.POST_TELEMETRY_REQUEST) {
                                 toDeviceMsg = BasicStatusCodeResponse.onSuccess(deviceMsg.getMsgType(), BasicRequest.DEFAULT_REQUEST_ID);
-                            } else if (deviceMsg.getMsgType() == MsgType.GET_ATTRIBUTES_REQUEST) {
+                            } else if (deviceMsg.getMsgType() == SessionMsgType.GET_ATTRIBUTES_REQUEST) {
                                 List<AttributeKvEntry> data = new ArrayList<>();
                                 data.add(new BaseAttributeKvEntry(new StringDataEntry("key1", "value1"), System.currentTimeMillis()));
                                 data.add(new BaseAttributeKvEntry(new LongDataEntry("key2", 42L), System.currentTimeMillis()));
@@ -134,8 +134,8 @@ public class CoapServerTest {
         }
 
         @Bean
-        public static QuotaService quotaService() {
-            return key -> false;
+        public static HostRequestsQuotaService quotaService() {
+            return new HostRequestsQuotaService(null, null, null, null, false);
         }
     }
 
diff --git a/transport/http/pom.xml b/transport/http/pom.xml
index 63bbbe0..0d36ba5 100644
--- a/transport/http/pom.xml
+++ b/transport/http/pom.xml
@@ -20,7 +20,7 @@
     <modelVersion>4.0.0</modelVersion>
     <parent>
         <groupId>org.thingsboard</groupId>
-        <version>1.4.1-SNAPSHOT</version>
+        <version>2.0.0-SNAPSHOT</version>
         <artifactId>transport</artifactId>
     </parent>
     <groupId>org.thingsboard.transport</groupId>
diff --git a/transport/http/src/main/java/org/thingsboard/server/transport/http/DeviceApiController.java b/transport/http/src/main/java/org/thingsboard/server/transport/http/DeviceApiController.java
index 4d90b5f..d26d076 100644
--- a/transport/http/src/main/java/org/thingsboard/server/transport/http/DeviceApiController.java
+++ b/transport/http/src/main/java/org/thingsboard/server/transport/http/DeviceApiController.java
@@ -30,12 +30,13 @@ import org.thingsboard.server.common.data.security.DeviceTokenCredentials;
 import org.thingsboard.server.common.msg.core.*;
 import org.thingsboard.server.common.msg.session.AdaptorToSessionActorMsg;
 import org.thingsboard.server.common.msg.session.BasicAdaptorToSessionActorMsg;
-import org.thingsboard.server.common.msg.session.BasicToDeviceActorSessionMsg;
+import org.thingsboard.server.common.msg.session.BasicTransportToDeviceSessionActorMsg;
 import org.thingsboard.server.common.msg.session.FromDeviceMsg;
 import org.thingsboard.server.common.transport.SessionMsgProcessor;
 import org.thingsboard.server.common.transport.adaptor.JsonConverter;
 import org.thingsboard.server.common.transport.auth.DeviceAuthService;
 import org.thingsboard.server.common.transport.quota.QuotaService;
+import org.thingsboard.server.common.transport.quota.host.HostRequestsQuotaService;
 import org.thingsboard.server.transport.http.session.HttpSessionCtx;
 
 import javax.servlet.http.HttpServletRequest;
@@ -61,7 +62,7 @@ public class DeviceApiController {
     private DeviceAuthService authService;
 
     @Autowired(required = false)
-    private QuotaService quotaService;
+    private HostRequestsQuotaService quotaService;
 
     @RequestMapping(value = "/{deviceToken}/attributes", method = RequestMethod.GET, produces = "application/json")
     public DeferredResult<ResponseEntity> getDeviceAttributes(@PathVariable("deviceToken") String deviceToken,
@@ -219,7 +220,7 @@ public class DeviceApiController {
 
     private void process(HttpSessionCtx ctx, FromDeviceMsg request) {
         AdaptorToSessionActorMsg msg = new BasicAdaptorToSessionActorMsg(ctx, request);
-        processor.process(new BasicToDeviceActorSessionMsg(ctx.getDevice(), msg));
+        processor.process(new BasicTransportToDeviceSessionActorMsg(ctx.getDevice(), msg));
     }
 
     private boolean quotaExceeded(HttpServletRequest request, DeferredResult<ResponseEntity> responseWriter) {
diff --git a/transport/http/src/main/java/org/thingsboard/server/transport/http/session/HttpSessionCtx.java b/transport/http/src/main/java/org/thingsboard/server/transport/http/session/HttpSessionCtx.java
index 743b3e7..4732785 100644
--- a/transport/http/src/main/java/org/thingsboard/server/transport/http/session/HttpSessionCtx.java
+++ b/transport/http/src/main/java/org/thingsboard/server/transport/http/session/HttpSessionCtx.java
@@ -57,7 +57,7 @@ public class HttpSessionCtx extends DeviceAwareSessionContext {
     @Override
     public void onMsg(SessionActorToAdaptorMsg source) throws SessionException {
         ToDeviceMsg msg = source.getMsg();
-        switch (msg.getMsgType()) {
+        switch (msg.getSessionMsgType()) {
             case GET_ATTRIBUTES_RESPONSE:
                 reply((GetAttributesResponse) msg);
                 return;
@@ -84,11 +84,11 @@ public class HttpSessionCtx extends DeviceAwareSessionContext {
     private void reply(RuleEngineErrorMsg msg) {
         HttpStatus status = HttpStatus.INTERNAL_SERVER_ERROR;
         switch (msg.getError()) {
-            case PLUGIN_TIMEOUT:
+            case QUEUE_PUT_TIMEOUT:
                 status = HttpStatus.REQUEST_TIMEOUT;
                 break;
             default:
-                if (msg.getInMsgType() == MsgType.TO_SERVER_RPC_REQUEST) {
+                if (msg.getInSessionMsgType() == SessionMsgType.TO_SERVER_RPC_REQUEST) {
                     status = HttpStatus.BAD_REQUEST;
                 }
                 break;
diff --git a/transport/mqtt/pom.xml b/transport/mqtt/pom.xml
index a048218..239721d 100644
--- a/transport/mqtt/pom.xml
+++ b/transport/mqtt/pom.xml
@@ -20,7 +20,7 @@
     <modelVersion>4.0.0</modelVersion>
     <parent>
         <groupId>org.thingsboard</groupId>
-        <version>1.4.1-SNAPSHOT</version>
+        <version>2.0.0-SNAPSHOT</version>
         <artifactId>transport</artifactId>
     </parent>
     <groupId>org.thingsboard.transport</groupId>
diff --git a/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/adaptors/JsonMqttAdaptor.java b/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/adaptors/JsonMqttAdaptor.java
index 64df6bc..f0b29cb 100644
--- a/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/adaptors/JsonMqttAdaptor.java
+++ b/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/adaptors/JsonMqttAdaptor.java
@@ -53,7 +53,7 @@ public class JsonMqttAdaptor implements MqttTransportAdaptor {
     private static final ByteBufAllocator ALLOCATOR = new UnpooledByteBufAllocator(false);
 
     @Override
-    public AdaptorToSessionActorMsg convertToActorMsg(DeviceSessionCtx ctx, MsgType type, MqttMessage inbound) throws AdaptorException {
+    public AdaptorToSessionActorMsg convertToActorMsg(DeviceSessionCtx ctx, SessionMsgType type, MqttMessage inbound) throws AdaptorException {
         FromDeviceMsg msg;
         switch (type) {
             case POST_TELEMETRY_REQUEST:
@@ -94,7 +94,7 @@ public class JsonMqttAdaptor implements MqttTransportAdaptor {
     public Optional<MqttMessage> convertToAdaptorMsg(DeviceSessionCtx ctx, SessionActorToAdaptorMsg sessionMsg) throws AdaptorException {
         MqttMessage result = null;
         ToDeviceMsg msg = sessionMsg.getMsg();
-        switch (msg.getMsgType()) {
+        switch (msg.getSessionMsgType()) {
             case STATUS_CODE_RESPONSE:
             case GET_ATTRIBUTES_RESPONSE:
                 ResponseMsg<?> responseMsg = (ResponseMsg) msg;
@@ -134,12 +134,12 @@ public class JsonMqttAdaptor implements MqttTransportAdaptor {
     private MqttMessage convertResponseMsg(DeviceSessionCtx ctx, ToDeviceMsg msg,
                                            ResponseMsg<?> responseMsg, Optional<Exception> responseError) throws AdaptorException {
         MqttMessage result = null;
-        MsgType requestMsgType = responseMsg.getRequestMsgType();
+        SessionMsgType requestMsgType = responseMsg.getRequestMsgType();
         Integer requestId = responseMsg.getRequestId();
         if (requestId >= 0) {
-            if (requestMsgType == MsgType.POST_ATTRIBUTES_REQUEST || requestMsgType == MsgType.POST_TELEMETRY_REQUEST) {
+            if (requestMsgType == SessionMsgType.POST_ATTRIBUTES_REQUEST || requestMsgType == SessionMsgType.POST_TELEMETRY_REQUEST) {
                 result = MqttTransportHandler.createMqttPubAckMsg(requestId);
-            } else if (requestMsgType == MsgType.GET_ATTRIBUTES_REQUEST) {
+            } else if (requestMsgType == SessionMsgType.GET_ATTRIBUTES_REQUEST) {
                 GetAttributesResponse response = (GetAttributesResponse) msg;
                 Optional<AttributesKVMsg> responseData = response.getData();
                 if (response.isSuccess() && responseData.isPresent()) {
@@ -219,7 +219,7 @@ public class JsonMqttAdaptor implements MqttTransportAdaptor {
         }
     }
 
-    private UpdateAttributesRequest convertToUpdateAttributesRequest(SessionContext ctx, MqttPublishMessage inbound) throws AdaptorException {
+    private AttributesUpdateRequest convertToUpdateAttributesRequest(SessionContext ctx, MqttPublishMessage inbound) throws AdaptorException {
         String payload = validatePayload(ctx.getSessionId(), inbound.payload());
         try {
             return JsonConverter.convertToAttributes(new JsonParser().parse(payload), inbound.variableHeader().messageId());
diff --git a/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java b/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java
index 8766599..185b7a8 100644
--- a/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java
+++ b/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java
@@ -29,8 +29,10 @@ import org.springframework.util.StringUtils;
 import org.thingsboard.server.common.data.Device;
 import org.thingsboard.server.common.data.security.DeviceTokenCredentials;
 import org.thingsboard.server.common.data.security.DeviceX509Credentials;
+import org.thingsboard.server.common.msg.core.SessionOpenMsg;
 import org.thingsboard.server.common.msg.session.AdaptorToSessionActorMsg;
-import org.thingsboard.server.common.msg.session.BasicToDeviceActorSessionMsg;
+import org.thingsboard.server.common.msg.session.BasicAdaptorToSessionActorMsg;
+import org.thingsboard.server.common.msg.session.BasicTransportToDeviceSessionActorMsg;
 import org.thingsboard.server.common.msg.session.ctrl.SessionCloseMsg;
 import org.thingsboard.server.common.transport.SessionMsgProcessor;
 import org.thingsboard.server.common.transport.adaptor.AdaptorException;
@@ -53,7 +55,7 @@ import java.util.List;
 import static io.netty.handler.codec.mqtt.MqttConnectReturnCode.*;
 import static io.netty.handler.codec.mqtt.MqttMessageType.*;
 import static io.netty.handler.codec.mqtt.MqttQoS.*;
-import static org.thingsboard.server.common.msg.session.MsgType.*;
+import static org.thingsboard.server.common.msg.session.SessionMsgType.*;
 import static org.thingsboard.server.transport.mqtt.MqttTopics.*;
 
 /**
@@ -95,6 +97,8 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement
         log.trace("[{}] Processing msg: {}", sessionId, msg);
         if (msg instanceof MqttMessage) {
             processMqttMsg(ctx, (MqttMessage) msg);
+        } else {
+            ctx.close();
         }
     }
 
@@ -207,7 +211,7 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement
             log.warn("[{}] Failed to process publish msg [{}][{}]", sessionId, topicName, msgId, e);
         }
         if (msg != null) {
-            processor.process(new BasicToDeviceActorSessionMsg(deviceSessionCtx.getDevice(), msg));
+            processor.process(new BasicTransportToDeviceSessionActorMsg(deviceSessionCtx.getDevice(), msg));
         } else {
             log.info("[{}] Closing current session due to invalid publish msg [{}][{}]", sessionId, topicName, msgId);
             ctx.close();
@@ -227,11 +231,11 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement
             try {
                 if (topicName.equals(DEVICE_ATTRIBUTES_TOPIC)) {
                     AdaptorToSessionActorMsg msg = adaptor.convertToActorMsg(deviceSessionCtx, SUBSCRIBE_ATTRIBUTES_REQUEST, mqttMsg);
-                    processor.process(new BasicToDeviceActorSessionMsg(deviceSessionCtx.getDevice(), msg));
+                    processor.process(new BasicTransportToDeviceSessionActorMsg(deviceSessionCtx.getDevice(), msg));
                     grantedQoSList.add(getMinSupportedQos(reqQoS));
                 } else if (topicName.equals(DEVICE_RPC_REQUESTS_SUB_TOPIC)) {
                     AdaptorToSessionActorMsg msg = adaptor.convertToActorMsg(deviceSessionCtx, SUBSCRIBE_RPC_COMMANDS_REQUEST, mqttMsg);
-                    processor.process(new BasicToDeviceActorSessionMsg(deviceSessionCtx.getDevice(), msg));
+                    processor.process(new BasicTransportToDeviceSessionActorMsg(deviceSessionCtx.getDevice(), msg));
                     grantedQoSList.add(getMinSupportedQos(reqQoS));
                 } else if (topicName.equals(DEVICE_RPC_RESPONSE_SUB_TOPIC)) {
                     grantedQoSList.add(getMinSupportedQos(reqQoS));
@@ -261,10 +265,10 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement
             try {
                 if (topicName.equals(DEVICE_ATTRIBUTES_TOPIC)) {
                     AdaptorToSessionActorMsg msg = adaptor.convertToActorMsg(deviceSessionCtx, UNSUBSCRIBE_ATTRIBUTES_REQUEST, mqttMsg);
-                    processor.process(new BasicToDeviceActorSessionMsg(deviceSessionCtx.getDevice(), msg));
+                    processor.process(new BasicTransportToDeviceSessionActorMsg(deviceSessionCtx.getDevice(), msg));
                 } else if (topicName.equals(DEVICE_RPC_REQUESTS_SUB_TOPIC)) {
                     AdaptorToSessionActorMsg msg = adaptor.convertToActorMsg(deviceSessionCtx, UNSUBSCRIBE_RPC_COMMANDS_REQUEST, mqttMsg);
-                    processor.process(new BasicToDeviceActorSessionMsg(deviceSessionCtx.getDevice(), msg));
+                    processor.process(new BasicTransportToDeviceSessionActorMsg(deviceSessionCtx.getDevice(), msg));
                 } else if (topicName.equals(DEVICE_ATTRIBUTES_RESPONSES_TOPIC)) {
                     deviceSessionCtx.setDisallowAttributeResponses();
                 }
@@ -303,6 +307,8 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement
         } else {
             ctx.writeAndFlush(createMqttConnAckMsg(CONNECTION_ACCEPTED));
             connected = true;
+            processor.process(new BasicTransportToDeviceSessionActorMsg(deviceSessionCtx.getDevice(),
+                    new BasicAdaptorToSessionActorMsg(deviceSessionCtx, new SessionOpenMsg())));
             checkGatewaySession();
         }
     }
@@ -314,6 +320,8 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement
             if (deviceSessionCtx.login(new DeviceX509Credentials(sha3Hash))) {
                 ctx.writeAndFlush(createMqttConnAckMsg(CONNECTION_ACCEPTED));
                 connected = true;
+                processor.process(new BasicTransportToDeviceSessionActorMsg(deviceSessionCtx.getDevice(),
+                        new BasicAdaptorToSessionActorMsg(deviceSessionCtx, new SessionOpenMsg())));
                 checkGatewaySession();
             } else {
                 ctx.writeAndFlush(createMqttConnAckMsg(CONNECTION_REFUSED_NOT_AUTHORIZED));
diff --git a/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportService.java b/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportService.java
index cbe3ba6..bb8d4ad 100644
--- a/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportService.java
+++ b/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportService.java
@@ -29,7 +29,7 @@ import org.springframework.context.ApplicationContext;
 import org.springframework.stereotype.Service;
 import org.thingsboard.server.common.transport.SessionMsgProcessor;
 import org.thingsboard.server.common.transport.auth.DeviceAuthService;
-import org.thingsboard.server.common.transport.quota.QuotaService;
+import org.thingsboard.server.common.transport.quota.host.HostRequestsQuotaService;
 import org.thingsboard.server.dao.device.DeviceService;
 import org.thingsboard.server.dao.relation.RelationService;
 import org.thingsboard.server.transport.mqtt.adaptors.MqttTransportAdaptor;
@@ -67,7 +67,7 @@ public class MqttTransportService {
     private MqttSslHandlerProvider sslHandlerProvider;
 
     @Autowired(required = false)
-    private QuotaService quotaService;
+    private HostRequestsQuotaService quotaService;
 
     @Value("${mqtt.bind_address}")
     private String host;
diff --git a/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/GatewayDeviceSessionCtx.java b/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/GatewayDeviceSessionCtx.java
index 632ab28..6377fad 100644
--- a/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/GatewayDeviceSessionCtx.java
+++ b/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/GatewayDeviceSessionCtx.java
@@ -80,13 +80,13 @@ public class GatewayDeviceSessionCtx extends DeviceAwareSessionContext {
 
     private Optional<MqttMessage> getToDeviceMsg(SessionActorToAdaptorMsg sessionMsg) {
         ToDeviceMsg msg = sessionMsg.getMsg();
-        switch (msg.getMsgType()) {
+        switch (msg.getSessionMsgType()) {
             case STATUS_CODE_RESPONSE:
                 ResponseMsg<?> responseMsg = (ResponseMsg) msg;
                 if (responseMsg.isSuccess()) {
-                    MsgType requestMsgType = responseMsg.getRequestMsgType();
+                    SessionMsgType requestMsgType = responseMsg.getRequestMsgType();
                     Integer requestId = responseMsg.getRequestId();
-                    if (requestId >= 0 && requestMsgType == MsgType.POST_ATTRIBUTES_REQUEST || requestMsgType == MsgType.POST_TELEMETRY_REQUEST) {
+                    if (requestId >= 0 && requestMsgType == SessionMsgType.POST_ATTRIBUTES_REQUEST || requestMsgType == SessionMsgType.POST_TELEMETRY_REQUEST) {
                         return Optional.of(MqttTransportHandler.createMqttPubAckMsg(requestId));
                     }
                 }
diff --git a/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/GatewaySessionCtx.java b/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/GatewaySessionCtx.java
index 7eda5bd..f666bb8 100644
--- a/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/GatewaySessionCtx.java
+++ b/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/GatewaySessionCtx.java
@@ -30,7 +30,7 @@ import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry;
 import org.thingsboard.server.common.data.relation.EntityRelation;
 import org.thingsboard.server.common.msg.core.*;
 import org.thingsboard.server.common.msg.session.BasicAdaptorToSessionActorMsg;
-import org.thingsboard.server.common.msg.session.BasicToDeviceActorSessionMsg;
+import org.thingsboard.server.common.msg.session.BasicTransportToDeviceSessionActorMsg;
 import org.thingsboard.server.common.msg.session.ctrl.SessionCloseMsg;
 import org.thingsboard.server.common.transport.SessionMsgProcessor;
 import org.thingsboard.server.common.transport.adaptor.AdaptorException;
@@ -96,8 +96,8 @@ public class GatewaySessionCtx {
             GatewayDeviceSessionCtx ctx = new GatewayDeviceSessionCtx(this, device);
             devices.put(deviceName, ctx);
             log.debug("[{}] Added device [{}] to the gateway session", gatewaySessionId, deviceName);
-            processor.process(new BasicToDeviceActorSessionMsg(device, new BasicAdaptorToSessionActorMsg(ctx, new AttributesSubscribeMsg())));
-            processor.process(new BasicToDeviceActorSessionMsg(device, new BasicAdaptorToSessionActorMsg(ctx, new RpcSubscribeMsg())));
+            processor.process(new BasicTransportToDeviceSessionActorMsg(device, new BasicAdaptorToSessionActorMsg(ctx, new AttributesSubscribeMsg())));
+            processor.process(new BasicTransportToDeviceSessionActorMsg(device, new BasicAdaptorToSessionActorMsg(ctx, new RpcSubscribeMsg())));
         }
     }
 
@@ -136,7 +136,7 @@ public class GatewaySessionCtx {
                     JsonConverter.parseWithTs(request, element.getAsJsonObject());
                 }
                 GatewayDeviceSessionCtx deviceSessionCtx = devices.get(deviceName);
-                processor.process(new BasicToDeviceActorSessionMsg(deviceSessionCtx.getDevice(),
+                processor.process(new BasicTransportToDeviceSessionActorMsg(deviceSessionCtx.getDevice(),
                         new BasicAdaptorToSessionActorMsg(deviceSessionCtx, request)));
             }
         } else {
@@ -152,7 +152,7 @@ public class GatewaySessionCtx {
             Integer requestId = jsonObj.get("id").getAsInt();
             String data = jsonObj.get("data").toString();
             GatewayDeviceSessionCtx deviceSessionCtx = devices.get(deviceName);
-            processor.process(new BasicToDeviceActorSessionMsg(deviceSessionCtx.getDevice(),
+            processor.process(new BasicTransportToDeviceSessionActorMsg(deviceSessionCtx.getDevice(),
                     new BasicAdaptorToSessionActorMsg(deviceSessionCtx, new ToDeviceRpcResponseMsg(requestId, data))));
         } else {
             throw new JsonSyntaxException(CAN_T_PARSE_VALUE + json);
@@ -170,11 +170,11 @@ public class GatewaySessionCtx {
                     throw new JsonSyntaxException(CAN_T_PARSE_VALUE + json);
                 }
                 long ts = System.currentTimeMillis();
-                BasicUpdateAttributesRequest request = new BasicUpdateAttributesRequest(requestId);
+                BasicAttributesUpdateRequest request = new BasicAttributesUpdateRequest(requestId);
                 JsonObject deviceData = deviceEntry.getValue().getAsJsonObject();
                 request.add(JsonConverter.parseValues(deviceData).stream().map(kv -> new BaseAttributeKvEntry(kv, ts)).collect(Collectors.toList()));
                 GatewayDeviceSessionCtx deviceSessionCtx = devices.get(deviceName);
-                processor.process(new BasicToDeviceActorSessionMsg(deviceSessionCtx.getDevice(),
+                processor.process(new BasicTransportToDeviceSessionActorMsg(deviceSessionCtx.getDevice(),
                         new BasicAdaptorToSessionActorMsg(deviceSessionCtx, request)));
             }
         } else {
@@ -207,7 +207,7 @@ public class GatewaySessionCtx {
                 request = new BasicGetAttributesRequest(requestId, null, keys);
             }
             GatewayDeviceSessionCtx deviceSessionCtx = devices.get(deviceName);
-            processor.process(new BasicToDeviceActorSessionMsg(deviceSessionCtx.getDevice(),
+            processor.process(new BasicTransportToDeviceSessionActorMsg(deviceSessionCtx.getDevice(),
                     new BasicAdaptorToSessionActorMsg(deviceSessionCtx, request)));
             ack(msg);
         } else {
diff --git a/transport/pom.xml b/transport/pom.xml
index e3e6d66..b88598f 100644
--- a/transport/pom.xml
+++ b/transport/pom.xml
@@ -20,7 +20,7 @@
     <modelVersion>4.0.0</modelVersion>
     <parent>
         <groupId>org.thingsboard</groupId>
-        <version>1.4.1-SNAPSHOT</version>
+        <version>2.0.0-SNAPSHOT</version>
         <artifactId>thingsboard</artifactId>
     </parent>
     <groupId>org.thingsboard</groupId>

ui/package.json 3(+2 -1)

diff --git a/ui/package.json b/ui/package.json
index ad9a7a6..bbf4bc3 100644
--- a/ui/package.json
+++ b/ui/package.json
@@ -1,7 +1,7 @@
 {
   "name": "thingsboard",
   "private": true,
-  "version": "1.4.1",
+  "version": "2.0.0",
   "description": "Thingsboard UI",
   "licenses": [
     {
@@ -69,6 +69,7 @@
     "moment": "^2.15.0",
     "ngclipboard": "^1.1.1",
     "ngreact": "^0.3.0",
+    "ngFlowchart": "git://github.com/thingsboard/ngFlowchart.git#master",
     "objectpath": "^1.2.1",
     "oclazyload": "^1.0.9",
     "raphael": "^2.2.7",

ui/pom.xml 2(+1 -1)

diff --git a/ui/pom.xml b/ui/pom.xml
index 12e5979..5e89278 100644
--- a/ui/pom.xml
+++ b/ui/pom.xml
@@ -20,7 +20,7 @@
     <modelVersion>4.0.0</modelVersion>
     <parent>
         <groupId>org.thingsboard</groupId>
-        <version>1.4.1-SNAPSHOT</version>
+        <version>2.0.0-SNAPSHOT</version>
         <artifactId>thingsboard</artifactId>
     </parent>
     <groupId>org.thingsboard</groupId>

ui/server.js 20(+20 -0)

diff --git a/ui/server.js b/ui/server.js
index fae132f..65a2bc7 100644
--- a/ui/server.js
+++ b/ui/server.js
@@ -30,6 +30,9 @@ const httpProxy = require('http-proxy');
 const forwardHost = 'localhost';
 const forwardPort = 8080;
 
+const ruleNodeUiforwardHost = 'localhost';
+const ruleNodeUiforwardPort = 8080;
+
 const app = express();
 const server = http.createServer(app);
 
@@ -52,17 +55,34 @@ const apiProxy = httpProxy.createProxyServer({
     }
 });
 
+const ruleNodeUiApiProxy = httpProxy.createProxyServer({
+    target: {
+        host: ruleNodeUiforwardHost,
+        port: ruleNodeUiforwardPort
+    }
+});
+
 apiProxy.on('error', function (err, req, res) {
     console.warn('API proxy error: ' + err);
     res.end('Error.');
 });
 
+ruleNodeUiApiProxy.on('error', function (err, req, res) {
+    console.warn('RuleNode UI API proxy error: ' + err);
+    res.end('Error.');
+});
+
 console.info(`Forwarding API requests to http://${forwardHost}:${forwardPort}`);
+console.info(`Forwarding Rule Node UI requests to http://${ruleNodeUiforwardHost}:${ruleNodeUiforwardPort}`);
 
 app.all('/api/*', (req, res) => {
     apiProxy.web(req, res);
 });
 
+app.all('/static/rulenode/*', (req, res) => {
+    ruleNodeUiApiProxy.web(req, res);
+});
+
 app.get('*', function(req, res) {
     res.sendFile(path.join(__dirname, 'src/index.html'));
 });
diff --git a/ui/src/app/alarm/alarm-table.directive.js b/ui/src/app/alarm/alarm-table.directive.js
index 03470c6..775088c 100644
--- a/ui/src/app/alarm/alarm-table.directive.js
+++ b/ui/src/app/alarm/alarm-table.directive.js
@@ -42,7 +42,7 @@ export default function AlarmTableDirective($compile, $templateCache, $rootScope
             history: {
                 timewindowMs: 24 * 60 * 60 * 1000 // 1 day
             }
-        }
+        };
 
         scope.topIndex = 0;
 
@@ -98,6 +98,8 @@ export default function AlarmTableDirective($compile, $templateCache, $rootScope
             }
         };
 
+        scope.reload = reload;
+
         scope.$watch("entityId", function(newVal, prevVal) {
             if (newVal && !angular.equals(newVal, prevVal)) {
                 resetFilter();
diff --git a/ui/src/app/alarm/alarm-table.tpl.html b/ui/src/app/alarm/alarm-table.tpl.html
index b9e466f..658362e 100644
--- a/ui/src/app/alarm/alarm-table.tpl.html
+++ b/ui/src/app/alarm/alarm-table.tpl.html
@@ -26,6 +26,13 @@
             </md-select>
         </md-input-container>
         <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>
     <div flex layout="column" class="tb-alarm-container md-whiteframe-z1">
         <md-list flex layout="column" class="tb-alarm-table">
diff --git a/ui/src/app/api/component-descriptor.service.js b/ui/src/app/api/component-descriptor.service.js
index 4478d71..cc3f710 100644
--- a/ui/src/app/api/component-descriptor.service.js
+++ b/ui/src/app/api/component-descriptor.service.js
@@ -26,7 +26,8 @@ function ComponentDescriptorService($http, $q) {
     var service = {
         getComponentDescriptorsByType: getComponentDescriptorsByType,
         getComponentDescriptorByClazz: getComponentDescriptorByClazz,
-        getPluginActionsByPluginClazz: getPluginActionsByPluginClazz
+        getPluginActionsByPluginClazz: getPluginActionsByPluginClazz,
+        getComponentDescriptorsByTypes: getComponentDescriptorsByTypes
     }
 
     return service;
@@ -52,6 +53,41 @@ function ComponentDescriptorService($http, $q) {
         return deferred.promise;
     }
 
+    function getComponentDescriptorsByTypes(componentTypes) {
+        var deferred = $q.defer();
+        var result = [];
+        for (var i=componentTypes.length-1;i>=0;i--) {
+            var componentType = componentTypes[i];
+            if (componentsByType[componentType]) {
+                result = result.concat(componentsByType[componentType]);
+                componentTypes.splice(i, 1);
+            }
+        }
+        if (!componentTypes.length) {
+            deferred.resolve(result);
+        } else {
+            var url = '/api/components?componentTypes=' + componentTypes.join(',');
+            $http.get(url, null).then(function success(response) {
+                var components = response.data;
+                for (var i = 0; i < components.length; i++) {
+                    var component = components[i];
+                    var componentsList = componentsByType[component.type];
+                    if (!componentsList) {
+                        componentsList = [];
+                        componentsByType[component.type] = componentsList;
+                    }
+                    componentsList.push(component);
+                    componentsByClazz[component.clazz] = component;
+                }
+                result = result.concat(components);
+                deferred.resolve(components);
+            }, function fail() {
+                deferred.reject();
+            });
+        }
+        return deferred.promise;
+    }
+
     function getComponentDescriptorByClazz(componentDescriptorClazz) {
         var deferred = $q.defer();
         if (componentsByClazz[componentDescriptorClazz]) {
diff --git a/ui/src/app/api/entity.service.js b/ui/src/app/api/entity.service.js
index e4c51a2..762bf1a 100644
--- a/ui/src/app/api/entity.service.js
+++ b/ui/src/app/api/entity.service.js
@@ -21,8 +21,7 @@ export default angular.module('thingsboard.api.entity', [thingsboardTypes])
 
 /*@ngInject*/
 function EntityService($http, $q, $filter, $translate, $log, userService, deviceService,
-                       assetService, tenantService, customerService,
-                       ruleService, pluginService, dashboardService, entityRelationService, attributeService, types, utils) {
+                       assetService, tenantService, customerService, ruleChainService, dashboardService, entityRelationService, attributeService, types, utils) {
     var service = {
         getEntity: getEntity,
         getEntities: getEntities,
@@ -61,18 +60,15 @@ function EntityService($http, $q, $filter, $translate, $log, userService, device
             case types.entityType.customer:
                 promise = customerService.getCustomer(entityId, config);
                 break;
-            case types.entityType.rule:
-                promise = ruleService.getRule(entityId, config);
-                break;
-            case types.entityType.plugin:
-                promise = pluginService.getPlugin(entityId, config);
-                break;
             case types.entityType.dashboard:
                 promise = dashboardService.getDashboardInfo(entityId, config);
                 break;
             case types.entityType.user:
                 promise = userService.getUser(entityId, true, config);
                 break;
+            case types.entityType.rulechain:
+                promise = ruleChainService.getRuleChain(entityId, config);
+                break;
             case types.entityType.alarm:
                 $log.error('Get Alarm Entity is not implemented!');
                 break;
@@ -143,14 +139,6 @@ function EntityService($http, $q, $filter, $translate, $log, userService, device
                 promise = getEntitiesByIdsPromise(
                     (id) => customerService.getCustomer(id, config), entityIds);
                 break;
-            case types.entityType.rule:
-                promise = getEntitiesByIdsPromise(
-                    (id) => ruleService.getRule(id, config), entityIds);
-                break;
-            case types.entityType.plugin:
-                promise = getEntitiesByIdsPromise(
-                    (id) => pluginService.getPlugin(id, config), entityIds);
-                break;
             case types.entityType.dashboard:
                 promise = getEntitiesByIdsPromise(
                     (id) => dashboardService.getDashboardInfo(id, config), entityIds);
@@ -265,11 +253,8 @@ function EntityService($http, $q, $filter, $translate, $log, userService, device
                     promise = customerService.getCustomers(pageLink, config);
                 }
                 break;
-            case types.entityType.rule:
-                promise = ruleService.getAllRules(pageLink, config);
-                break;
-            case types.entityType.plugin:
-                promise = pluginService.getAllPlugins(pageLink, config);
+            case types.entityType.rulechain:
+                promise = ruleChainService.getRuleChains(pageLink, config);
                 break;
             case types.entityType.dashboard:
                 if (user.authority === 'CUSTOMER_USER') {
@@ -736,16 +721,12 @@ function EntityService($http, $q, $filter, $translate, $log, userService, device
         switch(authority) {
             case 'SYS_ADMIN':
                 entityTypes.tenant = types.entityType.tenant;
-                entityTypes.rule = types.entityType.rule;
-                entityTypes.plugin = types.entityType.plugin;
                 break;
             case 'TENANT_ADMIN':
                 entityTypes.device = types.entityType.device;
                 entityTypes.asset = types.entityType.asset;
                 entityTypes.tenant = types.entityType.tenant;
                 entityTypes.customer = types.entityType.customer;
-                entityTypes.rule = types.entityType.rule;
-                entityTypes.plugin = types.entityType.plugin;
                 entityTypes.dashboard = types.entityType.dashboard;
                 if (useAliasEntityTypes) {
                     entityTypes.current_customer = types.aliasEntityType.current_customer;
diff --git a/ui/src/app/api/rule-chain.service.js b/ui/src/app/api/rule-chain.service.js
new file mode 100644
index 0000000..e7436de
--- /dev/null
+++ b/ui/src/app/api/rule-chain.service.js
@@ -0,0 +1,298 @@
+/*
+ * 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.
+ */
+export default angular.module('thingsboard.api.ruleChain', [])
+    .factory('ruleChainService', RuleChainService).name;
+
+/*@ngInject*/
+function RuleChainService($http, $q, $filter, $ocLazyLoad, $translate, types, componentDescriptorService) {
+
+    var ruleNodeComponents = null;
+
+    var service = {
+        getRuleChains: getRuleChains,
+        getRuleChain: getRuleChain,
+        saveRuleChain: saveRuleChain,
+        setRootRuleChain: setRootRuleChain,
+        deleteRuleChain: deleteRuleChain,
+        getRuleChainMetaData: getRuleChainMetaData,
+        saveRuleChainMetaData: saveRuleChainMetaData,
+        getRuleNodeComponents: getRuleNodeComponents,
+        getRuleNodeComponentByClazz: getRuleNodeComponentByClazz,
+        getRuleNodeSupportedLinks: getRuleNodeSupportedLinks,
+        resolveTargetRuleChains: resolveTargetRuleChains,
+        testScript: testScript,
+        getLatestRuleNodeDebugInput: getLatestRuleNodeDebugInput
+    };
+
+    return service;
+
+    function getRuleChains (pageLink, config) {
+        var deferred = $q.defer();
+        var url = '/api/ruleChains?limit=' + pageLink.limit;
+        if (angular.isDefined(pageLink.textSearch)) {
+            url += '&textSearch=' + pageLink.textSearch;
+        }
+        if (angular.isDefined(pageLink.idOffset)) {
+            url += '&idOffset=' + pageLink.idOffset;
+        }
+        if (angular.isDefined(pageLink.textOffset)) {
+            url += '&textOffset=' + pageLink.textOffset;
+        }
+        $http.get(url, config).then(function success(response) {
+            deferred.resolve(response.data);
+        }, function fail() {
+            deferred.reject();
+        });
+        return deferred.promise;
+    }
+
+    function getRuleChain(ruleChainId, config) {
+        var deferred = $q.defer();
+        var url = '/api/ruleChain/' + ruleChainId;
+        $http.get(url, config).then(function success(response) {
+            deferred.resolve(response.data);
+        }, function fail() {
+            deferred.reject();
+        });
+        return deferred.promise;
+    }
+
+    function saveRuleChain(ruleChain) {
+        var deferred = $q.defer();
+        var url = '/api/ruleChain';
+        $http.post(url, ruleChain).then(function success(response) {
+            deferred.resolve(response.data);
+        }, function fail() {
+            deferred.reject();
+        });
+        return deferred.promise;
+    }
+
+    function setRootRuleChain(ruleChainId) {
+        var deferred = $q.defer();
+        var url = '/api/ruleChain/' + ruleChainId + '/root';
+        $http.post(url).then(function success(response) {
+            deferred.resolve(response.data);
+        }, function fail() {
+            deferred.reject();
+        });
+        return deferred.promise;
+    }
+
+    function deleteRuleChain(ruleChainId) {
+        var deferred = $q.defer();
+        var url = '/api/ruleChain/' + ruleChainId;
+        $http.delete(url).then(function success() {
+            deferred.resolve();
+        }, function fail() {
+            deferred.reject();
+        });
+        return deferred.promise;
+    }
+
+    function getRuleChainMetaData(ruleChainId, config) {
+        var deferred = $q.defer();
+        var url = '/api/ruleChain/' + ruleChainId + '/metadata';
+        $http.get(url, config).then(function success(response) {
+            deferred.resolve(response.data);
+        }, function fail() {
+            deferred.reject();
+        });
+        return deferred.promise;
+    }
+
+    function saveRuleChainMetaData(ruleChainMetaData) {
+        var deferred = $q.defer();
+        var url = '/api/ruleChain/metadata';
+        $http.post(url, ruleChainMetaData).then(function success(response) {
+            deferred.resolve(response.data);
+        }, function fail() {
+            deferred.reject();
+        });
+        return deferred.promise;
+    }
+
+    function getRuleNodeSupportedLinks(component) {
+        var relationTypes = component.configurationDescriptor.nodeDefinition.relationTypes;
+        var customRelations = component.configurationDescriptor.nodeDefinition.customRelations;
+        var linkLabels = [];
+        for (var i=0;i<relationTypes.length;i++) {
+            linkLabels.push({
+                name: relationTypes[i], custom: false
+            });
+        }
+        if (customRelations) {
+            linkLabels.push(
+                { name: 'Custom', custom: true }
+            );
+        }
+        return linkLabels;
+    }
+
+    function getRuleNodeComponents() {
+        var deferred = $q.defer();
+        if (ruleNodeComponents) {
+            deferred.resolve(ruleNodeComponents);
+        } else {
+            loadRuleNodeComponents().then(
+                (components) => {
+                    resolveRuleNodeComponentsUiResources(components).then(
+                        (components) => {
+                            ruleNodeComponents = components;
+                            ruleNodeComponents.push(
+                                types.ruleChainNodeComponent
+                            );
+                            ruleNodeComponents.sort(
+                                (comp1, comp2) => {
+                                    var result = comp1.type.localeCompare(comp2.type);
+                                    if (result == 0) {
+                                        result = comp1.name.localeCompare(comp2.name);
+                                    }
+                                    return result;
+                                }
+                            );
+                            deferred.resolve(ruleNodeComponents);
+                        },
+                        () => {
+                            deferred.reject();
+                        }
+                    );
+                },
+                () => {
+                    deferred.reject();
+                }
+            );
+        }
+        return deferred.promise;
+    }
+
+    function resolveRuleNodeComponentsUiResources(components) {
+        var deferred = $q.defer();
+        var tasks = [];
+        for (var i=0;i<components.length;i++) {
+            var component = components[i];
+            tasks.push(resolveRuleNodeComponentUiResources(component));
+        }
+        $q.all(tasks).then(
+            (components) => {
+                deferred.resolve(components);
+            },
+            () => {
+                deferred.resolve(components);
+            }
+        );
+        return deferred.promise;
+    }
+
+    function resolveRuleNodeComponentUiResources(component) {
+        var deferred = $q.defer();
+        var uiResources = component.configurationDescriptor.nodeDefinition.uiResources;
+        if (uiResources && uiResources.length) {
+            var tasks = [];
+            for (var i=0;i<uiResources.length;i++) {
+                var uiResource = uiResources[i];
+                tasks.push($ocLazyLoad.load(uiResource));
+            }
+            $q.all(tasks).then(
+                () => {
+                    deferred.resolve(component);
+                },
+                () => {
+                    component.configurationDescriptor.nodeDefinition.uiResourceLoadError = $translate.instant('rulenode.ui-resources-load-error');
+                    deferred.resolve(component);
+                }
+            )
+        } else {
+            deferred.resolve(component);
+        }
+        return deferred.promise;
+    }
+
+    function getRuleNodeComponentByClazz(clazz) {
+        var res = $filter('filter')(ruleNodeComponents, {clazz: clazz}, true);
+        if (res && res.length) {
+            return res[0];
+        }
+        return null;
+    }
+
+    function resolveTargetRuleChains(ruleChainConnections) {
+        var deferred = $q.defer();
+        if (ruleChainConnections && ruleChainConnections.length) {
+            var tasks = [];
+            for (var i = 0; i < ruleChainConnections.length; i++) {
+                tasks.push(resolveRuleChain(ruleChainConnections[i].targetRuleChainId.id));
+            }
+            $q.all(tasks).then(
+                (ruleChains) => {
+                    var ruleChainsMap = {};
+                    for (var i = 0; i < ruleChains.length; i++) {
+                        ruleChainsMap[ruleChains[i].id.id] = ruleChains[i];
+                    }
+                    deferred.resolve(ruleChainsMap);
+                },
+                () => {
+                    deferred.reject();
+                }
+            );
+        } else {
+            deferred.resolve({});
+        }
+        return deferred.promise;
+    }
+
+    function resolveRuleChain(ruleChainId) {
+        var deferred = $q.defer();
+        getRuleChain(ruleChainId, {ignoreErrors: true}).then(
+            (ruleChain) => {
+                deferred.resolve(ruleChain);
+            },
+            () => {
+                deferred.resolve({
+                    id: {id: ruleChainId, entityType: types.entityType.rulechain}
+                });
+            }
+        );
+        return deferred.promise;
+    }
+
+    function loadRuleNodeComponents() {
+        return componentDescriptorService.getComponentDescriptorsByTypes(types.ruleNodeTypeComponentTypes);
+    }
+
+    function testScript(inputParams) {
+        var deferred = $q.defer();
+        var url = '/api/ruleChain/testScript';
+        $http.post(url, inputParams).then(function success(response) {
+            deferred.resolve(response.data);
+        }, function fail() {
+            deferred.reject();
+        });
+        return deferred.promise;
+    }
+
+    function getLatestRuleNodeDebugInput(ruleNodeId) {
+        var deferred = $q.defer();
+        var url = '/api/ruleNode/' + ruleNodeId + '/debugIn';
+        $http.get(url).then(function success(response) {
+            deferred.resolve(response.data);
+        }, function fail() {
+            deferred.reject();
+        });
+        return deferred.promise;
+    }
+
+}
diff --git a/ui/src/app/app.js b/ui/src/app/app.js
index c328a5a..f021efb 100644
--- a/ui/src/app/app.js
+++ b/ui/src/app/app.js
@@ -49,6 +49,7 @@ import 'material-ui';
 import 'react-schema-form';
 import react from 'ngreact';
 import '@flowjs/ng-flow/dist/ng-flow-standalone.min';
+import 'ngFlowchart/dist/ngFlowchart';
 
 import thingsboardLocales from './locale/locale.constant';
 import thingsboardLogin from './login';
@@ -73,6 +74,8 @@ import thingsboardApiAttribute from './api/attribute.service';
 import thingsboardApiEntity from './api/entity.service';
 import thingsboardApiAlarm from './api/alarm.service';
 import thingsboardApiAuditLog from './api/audit-log.service';
+import thingsboardApiComponentDescriptor from './api/component-descriptor.service';
+import thingsboardApiRuleChain from './api/rule-chain.service';
 
 import 'typeface-roboto';
 import 'font-awesome/css/font-awesome.min.css';
@@ -85,6 +88,7 @@ import 'mdPickers/dist/mdPickers.min.css';
 import 'angular-hotkeys/build/hotkeys.min.css';
 import 'angular-carousel/dist/angular-carousel.min.css';
 import 'angular-material-expansion-panel/dist/md-expansion-panel.min.css';
+import 'ngFlowchart/dist/flowchart.css';
 import '../scss/main.scss';
 
 import AppConfig from './app.config';
@@ -112,6 +116,7 @@ angular.module('thingsboard', [
     'ngclipboard',
     react.name,
     'flow',
+    'flowchart',
     thingsboardLocales,
     thingsboardLogin,
     thingsboardDialogs,
@@ -135,6 +140,8 @@ angular.module('thingsboard', [
     thingsboardApiEntity,
     thingsboardApiAlarm,
     thingsboardApiAuditLog,
+    thingsboardApiComponentDescriptor,
+    thingsboardApiRuleChain,
     uiRouter])
     .config(AppConfig)
     .factory('globalInterceptor', GlobalInterceptor)
diff --git a/ui/src/app/common/types.constant.js b/ui/src/app/common/types.constant.js
index 66ed196..cf1023e 100644
--- a/ui/src/app/common/types.constant.js
+++ b/ui/src/app/common/types.constant.js
@@ -279,22 +279,40 @@ export default angular.module('thingsboard.types', [])
                 function: "function",
                 alarm: "alarm"
             },
+            contentType: {
+                "JSON": {
+                    value: "JSON",
+                    name: "content-type.json",
+                    code: "json"
+                },
+                "TEXT": {
+                    value: "TEXT",
+                    name: "content-type.text",
+                    code: "text"
+                },
+                "BINARY": {
+                    value: "BINARY",
+                    name: "content-type.binary",
+                    code: "text"
+                }
+            },
             componentType: {
+                enrichment: "ENRICHMENT",
                 filter: "FILTER",
-                processor: "PROCESSOR",
+                transformation: "TRANSFORMATION",
                 action: "ACTION",
-                plugin: "PLUGIN"
+                external: "EXTERNAL"
             },
             entityType: {
                 device: "DEVICE",
                 asset: "ASSET",
-                rule: "RULE",
-                plugin: "PLUGIN",
                 tenant: "TENANT",
                 customer: "CUSTOMER",
                 user: "USER",
                 dashboard: "DASHBOARD",
-                alarm: "ALARM"
+                alarm: "ALARM",
+                rulechain: "RULE_CHAIN",
+                rulenode: "RULE_NODE"
             },
             aliasEntityType: {
                 current_customer: "CURRENT_CUSTOMER"
@@ -312,18 +330,6 @@ export default angular.module('thingsboard.types', [])
                     list: 'entity.list-of-assets',
                     nameStartsWith: 'entity.asset-name-starts-with'
                 },
-                "RULE": {
-                    type: 'entity.type-rule',
-                    typePlural: 'entity.type-rules',
-                    list: 'entity.list-of-rules',
-                    nameStartsWith: 'entity.rule-name-starts-with'
-                },
-                "PLUGIN": {
-                    type: 'entity.type-plugin',
-                    typePlural: 'entity.type-plugins',
-                    list: 'entity.list-of-plugins',
-                    nameStartsWith: 'entity.plugin-name-starts-with'
-                },
                 "TENANT": {
                     type: 'entity.type-tenant',
                     typePlural: 'entity.type-tenants',
@@ -354,6 +360,12 @@ export default angular.module('thingsboard.types', [])
                     list: 'entity.list-of-alarms',
                     nameStartsWith: 'entity.alarm-name-starts-with'
                 },
+                "RULE_CHAIN": {
+                    type: 'entity.type-rulechain',
+                    typePlural: 'entity.type-rulechains',
+                    list: 'entity.list-of-rulechains',
+                    nameStartsWith: 'entity.rulechain-name-starts-with'
+                },
                 "CURRENT_CUSTOMER": {
                     type: 'entity.type-current-customer',
                     list: 'entity.type-current-customer'
@@ -381,6 +393,16 @@ export default angular.module('thingsboard.types', [])
                     name: "event.type-stats"
                 }
             },
+            debugEventType: {
+                debugRuleNode: {
+                    value: "DEBUG_RULE_NODE",
+                    name: "event.type-debug-rule-node"
+                },
+                debugRuleChain: {
+                    value: "DEBUG_RULE_CHAIN",
+                    name: "event.type-debug-rule-chain"
+                }
+            },
             extensionType: {
                 http: "HTTP",
                 mqtt: "MQTT",
@@ -471,6 +493,80 @@ export default angular.module('thingsboard.types', [])
                     clientSide: false
                 }
             },
+            ruleNodeTypeComponentTypes: ["FILTER", "ENRICHMENT", "TRANSFORMATION", "ACTION", "EXTERNAL"],
+            ruleChainNodeComponent: {
+                type: 'RULE_CHAIN',
+                name: 'rule chain',
+                clazz: 'tb.internal.RuleChain',
+                configurationDescriptor: {
+                    nodeDefinition: {
+                        description: "",
+                        details: "Forwards incoming messages to specified Rule Chain",
+                        inEnabled: true,
+                        outEnabled: false,
+                        relationTypes: [],
+                        customRelations: false,
+                        defaultConfiguration: {}
+                    }
+                }
+            },
+            inputNodeComponent: {
+                type: 'INPUT',
+                name: 'Input',
+                clazz: 'tb.internal.Input'
+            },
+            ruleNodeType: {
+                FILTER: {
+                    value: "FILTER",
+                    name: "rulenode.type-filter",
+                    details: "rulenode.type-filter-details",
+                    nodeClass: "tb-filter-type",
+                    icon: "filter_list"
+                },
+                ENRICHMENT: {
+                    value: "ENRICHMENT",
+                    name: "rulenode.type-enrichment",
+                    details: "rulenode.type-enrichment-details",
+                    nodeClass: "tb-enrichment-type",
+                    icon: "playlist_add"
+                },
+                TRANSFORMATION: {
+                    value: "TRANSFORMATION",
+                    name: "rulenode.type-transformation",
+                    details: "rulenode.type-transformation-details",
+                    nodeClass: "tb-transformation-type",
+                    icon: "transform"
+                },
+                ACTION: {
+                    value: "ACTION",
+                    name: "rulenode.type-action",
+                    details: "rulenode.type-action-details",
+                    nodeClass: "tb-action-type",
+                    icon: "flash_on"
+                },
+                EXTERNAL: {
+                    value: "EXTERNAL",
+                    name: "rulenode.type-external",
+                    details: "rulenode.type-external-details",
+                    nodeClass: "tb-external-type",
+                    icon: "cloud_upload"
+                },
+                RULE_CHAIN: {
+                    value: "RULE_CHAIN",
+                    name: "rulenode.type-rule-chain",
+                    details: "rulenode.type-rule-chain-details",
+                    nodeClass: "tb-rule-chain-type",
+                    icon: "settings_ethernet"
+                },
+                INPUT: {
+                    value: "INPUT",
+                    name: "rulenode.type-input",
+                    details: "rulenode.type-input-details",
+                    nodeClass: "tb-input-type",
+                    icon: "input",
+                    special: true
+                }
+            },
             valueType: {
                 string: {
                     value: "string",
diff --git a/ui/src/app/components/ace-editor-fix.js b/ui/src/app/components/ace-editor-fix.js
new file mode 100644
index 0000000..f68767e
--- /dev/null
+++ b/ui/src/app/components/ace-editor-fix.js
@@ -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.
+ */
+
+export default function fixAceEditor(aceEditor) {
+    aceEditor.$blockScrolling = Infinity;
+    aceEditor.on("showGutterTooltip", function (tooltip) {
+        if (!tooltip.isAttachedToBody) {
+            document.body.appendChild(tooltip.$element); //eslint-disable-line
+            tooltip.isAttachedToBody = true;
+            onElementRemoved(tooltip.$parentNode, () => {
+                if (tooltip.$element.parentNode != null) {
+                    tooltip.$element.parentNode.removeChild(tooltip.$element);
+                }
+            });
+        }
+    });
+}
+
+function onElementRemoved(element, callback) {
+    if (!document.body.contains(element)) { //eslint-disable-line
+        callback();
+    } else {
+        var observer;
+        observer = new MutationObserver(function(mutations) { //eslint-disable-line
+            if (!document.body.contains(element)) { //eslint-disable-line
+                callback();
+                observer.disconnect();
+            }
+        });
+        observer.observe(document.body, {childList: true}); //eslint-disable-line
+    }
+}
diff --git a/ui/src/app/components/confirm-on-exit.directive.js b/ui/src/app/components/confirm-on-exit.directive.js
index fe9a9bd..e04e110 100644
--- a/ui/src/app/components/confirm-on-exit.directive.js
+++ b/ui/src/app/components/confirm-on-exit.directive.js
@@ -18,17 +18,17 @@ export default angular.module('thingsboard.directives.confirmOnExit', [])
     .name;
 
 /*@ngInject*/
-function ConfirmOnExit($state, $mdDialog, $window, $filter, userService) {
+function ConfirmOnExit($state, $mdDialog, $window, $filter, $parse, userService) {
     return {
-        link: function ($scope) {
-
+        link: function ($scope, $element, $attributes) {
+            $scope.confirmForm = $scope.$eval($attributes.confirmForm);
             $window.onbeforeunload = function () {
-                if (userService.isAuthenticated() && (($scope.confirmForm && $scope.confirmForm.$dirty) || $scope.isDirty)) {
+                if (userService.isAuthenticated() && (($scope.confirmForm && $scope.confirmForm.$dirty) || $scope.$eval($attributes.isDirty))) {
                     return $filter('translate')('confirm-on-exit.message');
                 }
             }
             $scope.$on('$stateChangeStart', function (event, next, current, params) {
-                if (userService.isAuthenticated() && (($scope.confirmForm && $scope.confirmForm.$dirty) || $scope.isDirty)) {
+                if (userService.isAuthenticated() && (($scope.confirmForm && $scope.confirmForm.$dirty) || $scope.$eval($attributes.isDirty))) {
                     event.preventDefault();
                     var confirm = $mdDialog.confirm()
                         .title($filter('translate')('confirm-on-exit.title'))
@@ -40,7 +40,9 @@ function ConfirmOnExit($state, $mdDialog, $window, $filter, userService) {
                         if ($scope.confirmForm) {
                             $scope.confirmForm.$setPristine();
                         } else {
-                            $scope.isDirty = false;
+                            var remoteSetter = $parse($attributes.isDirty).assign;
+                            remoteSetter($scope, false);
+                            //$scope.isDirty = false;
                         }
                         $state.go(next.name, params);
                     }, function () {
@@ -48,9 +50,6 @@ function ConfirmOnExit($state, $mdDialog, $window, $filter, userService) {
                 }
             });
         },
-        scope: {
-            confirmForm: '=',
-            isDirty: '='
-        }
+        scope: false
     };
 }
\ No newline at end of file
diff --git a/ui/src/app/components/dashboard.tpl.html b/ui/src/app/components/dashboard.tpl.html
index 376f2d6..54a1a09 100644
--- a/ui/src/app/components/dashboard.tpl.html
+++ b/ui/src/app/components/dashboard.tpl.html
@@ -122,9 +122,9 @@
 							<md-menu-content id="menu" width="4" ng-mouseleave="$mdCloseMousepointMenu()">
 								<md-menu-item ng-repeat ="item in vm.widgetContextMenuItems">
 									<md-button ng-disabled="!item.enabled" ng-click="item.action(vm.widgetContextMenuEvent, widget)">
+										<span ng-if="item.shortcut" class="tb-alt-text"> {{ item.shortcut | keyboardShortcut }}</span>
 										<md-icon ng-if="item.icon" md-menu-align-target aria-label="{{ item.value | translate }}" class="material-icons">{{item.icon}}</md-icon>
 										<span translate>{{item.value}}</span>
-										<span ng-if="item.shortcut" class="tb-alt-text"> {{ item.shortcut | keyboardShortcut }}</span>
 									</md-button>
 								</md-menu-item>
 							</md-menu-content>
@@ -137,9 +137,9 @@
 	<md-menu-content id="menu" width="4" ng-mouseleave="$mdCloseMousepointMenu()">
 		<md-menu-item ng-repeat ="item in vm.contextMenuItems">
 			<md-button ng-disabled="!item.enabled" ng-click="item.action(vm.contextMenuEvent)">
+				<span ng-if="item.shortcut" class="tb-alt-text"> {{ item.shortcut | keyboardShortcut }}</span>
 				<md-icon ng-if="item.icon" md-menu-align-target aria-label="{{ item.value | translate }}" class="material-icons">{{item.icon}}</md-icon>
 				<span translate>{{item.value}}</span>
-				<span ng-if="item.shortcut" class="tb-alt-text"> {{ item.shortcut | keyboardShortcut }}</span>
 			</md-button>
 		</md-menu-item>
 	</md-menu-content>
diff --git a/ui/src/app/components/details-sidenav.directive.js b/ui/src/app/components/details-sidenav.directive.js
index e455a80..2516134 100644
--- a/ui/src/app/components/details-sidenav.directive.js
+++ b/ui/src/app/components/details-sidenav.directive.js
@@ -26,7 +26,7 @@ export default angular.module('thingsboard.directives.detailsSidenav', [])
     .name;
 
 /*@ngInject*/
-function DetailsSidenav($timeout) {
+function DetailsSidenav($timeout, $mdUtil, $q, $animate) {
 
     var linker = function (scope, element, attrs) {
 
@@ -42,6 +42,63 @@ function DetailsSidenav($timeout) {
             scope.isEdit = true;
         }
 
+        var backdrop;
+        var previousContainerStyles;
+
+        if (attrs.hasOwnProperty('tbEnableBackdrop')) {
+            backdrop = $mdUtil.createBackdrop(scope, "md-sidenav-backdrop md-opaque ng-enter");
+            element.on('$destroy', function() {
+                backdrop && backdrop.remove();
+            });
+            scope.$on('$destroy', function(){
+                backdrop && backdrop.remove();
+            });
+            scope.$watch('isOpen', updateIsOpen);
+        }
+
+        function updateIsOpen(isOpen) {
+            backdrop[isOpen ? 'on' : 'off']('click', (ev)=>{
+                ev.preventDefault();
+                scope.isOpen = false;
+                scope.$apply();
+            });
+            var parent = element.parent();
+            var restorePositioning = updateContainerPositions(parent, isOpen);
+
+            return $q.all([
+                isOpen && backdrop ? $animate.enter(backdrop, parent) : backdrop ?
+                    $animate.leave(backdrop) : $q.when(true)
+            ]).then(function() {
+                restorePositioning && restorePositioning();
+            });
+        }
+
+        function updateContainerPositions(parent, willOpen) {
+            var drawerEl = element[0];
+            var scrollTop = parent[0].scrollTop;
+            if (willOpen && scrollTop) {
+                previousContainerStyles = {
+                    top: drawerEl.style.top,
+                    bottom: drawerEl.style.bottom,
+                    height: drawerEl.style.height
+                };
+                var positionStyle = {
+                    top: scrollTop + 'px',
+                    bottom: 'auto',
+                    height: parent[0].clientHeight + 'px'
+                };
+                backdrop.css(positionStyle);
+            }
+            if (!willOpen && previousContainerStyles) {
+                return function() {
+                    backdrop[0].style.top = null;
+                    backdrop[0].style.bottom = null;
+                    backdrop[0].style.height = null;
+                    previousContainerStyles = null;
+                };
+            }
+        }
+
         scope.toggleDetailsEditMode = function () {
             if (!scope.isAlwaysEdit) {
                 if (!scope.isEdit) {
diff --git a/ui/src/app/components/details-sidenav.scss b/ui/src/app/components/details-sidenav.scss
index c7e9919..360b133 100644
--- a/ui/src/app/components/details-sidenav.scss
+++ b/ui/src/app/components/details-sidenav.scss
@@ -59,4 +59,14 @@ md-sidenav.tb-sidenav-details {
       background-color: $primary-hue-3;
     }
   }
+
+  md-tab-content.md-active > div {
+    height: 100%;
+    & > *:first-child {
+      height: 100%;
+    }
+    md-content {
+      height: 100%;
+    }
+  }
 }
diff --git a/ui/src/app/components/details-sidenav.tpl.html b/ui/src/app/components/details-sidenav.tpl.html
index c504a24..763bc22 100644
--- a/ui/src/app/components/details-sidenav.tpl.html
+++ b/ui/src/app/components/details-sidenav.tpl.html
@@ -16,7 +16,7 @@
 
 -->
 <md-sidenav class="md-sidenav-right md-whiteframe-4dp tb-sidenav-details"
-      md-disable-backdrop="true"
+      md-disable-backdrop
       md-is-open="isOpen"
       md-component-id="right"
       layout="column">
diff --git a/ui/src/app/components/js-func.directive.js b/ui/src/app/components/js-func.directive.js
index f95d003..f13f13c 100644
--- a/ui/src/app/components/js-func.directive.js
+++ b/ui/src/app/components/js-func.directive.js
@@ -22,12 +22,18 @@ import thingsboardToast from '../services/toast';
 import thingsboardUtils from '../common/utils.service';
 import thingsboardExpandFullscreen from './expand-fullscreen.directive';
 
+import fixAceEditor from './ace-editor-fix';
+
 /* eslint-disable import/no-unresolved, import/default */
 
 import jsFuncTemplate from './js-func.tpl.html';
 
 /* eslint-enable import/no-unresolved, import/default */
 
+import beautify from 'js-beautify';
+
+const js_beautify = beautify.js;
+
 /* eslint-disable angular/angularelement */
 
 export default angular.module('thingsboard.directives.jsFunc', [thingsboardToast, thingsboardUtils, thingsboardExpandFullscreen])
@@ -41,6 +47,7 @@ function JsFunc($compile, $templateCache, toast, utils, $translate) {
         var template = $templateCache.get(jsFuncTemplate);
         element.html(template);
 
+        scope.functionName = attrs.functionName;
         scope.functionArgs = scope.$eval(attrs.functionArgs);
         scope.validationArgs = scope.$eval(attrs.validationArgs);
         scope.resultType = attrs.resultType;
@@ -48,6 +55,8 @@ function JsFunc($compile, $templateCache, toast, utils, $translate) {
             scope.resultType = "nocheck";
         }
 
+        scope.validationTriggerArg = attrs.validationTriggerArg;
+
         scope.functionValid = true;
 
         var Range = ace.acequire("ace/range").Range;
@@ -56,7 +65,7 @@ function JsFunc($compile, $templateCache, toast, utils, $translate) {
 
 
         scope.functionArgsString = '';
-        for (var i in scope.functionArgs) {
+        for (var i = 0; i < scope.functionArgs.length; i++) {
             if (scope.functionArgsString.length > 0) {
                 scope.functionArgsString += ', ';
             }
@@ -64,11 +73,20 @@ function JsFunc($compile, $templateCache, toast, utils, $translate) {
         }
 
         scope.onFullscreenChanged = function () {
+            updateEditorSize();
+        };
+
+        scope.beautifyJs = function () {
+            var res = js_beautify(scope.functionBody, {indent_size: 4, wrap_line_length: 60});
+            scope.functionBody = res;
+        };
+
+        function updateEditorSize() {
             if (scope.js_editor) {
                 scope.js_editor.resize();
                 scope.js_editor.renderer.updateFull();
             }
-        };
+        }
 
         scope.jsEditorOptions = {
             useWrapMode: true,
@@ -83,6 +101,7 @@ function JsFunc($compile, $templateCache, toast, utils, $translate) {
                 scope.js_editor.session.on("change", function () {
                     scope.cleanupJsErrors();
                 });
+                fixAceEditor(_ace);
             }
         };
 
@@ -128,6 +147,9 @@ function JsFunc($compile, $templateCache, toast, utils, $translate) {
         scope.validate = function () {
             try {
                 var toValidate = new Function(scope.functionArgsString, scope.functionBody);
+                if (scope.noValidate) {
+                    return true;
+                }
                 var res;
                 var validationError;
                 for (var i=0;i<scope.validationArgs.length;i++) {
@@ -197,9 +219,19 @@ function JsFunc($compile, $templateCache, toast, utils, $translate) {
             }
         };
 
-        scope.$on('form-submit', function () {
-            scope.functionValid = scope.validate();
-            scope.updateValidity();
+        scope.$on('form-submit', function (event, args) {
+            if (!args || scope.validationTriggerArg && scope.validationTriggerArg == args) {
+                scope.validationArgs = scope.$eval(attrs.validationArgs);
+                scope.cleanupJsErrors();
+                scope.functionValid = true;
+                scope.updateValidity();
+                scope.functionValid = scope.validate();
+                scope.updateValidity();
+            }
+        });
+
+        scope.$on('update-ace-editor-size', function () {
+            updateEditorSize();
         });
 
         $compile(element.contents())(scope);
@@ -208,7 +240,11 @@ function JsFunc($compile, $templateCache, toast, utils, $translate) {
     return {
         restrict: "E",
         require: "^ngModel",
-        scope: {},
+        scope: {
+            disabled:'=ngDisabled',
+            noValidate: '=?',
+            fillHeight:'=?'
+        },
         link: linker
     };
 }
diff --git a/ui/src/app/components/js-func.scss b/ui/src/app/components/js-func.scss
index 2bd5df1..ade2830 100644
--- a/ui/src/app/components/js-func.scss
+++ b/ui/src/app/components/js-func.scss
@@ -15,16 +15,37 @@
  */
 tb-js-func {
   position: relative;
+  .tb-disabled {
+    color: rgba(0,0,0,0.38);
+  }
+  .fill-height {
+    height: 100%;
+  }
+}
+
+.tb-js-func-toolbar {
+  .md-button.tidy {
+    color: #7B7B7B;
+    min-width: 32px;
+    min-height: 15px;
+    line-height: 15px;
+    font-size: 0.800rem;
+    margin: 0 5px 0 0;
+    padding: 4px;
+    background: rgba(220, 220, 220, 0.35);
+  }
 }
 
 .tb-js-func-panel {
   margin-left: 15px;
   border: 1px solid #C0C0C0;
-  height: 100%;
+  height: calc(100% - 80px);
   #tb-javascript-input {
     min-width: 200px;
-    min-height: 200px;
     width: 100%;
     height: 100%;
+    &:not(.fill-height) {
+      min-height: 200px;
+    }
   }
 }
diff --git a/ui/src/app/components/js-func.tpl.html b/ui/src/app/components/js-func.tpl.html
index 806de4a..58675cb 100644
--- a/ui/src/app/components/js-func.tpl.html
+++ b/ui/src/app/components/js-func.tpl.html
@@ -15,19 +15,23 @@
     limitations under the License.
 
 -->
-<div style="background: #fff;" tb-expand-fullscreen fullscreen-zindex="100" expand-button-id="expand-button" on-fullscreen-changed="onFullscreenChanged()" layout="column">
-	<div layout="row" layout-align="start center" style="height: 40px;">
-		<span style="font-style: italic;">function({{ functionArgsString }}) {</span>
+<div style="background: #fff;" ng-class="{'tb-disabled': disabled, 'fill-height': fillHeight}" tb-expand-fullscreen fullscreen-zindex="100" expand-button-id="expand-button" on-fullscreen-changed="onFullscreenChanged()">
+	<div layout="row" layout-align="start center" style="height: 40px;" class="tb-js-func-toolbar">
+		<label class="tb-title no-padding">function {{ functionName }}({{ functionArgsString }}) {</label>
 		<span flex></span>
+		<md-button ng-if="!disabled" class="tidy" aria-label="{{ 'js-func.tidy' | translate }}" ng-click="beautifyJs()">{{
+			'js-func.tidy' | translate }}
+		</md-button>
 		<div id="expand-button" layout="column" aria-label="Fullscreen" class="md-button md-icon-button tb-md-32 tb-fullscreen-button-style"></div>
 	</div>
-	<div flex id="tb-javascript-panel" class="tb-js-func-panel" layout="column">
-		<div flex id="tb-javascript-input"
-			 ui-ace="jsEditorOptions" 
+	<div id="tb-javascript-panel" class="tb-js-func-panel">
+		<div id="tb-javascript-input" ng-class="{'fill-height': fillHeight}"
+			 ui-ace="jsEditorOptions"
+			 ng-readonly="disabled"
 			 ng-model="functionBody">
 		</div>
 	</div>
 	<div layout="row" layout-align="start center"  style="height: 40px;">
-		<span style="font-style: italic;">}</span>
-	</div>	   
-</div>
\ No newline at end of file
+		<label class="tb-title no-padding">}</label>
+	</div>
+</div>
diff --git a/ui/src/app/components/json-content.directive.js b/ui/src/app/components/json-content.directive.js
new file mode 100644
index 0000000..e945079
--- /dev/null
+++ b/ui/src/app/components/json-content.directive.js
@@ -0,0 +1,184 @@
+/*
+ * 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.
+ */
+import './json-content.scss';
+
+import 'brace/ext/language_tools';
+import 'brace/mode/json';
+import 'brace/mode/text';
+import 'ace-builds/src-min-noconflict/snippets/json';
+import 'ace-builds/src-min-noconflict/snippets/text';
+
+import fixAceEditor from './ace-editor-fix';
+
+/* eslint-disable import/no-unresolved, import/default */
+
+import jsonContentTemplate from './json-content.tpl.html';
+
+/* eslint-enable import/no-unresolved, import/default */
+
+import beautify from 'js-beautify';
+
+const js_beautify = beautify.js;
+
+export default angular.module('thingsboard.directives.jsonContent', [])
+    .directive('tbJsonContent', JsonContent)
+    .name;
+
+/*@ngInject*/
+function JsonContent($compile, $templateCache, toast, types, utils) {
+
+    var linker = function (scope, element, attrs, ngModelCtrl) {
+        var template = $templateCache.get(jsonContentTemplate);
+        element.html(template);
+
+        scope.label = attrs.label;
+
+        scope.validationTriggerArg = attrs.validationTriggerArg;
+
+        scope.contentValid = true;
+
+        scope.json_editor;
+
+        scope.onFullscreenChanged = function () {
+            updateEditorSize();
+        };
+
+        scope.beautifyJson = function () {
+            var res = js_beautify(scope.contentBody, {indent_size: 4, wrap_line_length: 60});
+            scope.contentBody = res;
+        };
+
+        function updateEditorSize() {
+            if (scope.json_editor) {
+                scope.json_editor.resize();
+                scope.json_editor.renderer.updateFull();
+            }
+        }
+
+        var mode;
+        if (scope.contentType) {
+            mode = types.contentType[scope.contentType].code;
+        } else {
+            mode = 'text';
+        }
+
+        scope.jsonEditorOptions = {
+            useWrapMode: true,
+            mode: mode,
+            advanced: {
+                enableSnippets: true,
+                enableBasicAutocompletion: true,
+                enableLiveAutocompletion: true
+            },
+            onLoad: function (_ace) {
+                scope.json_editor = _ace;
+                scope.json_editor.session.on("change", function () {
+                    scope.cleanupJsonErrors();
+                });
+                fixAceEditor(_ace);
+            }
+        };
+
+        scope.$watch('contentType', () => {
+            var mode;
+            if (scope.contentType) {
+                mode = types.contentType[scope.contentType].code;
+            } else {
+                mode = 'text';
+            }
+            if (scope.json_editor) {
+                scope.json_editor.session.setMode('ace/mode/' + mode);
+            }
+        });
+
+        scope.cleanupJsonErrors = function () {
+            toast.hide();
+        };
+
+        scope.updateValidity = function () {
+            ngModelCtrl.$setValidity('contentBody', scope.contentValid);
+        };
+
+        scope.$watch('contentBody', function (newContent, oldContent) {
+            ngModelCtrl.$setViewValue(scope.contentBody);
+            if (!angular.equals(newContent, oldContent)) {
+                scope.contentValid = true;
+            }
+            scope.updateValidity();
+        });
+
+        ngModelCtrl.$render = function () {
+            scope.contentBody = ngModelCtrl.$viewValue;
+        };
+
+        scope.showError = function (error) {
+            var toastParent = angular.element('#tb-json-panel', element);
+            toast.showError(error, toastParent, 'bottom left');
+        };
+
+        scope.validate = function () {
+            try {
+                if (scope.validateContent) {
+                    if (scope.contentType == types.contentType.JSON.value) {
+                        angular.fromJson(scope.contentBody);
+                    }
+                }
+                return true;
+            } catch (e) {
+                var details = utils.parseException(e);
+                var errorInfo = 'Error:';
+                if (details.name) {
+                    errorInfo += ' ' + details.name + ':';
+                }
+                if (details.message) {
+                    errorInfo += ' ' + details.message;
+                }
+                scope.showError(errorInfo);
+                return false;
+            }
+        };
+
+        scope.$on('form-submit', function (event, args) {
+            if (!scope.readonly) {
+                if (!args || scope.validationTriggerArg && scope.validationTriggerArg == args) {
+                    scope.cleanupJsonErrors();
+                    scope.contentValid = true;
+                    scope.updateValidity();
+                    scope.contentValid = scope.validate();
+                    scope.updateValidity();
+                }
+            }
+        });
+
+        scope.$on('update-ace-editor-size', function () {
+            updateEditorSize();
+        });
+
+        $compile(element.contents())(scope);
+    }
+
+    return {
+        restrict: "E",
+        require: "^ngModel",
+        scope: {
+            contentType: '=',
+            validateContent: '=?',
+            readonly:'=ngReadonly',
+            fillHeight:'=?'
+        },
+        link: linker
+    };
+}
diff --git a/ui/src/app/components/json-content.scss b/ui/src/app/components/json-content.scss
new file mode 100644
index 0000000..287c7e3
--- /dev/null
+++ b/ui/src/app/components/json-content.scss
@@ -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.
+ */
+tb-json-content {
+  position: relative;
+  .fill-height {
+    height: 100%;
+  }
+}
+
+.tb-json-content-toolbar {
+  .md-button.tidy {
+    color: #7B7B7B;
+    min-width: 32px;
+    min-height: 15px;
+    line-height: 15px;
+    font-size: 0.800rem;
+    margin: 0 5px 0 0;
+    padding: 4px;
+    background: rgba(220, 220, 220, 0.35);
+  }
+}
+
+.tb-json-content-panel {
+  margin-left: 15px;
+  border: 1px solid #C0C0C0;
+  height: 100%;
+  #tb-json-input {
+    min-width: 200px;
+    width: 100%;
+    height: 100%;
+    &:not(.fill-height) {
+      min-height: 200px;
+    }
+  }
+}
diff --git a/ui/src/app/components/json-content.tpl.html b/ui/src/app/components/json-content.tpl.html
new file mode 100644
index 0000000..a902b99
--- /dev/null
+++ b/ui/src/app/components/json-content.tpl.html
@@ -0,0 +1,34 @@
+<!--
+
+    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.
+
+-->
+<div style="background: #fff;" ng-class="{'fill-height': fillHeight}" tb-expand-fullscreen fullscreen-zindex="100" expand-button-id="expand-button" on-fullscreen-changed="onFullscreenChanged()" layout="column">
+    <div layout="row" layout-align="start center" style="height: 40px;" class="tb-json-content-toolbar">
+        <label class="tb-title no-padding">{{ label }}</label>
+        <span flex></span>
+        <md-button ng-if="!readonly" class="tidy" aria-label="{{ 'js-func.tidy' | translate }}" ng-click="beautifyJson()">{{
+            'js-func.tidy' | translate }}
+        </md-button>
+        <md-button id="expand-button" aria-label="Fullscreen" class="md-icon-button tb-md-32 tb-fullscreen-button-style"></md-button>
+    </div>
+    <div flex id="tb-json-panel" class="tb-json-content-panel" layout="column">
+        <div flex id="tb-json-input" ng-class="{'fill-height': fillHeight}"
+             ng-readonly="readonly"
+             ui-ace="jsonEditorOptions"
+             ng-model="contentBody">
+        </div>
+    </div>
+</div>
diff --git a/ui/src/app/components/json-object-edit.directive.js b/ui/src/app/components/json-object-edit.directive.js
new file mode 100644
index 0000000..215b7b9
--- /dev/null
+++ b/ui/src/app/components/json-object-edit.directive.js
@@ -0,0 +1,183 @@
+/*
+ * 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.
+ */
+import './json-object-edit.scss';
+
+import 'brace/ext/language_tools';
+import 'brace/mode/json';
+import 'ace-builds/src-min-noconflict/snippets/json';
+
+import fixAceEditor from './ace-editor-fix';
+
+/* eslint-disable import/no-unresolved, import/default */
+
+import jsonObjectEditTemplate from './json-object-edit.tpl.html';
+
+/* eslint-enable import/no-unresolved, import/default */
+
+export default angular.module('thingsboard.directives.jsonObjectEdit', [])
+    .directive('tbJsonObjectEdit', JsonObjectEdit)
+    .name;
+
+/*@ngInject*/
+function JsonObjectEdit($compile, $templateCache, $document, toast, utils) {
+
+    var linker = function (scope, element, attrs, ngModelCtrl) {
+        var template = $templateCache.get(jsonObjectEditTemplate);
+        element.html(template);
+
+        scope.label = attrs.label;
+
+        scope.objectValid = true;
+        scope.validationError = '';
+
+        scope.json_editor;
+
+        scope.onFullscreenChanged = function () {
+            updateEditorSize();
+        };
+
+        function updateEditorSize() {
+            if (scope.json_editor) {
+                scope.json_editor.resize();
+                scope.json_editor.renderer.updateFull();
+            }
+        }
+
+        scope.jsonEditorOptions = {
+            useWrapMode: true,
+            mode: 'json',
+            advanced: {
+                enableSnippets: true,
+                enableBasicAutocompletion: true,
+                enableLiveAutocompletion: true
+            },
+            onLoad: function (_ace) {
+                scope.json_editor = _ace;
+                scope.json_editor.session.on("change", function () {
+                    scope.cleanupJsonErrors();
+                });
+                fixAceEditor(_ace);
+            }
+        };
+
+        scope.cleanupJsonErrors = function () {
+            toast.hide();
+        };
+
+        scope.updateValidity = function () {
+            ngModelCtrl.$setValidity('objectValid', scope.objectValid);
+        };
+
+        scope.$watch('contentBody', function (newVal, prevVal) {
+            if (!angular.equals(newVal, prevVal)) {
+                var object = scope.validate();
+                if (scope.objectValid) {
+                    if (object == null) {
+                        scope.object = null;
+                    } else {
+                        if (scope.object == null) {
+                            scope.object = {};
+                        }
+                        Object.keys(scope.object).forEach(function (key) {
+                            delete scope.object[key];
+                        });
+                        Object.keys(object).forEach(function (key) {
+                            scope.object[key] = object[key];
+                        });
+                    }
+                    ngModelCtrl.$setViewValue(scope.object);
+                }
+                scope.updateValidity();
+            }
+        });
+
+        ngModelCtrl.$render = function () {
+            scope.object = ngModelCtrl.$viewValue;
+            var content = '';
+            try {
+                if (scope.object) {
+                    content = angular.toJson(scope.object, true);
+                }
+            } catch (e) {
+                //
+            }
+            scope.contentBody = content;
+        };
+
+        scope.showError = function (error) {
+            var toastParent = angular.element('#tb-json-panel', element);
+            toast.showError(error, toastParent, 'bottom left');
+        };
+
+        scope.validate = function () {
+            if (!scope.contentBody || !scope.contentBody.length) {
+                if (scope.required) {
+                    scope.validationError = 'Json object is required.';
+                    scope.objectValid = false;
+                } else {
+                    scope.validationError = '';
+                    scope.objectValid = true;
+                }
+                return null;
+            } else {
+                try {
+                    var object = angular.fromJson(scope.contentBody);
+                    scope.validationError = '';
+                    scope.objectValid = true;
+                    return object;
+                } catch (e) {
+                    var details = utils.parseException(e);
+                    var errorInfo = 'Error:';
+                    if (details.name) {
+                        errorInfo += ' ' + details.name + ':';
+                    }
+                    if (details.message) {
+                        errorInfo += ' ' + details.message;
+                    }
+                    scope.validationError = errorInfo;
+                    scope.objectValid = false;
+                    return null;
+                }
+            }
+        };
+
+        scope.$on('form-submit', function () {
+            if (!scope.readonly) {
+                scope.cleanupJsonErrors();
+                if (!scope.objectValid) {
+                    scope.showError(scope.validationError);
+                }
+            }
+        });
+
+        scope.$on('update-ace-editor-size', function () {
+            updateEditorSize();
+        });
+
+        $compile(element.contents())(scope);
+    }
+
+    return {
+        restrict: "E",
+        require: "^ngModel",
+        scope: {
+            required:'=ngRequired',
+            readonly:'=ngReadonly',
+            fillHeight:'=?'
+        },
+        link: linker
+    };
+}
diff --git a/ui/src/app/components/json-object-edit.tpl.html b/ui/src/app/components/json-object-edit.tpl.html
new file mode 100644
index 0000000..ebab3c7
--- /dev/null
+++ b/ui/src/app/components/json-object-edit.tpl.html
@@ -0,0 +1,34 @@
+<!--
+
+    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.
+
+-->
+<div style="background: #fff;" ng-class="{'fill-height': fillHeight}" tb-expand-fullscreen fullscreen-zindex="100" expand-button-id="expand-button" on-fullscreen-changed="onFullscreenChanged()" layout="column">
+    <div layout="row" layout-align="start center">
+        <label class="tb-title no-padding"
+               ng-class="{'tb-required': required,
+                          'tb-readonly': readonly,
+                          'tb-error': !objectValid}">{{ label }}</label>
+        <span flex></span>
+        <md-button id="expand-button" aria-label="Fullscreen" class="md-icon-button tb-md-32 tb-fullscreen-button-style"></md-button>
+    </div>
+    <div flex id="tb-json-panel" class="tb-json-object-panel" layout="column">
+        <div flex id="tb-json-input" ng-class="{'fill-height': fillHeight}"
+             ng-readonly="readonly"
+             ui-ace="jsonEditorOptions"
+             ng-model="contentBody">
+        </div>
+    </div>
+</div>
diff --git a/ui/src/app/components/kv-map.directive.js b/ui/src/app/components/kv-map.directive.js
new file mode 100644
index 0000000..bc8865f
--- /dev/null
+++ b/ui/src/app/components/kv-map.directive.js
@@ -0,0 +1,119 @@
+/*
+ * 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.
+ */
+import './kv-map.scss';
+
+/* eslint-disable import/no-unresolved, import/default */
+
+import kvMapTemplate from './kv-map.tpl.html';
+
+/* eslint-enable import/no-unresolved, import/default */
+
+export default angular.module('thingsboard.directives.keyValMap', [])
+    .directive('tbKeyValMap', KeyValMap)
+    .name;
+
+/*@ngInject*/
+function KeyValMap() {
+    return {
+        restrict: "E",
+        scope: true,
+        bindToController: {
+            disabled:'=ngDisabled',
+            titleText: '@?',
+            keyPlaceholderText: '@?',
+            valuePlaceholderText: '@?',
+            noDataText: '@?',
+            keyValMap: '='
+        },
+        controller: KeyValMapController,
+        controllerAs: 'vm',
+        templateUrl: kvMapTemplate
+    };
+}
+
+/*@ngInject*/
+function KeyValMapController($scope, $mdUtil) {
+
+    let vm = this;
+
+    vm.kvList = [];
+
+    vm.removeKeyVal = removeKeyVal;
+    vm.addKeyVal = addKeyVal;
+
+    $scope.$watch('vm.keyValMap', () => {
+        stopWatchKvList();
+        vm.kvList.length = 0;
+        if (vm.keyValMap) {
+            for (var property in vm.keyValMap) {
+                if (vm.keyValMap.hasOwnProperty(property)) {
+                    vm.kvList.push(
+                        {
+                            key: property + '',
+                            value: vm.keyValMap[property]
+                        }
+                    );
+                }
+            }
+        }
+        $mdUtil.nextTick(() => {
+            watchKvList();
+        });
+    });
+
+    function watchKvList() {
+        $scope.kvListWatcher = $scope.$watch('vm.kvList', () => {
+            if (!vm.keyValMap) {
+                return;
+            }
+            for (var property in vm.keyValMap) {
+                if (vm.keyValMap.hasOwnProperty(property)) {
+                    delete vm.keyValMap[property];
+                }
+            }
+            for (var i=0;i<vm.kvList.length;i++) {
+                var entry = vm.kvList[i];
+                vm.keyValMap[entry.key] = entry.value;
+            }
+        }, true);
+    }
+
+    function stopWatchKvList() {
+        if ($scope.kvListWatcher) {
+            $scope.kvListWatcher();
+            $scope.kvListWatcher = null;
+        }
+    }
+
+
+    function removeKeyVal(index) {
+        if (index > -1) {
+            vm.kvList.splice(index, 1);
+        }
+    }
+
+    function addKeyVal() {
+        if (!vm.kvList) {
+            vm.kvList = [];
+        }
+        vm.kvList.push(
+            {
+                key: '',
+                value: ''
+            }
+        );
+    }
+}
diff --git a/ui/src/app/components/kv-map.tpl.html b/ui/src/app/components/kv-map.tpl.html
new file mode 100644
index 0000000..3e550f3
--- /dev/null
+++ b/ui/src/app/components/kv-map.tpl.html
@@ -0,0 +1,58 @@
+<!--
+
+    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.
+
+-->
+<section layout="column" class="tb-kv-map">
+    <label translate class="tb-title no-padding">{{ vm.titleText }}</label>
+    <div layout="row"
+         ng-repeat="keyVal in vm.kvList track by $index"
+         style="max-height: 40px;" layout-align="start center">
+        <md-input-container flex md-no-float class="md-block"
+                            style="margin: 10px 0px 0px 0px; max-height: 40px;">
+            <input placeholder="{{ (vm.keyPlaceholderText ? vm.keyPlaceholderText : 'key-val.key') | translate }}"
+                   ng-disabled="vm.disabled" ng-required="true" name="key" ng-model="keyVal.key">
+        </md-input-container>
+        <md-input-container flex md-no-float class="md-block"
+                            style="margin: 10px 0px 0px 0px; max-height: 40px;">
+            <input placeholder="{{ (vm.valuePlaceholderText ? vm.valuePlaceholderText : 'key-val.value') | translate }}"
+                   ng-disabled="vm.disabled" ng-required="true" name="value" ng-model="keyVal.value">
+        </md-input-container>
+        <md-button ng-show="!vm.disabled" ng-disabled="$root.loading" class="md-icon-button md-primary"
+                   ng-click="vm.removeKeyVal($index)"
+                   aria-label="{{ 'action.remove' | translate }}">
+            <md-tooltip md-direction="top">
+                {{ 'key-val.remove-entry' | translate }}
+            </md-tooltip>
+            <md-icon aria-label="{{ 'action.delete' | translate }}"
+                     class="material-icons">
+                close
+            </md-icon>
+        </md-button>
+    </div>
+    <span ng-show="!vm.kvList.length"
+          layout-align="center center" ng-class="{'disabled': vm.disabled}"
+          class="no-data-found" translate>{{vm.noDataText ? vm.noDataText : 'key-val.no-data'}}</span>
+    <div>
+        <md-button ng-show="!vm.disabled" ng-disabled="$root.loading" class="md-primary md-raised"
+                   ng-click="vm.addKeyVal()"
+                   aria-label="{{ 'action.add' | translate }}">
+            <md-tooltip md-direction="top">
+                {{ 'key-val.add-entry' | translate }}
+            </md-tooltip>
+            <span translate>action.add</span>
+        </md-button>
+    </div>
+</section>
diff --git a/ui/src/app/components/mousepoint-menu.directive.js b/ui/src/app/components/mousepoint-menu.directive.js
index 2a0ab52..9015268 100644
--- a/ui/src/app/components/mousepoint-menu.directive.js
+++ b/ui/src/app/components/mousepoint-menu.directive.js
@@ -27,7 +27,12 @@ function MousepointMenu() {
                 var offset = $element.offset();
                 var x = $event.pageX - offset.left;
                 var y = $event.pageY - offset.top;
-
+                if ($attrs.tbOffsetX) {
+                    x += Number($attrs.tbOffsetX);
+                }
+                if ($attrs.tbOffsetY) {
+                    y += Number($attrs.tbOffsetY);
+                }
                 var offsets = {
                     left: x,
                     top: y
diff --git a/ui/src/app/components/react/json-form-ace-editor.jsx b/ui/src/app/components/react/json-form-ace-editor.jsx
index 1c4c02e..5afd3d1 100644
--- a/ui/src/app/components/react/json-form-ace-editor.jsx
+++ b/ui/src/app/components/react/json-form-ace-editor.jsx
@@ -23,6 +23,8 @@ import FlatButton from 'material-ui/FlatButton';
 import 'brace/ext/language_tools';
 import 'brace/theme/github';
 
+import fixAceEditor from './../ace-editor-fix';
+
 class ThingsboardAceEditor extends React.Component {
 
     constructor(props) {
@@ -31,6 +33,7 @@ class ThingsboardAceEditor extends React.Component {
         this.onBlur = this.onBlur.bind(this);
         this.onFocus = this.onFocus.bind(this);
         this.onTidy = this.onTidy.bind(this);
+        this.onLoad = this.onLoad.bind(this);
         var value = props.value ? props.value + '' : '';
         this.state = {
             value: value,
@@ -72,6 +75,10 @@ class ThingsboardAceEditor extends React.Component {
         }
     }
 
+    onLoad(editor) {
+        fixAceEditor(editor);
+    }
+
     render() {
 
         const styles = reactCSS({
@@ -117,6 +124,7 @@ class ThingsboardAceEditor extends React.Component {
                                onChange={this.onValueChanged}
                                onFocus={this.onFocus}
                                onBlur={this.onBlur}
+                               onLoad={this.onLoad}
                                name={this.props.form.title}
                                value={this.state.value}
                                readOnly={this.props.form.readonly}
diff --git a/ui/src/app/components/widget/widget-config.directive.js b/ui/src/app/components/widget/widget-config.directive.js
index d0ee6a3..4d4d958 100644
--- a/ui/src/app/components/widget/widget-config.directive.js
+++ b/ui/src/app/components/widget/widget-config.directive.js
@@ -23,6 +23,8 @@ import thingsboardJsonForm from '../json-form.directive';
 import thingsboardManageWidgetActions from './action/manage-widget-actions.directive';
 import 'angular-ui-ace';
 
+import fixAceEditor from './../ace-editor-fix';
+
 import './widget-config.scss';
 
 /* eslint-disable import/no-unresolved, import/default */
@@ -72,6 +74,9 @@ function WidgetConfig($compile, $templateCache, $rootScope, $translate, $timeout
                 enableSnippets: true,
                 enableBasicAutocompletion: true,
                 enableLiveAutocompletion: true
+            },
+            onLoad: function (_ace) {
+                fixAceEditor(_ace);
             }
         };
 
diff --git a/ui/src/app/entity/entity-autocomplete.directive.js b/ui/src/app/entity/entity-autocomplete.directive.js
index c2053c0..e46c614 100644
--- a/ui/src/app/entity/entity-autocomplete.directive.js
+++ b/ui/src/app/entity/entity-autocomplete.directive.js
@@ -131,17 +131,11 @@ export default function EntityAutocomplete($compile, $templateCache, $q, $filter
                     scope.noEntitiesMatchingText = 'device.no-devices-matching';
                     scope.entityRequiredText = 'device.device-required';
                     break;
-                case types.entityType.rule:
-                    scope.selectEntityText = 'rule.select-rule';
-                    scope.entityText = 'rule.rule';
-                    scope.noEntitiesMatchingText = 'rule.no-rules-matching';
-                    scope.entityRequiredText = 'rule.rule-required';
-                    break;
-                case types.entityType.plugin:
-                    scope.selectEntityText = 'plugin.select-plugin';
-                    scope.entityText = 'plugin.plugin';
-                    scope.noEntitiesMatchingText = 'plugin.no-plugins-matching';
-                    scope.entityRequiredText = 'plugin.plugin-required';
+                case types.entityType.rulechain:
+                    scope.selectEntityText = 'rulechain.select-rulechain';
+                    scope.entityText = 'rulechain.rulechain';
+                    scope.noEntitiesMatchingText = 'rulechain.no-rulechains-matching';
+                    scope.entityRequiredText = 'rulechain.rulechain-required';
                     break;
                 case types.entityType.tenant:
                     scope.selectEntityText = 'tenant.select-tenant';
diff --git a/ui/src/app/entity/entity-type-list.tpl.html b/ui/src/app/entity/entity-type-list.tpl.html
index eb32124..d7d3918 100644
--- a/ui/src/app/entity/entity-type-list.tpl.html
+++ b/ui/src/app/entity/entity-type-list.tpl.html
@@ -16,8 +16,7 @@
 
 -->
 <section flex layout='column' class="tb-entity-type-list">
-    <md-chips flex
-              readonly="disabled"
+    <md-chips readonly="disabled"
               id="entity_type_list_chips"
               ng-required="tbRequired"
               ng-model="entityTypeList"
diff --git a/ui/src/app/entity/relation/relation-filters.directive.js b/ui/src/app/entity/relation/relation-filters.directive.js
index 00d3b26..9ab66ca 100644
--- a/ui/src/app/entity/relation/relation-filters.directive.js
+++ b/ui/src/app/entity/relation/relation-filters.directive.js
@@ -46,6 +46,7 @@ export default function RelationFilters($compile, $templateCache) {
         ngModelCtrl.$render = function () {
             if (ngModelCtrl.$viewValue) {
                 var value = ngModelCtrl.$viewValue;
+                scope.relationFilters.length = 0;
                 value.forEach(function (filter) {
                     scope.relationFilters.push(filter);
                 });
diff --git a/ui/src/app/entity/relation/relation-filters.scss b/ui/src/app/entity/relation/relation-filters.scss
index 649879d..34254d0 100644
--- a/ui/src/app/entity/relation/relation-filters.scss
+++ b/ui/src/app/entity/relation/relation-filters.scss
@@ -51,9 +51,6 @@
       md-chips-wrap {
         padding: 0px;
         margin: 0 0 24px;
-        .md-chip-input-container {
-          margin: 0;
-        }
         md-autocomplete {
           height: 30px;
           md-autocomplete-wrap {
diff --git a/ui/src/app/event/event.scss b/ui/src/app/event/event.scss
index b3be35c..b0fc46f 100644
--- a/ui/src/app/event/event.scss
+++ b/ui/src/app/event/event.scss
@@ -24,6 +24,17 @@ md-list.tb-event-table {
     height: 48px;
     padding: 0px;
     overflow: hidden;
+    .tb-cell {
+      text-overflow: ellipsis;
+      &.tb-scroll {
+        white-space: nowrap;
+        overflow-y: hidden;
+        overflow-x: auto;
+      }
+      &.tb-nowrap {
+        white-space: nowrap;
+      }
+    }
   }
 
   .tb-row:hover {
@@ -39,13 +50,19 @@ md-list.tb-event-table {
         color: rgba(0,0,0,.54);
         font-size: 12px;
         font-weight: 700;
-        white-space: nowrap;
         background: none;
+        white-space: nowrap;
       }
   }
 
   .tb-cell {
-      padding: 0 24px;
+      &:first-child {
+        padding-left: 14px;
+      }
+      &:last-child {
+        padding-right: 14px;
+      }
+      padding: 0 6px;
       margin: auto 0;
       color: rgba(0,0,0,.87);
       font-size: 13px;
@@ -53,8 +70,8 @@ md-list.tb-event-table {
       text-align: left;
       overflow: hidden;
       .md-button {
-        padding: 0;
-        margin: 0;
+          padding: 0;
+          margin: 0;
       }
   }
 
diff --git a/ui/src/app/event/event-content-dialog.controller.js b/ui/src/app/event/event-content-dialog.controller.js
index 108f95e..b780b78 100644
--- a/ui/src/app/event/event-content-dialog.controller.js
+++ b/ui/src/app/event/event-content-dialog.controller.js
@@ -17,11 +17,14 @@ import $ from 'jquery';
 import 'brace/ext/language_tools';
 import 'brace/mode/java';
 import 'brace/theme/github';
+import beautify from 'js-beautify';
 
 /* eslint-disable angular/angularelement */
 
+const js_beautify = beautify.js;
+
 /*@ngInject*/
-export default function EventContentDialogController($mdDialog, content, title, showingCallback) {
+export default function EventContentDialogController($mdDialog, types, content, contentType, title, showingCallback) {
 
     var vm = this;
 
@@ -32,9 +35,19 @@ export default function EventContentDialogController($mdDialog, content, title, 
     vm.content = content;
     vm.title = title;
 
+    var mode;
+    if (contentType) {
+        mode = types.contentType[contentType].code;
+        if (contentType == types.contentType.JSON.value && vm.content) {
+            vm.content = js_beautify(vm.content, {indent_size: 4});
+        }
+    } else {
+        mode = 'java';
+    }
+
     vm.contentOptions = {
         useWrapMode: false,
-        mode: 'java',
+        mode: mode,
         showGutter: false,
         showPrintMargin: false,
         theme: 'github',
@@ -55,7 +68,7 @@ export default function EventContentDialogController($mdDialog, content, title, 
             var lines = vm.content.split('\n');
             newHeight = 16 * lines.length + 16;
             var maxLineLength = 0;
-            for (var i in lines) {
+            for (var i = 0; i < lines.length; i++) {
                 var line = lines[i].replace(/\t/g, '    ').replace(/\n/g, '');
                 var lineLength = line.length;
                 maxLineLength = Math.max(maxLineLength, lineLength);
diff --git a/ui/src/app/event/event-header.directive.js b/ui/src/app/event/event-header.directive.js
index afac804..bc4cdbe 100644
--- a/ui/src/app/event/event-header.directive.js
+++ b/ui/src/app/event/event-header.directive.js
@@ -18,6 +18,7 @@
 import eventHeaderLcEventTemplate from './event-header-lc-event.tpl.html';
 import eventHeaderStatsTemplate from './event-header-stats.tpl.html';
 import eventHeaderErrorTemplate from './event-header-error.tpl.html';
+import eventHeaderDebugRuleNodeTemplate from './event-header-debug-rulenode.tpl.html';
 
 /* eslint-enable import/no-unresolved, import/default */
 
@@ -38,6 +39,12 @@ export default function EventHeaderDirective($compile, $templateCache, types) {
                 case types.eventType.error.value:
                     template = eventHeaderErrorTemplate;
                     break;
+                case types.debugEventType.debugRuleNode.value:
+                    template = eventHeaderDebugRuleNodeTemplate;
+                    break;
+                case types.debugEventType.debugRuleChain.value:
+                    template = eventHeaderDebugRuleNodeTemplate;
+                    break;
             }
             return $templateCache.get(template);
         }
diff --git a/ui/src/app/event/event-header-debug-rulenode.tpl.html b/ui/src/app/event/event-header-debug-rulenode.tpl.html
new file mode 100644
index 0000000..bbf6ae0
--- /dev/null
+++ b/ui/src/app/event/event-header-debug-rulenode.tpl.html
@@ -0,0 +1,27 @@
+<!--
+
+    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.
+
+-->
+<div hide-xs hide-sm translate class="tb-cell" flex="25">event.event-time</div>
+<div translate class="tb-cell" flex="20">event.server</div>
+<div translate class="tb-cell" flex="10">event.type</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-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.metadata</div>
+<div translate class="tb-cell" flex="10">event.error</div>
diff --git a/ui/src/app/event/event-row.directive.js b/ui/src/app/event/event-row.directive.js
index f005542..b808fb8 100644
--- a/ui/src/app/event/event-row.directive.js
+++ b/ui/src/app/event/event-row.directive.js
@@ -20,6 +20,7 @@ import eventErrorDialogTemplate from './event-content-dialog.tpl.html';
 import eventRowLcEventTemplate from './event-row-lc-event.tpl.html';
 import eventRowStatsTemplate from './event-row-stats.tpl.html';
 import eventRowErrorTemplate from './event-row-error.tpl.html';
+import eventRowDebugRuleNodeTemplate from './event-row-debug-rulenode.tpl.html';
 
 /* eslint-enable import/no-unresolved, import/default */
 
@@ -40,6 +41,12 @@ export default function EventRowDirective($compile, $templateCache, $mdDialog, $
                 case types.eventType.error.value:
                     template = eventRowErrorTemplate;
                     break;
+                case types.debugEventType.debugRuleNode.value:
+                    template = eventRowDebugRuleNodeTemplate;
+                    break;
+                case types.debugEventType.debugRuleChain.value:
+                    template = eventRowDebugRuleNodeTemplate;
+                    break;
             }
             return $templateCache.get(template);
         }
@@ -53,17 +60,22 @@ export default function EventRowDirective($compile, $templateCache, $mdDialog, $
             scope.loadTemplate();
         });
 
+        scope.types = types;
+
         scope.event = attrs.event;
 
-        scope.showContent = function($event, content, title) {
+        scope.showContent = function($event, content, title, contentType) {
             var onShowingCallback = {
                 onShowing: function(){}
             }
+            if (!contentType) {
+                contentType = null;
+            }
             $mdDialog.show({
                 controller: 'EventContentDialogController',
                 controllerAs: 'vm',
                 templateUrl: eventErrorDialogTemplate,
-                locals: {content: content, title: title, showingCallback: onShowingCallback},
+                locals: {content: content, title: title, contentType: contentType, showingCallback: onShowingCallback},
                 parent: angular.element($document[0].body),
                 fullscreen: true,
                 targetEvent: $event,
@@ -74,6 +86,14 @@ export default function EventRowDirective($compile, $templateCache, $mdDialog, $
             });
         }
 
+        scope.checkTooltip = function($event) {
+            var el = $event.target;
+            var $el = angular.element(el);
+            if(el.offsetWidth < el.scrollWidth && !$el.attr('title')){
+                $el.attr('title', $el.text());
+            }
+        }
+
         $compile(element.contents())(scope);
     }
 
diff --git a/ui/src/app/event/event-row-debug-rulenode.tpl.html b/ui/src/app/event/event-row-debug-rulenode.tpl.html
new file mode 100644
index 0000000..857cd8c
--- /dev/null
+++ b/ui/src/app/event/event-row-debug-rulenode.tpl.html
@@ -0,0 +1,63 @@
+<!--
+
+    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.
+
+-->
+<div hide-xs hide-sm class="tb-cell" flex="25">{{event.createdTime | date :  'yyyy-MM-dd HH:mm:ss'}}</div>
+<div class="tb-cell" flex="20">{{event.body.server}}</div>
+<div class="tb-cell" flex="10">{{event.body.type}}</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" flex="20" ng-mouseenter="checkTooltip($event)">{{event.body.msgType}}</div>
+<div class="tb-cell" flex="15">{{event.body.relationType}}</div>
+<div class="tb-cell" flex="10">
+    <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)"
+               aria-label="{{ 'action.view' | translate }}">
+        <md-tooltip md-direction="top">
+            {{ 'action.view' | translate }}
+        </md-tooltip>
+        <md-icon aria-label="{{ 'action.view' | translate }}"
+                 class="material-icons">
+            more_horiz
+        </md-icon>
+    </md-button>
+</div>
+<div class="tb-cell" flex="10">
+    <md-button ng-if="event.body.metadata" class="md-icon-button md-primary"
+               ng-click="showContent($event, event.body.metadata, 'event.metadata', 'JSON')"
+               aria-label="{{ 'action.view' | translate }}">
+        <md-tooltip md-direction="top">
+            {{ 'action.view' | translate }}
+        </md-tooltip>
+        <md-icon aria-label="{{ 'action.view' | translate }}"
+                 class="material-icons">
+            more_horiz
+        </md-icon>
+    </md-button>
+</div>
+<div class="tb-cell" flex="10">
+    <md-button ng-if="event.body.error" class="md-icon-button md-primary"
+               ng-click="showContent($event, event.body.error, 'event.error')"
+               aria-label="{{ 'action.view' | translate }}">
+        <md-tooltip md-direction="top">
+            {{ 'action.view' | translate }}
+        </md-tooltip>
+        <md-icon aria-label="{{ 'action.view' | translate }}"
+                 class="material-icons">
+            more_horiz
+        </md-icon>
+    </md-button>
+</div>
diff --git a/ui/src/app/event/event-table.directive.js b/ui/src/app/event/event-table.directive.js
index 4291014..c61078d 100644
--- a/ui/src/app/event/event-table.directive.js
+++ b/ui/src/app/event/event-table.directive.js
@@ -36,8 +36,8 @@ export default function EventTableDirective($compile, $templateCache, $rootScope
             for (var type in types.eventType) {
                 var eventType = types.eventType[type];
                 var enabled = true;
-                for (var disabledType in disabledEventTypes) {
-                    if (eventType.value === disabledEventTypes[disabledType]) {
+                for (var i=0;i<disabledEventTypes.length;i++) {
+                    if (eventType.value === disabledEventTypes[i]) {
                         enabled = false;
                         break;
                     }
@@ -47,7 +47,19 @@ export default function EventTableDirective($compile, $templateCache, $rootScope
                 }
             }
         } else {
-            scope.eventTypes = types.eventType;
+            scope.eventTypes = angular.copy(types.eventType);
+        }
+
+        if (attrs.debugEventTypes) {
+            var debugEventTypes = attrs.debugEventTypes.split(',');
+            for (i=0;i<debugEventTypes.length;i++) {
+                for (type in types.debugEventType) {
+                    eventType = types.debugEventType[type];
+                    if (eventType.value === debugEventTypes[i]) {
+                        scope.eventTypes[type] = eventType;
+                    }
+                }
+            }
         }
 
         scope.eventType = attrs.defaultEventType;
diff --git a/ui/src/app/help/help.directive.js b/ui/src/app/help/help.directive.js
index bc7e84f..9227d44 100644
--- a/ui/src/app/help/help.directive.js
+++ b/ui/src/app/help/help.directive.js
@@ -35,6 +35,10 @@ function Help($compile, $window, helpLinks) {
                 $event.stopPropagation();
             }
             var helpUrl = helpLinks.linksMap[scope.helpLinkId];
+            if (!helpUrl && scope.helpLinkId &&
+                    (scope.helpLinkId.startsWith('http://') || scope.helpLinkId.startsWith('https://'))) {
+                helpUrl = scope.helpLinkId;
+            }
             if (helpUrl) {
                 $window.open(helpUrl, '_blank');
             }
diff --git a/ui/src/app/help/help-links.constant.js b/ui/src/app/help/help-links.constant.js
index 9b1f84f..37ed579 100644
--- a/ui/src/app/help/help-links.constant.js
+++ b/ui/src/app/help/help-links.constant.js
@@ -13,36 +13,37 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-var pluginClazzHelpLinkMap = {
-    'org.thingsboard.server.extensions.core.plugin.messaging.DeviceMessagingPlugin': 'pluginDeviceMessaging',
-    'org.thingsboard.server.extensions.core.plugin.telemetry.TelemetryStoragePlugin': 'pluginTelemetryStorage',
-    'org.thingsboard.server.extensions.core.plugin.rpc.RpcPlugin': 'pluginRpcPlugin',
-    'org.thingsboard.server.extensions.core.plugin.mail.MailPlugin': 'pluginMailPlugin',
-    'org.thingsboard.server.extensions.rest.plugin.RestApiCallPlugin': 'pluginRestApiCallPlugin',
-    'org.thingsboard.server.extensions.core.plugin.time.TimePlugin': 'pluginTimePlugin',
-    'org.thingsboard.server.extensions.kafka.plugin.KafkaPlugin': 'pluginKafkaPlugin',
-    'org.thingsboard.server.extensions.rabbitmq.plugin.RabbitMqPlugin': 'pluginRabbitMqPlugin'
 
-};
-
-var filterClazzHelpLinkMap = {
-    'org.thingsboard.server.extensions.core.filter.MsgTypeFilter': 'filterMsgType',
-    'org.thingsboard.server.extensions.core.filter.DeviceTelemetryFilter': 'filterDeviceTelemetry',
-    'org.thingsboard.server.extensions.core.filter.MethodNameFilter': 'filterMethodName',
-    'org.thingsboard.server.extensions.core.filter.DeviceAttributesFilter': 'filterDeviceAttributes'
-};
-
-var processorClazzHelpLinkMap = {
-    'org.thingsboard.server.extensions.core.processor.AlarmDeduplicationProcessor': 'processorAlarmDeduplication'
-};
-
-var pluginActionsClazzHelpLinkMap = {
-    'org.thingsboard.server.extensions.core.action.rpc.RpcPluginAction': 'pluginActionRpc',
-    'org.thingsboard.server.extensions.core.action.mail.SendMailAction': 'pluginActionSendMail',
-    'org.thingsboard.server.extensions.core.action.telemetry.TelemetryPluginAction': 'pluginActionTelemetry',
-    'org.thingsboard.server.extensions.kafka.action.KafkaPluginAction': 'pluginActionKafka',
-    'org.thingsboard.server.extensions.rabbitmq.action.RabbitMqPluginAction': 'pluginActionRabbitMq',
-    'org.thingsboard.server.extensions.rest.action.RestApiCallPluginAction': 'pluginActionRestApiCall'
+var ruleNodeClazzHelpLinkMap = {
+    'org.thingsboard.rule.engine.filter.TbJsFilterNode': 'ruleNodeJsFilter',
+    'org.thingsboard.rule.engine.filter.TbJsSwitchNode': 'ruleNodeJsSwitch',
+    'org.thingsboard.rule.engine.filter.TbMsgTypeFilterNode': 'ruleNodeMessageTypeFilter',
+    'org.thingsboard.rule.engine.filter.TbMsgTypeSwitchNode': 'ruleNodeMessageTypeSwitch',
+    'org.thingsboard.rule.engine.filter.TbOriginatorTypeSwitchNode': 'ruleNodeOriginatorTypeSwitch',
+    'org.thingsboard.rule.engine.metadata.TbGetAttributesNode': 'ruleNodeOriginatorAttributes',
+    'org.thingsboard.rule.engine.metadata.TbGetCustomerAttributeNode': 'ruleNodeCustomerAttributes',
+    'org.thingsboard.rule.engine.metadata.TbGetDeviceAttrNode': 'ruleNodeDeviceAttributes',
+    'org.thingsboard.rule.engine.metadata.TbGetRelatedAttributeNode': 'ruleNodeRelatedAttributes',
+    'org.thingsboard.rule.engine.metadata.TbGetTenantAttributeNode': 'ruleNodeTenantAttributes',
+    'org.thingsboard.rule.engine.transform.TbChangeOriginatorNode': 'ruleNodeChangeOriginator',
+    'org.thingsboard.rule.engine.transform.TbTransformMsgNode': 'ruleNodeTransformMsg',
+    'org.thingsboard.rule.engine.mail.TbMsgToEmailNode': 'ruleNodeMsgToEmail',
+    'org.thingsboard.rule.engine.action.TbClearAlarmNode': 'ruleNodeClearAlarm',
+    'org.thingsboard.rule.engine.action.TbCreateAlarmNode': 'ruleNodeCrateAlarm',
+    'org.thingsboard.rule.engine.debug.TbMsgGeneratorNode': 'ruleNodeMsgGenerator',
+    'org.thingsboard.rule.engine.action.TbLogNode': 'ruleNodeLog',
+    'org.thingsboard.rule.engine.rpc.TbSendRPCReplyNode': 'ruleNodeRpcCallReply',
+    'org.thingsboard.rule.engine.rpc.TbSendRPCRequestNode': 'ruleNodeRpcCallRequest',
+    'org.thingsboard.rule.engine.telemetry.TbMsgAttributesNode': 'ruleNodeSaveAttributes',
+    'org.thingsboard.rule.engine.telemetry.TbMsgTimeseriesNode': 'ruleNodeSaveTimeseries',
+    'tb.internal.RuleChain': 'ruleNodeRuleChain',
+    'org.thingsboard.rule.engine.aws.sns.TbSnsNode': 'ruleNodeAwsSns',
+    'org.thingsboard.rule.engine.aws.sqs.TbSqsNode': 'ruleNodeAwsSqs',
+    'org.thingsboard.rule.engine.kafka.TbKafkaNode': 'ruleNodeKafka',
+    'org.thingsboard.rule.engine.mqtt.TbMqttNode': 'ruleNodeMqtt',
+    'org.thingsboard.rule.engine.rabbitmq.TbRabbitMqNode': 'ruleNodeRabbitMq',
+    'org.thingsboard.rule.engine.rest.TbRestApiCallNode': 'ruleNodeRestApiCall',
+    'org.thingsboard.rule.engine.mail.TbSendEmailNode': 'ruleNodeSendEmail'
 };
 
 var helpBaseUrl = "https://thingsboard.io";
@@ -52,30 +53,37 @@ export default angular.module('thingsboard.help', [])
         {
             linksMap: {
                 outgoingMailSettings: helpBaseUrl + "/docs/user-guide/ui/mail-settings",
-                plugins: helpBaseUrl + "/docs/user-guide/rule-engine/#plugins",
-                pluginDeviceMessaging: helpBaseUrl + "/docs/reference/plugins/messaging/",
-                pluginTelemetryStorage: helpBaseUrl + "/docs/reference/plugins/telemetry/",
-                pluginRpcPlugin: helpBaseUrl + "/docs/reference/plugins/rpc/",
-                pluginMailPlugin: helpBaseUrl + "/docs/reference/plugins/mail/",
-                pluginRestApiCallPlugin: helpBaseUrl + "/docs/reference/plugins/rest/",
-                pluginTimePlugin: helpBaseUrl + "/docs/reference/plugins/time/",
-                pluginKafkaPlugin: helpBaseUrl + "/docs/reference/plugins/kafka/",
-                pluginRabbitMqPlugin: helpBaseUrl + "/docs/reference/plugins/rabbitmq/",
-                rules: helpBaseUrl + "/docs/user-guide/rule-engine/#rules",
-                filters: helpBaseUrl + "/docs/user-guide/rule-engine/#filters",
-                filterMsgType: helpBaseUrl + "/docs/reference/filters/message-type-filter",
-                filterDeviceTelemetry: helpBaseUrl + "/docs/reference/filters/device-telemetry-filter",
-                filterMethodName: helpBaseUrl + "/docs/reference/filters/method-name-filter/",
-                filterDeviceAttributes: helpBaseUrl + "/docs/reference/filters/device-attributes-filter",
-                processors: helpBaseUrl + "/docs/user-guide/rule-engine/#processors",
-                processorAlarmDeduplication: "http://thingsboard.io/docs/#q=processorAlarmDeduplication",
-                pluginActions: helpBaseUrl + "/docs/user-guide/rule-engine/#actions",
-                pluginActionRpc: helpBaseUrl + "/docs/reference/actions/rpc-plugin-action",
-                pluginActionSendMail: helpBaseUrl + "/docs/reference/actions/send-mail-action",
-                pluginActionTelemetry: helpBaseUrl + "/docs/reference/actions/telemetry-plugin-action/",
-                pluginActionKafka: helpBaseUrl + "/docs/reference/actions/kafka-plugin-action",
-                pluginActionRabbitMq: helpBaseUrl + "/docs/reference/actions/rabbitmq-plugin-action",
-                pluginActionRestApiCall: helpBaseUrl + "/docs/reference/actions/rest-api-call-plugin-action",
+                ruleEngine: helpBaseUrl + "/docs/user-guide/rule-engine-2-0/overview/",
+                ruleNodeJsFilter: helpBaseUrl + "/docs/user-guide/rule-engine-2-0/filter-nodes/#script-filter-node",
+                ruleNodeJsSwitch: helpBaseUrl + "/docs/user-guide/rule-engine-2-0/filter-nodes/#switch-node",
+                ruleNodeMessageTypeFilter: helpBaseUrl + "/docs/user-guide/rule-engine-2-0/filter-nodes/#message-type-filter-node",
+                ruleNodeMessageTypeSwitch: helpBaseUrl + "/docs/user-guide/rule-engine-2-0/filter-nodes/#message-type-switch-node",
+                ruleNodeOriginatorTypeSwitch: helpBaseUrl + "/docs/user-guide/rule-engine-2-0/filter-nodes/#originator-type-switch-node",
+                ruleNodeOriginatorAttributes: helpBaseUrl + "/docs/user-guide/rule-engine-2-0/enrichment-nodes/#originator-attributes",
+                ruleNodeCustomerAttributes: helpBaseUrl + "/docs/user-guide/rule-engine-2-0/enrichment-nodes/#customer-attributes",
+                ruleNodeDeviceAttributes: helpBaseUrl + "/docs/user-guide/rule-engine-2-0/enrichment-nodes/#device-attributes",
+                ruleNodeRelatedAttributes: helpBaseUrl + "/docs/user-guide/rule-engine-2-0/enrichment-nodes/#related-attributes",
+                ruleNodeTenantAttributes: helpBaseUrl + "/docs/user-guide/rule-engine-2-0/enrichment-nodes/#tenant-attributes",
+                ruleNodeChangeOriginator: helpBaseUrl + "/docs/user-guide/rule-engine-2-0/transformation-nodes/#change-originator",
+                ruleNodeTransformMsg: helpBaseUrl + "/docs/user-guide/rule-engine-2-0/transformation-nodes/#script-transformation-node",
+                ruleNodeMsgToEmail: helpBaseUrl + "/docs/user-guide/rule-engine-2-0/transformation-nodes/#to-email-node",
+                ruleNodeClearAlarm: helpBaseUrl + "/docs/user-guide/rule-engine-2-0/action-nodes/#clear-alarm-node",
+                ruleNodeCrateAlarm: helpBaseUrl + "/docs/user-guide/rule-engine-2-0/action-nodes/#create-alarm-node",
+                ruleNodeMsgGenerator: helpBaseUrl + "/docs/user-guide/rule-engine-2-0/action-nodes/#generator-node",
+                ruleNodeLog: helpBaseUrl + "/docs/user-guide/rule-engine-2-0/action-nodes/#log-node",
+                ruleNodeRpcCallReply: helpBaseUrl + "/docs/user-guide/rule-engine-2-0/action-nodes/#rpc-call-reply-node",
+                ruleNodeRpcCallRequest: helpBaseUrl + "/docs/user-guide/rule-engine-2-0/action-nodes/#rpc-call-request-node",
+                ruleNodeSaveAttributes: helpBaseUrl + "/docs/user-guide/rule-engine-2-0/action-nodes/#save-attributes-node",
+                ruleNodeSaveTimeseries: helpBaseUrl + "/docs/user-guide/rule-engine-2-0/action-nodes/#save-timeseries-node",
+                ruleNodeRuleChain: helpBaseUrl + "/docs/user-guide/ui/rule-chains/",
+                ruleNodeAwsSns: helpBaseUrl + "/docs/user-guide/rule-engine-2-0/external-nodes/#aws-sns-node",
+                ruleNodeAwsSqs: helpBaseUrl + "/docs/user-guide/rule-engine-2-0/external-nodes/#aws-sqs-node",
+                ruleNodeKafka: helpBaseUrl + "/docs/user-guide/rule-engine-2-0/external-nodes/#kafka-node",
+                ruleNodeMqtt: helpBaseUrl + "/docs/user-guide/rule-engine-2-0/external-nodes/#mqtt-node",
+                ruleNodeRabbitMq: helpBaseUrl + "/docs/user-guide/rule-engine-2-0/external-nodes/#rabbitmq-node",
+                ruleNodeRestApiCall: helpBaseUrl + "/docs/user-guide/rule-engine-2-0/external-nodes/#rest-api-call-node",
+                ruleNodeSendEmail: helpBaseUrl + "/docs/user-guide/rule-engine-2-0/external-nodes/#send-email-node",
+                rulechains: helpBaseUrl + "/docs/user-guide/ui/rule-chains/",
                 tenants: helpBaseUrl + "/docs/user-guide/ui/tenants",
                 customers: helpBaseUrl + "/docs/user-guide/ui/customers",
                 assets: helpBaseUrl + "/docs/user-guide/ui/assets",
@@ -90,41 +98,19 @@ export default angular.module('thingsboard.help', [])
                 widgetsConfigAlarm: helpBaseUrl +  "/docs/user-guide/ui/dashboards#alarm",
                 widgetsConfigStatic: helpBaseUrl +  "/docs/user-guide/ui/dashboards#static",
             },
-            getPluginLink: function(plugin) {
-                var link = 'plugins';
-                if (plugin && plugin.clazz) {
-                    if (pluginClazzHelpLinkMap[plugin.clazz]) {
-                        link = pluginClazzHelpLinkMap[plugin.clazz];
-                    }
-                }
-                return link;
-            },
-            getFilterLink: function(filter) {
-                var link = 'filters';
-                if (filter && filter.clazz) {
-                    if (filterClazzHelpLinkMap[filter.clazz]) {
-                        link = filterClazzHelpLinkMap[filter.clazz];
-                    }
-                }
-                return link;
-            },
-            getProcessorLink: function(processor) {
-                var link = 'processors';
-                if (processor && processor.clazz) {
-                    if (processorClazzHelpLinkMap[processor.clazz]) {
-                        link = processorClazzHelpLinkMap[processor.clazz];
-                    }
-                }
-                return link;
-            },
-            getPluginActionLink: function(pluginAction) {
-                var link = 'pluginActions';
-                if (pluginAction && pluginAction.clazz) {
-                    if (pluginActionsClazzHelpLinkMap[pluginAction.clazz]) {
-                        link = pluginActionsClazzHelpLinkMap[pluginAction.clazz];
+            getRuleNodeLink: function(ruleNode) {
+                if (ruleNode && ruleNode.component) {
+                    if (ruleNode.component.configurationDescriptor &&
+                        ruleNode.component.configurationDescriptor.nodeDefinition &&
+                        ruleNode.component.configurationDescriptor.nodeDefinition.docUrl) {
+                        return ruleNode.component.configurationDescriptor.nodeDefinition.docUrl;
+                    } else if (ruleNode.component.clazz) {
+                        if (ruleNodeClazzHelpLinkMap[ruleNode.component.clazz]) {
+                            return ruleNodeClazzHelpLinkMap[ruleNode.component.clazz];
+                        }
                     }
                 }
-                return link;
+                return 'ruleEngine';
             }
         }
     ).name;
diff --git a/ui/src/app/import-export/import-export.service.js b/ui/src/app/import-export/import-export.service.js
index 6071fd2..d64441f 100644
--- a/ui/src/app/import-export/import-export.service.js
+++ b/ui/src/app/import-export/import-export.service.js
@@ -25,8 +25,7 @@ import entityAliasesTemplate from '../entity/alias/entity-aliases.tpl.html';
 
 /*@ngInject*/
 export default function ImportExport($log, $translate, $q, $mdDialog, $document, $http, itembuffer, utils, types,
-                                     dashboardUtils, entityService, dashboardService, pluginService, ruleService,
-                                     widgetService, toast, attributeService) {
+                                     dashboardUtils, entityService, dashboardService, ruleChainService, widgetService, toast, attributeService) {
 
 
     var service = {
@@ -34,10 +33,8 @@ export default function ImportExport($log, $translate, $q, $mdDialog, $document,
         importDashboard: importDashboard,
         exportWidget: exportWidget,
         importWidget: importWidget,
-        exportPlugin: exportPlugin,
-        importPlugin: importPlugin,
-        exportRule: exportRule,
-        importRule: importRule,
+        exportRuleChain: exportRuleChain,
+        importRuleChain: importRuleChain,
         exportWidgetType: exportWidgetType,
         importWidgetType: importWidgetType,
         exportWidgetsBundle: exportWidgetsBundle,
@@ -219,101 +216,68 @@ export default function ImportExport($log, $translate, $q, $mdDialog, $document,
         return true;
     }
 
-    // Rule functions
+    // Rule chain functions
 
-    function exportRule(ruleId) {
-        ruleService.getRule(ruleId).then(
-            function success(rule) {
-                var name = rule.name;
-                name = name.toLowerCase().replace(/\W/g,"_");
-                exportToPc(prepareExport(rule), name + '.json');
+    function exportRuleChain(ruleChainId) {
+        ruleChainService.getRuleChain(ruleChainId).then(
+            (ruleChain) => {
+                ruleChainService.getRuleChainMetaData(ruleChainId).then(
+                    (ruleChainMetaData) => {
+                        var ruleChainExport = {
+                            ruleChain: prepareRuleChain(ruleChain),
+                            metadata: prepareRuleChainMetaData(ruleChainMetaData)
+                        };
+                        var name = ruleChain.name;
+                        name = name.toLowerCase().replace(/\W/g,"_");
+                        exportToPc(ruleChainExport, name + '.json');
+                    },
+                    (rejection) => {
+                        processExportRuleChainRejection(rejection);
+                    }
+                );
             },
-            function fail(rejection) {
-                var message = rejection;
-                if (!message) {
-                    message = $translate.instant('error.unknown-error');
-                }
-                toast.showError($translate.instant('rule.export-failed-error', {error: message}));
+            (rejection) => {
+                processExportRuleChainRejection(rejection);
             }
         );
     }
 
-    function importRule($event) {
-        var deferred = $q.defer();
-        openImportDialog($event, 'rule.import', 'rule.rule-file').then(
-            function success(rule) {
-                if (!validateImportedRule(rule)) {
-                    toast.showError($translate.instant('rule.invalid-rule-file-error'));
-                    deferred.reject();
-                } else {
-                    rule.state = 'SUSPENDED';
-                    ruleService.saveRule(rule).then(
-                        function success() {
-                            deferred.resolve();
-                        },
-                        function fail() {
-                            deferred.reject();
-                        }
-                    );
-                }
-            },
-            function fail() {
-                deferred.reject();
-            }
-        );
-        return deferred.promise;
+    function prepareRuleChain(ruleChain) {
+        ruleChain = prepareExport(ruleChain);
+        if (ruleChain.firstRuleNodeId) {
+            ruleChain.firstRuleNodeId = null;
+        }
+        ruleChain.root = false;
+        return ruleChain;
     }
 
-    function validateImportedRule(rule) {
-        if (angular.isUndefined(rule.name)
-            || angular.isUndefined(rule.pluginToken)
-            || angular.isUndefined(rule.filters)
-            || angular.isUndefined(rule.action))
-        {
-            return false;
+    function prepareRuleChainMetaData(ruleChainMetaData) {
+        delete ruleChainMetaData.ruleChainId;
+        for (var i=0;i<ruleChainMetaData.nodes.length;i++) {
+            var node = ruleChainMetaData.nodes[i];
+            delete node.ruleChainId;
+            ruleChainMetaData.nodes[i] = prepareExport(node);
         }
-        return true;
+        return ruleChainMetaData;
     }
 
-    // Plugin functions
-
-    function exportPlugin(pluginId) {
-        pluginService.getPlugin(pluginId).then(
-            function success(plugin) {
-                if (!plugin.configuration || plugin.configuration === null) {
-                    plugin.configuration = {};
-                }
-                var name = plugin.name;
-                name = name.toLowerCase().replace(/\W/g,"_");
-                exportToPc(prepareExport(plugin), name + '.json');
-            },
-            function fail(rejection) {
-                var message = rejection;
-                if (!message) {
-                    message = $translate.instant('error.unknown-error');
-                }
-                toast.showError($translate.instant('plugin.export-failed-error', {error: message}));
-            }
-        );
+    function processExportRuleChainRejection(rejection) {
+        var message = rejection;
+        if (!message) {
+            message = $translate.instant('error.unknown-error');
+        }
+        toast.showError($translate.instant('rulechain.export-failed-error', {error: message}));
     }
 
-    function importPlugin($event) {
+    function importRuleChain($event) {
         var deferred = $q.defer();
-        openImportDialog($event, 'plugin.import', 'plugin.plugin-file').then(
-            function success(plugin) {
-                if (!validateImportedPlugin(plugin)) {
-                    toast.showError($translate.instant('plugin.invalid-plugin-file-error'));
+        openImportDialog($event, 'rulechain.import', 'rulechain.rulechain-file').then(
+            function success(ruleChainImport) {
+                if (!validateImportedRuleChain(ruleChainImport)) {
+                    toast.showError($translate.instant('rulechain.invalid-rulechain-file-error'));
                     deferred.reject();
                 } else {
-                    plugin.state = 'SUSPENDED';
-                    pluginService.savePlugin(plugin).then(
-                        function success() {
-                            deferred.resolve();
-                        },
-                        function fail() {
-                            deferred.reject();
-                        }
-                    );
+                    deferred.resolve(ruleChainImport);
                 }
             },
             function fail() {
@@ -323,12 +287,14 @@ export default function ImportExport($log, $translate, $q, $mdDialog, $document,
         return deferred.promise;
     }
 
-    function validateImportedPlugin(plugin) {
-        if (angular.isUndefined(plugin.name)
-            || angular.isUndefined(plugin.clazz)
-            || angular.isUndefined(plugin.apiToken)
-            || angular.isUndefined(plugin.configuration))
-        {
+    function validateImportedRuleChain(ruleChainImport) {
+        if (angular.isUndefined(ruleChainImport.ruleChain)) {
+            return false;
+        }
+        if (angular.isUndefined(ruleChainImport.metadata)) {
+            return false;
+        }
+        if (angular.isUndefined(ruleChainImport.ruleChain.name)) {
             return false;
         }
         return true;
diff --git a/ui/src/app/layout/index.js b/ui/src/app/layout/index.js
index e5ca958..8f2958d 100644
--- a/ui/src/app/layout/index.js
+++ b/ui/src/app/layout/index.js
@@ -29,6 +29,9 @@ import thingsboardNoAnimate from '../components/no-animate.directive';
 import thingsboardOnFinishRender from '../components/finish-render.directive';
 import thingsboardSideMenu from '../components/side-menu.directive';
 import thingsboardDashboardAutocomplete from '../components/dashboard-autocomplete.directive';
+import thingsboardKvMap from '../components/kv-map.directive';
+import thingsboardJsonObjectEdit from '../components/json-object-edit.directive';
+import thingsboardJsonContent from '../components/json-content.directive';
 
 import thingsboardUserMenu from './user-menu.directive';
 
@@ -47,8 +50,7 @@ import thingsboardAsset from '../asset';
 import thingsboardDevice from '../device';
 import thingsboardWidgetLibrary from '../widget';
 import thingsboardDashboard from '../dashboard';
-import thingsboardPlugin from '../plugin';
-import thingsboardRule from '../rule';
+import thingsboardRuleChain from '../rulechain';
 
 import thingsboardJsonForm from '../jsonform';
 
@@ -79,8 +81,7 @@ export default angular.module('thingsboard.home', [
     thingsboardDevice,
     thingsboardWidgetLibrary,
     thingsboardDashboard,
-    thingsboardPlugin,
-    thingsboardRule,
+    thingsboardRuleChain,
     thingsboardJsonForm,
     thingsboardApiDevice,
     thingsboardApiLogin,
@@ -88,7 +89,10 @@ export default angular.module('thingsboard.home', [
     thingsboardNoAnimate,
     thingsboardOnFinishRender,
     thingsboardSideMenu,
-    thingsboardDashboardAutocomplete
+    thingsboardDashboardAutocomplete,
+    thingsboardKvMap,
+    thingsboardJsonObjectEdit,
+    thingsboardJsonContent
 ])
     .config(HomeRoutes)
     .controller('HomeController', HomeController)
diff --git a/ui/src/app/locale/locale.constant.js b/ui/src/app/locale/locale.constant.js
index 0f0a1df..5bb599c 100644
--- a/ui/src/app/locale/locale.constant.js
+++ b/ui/src/app/locale/locale.constant.js
@@ -43,6 +43,7 @@ export default angular.module('thingsboard.locale', [])
                     "update": "Update",
                     "remove": "Remove",
                     "search": "Search",
+                    "clear-search": "Clear search",
                     "assign": "Assign",
                     "unassign": "Unassign",
                     "share": "Share",
@@ -341,6 +342,11 @@ export default angular.module('thingsboard.locale', [])
                     "enter-password": "Enter password",
                     "enter-search": "Enter search"
                 },
+                "content-type": {
+                    "json": "Json",
+                    "text": "Text",
+                    "binary": "Binary (Base64)"
+                },
                 "customer": {
                     "customer": "Customer",
                     "customers": "Customers",
@@ -745,6 +751,10 @@ export default angular.module('thingsboard.locale', [])
                     "type-alarms": "Alarms",
                     "list-of-alarms": "{ count, select, 1 {One alarms} other {List of # alarms} }",
                     "alarm-name-starts-with": "Alarms whose names start with '{{prefix}}'",
+                    "type-rulechain": "Rule chain",
+                    "type-rulechains": "Rule chains",
+                    "list-of-rulechains": "{ count, select, 1 {One rule chain} other {List of # rule chains} }",
+                    "rulechain-name-starts-with": "Rule chains whose names start with '{{prefix}}'",
                     "type-current-customer": "Current Customer",
                     "search": "Search entities",
                     "selected-entities": "{ count, select, 1 {1 entity} other {# entities} } selected",
@@ -758,6 +768,8 @@ export default angular.module('thingsboard.locale', [])
                     "type-error": "Error",
                     "type-lc-event": "Lifecycle event",
                     "type-stats": "Statistics",
+                    "type-debug-rule-node": "Debug",
+                    "type-debug-rule-chain": "Debug",
                     "no-events-prompt": "No events found",
                     "error": "Error",
                     "alarm": "Alarm",
@@ -765,6 +777,14 @@ export default angular.module('thingsboard.locale', [])
                     "server": "Server",
                     "body": "Body",
                     "method": "Method",
+                    "type": "Type",
+                    "entity": "Entity",
+                    "message-id": "Message Id",
+                    "message-type": "Message Type",
+                    "data-type": "Data Type",
+                    "relation-type": "Relation Type",
+                    "metadata": "Metadata",
+                    "data": "Data",
                     "event": "Event",
                     "status": "Status",
                     "success": "Success",
@@ -971,7 +991,15 @@ export default angular.module('thingsboard.locale', [])
                 },
                 "js-func": {
                     "no-return-error": "Function must return value!",
-                    "return-type-mismatch": "Function must return value of '{{type}}' type!"
+                    "return-type-mismatch": "Function must return value of '{{type}}' type!",
+                    "tidy": "Tidy"
+                },
+                "key-val": {
+                    "key": "Key",
+                    "value": "Value",
+                    "remove-entry": "Remove entry",
+                    "add-entry": "Add entry",
+                    "no-data": "No entries"
                 },
                 "layout": {
                     "layout": "Layout",
@@ -1011,47 +1039,6 @@ export default angular.module('thingsboard.locale', [])
                     "password-link-sent-message": "Password reset link was successfully sent!",
                     "email": "Email"
                 },
-                "plugin": {
-                    "plugins": "Plugins",
-                    "delete": "Delete plugin",
-                    "activate": "Activate plugin",
-                    "suspend": "Suspend plugin",
-                    "active": "Active",
-                    "suspended": "Suspended",
-                    "name": "Name",
-                    "name-required": "Name is required.",
-                    "description": "Description",
-                    "add": "Add Plugin",
-                    "delete-plugin-title": "Are you sure you want to delete the plugin '{{pluginName}}'?",
-                    "delete-plugin-text": "Be careful, after the confirmation the plugin and all related data will become unrecoverable.",
-                    "delete-plugins-title": "Are you sure you want to delete { count, select, 1 {1 plugin} other {# plugins} }?",
-                    "delete-plugins-action-title": "Delete { count, select, 1 {1 plugin} other {# plugins} }",
-                    "delete-plugins-text": "Be careful, after the confirmation all selected plugins will be removed and all related data will become unrecoverable.",
-                    "add-plugin-text": "Add new plugin",
-                    "no-plugins-text": "No plugins found",
-                    "plugin-details": "Plugin details",
-                    "api-token": "API token",
-                    "api-token-required": "API token is required.",
-                    "type": "Plugin type",
-                    "type-required": "Plugin type is required.",
-                    "configuration": "Plugin configuration",
-                    "system": "System",
-                    "select-plugin": "Select plugin",
-                    "plugin": "Plugin",
-                    "no-plugins-matching": "No plugins matching '{{entity}}' were found.",
-                    "plugin-required": "Plugin is required.",
-                    "plugin-require-match": "Please select an existing plugin.",
-                    "events": "Events",
-                    "details": "Details",
-                    "import": "Import plugin",
-                    "export": "Export plugin",
-                    "export-failed-error": "Unable to export plugin: {{error}}",
-                    "create-new-plugin": "Create new plugin",
-                    "plugin-file": "Plugin file",
-                    "invalid-plugin-file-error": "Unable to import plugin: Invalid plugin data structure.",
-                    "copyId": "Copy plugin Id",
-                    "idCopiedMessage": "Plugin Id has been copied to clipboard"
-                },
                 "position": {
                     "top": "Top",
                     "bottom": "Bottom",
@@ -1105,66 +1092,97 @@ export default angular.module('thingsboard.locale', [])
                     "additional-info": "Additional info (JSON)",
                     "invalid-additional-info": "Unable to parse additional info json."
                 },
-                "rule": {
-                    "rule": "Rule",
-                    "rules": "Rules",
-                    "delete": "Delete rule",
-                    "activate": "Activate rule",
-                    "suspend": "Suspend rule",
-                    "active": "Active",
-                    "suspended": "Suspended",
+                "rulechain": {
+                    "rulechain": "Rule chain",
+                    "rulechains": "Rule chains",
+                    "root": "Root",
+                    "delete": "Delete rule chain",
                     "name": "Name",
                     "name-required": "Name is required.",
                     "description": "Description",
-                    "add": "Add Rule",
-                    "delete-rule-title": "Are you sure you want to delete the rule '{{ruleName}}'?",
-                    "delete-rule-text": "Be careful, after the confirmation the rule and all related data will become unrecoverable.",
-                    "delete-rules-title": "Are you sure you want to delete { count, select, 1 {1 rule} other {# rules} }?",
-                    "delete-rules-action-title": "Delete { count, select, 1 {1 rule} other {# rules} }",
-                    "delete-rules-text": "Be careful, after the confirmation all selected rules will be removed and all related data will become unrecoverable.",
-                    "add-rule-text": "Add new rule",
-                    "no-rules-text": "No rules found",
-                    "rule-details": "Rule details",
-                    "filters": "Filters",
-                    "filter": "Filter",
-                    "add-filter-prompt": "Please add filter",
-                    "remove-filter": "Remove filter",
-                    "add-filter": "Add filter",
-                    "filter-name": "Filter name",
-                    "filter-type": "Filter type",
-                    "edit-filter": "Edit filter",
-                    "view-filter": "View filter",
-                    "component-name": "Name",
-                    "component-name-required": "Name is required.",
-                    "component-type": "Type",
-                    "component-type-required": "Type is required.",
-                    "processor": "Processor",
-                    "no-processor-configured": "No processor configured",
-                    "create-processor": "Create processor",
-                    "processor-name": "Processor name",
-                    "processor-type": "Processor type",
-                    "plugin-action": "Plugin action",
-                    "action-name": "Action name",
-                    "action-type": "Action type",
-                    "create-action-prompt": "Please create action",
-                    "create-action": "Create action",
+                    "add": "Add Rule Chain",
+                    "set-root": "Make rule chain root",
+                    "set-root-rulechain-title": "Are you sure you want to make the rule chain '{{ruleChainName}}' root?",
+                    "set-root-rulechain-text": "After the confirmation the rule chain will become root and will handle all incoming transport messages.",
+                    "delete-rulechain-title": "Are you sure you want to delete the rule chain '{{ruleChainName}}'?",
+                    "delete-rulechain-text": "Be careful, after the confirmation the rule chain and all related data will become unrecoverable.",
+                    "delete-rulechains-title": "Are you sure you want to delete { count, select, 1 {1 rule chain} other {# rule chains} }?",
+                    "delete-rulechains-action-title": "Delete { count, select, 1 {1 rule chain} other {# rule chains} }",
+                    "delete-rulechains-text": "Be careful, after the confirmation all selected rule chains will be removed and all related data will become unrecoverable.",
+                    "add-rulechain-text": "Add new rule chain",
+                    "no-rulechains-text": "No rule chains found",
+                    "rulechain-details": "Rule chain details",
                     "details": "Details",
                     "events": "Events",
                     "system": "System",
-                    "import": "Import rule",
-                    "export": "Export rule",
-                    "export-failed-error": "Unable to export rule: {{error}}",
-                    "create-new-rule": "Create new rule",
-                    "rule-file": "Rule file",
-                    "invalid-rule-file-error": "Unable to import rule: Invalid rule data structure.",
-                    "copyId": "Copy rule Id",
-                    "idCopiedMessage": "Rule Id has been copied to clipboard",
-                    "select-rule": "Select rule",
-                    "no-rules-matching": "No rules matching '{{entity}}' were found.",
-                    "rule-required": "Rule is required"
+                    "import": "Import rule chain",
+                    "export": "Export rule chain",
+                    "export-failed-error": "Unable to export rule chain: {{error}}",
+                    "create-new-rulechain": "Create new rule chain",
+                    "rulechain-file": "Rule chain file",
+                    "invalid-rulechain-file-error": "Unable to import rule chain: Invalid rule chain data structure.",
+                    "copyId": "Copy rule chain Id",
+                    "idCopiedMessage": "Rule chain Id has been copied to clipboard",
+                    "select-rulechain": "Select rule chain",
+                    "no-rulechains-matching": "No rule chains matching '{{entity}}' were found.",
+                    "rulechain-required": "Rule chain is required",
+                    "management": "Rules management",
+                    "debug-mode": "Debug mode"
                 },
-                "rule-plugin": {
-                    "management": "Rules and plugins management"
+                "rulenode": {
+                    "details": "Details",
+                    "events": "Events",
+                    "search": "Search nodes",
+                    "open-node-library": "Open node library",
+                    "add": "Add rule node",
+                    "name": "Name",
+                    "name-required": "Name is required.",
+                    "type": "Type",
+                    "description": "Description",
+                    "delete": "Delete rule node",
+                    "select-all-objects": "Select all nodes and connections",
+                    "deselect-all-objects": "Deselect all nodes and connections",
+                    "delete-selected-objects": "Delete selected nodes and connections",
+                    "delete-selected": "Delete selected",
+                    "select-all": "Select all",
+                    "copy-selected": "Copy selected",
+                    "deselect-all": "Deselect all",
+                    "rulenode-details": "Rule node details",
+                    "debug-mode": "Debug mode",
+                    "configuration": "Configuration",
+                    "link": "Link",
+                    "link-details": "Rule node link details",
+                    "add-link": "Add link",
+                    "link-label": "Link label",
+                    "link-label-required": "Link label is required.",
+                    "custom-link-label": "Custom link label",
+                    "custom-link-label-required": "Custom link label is required.",
+                    "type-filter": "Filter",
+                    "type-filter-details": "Filter incoming messages with configured conditions",
+                    "type-enrichment": "Enrichment",
+                    "type-enrichment-details": "Add additional information into Message Metadata",
+                    "type-transformation": "Transformation",
+                    "type-transformation-details": "Change Message payload and Metadata",
+                    "type-action": "Action",
+                    "type-action-details": "Perform special action",
+                    "type-external": "External",
+                    "type-external-details": "Interacts with external system",
+                    "type-rule-chain": "Rule Chain",
+                    "type-rule-chain-details": "Forwards incoming messages to specified Rule Chain",
+                    "type-input": "Input",
+                    "type-input-details": "Logical input of Rule Chain, forwards incoming messages to next related Rule Node",
+                    "directive-is-not-loaded": "Defined configuration directive '{{directiveName}}' is not available.",
+                    "ui-resources-load-error": "Failed to load configuration ui resources.",
+                    "invalid-target-rulechain": "Unable to resolve target rule chain!",
+                    "test-script-function": "Test script function",
+                    "message": "Message",
+                    "message-type": "Message type",
+                    "message-type-required": "Message type is required",
+                    "metadata": "Metadata",
+                    "metadata-required": "Metadata entries can't be empty.",
+                    "output": "Output",
+                    "test": "Test",
+                    "help": "Help"
                 },
                 "tenant": {
                     "tenant": "Tenant",
diff --git a/ui/src/app/rulechain/add-link.tpl.html b/ui/src/app/rulechain/add-link.tpl.html
new file mode 100644
index 0000000..0a21104
--- /dev/null
+++ b/ui/src/app/rulechain/add-link.tpl.html
@@ -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.
+
+-->
+<md-dialog aria-label="{{ 'rulenode.add-link' | translate }}" tb-help="'ruleEngine'" help-container-id="help-container">
+    <form name="theForm" ng-submit="vm.add()">
+        <md-toolbar>
+            <div class="md-toolbar-tools">
+                <h2 translate>rulenode.add-link</h2>
+                <span flex></span>
+                <div id="help-container"></div>
+                <md-button class="md-icon-button" ng-click="vm.cancel()">
+                    <ng-md-icon icon="close" aria-label="{{ 'dialog.close' | translate }}"></ng-md-icon>
+                </md-button>
+            </div>
+        </md-toolbar>
+        <md-progress-linear class="md-warn" md-mode="indeterminate" ng-disabled="!$root.loading" ng-show="$root.loading"></md-progress-linear>
+        <span style="min-height: 5px;" flex="" ng-show="!$root.loading"></span>
+        <md-dialog-content>
+            <div class="md-dialog-content">
+                <tb-rule-node-link link="vm.link" labels="vm.labels" is-edit="true" the-form="theForm"></tb-rule-node-link>
+            </div>
+        </md-dialog-content>
+        <md-dialog-actions layout="row">
+            <span flex></span>
+            <md-button ng-disabled="$root.loading || theForm.$invalid || !theForm.$dirty" type="submit"
+                       class="md-raised md-primary">
+                {{ 'action.add' | translate }}
+            </md-button>
+            <md-button ng-disabled="$root.loading" ng-click="vm.cancel()" style="margin-right:20px;">{{ 'action.cancel' |
+                translate }}
+            </md-button>
+        </md-dialog-actions>
+    </form>
+</md-dialog>
diff --git a/ui/src/app/rulechain/add-rulechain.tpl.html b/ui/src/app/rulechain/add-rulechain.tpl.html
new file mode 100644
index 0000000..44d0ec3
--- /dev/null
+++ b/ui/src/app/rulechain/add-rulechain.tpl.html
@@ -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.
+
+-->
+<md-dialog aria-label="{{ 'rulechain.add' | translate }}" tb-help="'rulechains'" help-container-id="help-container">
+    <form name="theForm" ng-submit="vm.add()">
+        <md-toolbar>
+            <div class="md-toolbar-tools">
+                <h2 translate>rulechain.add</h2>
+                <span flex></span>
+                <div id="help-container"></div>
+                <md-button class="md-icon-button" ng-click="vm.cancel()">
+                    <ng-md-icon icon="close" aria-label="{{ 'dialog.close' | translate }}"></ng-md-icon>
+                </md-button>
+            </div>
+        </md-toolbar>
+        <md-progress-linear class="md-warn" md-mode="indeterminate" ng-disabled="!$root.loading" ng-show="$root.loading"></md-progress-linear>
+        <span style="min-height: 5px;" flex="" ng-show="!$root.loading"></span>
+        <md-dialog-content>
+            <div class="md-dialog-content">
+                <tb-rule-chain rule-chain="vm.item" is-edit="true" the-form="theForm"></tb-rule-chain>
+            </div>
+        </md-dialog-content>
+        <md-dialog-actions layout="row">
+            <span flex></span>
+            <md-button ng-disabled="$root.loading || theForm.$invalid || !theForm.$dirty" type="submit"
+                       class="md-raised md-primary">
+                {{ 'action.add' | translate }}
+            </md-button>
+            <md-button ng-disabled="$root.loading" ng-click="vm.cancel()" style="margin-right:20px;">{{ 'action.cancel' |
+                translate }}
+            </md-button>
+        </md-dialog-actions>
+    </form>
+</md-dialog>
diff --git a/ui/src/app/rulechain/add-rulenode.tpl.html b/ui/src/app/rulechain/add-rulenode.tpl.html
new file mode 100644
index 0000000..c572915
--- /dev/null
+++ b/ui/src/app/rulechain/add-rulenode.tpl.html
@@ -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.
+
+-->
+<md-dialog aria-label="{{ 'rulenode.add' | translate }}" tb-help="vm.helpLinks.getRuleNodeLink(vm.ruleNode)" help-container-id="help-container" style="min-width: 650px;">
+    <form name="theForm" ng-submit="vm.add()">
+        <md-toolbar>
+            <div class="md-toolbar-tools">
+                <h2 translate>rulenode.add</h2>
+                <span flex></span>
+                <div id="help-container"></div>
+                <md-button class="md-icon-button" ng-click="vm.cancel()">
+                    <ng-md-icon icon="close" aria-label="{{ 'dialog.close' | translate }}"></ng-md-icon>
+                </md-button>
+            </div>
+        </md-toolbar>
+        <md-progress-linear class="md-warn" md-mode="indeterminate" ng-disabled="!$root.loading" ng-show="$root.loading"></md-progress-linear>
+        <span style="min-height: 5px;" flex="" ng-show="!$root.loading"></span>
+        <md-dialog-content>
+            <div class="md-dialog-content">
+                <tb-rule-node rule-node="vm.ruleNode" rule-chain-id="vm.ruleChainId" is-edit="true" the-form="theForm"></tb-rule-node>
+            </div>
+        </md-dialog-content>
+        <md-dialog-actions layout="row">
+            <span flex></span>
+            <md-button ng-disabled="$root.loading || theForm.$invalid || !theForm.$dirty" type="submit"
+                       class="md-raised md-primary">
+                {{ 'action.add' | translate }}
+            </md-button>
+            <md-button ng-disabled="$root.loading" ng-click="vm.cancel()" style="margin-right:20px;">{{ 'action.cancel' |
+                translate }}
+            </md-button>
+        </md-dialog-actions>
+    </form>
+</md-dialog>
diff --git a/ui/src/app/rulechain/index.js b/ui/src/app/rulechain/index.js
new file mode 100644
index 0000000..7740dd0
--- /dev/null
+++ b/ui/src/app/rulechain/index.js
@@ -0,0 +1,41 @@
+/*
+ * 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.
+ */
+
+import RuleChainRoutes from './rulechain.routes';
+import RuleChainsController from './rulechains.controller';
+import {RuleChainController, AddRuleNodeController, AddRuleNodeLinkController} from './rulechain.controller';
+import NodeScriptTestController from './script/node-script-test.controller';
+import RuleChainDirective from './rulechain.directive';
+import RuleNodeDefinedConfigDirective from './rulenode-defined-config.directive';
+import RuleNodeConfigDirective from './rulenode-config.directive';
+import RuleNodeDirective from './rulenode.directive';
+import LinkDirective from './link.directive';
+import NodeScriptTest from './script/node-script-test.service';
+
+export default angular.module('thingsboard.ruleChain', [])
+    .config(RuleChainRoutes)
+    .controller('RuleChainsController', RuleChainsController)
+    .controller('RuleChainController', RuleChainController)
+    .controller('AddRuleNodeController', AddRuleNodeController)
+    .controller('AddRuleNodeLinkController', AddRuleNodeLinkController)
+    .controller('NodeScriptTestController', NodeScriptTestController)
+    .directive('tbRuleChain', RuleChainDirective)
+    .directive('tbRuleNodeDefinedConfig', RuleNodeDefinedConfigDirective)
+    .directive('tbRuleNodeConfig', RuleNodeConfigDirective)
+    .directive('tbRuleNode', RuleNodeDirective)
+    .directive('tbRuleNodeLink', LinkDirective)
+    .factory('ruleNodeScriptTest', NodeScriptTest)
+    .name;
diff --git a/ui/src/app/rulechain/link.directive.js b/ui/src/app/rulechain/link.directive.js
new file mode 100644
index 0000000..b3565a3
--- /dev/null
+++ b/ui/src/app/rulechain/link.directive.js
@@ -0,0 +1,71 @@
+/*
+ * 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.
+ */
+
+/* eslint-disable import/no-unresolved, import/default */
+
+import linkFieldsetTemplate from './link-fieldset.tpl.html';
+
+/* eslint-enable import/no-unresolved, import/default */
+
+/*@ngInject*/
+export default function LinkDirective($compile, $templateCache, $filter) {
+    var linker = function (scope, element) {
+        var template = $templateCache.get(linkFieldsetTemplate);
+        element.html(template);
+
+        scope.selectedLabel = null;
+
+        scope.$watch('link', function() {
+            scope.selectedLabel = null;
+             if (scope.link && scope.labels) {
+                 if (scope.link.label) {
+                     var result = $filter('filter')(scope.labels, {name: scope.link.label});
+                     if (result && result.length) {
+                         scope.selectedLabel = result[0];
+                     } else {
+                         result = $filter('filter')(scope.labels, {custom: true});
+                         if (result && result.length && result[0].custom) {
+                             scope.selectedLabel = result[0];
+                         }
+                     }
+                 }
+             }
+        });
+
+        scope.selectedLabelChanged = function() {
+            if (scope.link && scope.selectedLabel) {
+                if (!scope.selectedLabel.custom) {
+                    scope.link.label = scope.selectedLabel.name;
+                } else {
+                    scope.link.label = "";
+                }
+            }
+        };
+
+        $compile(element.contents())(scope);
+    }
+    return {
+        restrict: "E",
+        link: linker,
+        scope: {
+            link: '=',
+            labels: '=',
+            isEdit: '=',
+            isReadOnly: '=',
+            theForm: '='
+        }
+    };
+}
diff --git a/ui/src/app/rulechain/link-fieldset.tpl.html b/ui/src/app/rulechain/link-fieldset.tpl.html
new file mode 100644
index 0000000..13ec6c3
--- /dev/null
+++ b/ui/src/app/rulechain/link-fieldset.tpl.html
@@ -0,0 +1,39 @@
+<!--
+
+    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.
+
+-->
+<md-content class="md-padding tb-link" layout="column">
+    <fieldset ng-disabled="$root.loading || !isEdit || isReadOnly">
+        <md-input-container class="md-block">
+            <label translate>rulenode.link-label</label>
+            <md-select ng-model="selectedLabel" ng-change="selectedLabelChanged()">
+                <md-option ng-repeat="label in labels" ng-value="label">
+                    {{label.name}}
+                </md-option>
+            </md-select>
+            <div ng-messages="theForm.linkLabel.$error">
+                <div translate ng-message="required">rulenode.link-label-required</div>
+            </div>
+        </md-input-container>
+        <md-input-container ng-if="selectedLabel.custom" class="md-block">
+            <label translate>rulenode.link-label</label>
+            <input required name="customLinkLabel" ng-model="link.label">
+            <div ng-messages="theForm.customLinkLabel.$error">
+                <div translate ng-message="required">rulenode.custom-link-label-required</div>
+            </div>
+        </md-input-container>
+    </fieldset>
+</md-content>
diff --git a/ui/src/app/rulechain/rulechain.controller.js b/ui/src/app/rulechain/rulechain.controller.js
new file mode 100644
index 0000000..06df2cd
--- /dev/null
+++ b/ui/src/app/rulechain/rulechain.controller.js
@@ -0,0 +1,1357 @@
+/*
+ * 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.
+ */
+
+import './rulechain.scss';
+
+import 'tooltipster/dist/css/tooltipster.bundle.min.css';
+import 'tooltipster/dist/js/tooltipster.bundle.min.js';
+import 'tooltipster/dist/css/plugins/tooltipster/sideTip/themes/tooltipster-sideTip-shadow.min.css';
+
+/* eslint-disable import/no-unresolved, import/default */
+
+import addRuleNodeTemplate from './add-rulenode.tpl.html';
+import addRuleNodeLinkTemplate from './add-link.tpl.html';
+
+/* eslint-enable import/no-unresolved, import/default */
+
+/*@ngInject*/
+export function RuleChainController($state, $scope, $compile, $q, $mdUtil, $timeout, $mdExpansionPanel, $window, $document, $mdDialog,
+                                    $filter, $translate, hotkeys, types, ruleChainService, itembuffer, Modelfactory, flowchartConstants,
+                                    ruleChain, ruleChainMetaData, ruleNodeComponents, helpLinks) {
+
+    var vm = this;
+
+    vm.$mdExpansionPanel = $mdExpansionPanel;
+    vm.types = types;
+
+    if ($state.current.data.import && !ruleChain) {
+        $state.go('home.ruleChains');
+        return;
+    }
+
+    vm.isImport = $state.current.data.import;
+    vm.isConfirmOnExit = false;
+
+    $scope.$watch(function() {
+        return vm.isDirty || vm.isImport;
+    }, (val) => {
+        vm.isConfirmOnExit = val;
+    });
+
+    vm.errorTooltips = {};
+
+    vm.isFullscreen = false;
+
+    vm.editingRuleNode = null;
+    vm.isEditingRuleNode = false;
+
+    vm.editingRuleNodeLink = null;
+    vm.isEditingRuleNodeLink = false;
+
+    vm.isLibraryOpen = true;
+    vm.enableHotKeys = true;
+
+    Object.defineProperty(vm, 'isLibraryOpenReadonly', {
+        get: function() { return vm.isLibraryOpen },
+        set: function() {}
+    });
+
+    vm.ruleNodeSearch = '';
+
+    vm.ruleChain = ruleChain;
+    vm.ruleChainMetaData = ruleChainMetaData;
+
+    vm.canvasControl = {};
+
+    vm.ruleChainModel = {
+        nodes: [],
+        edges: []
+    };
+
+    vm.ruleNodeTypesModel = {};
+    vm.ruleNodeTypesCanvasControl = {};
+    vm.ruleChainLibraryLoaded = false;
+    for (var type in types.ruleNodeType) {
+        if (!types.ruleNodeType[type].special) {
+            vm.ruleNodeTypesModel[type] = {
+                model: {
+                    nodes: [],
+                    edges: []
+                },
+                selectedObjects: []
+            };
+            vm.ruleNodeTypesCanvasControl[type] = {};
+        }
+    }
+
+
+
+    vm.selectedObjects = [];
+
+    vm.modelservice = Modelfactory(vm.ruleChainModel, vm.selectedObjects);
+
+    vm.saveRuleChain = saveRuleChain;
+    vm.revertRuleChain = revertRuleChain;
+
+    vm.objectsSelected = objectsSelected;
+    vm.deleteSelected = deleteSelected;
+
+    vm.triggerResize = triggerResize;
+
+    vm.openRuleChainContextMenu = openRuleChainContextMenu;
+
+    vm.helpLinkIdForRuleNodeType = helpLinkIdForRuleNodeType;
+
+    initHotKeys();
+
+    function openRuleChainContextMenu($event, $mdOpenMousepointMenu) {
+        if (vm.canvasControl.modelservice && !$event.ctrlKey && !$event.metaKey) {
+            var x = $event.clientX;
+            var y = $event.clientY;
+            var item = vm.canvasControl.modelservice.getItemInfoAtPoint(x, y);
+            vm.contextInfo = prepareContextMenu(item);
+            if (vm.contextInfo.items && vm.contextInfo.items.length > 0) {
+                vm.contextMenuEvent = $event;
+                $mdOpenMousepointMenu($event);
+                return false;
+            }
+        }
+    }
+
+    function prepareContextMenu(item) {
+        if (objectsSelected() || (!item.node && !item.edge)) {
+            return prepareRuleChainContextMenu();
+        } else if (item.node) {
+            return prepareRuleNodeContextMenu(item.node);
+        } else if (item.edge) {
+            return prepareEdgeContextMenu(item.edge);
+        }
+    }
+
+    function prepareRuleChainContextMenu() {
+        var contextInfo = {
+            headerClass: 'tb-rulechain',
+            icon: 'settings_ethernet',
+            title: vm.ruleChain.name,
+            subtitle: $translate.instant('rulechain.rulechain')
+        };
+        contextInfo.items = [];
+        if (vm.modelservice.nodes.getSelectedNodes().length) {
+            contextInfo.items.push(
+                {
+                    action: function () {
+                        copyRuleNodes();
+                    },
+                    enabled: true,
+                    value: "rulenode.copy-selected",
+                    icon: "content_copy",
+                    shortcut: "M-C"
+                }
+            );
+        }
+        contextInfo.items.push(
+            {
+                action: function ($event) {
+                    pasteRuleNodes($event);
+                },
+                enabled: itembuffer.hasRuleNodes(),
+                value: "action.paste",
+                icon: "content_paste",
+                shortcut: "M-V"
+            }
+        );
+        contextInfo.items.push(
+            {
+                divider: true
+            }
+        );
+        if (objectsSelected()) {
+            contextInfo.items.push(
+                {
+                    action: function () {
+                        vm.modelservice.deselectAll();
+                    },
+                    enabled: true,
+                    value: "rulenode.deselect-all",
+                    icon: "tab_unselected",
+                    shortcut: "Esc"
+                }
+            );
+            contextInfo.items.push(
+                {
+                    action: function () {
+                        vm.modelservice.deleteSelected();
+                    },
+                    enabled: true,
+                    value: "rulenode.delete-selected",
+                    icon: "clear",
+                    shortcut: "Del"
+                }
+            );
+        } else {
+            contextInfo.items.push(
+                {
+                    action: function () {
+                        vm.modelservice.selectAll();
+                    },
+                    enabled: true,
+                    value: "rulenode.select-all",
+                    icon: "select_all",
+                    shortcut: "M-A"
+                }
+            );
+        }
+        contextInfo.items.push(
+            {
+                divider: true
+            }
+        );
+        contextInfo.items.push(
+            {
+                action: function () {
+                    vm.saveRuleChain();
+                },
+                enabled: !(vm.isInvalid || (!vm.isDirty && !vm.isImport)),
+                value: "action.apply-changes",
+                icon: "done",
+                shortcut: "M-S"
+            }
+        );
+        contextInfo.items.push(
+            {
+                action: function () {
+                    vm.revertRuleChain();
+                },
+                enabled: vm.isDirty,
+                value: "action.decline-changes",
+                icon: "close",
+                shortcut: "M-Z"
+            }
+        );
+        return contextInfo;
+    }
+
+    function prepareRuleNodeContextMenu(node) {
+        var contextInfo = {
+            headerClass: node.nodeClass,
+            icon: node.icon,
+            iconUrl: node.iconUrl,
+            title: node.name,
+            subtitle: node.component.name
+        };
+        contextInfo.items = [];
+        if (!node.readonly) {
+            contextInfo.items.push(
+                {
+                    action: function () {
+                        openNodeDetails(node);
+                    },
+                    enabled: true,
+                    value: "rulenode.details",
+                    icon: "menu"
+                }
+            );
+            contextInfo.items.push(
+                {
+                    action: function () {
+                        copyNode(node);
+                    },
+                    enabled: true,
+                    value: "action.copy",
+                    icon: "content_copy"
+                }
+            );
+            contextInfo.items.push(
+                {
+                    action: function () {
+                        vm.canvasControl.modelservice.nodes.delete(node);
+                    },
+                    enabled: true,
+                    value: "action.delete",
+                    icon: "clear",
+                    shortcut: "M-X"
+                }
+            );
+        }
+        return contextInfo;
+    }
+
+    function prepareEdgeContextMenu(edge) {
+        var contextInfo = {
+            headerClass: 'tb-link',
+            icon: 'trending_flat',
+            title: edge.label,
+            subtitle: $translate.instant('rulenode.link')
+        };
+        contextInfo.items = [];
+        var sourceNode = vm.modelservice.nodes.getNodeByConnectorId(edge.source);
+        if (sourceNode.component.type != types.ruleNodeType.INPUT.value) {
+            contextInfo.items.push(
+                {
+                    action: function () {
+                        openLinkDetails(edge);
+                    },
+                    enabled: true,
+                    value: "rulenode.details",
+                    icon: "menu"
+                }
+            );
+        }
+        contextInfo.items.push(
+            {
+                action: function () {
+                    vm.canvasControl.modelservice.edges.delete(edge);
+                },
+                enabled: true,
+                value: "action.delete",
+                icon: "clear",
+                shortcut: "M-X"
+            }
+        );
+        return contextInfo;
+    }
+
+    function initHotKeys() {
+        hotkeys.bindTo($scope)
+            .add({
+                combo: 'ctrl+a',
+                description: $translate.instant('rulenode.select-all-objects'),
+                allowIn: ['INPUT', 'SELECT', 'TEXTAREA'],
+                callback: function (event) {
+                    if (vm.enableHotKeys) {
+                        event.preventDefault();
+                        vm.modelservice.selectAll();
+                    }
+                }
+            })
+            .add({
+                combo: 'ctrl+c',
+                description: $translate.instant('rulenode.copy-selected'),
+                allowIn: ['INPUT', 'SELECT', 'TEXTAREA'],
+                callback: function (event) {
+                    if (vm.enableHotKeys) {
+                        event.preventDefault();
+                        copyRuleNodes();
+                    }
+                }
+            })
+            .add({
+                combo: 'ctrl+v',
+                description: $translate.instant('action.paste'),
+                allowIn: ['INPUT', 'SELECT', 'TEXTAREA'],
+                callback: function (event) {
+                    if (vm.enableHotKeys) {
+                        event.preventDefault();
+                        if (itembuffer.hasRuleNodes()) {
+                            pasteRuleNodes();
+                        }
+                    }
+                }
+            })
+            .add({
+                combo: 'esc',
+                description: $translate.instant('rulenode.deselect-all-objects'),
+                allowIn: ['INPUT', 'SELECT', 'TEXTAREA'],
+                callback: function (event) {
+                    if (vm.enableHotKeys) {
+                        event.preventDefault();
+                        event.stopPropagation();
+                        vm.modelservice.deselectAll();
+                    }
+                }
+            })
+            .add({
+                combo: 'ctrl+s',
+                description: $translate.instant('action.apply'),
+                allowIn: ['INPUT', 'SELECT', 'TEXTAREA'],
+                callback: function (event) {
+                    if (vm.enableHotKeys) {
+                        event.preventDefault();
+                        vm.saveRuleChain();
+                    }
+                }
+            })
+            .add({
+                combo: 'ctrl+z',
+                description: $translate.instant('action.decline-changes'),
+                allowIn: ['INPUT', 'SELECT', 'TEXTAREA'],
+                callback: function (event) {
+                    if (vm.enableHotKeys) {
+                        event.preventDefault();
+                        vm.revertRuleChain();
+                    }
+                }
+            })
+            .add({
+                combo: 'del',
+                description: $translate.instant('rulenode.delete-selected-objects'),
+                allowIn: ['INPUT', 'SELECT', 'TEXTAREA'],
+                callback: function (event) {
+                    if (vm.enableHotKeys) {
+                        event.preventDefault();
+                        vm.modelservice.deleteSelected();
+                    }
+                }
+            })
+    }
+
+    vm.onEditRuleNodeClosed = function() {
+        vm.editingRuleNode = null;
+    };
+
+    vm.onEditRuleNodeLinkClosed = function() {
+        vm.editingRuleNodeLink = null;
+    };
+
+    vm.saveRuleNode = function(theForm) {
+        $scope.$broadcast('form-submit');
+        if (theForm.$valid) {
+            theForm.$setPristine();
+            if (vm.editingRuleNode.error) {
+                delete vm.editingRuleNode.error;
+            }
+            vm.ruleChainModel.nodes[vm.editingRuleNodeIndex] = vm.editingRuleNode;
+            vm.editingRuleNode = angular.copy(vm.editingRuleNode);
+            updateRuleNodesHighlight();
+        }
+    };
+
+    vm.saveRuleNodeLink = function(theForm) {
+        theForm.$setPristine();
+        vm.ruleChainModel.edges[vm.editingRuleNodeLinkIndex] = vm.editingRuleNodeLink;
+        vm.editingRuleNodeLink = angular.copy(vm.editingRuleNodeLink);
+    };
+
+    vm.onRevertRuleNodeEdit = function(theForm) {
+        theForm.$setPristine();
+        var node = vm.ruleChainModel.nodes[vm.editingRuleNodeIndex];
+        vm.editingRuleNode = angular.copy(node);
+    };
+
+    vm.onRevertRuleNodeLinkEdit = function(theForm) {
+        theForm.$setPristine();
+        var edge = vm.ruleChainModel.edges[vm.editingRuleNodeLinkIndex];
+        vm.editingRuleNodeLink = angular.copy(edge);
+    };
+
+    vm.nodeLibCallbacks = {
+        nodeCallbacks: {
+            'mouseEnter': function (event, node) {
+                displayLibNodeDescriptionTooltip(event, node);
+            },
+            'mouseLeave': function () {
+                destroyTooltips();
+            },
+            'mouseDown': function () {
+                destroyTooltips();
+            }
+        }
+    };
+
+    vm.typeHeaderMouseEnter = function(event, typeId) {
+        var ruleNodeType = types.ruleNodeType[typeId];
+        displayTooltip(event,
+            '<div class="tb-rule-node-tooltip tb-lib-tooltip">' +
+            '<div id="tb-node-content" layout="column">' +
+            '<div class="tb-node-title">' + $translate.instant(ruleNodeType.name) + '</div>' +
+            '<div class="tb-node-details">' + $translate.instant(ruleNodeType.details) + '</div>' +
+            '</div>' +
+            '</div>'
+        );
+    };
+
+    vm.destroyTooltips = destroyTooltips;
+
+    function helpLinkIdForRuleNodeType() {
+        return helpLinks.getRuleNodeLink(vm.editingRuleNode);
+    }
+
+    function destroyTooltips() {
+        if (vm.tooltipTimeout) {
+            $timeout.cancel(vm.tooltipTimeout);
+            vm.tooltipTimeout = null;
+        }
+        var instances = angular.element.tooltipster.instances();
+        instances.forEach((instance) => {
+            if (!instance.isErrorTooltip) {
+                instance.destroy();
+            }
+        });
+    }
+
+    function displayLibNodeDescriptionTooltip(event, node) {
+        displayTooltip(event,
+            '<div class="tb-rule-node-tooltip tb-lib-tooltip">' +
+            '<div id="tb-node-content" layout="column">' +
+            '<div class="tb-node-title">' + node.component.name + '</div>' +
+            '<div class="tb-node-description">' + node.component.configurationDescriptor.nodeDefinition.description + '</div>' +
+            '<div class="tb-node-details">' + node.component.configurationDescriptor.nodeDefinition.details + '</div>' +
+            '</div>' +
+            '</div>'
+        );
+    }
+
+    function displayNodeDescriptionTooltip(event, node) {
+        if (!vm.errorTooltips[node.id]) {
+            var name, desc, details;
+            if (node.component.type == vm.types.ruleNodeType.INPUT.value) {
+                name = $translate.instant(vm.types.ruleNodeType.INPUT.name) + '';
+                desc = $translate.instant(vm.types.ruleNodeType.INPUT.details) + '';
+            } else {
+                name = node.name;
+                desc = $translate.instant(vm.types.ruleNodeType[node.component.type].name) + ' - ' + node.component.name;
+                if (node.additionalInfo) {
+                    details = node.additionalInfo.description;
+                }
+            }
+            var tooltipContent = '<div class="tb-rule-node-tooltip">' +
+                '<div id="tb-node-content" layout="column">' +
+                '<div class="tb-node-title">' + name + '</div>' +
+                '<div class="tb-node-description">' + desc + '</div>';
+            if (details) {
+                tooltipContent += '<div class="tb-node-details">' + details + '</div>';
+            }
+            tooltipContent += '</div>' +
+                '</div>';
+            displayTooltip(event, tooltipContent);
+        }
+    }
+
+    function displayTooltip(event, content) {
+        destroyTooltips();
+        vm.tooltipTimeout = $timeout(() => {
+            var element = angular.element(event.target);
+            element.tooltipster(
+                {
+                    theme: 'tooltipster-shadow',
+                    delay: 100,
+                    trigger: 'custom',
+                    triggerOpen: {
+                        click: false,
+                        tap: false
+                    },
+                    triggerClose: {
+                        click: true,
+                        tap: true,
+                        scroll: true
+                    },
+                    side: 'right',
+                    trackOrigin: true
+                }
+            );
+            var contentElement = angular.element(content);
+            $compile(contentElement)($scope);
+            var tooltip = element.tooltipster('instance');
+            tooltip.content(contentElement);
+            tooltip.open();
+        }, 500);
+    }
+
+    function updateNodeErrorTooltip(node) {
+        if (node.error) {
+            var element = angular.element('#' + node.id);
+            var tooltip = vm.errorTooltips[node.id];
+            if (!tooltip || !element.hasClass("tooltipstered")) {
+                element.tooltipster(
+                    {
+                        theme: 'tooltipster-shadow',
+                        delay: 0,
+                        animationDuration: 0,
+                        trigger: 'custom',
+                        triggerOpen: {
+                            click: false,
+                            tap: false
+                        },
+                        triggerClose: {
+                            click: false,
+                            tap: false,
+                            scroll: false
+                        },
+                        side: 'top',
+                        trackOrigin: true
+                    }
+                );
+                var content = '<div class="tb-rule-node-error-tooltip">' +
+                    '<div id="tooltip-content" layout="column">' +
+                    '<div class="tb-node-details">' + node.error + '</div>' +
+                    '</div>' +
+                    '</div>';
+                var contentElement = angular.element(content);
+                $compile(contentElement)($scope);
+                tooltip = element.tooltipster('instance');
+                tooltip.isErrorTooltip = true;
+                tooltip.content(contentElement);
+                vm.errorTooltips[node.id] = tooltip;
+            }
+            $mdUtil.nextTick(() => {
+                tooltip.open();
+            });
+        } else {
+            if (vm.errorTooltips[node.id]) {
+                tooltip = vm.errorTooltips[node.id];
+                tooltip.destroy();
+                delete vm.errorTooltips[node.id];
+            }
+        }
+    }
+
+    function updateErrorTooltips(hide) {
+        for (var nodeId in vm.errorTooltips) {
+            var tooltip = vm.errorTooltips[nodeId];
+            if (hide) {
+                tooltip.close();
+            } else {
+                tooltip.open();
+            }
+        }
+    }
+
+    $scope.$watch(function() {
+        return vm.isEditingRuleNode || vm.isEditingRuleNodeLink;
+    }, (val) => {
+        vm.enableHotKeys = !val;
+        updateErrorTooltips(val);
+    });
+
+    vm.editCallbacks = {
+        edgeDoubleClick: function (event, edge) {
+            openLinkDetails(edge);
+        },
+        edgeEdit: function(event, edge) {
+            openLinkDetails(edge);
+        },
+        nodeCallbacks: {
+            'doubleClick': function (event, node) {
+                openNodeDetails(node);
+            },
+            'nodeEdit': function (event, node) {
+                openNodeDetails(node);
+            },
+            'mouseEnter': function (event, node) {
+                displayNodeDescriptionTooltip(event, node);
+            },
+            'mouseLeave': function () {
+                destroyTooltips();
+            },
+            'mouseDown': function () {
+                destroyTooltips();
+            }
+        },
+        isValidEdge: function (source, destination) {
+            return source.type === flowchartConstants.rightConnectorType && destination.type === flowchartConstants.leftConnectorType;
+        },
+        createEdge: function (event, edge) {
+            var deferred = $q.defer();
+            var sourceNode = vm.modelservice.nodes.getNodeByConnectorId(edge.source);
+            if (sourceNode.component.type == types.ruleNodeType.INPUT.value) {
+                var destNode = vm.modelservice.nodes.getNodeByConnectorId(edge.destination);
+                if (destNode.component.type == types.ruleNodeType.RULE_CHAIN.value) {
+                    deferred.reject();
+                } else {
+                    var res = $filter('filter')(vm.ruleChainModel.edges, {source: vm.inputConnectorId}, true);
+                    if (res && res.length) {
+                        vm.modelservice.edges.delete(res[0]);
+                    }
+                    deferred.resolve(edge);
+                }
+            } else {
+                if (edge.label) {
+                    deferred.resolve(edge);
+                } else {
+                    var labels = ruleChainService.getRuleNodeSupportedLinks(sourceNode.component);
+                    vm.enableHotKeys = false;
+                    addRuleNodeLink(event, edge, labels).then(
+                        (link) => {
+                            deferred.resolve(link);
+                            vm.enableHotKeys = true;
+                        },
+                        () => {
+                            deferred.reject();
+                            vm.enableHotKeys = true;
+                        }
+                    );
+                }
+            }
+            return deferred.promise;
+        },
+        dropNode: function (event, node) {
+            addRuleNode(event, node);
+        }
+    };
+
+    function openNodeDetails(node) {
+        if (node.component.type != types.ruleNodeType.INPUT.value) {
+            vm.isEditingRuleNodeLink = false;
+            vm.editingRuleNodeLink = null;
+            vm.isEditingRuleNode = true;
+            vm.editingRuleNodeIndex = vm.ruleChainModel.nodes.indexOf(node);
+            vm.editingRuleNode = angular.copy(node);
+            $mdUtil.nextTick(() => {
+                if (vm.ruleNodeForm) {
+                    vm.ruleNodeForm.$setPristine();
+                }
+            });
+        }
+    }
+
+    function openLinkDetails(edge) {
+        var sourceNode = vm.modelservice.nodes.getNodeByConnectorId(edge.source);
+        if (sourceNode.component.type != types.ruleNodeType.INPUT.value) {
+            vm.isEditingRuleNode = false;
+            vm.editingRuleNode = null;
+            vm.editingRuleNodeLinkLabels = ruleChainService.getRuleNodeSupportedLinks(sourceNode.component);
+            vm.isEditingRuleNodeLink = true;
+            vm.editingRuleNodeLinkIndex = vm.ruleChainModel.edges.indexOf(edge);
+            vm.editingRuleNodeLink = angular.copy(edge);
+            $mdUtil.nextTick(() => {
+                if (vm.ruleNodeLinkForm) {
+                    vm.ruleNodeLinkForm.$setPristine();
+                }
+            });
+        }
+    }
+
+    function copyNode(node) {
+        itembuffer.copyRuleNodes([node], []);
+    }
+
+    function copyRuleNodes() {
+        var nodes = vm.modelservice.nodes.getSelectedNodes();
+        var edges = vm.modelservice.edges.getSelectedEdges();
+        var connections = [];
+        for (var i=0;i<edges.length;i++) {
+            var edge = edges[i];
+            var sourceNode = vm.modelservice.nodes.getNodeByConnectorId(edge.source);
+            var destNode = vm.modelservice.nodes.getNodeByConnectorId(edge.destination);
+            var isInputSource = sourceNode.component.type == types.ruleNodeType.INPUT.value;
+            var fromIndex = nodes.indexOf(sourceNode);
+            var toIndex = nodes.indexOf(destNode);
+            if ( (isInputSource || fromIndex > -1) && toIndex > -1 ) {
+                var connection = {
+                    isInputSource: isInputSource,
+                    fromIndex: fromIndex,
+                    toIndex: toIndex,
+                    label: edge.label
+                };
+                connections.push(connection);
+            }
+        }
+        itembuffer.copyRuleNodes(nodes, connections);
+    }
+
+    function pasteRuleNodes(event) {
+        var canvas = angular.element(vm.canvasControl.modelservice.getCanvasHtmlElement());
+        var x,y;
+        if (event) {
+            var offset = canvas.offset();
+            x = Math.round(event.clientX - offset.left);
+            y = Math.round(event.clientY - offset.top);
+        } else {
+            var scrollParent = canvas.parent();
+            var scrollTop = scrollParent.scrollTop();
+            var scrollLeft = scrollParent.scrollLeft();
+            x = scrollLeft + scrollParent.width()/2;
+            y = scrollTop + scrollParent.height()/2;
+        }
+        var ruleNodes = itembuffer.pasteRuleNodes(x, y, event);
+        if (ruleNodes) {
+            vm.modelservice.deselectAll();
+            var nodes = [];
+            for (var i=0;i<ruleNodes.nodes.length;i++) {
+                var node = ruleNodes.nodes[i];
+                node.id = 'rule-chain-node-' + vm.nextNodeID++;
+                var component = node.component;
+                if (component.configurationDescriptor.nodeDefinition.inEnabled) {
+                    node.connectors.push(
+                        {
+                            type: flowchartConstants.leftConnectorType,
+                            id: vm.nextConnectorID++
+                        }
+                    );
+                }
+                if (component.configurationDescriptor.nodeDefinition.outEnabled) {
+                    node.connectors.push(
+                        {
+                            type: flowchartConstants.rightConnectorType,
+                            id: vm.nextConnectorID++
+                        }
+                    );
+                }
+                nodes.push(node);
+                vm.ruleChainModel.nodes.push(node);
+                vm.modelservice.nodes.select(node);
+            }
+            for (i=0;i<ruleNodes.connections.length;i++) {
+                var connection = ruleNodes.connections[i];
+                var sourceNode = nodes[connection.fromIndex];
+                var destNode = nodes[connection.toIndex];
+                if ( (connection.isInputSource || sourceNode) &&  destNode ) {
+                    var source, destination;
+                    if (connection.isInputSource) {
+                        source = vm.inputConnectorId;
+                    } else {
+                        var sourceConnectors = vm.modelservice.nodes.getConnectorsByType(sourceNode, flowchartConstants.rightConnectorType);
+                        if (sourceConnectors && sourceConnectors.length) {
+                            source = sourceConnectors[0].id;
+                        }
+                    }
+                    var destConnectors = vm.modelservice.nodes.getConnectorsByType(destNode, flowchartConstants.leftConnectorType);
+                    if (destConnectors && destConnectors.length) {
+                        destination = destConnectors[0].id;
+                    }
+                    if (source && destination) {
+                        var edge = {
+                            source: source,
+                            destination: destination,
+                            label: connection.label
+                        };
+                        vm.ruleChainModel.edges.push(edge);
+                        vm.modelservice.edges.select(edge);
+                    }
+                }
+            }
+
+            if (vm.canvasControl.adjustCanvasSize) {
+                vm.canvasControl.adjustCanvasSize();
+            }
+
+            updateRuleNodesHighlight();
+
+            validate();
+        }
+    }
+
+    loadRuleChainLibrary(ruleNodeComponents, true);
+
+    $scope.$watch('vm.ruleNodeSearch',
+        function (newVal, oldVal) {
+            if (!angular.equals(newVal, oldVal)) {
+                var res = $filter('filter')(ruleNodeComponents, {name: vm.ruleNodeSearch});
+                loadRuleChainLibrary(res);
+            }
+        }
+    );
+
+    $scope.$on('searchTextUpdated', function () {
+        updateRuleNodesHighlight();
+    });
+
+    function loadRuleChainLibrary(ruleNodeComponents, loadRuleChain) {
+        for (var componentType in vm.ruleNodeTypesModel) {
+            vm.ruleNodeTypesModel[componentType].model.nodes.length = 0;
+        }
+        for (var i=0;i<ruleNodeComponents.length;i++) {
+            var ruleNodeComponent = ruleNodeComponents[i];
+            componentType = ruleNodeComponent.type;
+            var model = vm.ruleNodeTypesModel[componentType].model;
+            var icon = vm.types.ruleNodeType[componentType].icon;
+            var iconUrl = null;
+            if (ruleNodeComponent.configurationDescriptor.nodeDefinition.icon) {
+                icon = ruleNodeComponent.configurationDescriptor.nodeDefinition.icon;
+            }
+            if (ruleNodeComponent.configurationDescriptor.nodeDefinition.iconUrl) {
+                iconUrl = ruleNodeComponent.configurationDescriptor.nodeDefinition.iconUrl;
+            }
+            var node = {
+                id: 'node-lib-' + componentType + '-' + model.nodes.length,
+                component: ruleNodeComponent,
+                name: '',
+                nodeClass: vm.types.ruleNodeType[componentType].nodeClass,
+                icon: icon,
+                iconUrl: iconUrl,
+                x: 30,
+                y: 10+50*model.nodes.length,
+                connectors: []
+            };
+            if (ruleNodeComponent.configurationDescriptor.nodeDefinition.inEnabled) {
+                node.connectors.push(
+                    {
+                        type: flowchartConstants.leftConnectorType,
+                        id: model.nodes.length * 2
+                    }
+                );
+            }
+            if (ruleNodeComponent.configurationDescriptor.nodeDefinition.outEnabled) {
+                node.connectors.push(
+                    {
+                        type: flowchartConstants.rightConnectorType,
+                        id: model.nodes.length * 2 + 1
+                    }
+                );
+            }
+            model.nodes.push(node);
+        }
+        vm.ruleChainLibraryLoaded = true;
+        if (loadRuleChain) {
+            prepareRuleChain();
+        }
+        $mdUtil.nextTick(() => {
+            for (componentType in vm.ruleNodeTypesCanvasControl) {
+                if (vm.ruleNodeTypesCanvasControl[componentType].adjustCanvasSize) {
+                    vm.ruleNodeTypesCanvasControl[componentType].adjustCanvasSize(true);
+                }
+            }
+            for (componentType in vm.ruleNodeTypesModel) {
+                var panel = vm.$mdExpansionPanel(componentType);
+                if (panel) {
+                    if (!vm.ruleNodeTypesModel[componentType].model.nodes.length) {
+                        panel.collapse();
+                    } else {
+                        panel.expand();
+                    }
+                }
+            }
+        });
+    }
+
+    function prepareRuleChain() {
+
+        if (vm.ruleChainWatch) {
+            vm.ruleChainWatch();
+            vm.ruleChainWatch = null;
+        }
+
+        vm.nextNodeID = 1;
+        vm.nextConnectorID = 1;
+
+        vm.selectedObjects.length = 0;
+        vm.ruleChainModel.nodes.length = 0;
+        vm.ruleChainModel.edges.length = 0;
+
+        vm.inputConnectorId = vm.nextConnectorID++;
+
+        vm.ruleChainModel.nodes.push(
+            {
+                id: 'rule-chain-node-' + vm.nextNodeID++,
+                component: types.inputNodeComponent,
+                name: "",
+                nodeClass: types.ruleNodeType.INPUT.nodeClass,
+                icon: types.ruleNodeType.INPUT.icon,
+                readonly: true,
+                x: 50,
+                y: 150,
+                connectors: [
+                    {
+                        type: flowchartConstants.rightConnectorType,
+                        id: vm.inputConnectorId
+                    },
+                ]
+
+            }
+        );
+        ruleChainService.resolveTargetRuleChains(vm.ruleChainMetaData.ruleChainConnections)
+            .then((ruleChainsMap) => {
+                createRuleChainModel(ruleChainsMap);
+            }
+        );
+    }
+
+    function createRuleChainModel(ruleChainsMap) {
+        var nodes = [];
+        for (var i=0;i<vm.ruleChainMetaData.nodes.length;i++) {
+            var ruleNode = vm.ruleChainMetaData.nodes[i];
+            var component = ruleChainService.getRuleNodeComponentByClazz(ruleNode.type);
+            if (component) {
+                var icon = vm.types.ruleNodeType[component.type].icon;
+                var iconUrl = null;
+                if (component.configurationDescriptor.nodeDefinition.icon) {
+                    icon = component.configurationDescriptor.nodeDefinition.icon;
+                }
+                if (component.configurationDescriptor.nodeDefinition.iconUrl) {
+                    iconUrl = component.configurationDescriptor.nodeDefinition.iconUrl;
+                }
+                var node = {
+                    id: 'rule-chain-node-' + vm.nextNodeID++,
+                    ruleNodeId: ruleNode.id,
+                    additionalInfo: ruleNode.additionalInfo,
+                    configuration: ruleNode.configuration,
+                    debugMode: ruleNode.debugMode,
+                    x: ruleNode.additionalInfo.layoutX,
+                    y: ruleNode.additionalInfo.layoutY,
+                    component: component,
+                    name: ruleNode.name,
+                    nodeClass: vm.types.ruleNodeType[component.type].nodeClass,
+                    icon: icon,
+                    iconUrl: iconUrl,
+                    connectors: []
+                };
+                if (component.configurationDescriptor.nodeDefinition.inEnabled) {
+                    node.connectors.push(
+                        {
+                            type: flowchartConstants.leftConnectorType,
+                            id: vm.nextConnectorID++
+                        }
+                    );
+                }
+                if (component.configurationDescriptor.nodeDefinition.outEnabled) {
+                    node.connectors.push(
+                        {
+                            type: flowchartConstants.rightConnectorType,
+                            id: vm.nextConnectorID++
+                        }
+                    );
+                }
+                nodes.push(node);
+                vm.ruleChainModel.nodes.push(node);
+            }
+        }
+
+        if (vm.ruleChainMetaData.firstNodeIndex > -1) {
+            var destNode = nodes[vm.ruleChainMetaData.firstNodeIndex];
+            if (destNode) {
+                var connectors = vm.modelservice.nodes.getConnectorsByType(destNode, flowchartConstants.leftConnectorType);
+                if (connectors && connectors.length) {
+                    var edge = {
+                        source: vm.inputConnectorId,
+                        destination: connectors[0].id
+                    };
+                    vm.ruleChainModel.edges.push(edge);
+                }
+            }
+        }
+
+        if (vm.ruleChainMetaData.connections) {
+            for (i = 0; i < vm.ruleChainMetaData.connections.length; i++) {
+                var connection = vm.ruleChainMetaData.connections[i];
+                var sourceNode = nodes[connection.fromIndex];
+                destNode = nodes[connection.toIndex];
+                if (sourceNode && destNode) {
+                    var sourceConnectors = vm.modelservice.nodes.getConnectorsByType(sourceNode, flowchartConstants.rightConnectorType);
+                    var destConnectors = vm.modelservice.nodes.getConnectorsByType(destNode, flowchartConstants.leftConnectorType);
+                    if (sourceConnectors && sourceConnectors.length && destConnectors && destConnectors.length) {
+                        edge = {
+                            source: sourceConnectors[0].id,
+                            destination: destConnectors[0].id,
+                            label: connection.type
+                        };
+                        vm.ruleChainModel.edges.push(edge);
+                    }
+                }
+            }
+        }
+
+        if (vm.ruleChainMetaData.ruleChainConnections) {
+            var ruleChainNodesMap = {};
+            for (i = 0; i < vm.ruleChainMetaData.ruleChainConnections.length; i++) {
+                var ruleChainConnection = vm.ruleChainMetaData.ruleChainConnections[i];
+                var ruleChain = ruleChainsMap[ruleChainConnection.targetRuleChainId.id];
+                if (ruleChainConnection.additionalInfo && ruleChainConnection.additionalInfo.ruleChainNodeId) {
+                    var ruleChainNode = ruleChainNodesMap[ruleChainConnection.additionalInfo.ruleChainNodeId];
+                    if (!ruleChainNode) {
+                        ruleChainNode = {
+                            id: 'rule-chain-node-' + vm.nextNodeID++,
+                            additionalInfo: ruleChainConnection.additionalInfo,
+                            x: ruleChainConnection.additionalInfo.layoutX,
+                            y: ruleChainConnection.additionalInfo.layoutY,
+                            component: types.ruleChainNodeComponent,
+                            nodeClass: vm.types.ruleNodeType.RULE_CHAIN.nodeClass,
+                            icon: vm.types.ruleNodeType.RULE_CHAIN.icon,
+                            connectors: [
+                                {
+                                    type: flowchartConstants.leftConnectorType,
+                                    id: vm.nextConnectorID++
+                                }
+                            ]
+                        };
+                        if (ruleChain.name) {
+                            ruleChainNode.name = ruleChain.name;
+                            ruleChainNode.targetRuleChainId = ruleChainConnection.targetRuleChainId.id;
+                        } else {
+                            ruleChainNode.name = "Unresolved";
+                            ruleChainNode.targetRuleChainId = null;
+                            ruleChainNode.error = $translate.instant('rulenode.invalid-target-rulechain');
+                        }
+                        ruleChainNodesMap[ruleChainConnection.additionalInfo.ruleChainNodeId] = ruleChainNode;
+                        vm.ruleChainModel.nodes.push(ruleChainNode);
+                    }
+                    sourceNode = nodes[ruleChainConnection.fromIndex];
+                    if (sourceNode) {
+                        connectors = vm.modelservice.nodes.getConnectorsByType(sourceNode, flowchartConstants.rightConnectorType);
+                        if (connectors && connectors.length) {
+                            var ruleChainEdge = {
+                                source: connectors[0].id,
+                                destination: ruleChainNode.connectors[0].id,
+                                label: ruleChainConnection.type
+                            };
+                            vm.ruleChainModel.edges.push(ruleChainEdge);
+                        }
+                    }
+                }
+            }
+        }
+
+        if (vm.canvasControl.adjustCanvasSize) {
+            vm.canvasControl.adjustCanvasSize(true);
+        }
+
+        vm.isDirty = false;
+
+        updateRuleNodesHighlight();
+
+        validate();
+
+        $mdUtil.nextTick(() => {
+            vm.ruleChainWatch = $scope.$watch('vm.ruleChainModel',
+                function (newVal, oldVal) {
+                    if (!angular.equals(newVal, oldVal)) {
+                        validate();
+                        if (!vm.isDirty) {
+                            vm.isDirty = true;
+                        }
+                    }
+                }, true
+            );
+        });
+    }
+
+    function updateRuleNodesHighlight() {
+        for (var i = 0; i < vm.ruleChainModel.nodes.length; i++) {
+            vm.ruleChainModel.nodes[i].highlighted = false;
+        }
+        if ($scope.searchConfig.searchText) {
+            var res = $filter('filter')(vm.ruleChainModel.nodes, {name: $scope.searchConfig.searchText});
+            if (res) {
+                for (i = 0; i < res.length; i++) {
+                    res[i].highlighted = true;
+                }
+            }
+        }
+    }
+
+    function validate() {
+        $mdUtil.nextTick(() => {
+            vm.isInvalid = false;
+            for (var i = 0; i < vm.ruleChainModel.nodes.length; i++) {
+                if (vm.ruleChainModel.nodes[i].error) {
+                    vm.isInvalid = true;
+                }
+                updateNodeErrorTooltip(vm.ruleChainModel.nodes[i]);
+            }
+        });
+    }
+
+    function saveRuleChain() {
+        var saveRuleChainPromise;
+        if (vm.isImport) {
+            saveRuleChainPromise = ruleChainService.saveRuleChain(vm.ruleChain);
+        } else {
+            saveRuleChainPromise = $q.when(vm.ruleChain);
+        }
+        saveRuleChainPromise.then(
+            (ruleChain) => {
+                vm.ruleChain = ruleChain;
+                var ruleChainMetaData = {
+                    ruleChainId: vm.ruleChain.id,
+                    nodes: [],
+                    connections: [],
+                    ruleChainConnections: []
+                };
+
+                var nodes = [];
+
+                for (var i=0;i<vm.ruleChainModel.nodes.length;i++) {
+                    var node = vm.ruleChainModel.nodes[i];
+                    if (node.component.type != types.ruleNodeType.INPUT.value && node.component.type != types.ruleNodeType.RULE_CHAIN.value) {
+                        var ruleNode = {};
+                        if (node.ruleNodeId) {
+                            ruleNode.id = node.ruleNodeId;
+                        }
+                        ruleNode.type = node.component.clazz;
+                        ruleNode.name = node.name;
+                        ruleNode.configuration = node.configuration;
+                        ruleNode.additionalInfo = node.additionalInfo;
+                        ruleNode.debugMode = node.debugMode;
+                        if (!ruleNode.additionalInfo) {
+                            ruleNode.additionalInfo = {};
+                        }
+                        ruleNode.additionalInfo.layoutX = node.x;
+                        ruleNode.additionalInfo.layoutY = node.y;
+                        ruleChainMetaData.nodes.push(ruleNode);
+                        nodes.push(node);
+                    }
+                }
+                var res = $filter('filter')(vm.ruleChainModel.edges, {source: vm.inputConnectorId}, true);
+                if (res && res.length) {
+                    var firstNodeEdge = res[0];
+                    var firstNode = vm.modelservice.nodes.getNodeByConnectorId(firstNodeEdge.destination);
+                    ruleChainMetaData.firstNodeIndex = nodes.indexOf(firstNode);
+                }
+                for (i=0;i<vm.ruleChainModel.edges.length;i++) {
+                    var edge = vm.ruleChainModel.edges[i];
+                    var sourceNode = vm.modelservice.nodes.getNodeByConnectorId(edge.source);
+                    var destNode = vm.modelservice.nodes.getNodeByConnectorId(edge.destination);
+                    if (sourceNode.component.type != types.ruleNodeType.INPUT.value) {
+                        var fromIndex = nodes.indexOf(sourceNode);
+                        if (destNode.component.type == types.ruleNodeType.RULE_CHAIN.value) {
+                            var ruleChainConnection = {
+                                fromIndex: fromIndex,
+                                targetRuleChainId: {entityType: vm.types.entityType.rulechain, id: destNode.targetRuleChainId},
+                                additionalInfo: destNode.additionalInfo,
+                                type: edge.label
+                            };
+                            if (!ruleChainConnection.additionalInfo) {
+                                ruleChainConnection.additionalInfo = {};
+                            }
+                            ruleChainConnection.additionalInfo.layoutX = destNode.x;
+                            ruleChainConnection.additionalInfo.layoutY = destNode.y;
+                            ruleChainConnection.additionalInfo.ruleChainNodeId = destNode.id;
+                            ruleChainMetaData.ruleChainConnections.push(ruleChainConnection);
+                        } else {
+                            var toIndex = nodes.indexOf(destNode);
+                            var nodeConnection = {
+                                fromIndex: fromIndex,
+                                toIndex: toIndex,
+                                type: edge.label
+                            };
+                            ruleChainMetaData.connections.push(nodeConnection);
+                        }
+                    }
+                }
+                ruleChainService.saveRuleChainMetaData(ruleChainMetaData).then(
+                    (ruleChainMetaData) => {
+                        vm.ruleChainMetaData = ruleChainMetaData;
+                        if (vm.isImport) {
+                            vm.isDirty = false;
+                            vm.isImport = false;
+                            $mdUtil.nextTick(() => {
+                                $state.go('home.ruleChains.ruleChain', {ruleChainId: vm.ruleChain.id.id});
+                            });
+                        } else {
+                            prepareRuleChain();
+                        }
+                    }
+                );
+            }
+        );
+    }
+
+    function revertRuleChain() {
+        prepareRuleChain();
+    }
+
+    function addRuleNode($event, ruleNode) {
+
+        ruleNode.configuration = angular.copy(ruleNode.component.configurationDescriptor.nodeDefinition.defaultConfiguration);
+
+        var ruleChainId = vm.ruleChain.id ? vm.ruleChain.id.id : null;
+
+        vm.enableHotKeys = false;
+
+        $mdDialog.show({
+            controller: 'AddRuleNodeController',
+            controllerAs: 'vm',
+            templateUrl: addRuleNodeTemplate,
+            parent: angular.element($document[0].body),
+            locals: {ruleNode: ruleNode, ruleChainId: ruleChainId},
+            fullscreen: true,
+            targetEvent: $event
+        }).then(function (ruleNode) {
+            ruleNode.id = 'rule-chain-node-' + vm.nextNodeID++;
+            ruleNode.connectors = [];
+            if (ruleNode.component.configurationDescriptor.nodeDefinition.inEnabled) {
+                ruleNode.connectors.push(
+                    {
+                        id: vm.nextConnectorID++,
+                        type: flowchartConstants.leftConnectorType
+                    }
+                );
+            }
+            if (ruleNode.component.configurationDescriptor.nodeDefinition.outEnabled) {
+                ruleNode.connectors.push(
+                    {
+                        id: vm.nextConnectorID++,
+                        type: flowchartConstants.rightConnectorType
+                    }
+                );
+            }
+            vm.ruleChainModel.nodes.push(ruleNode);
+            updateRuleNodesHighlight();
+            vm.enableHotKeys = true;
+        }, function () {
+            vm.enableHotKeys = true;
+        });
+    }
+
+    function addRuleNodeLink($event, link, labels) {
+        return $mdDialog.show({
+            controller: 'AddRuleNodeLinkController',
+            controllerAs: 'vm',
+            templateUrl: addRuleNodeLinkTemplate,
+            parent: angular.element($document[0].body),
+            locals: {link: link, labels: labels},
+            fullscreen: true,
+            targetEvent: $event
+        });
+    }
+
+    function objectsSelected() {
+        return vm.modelservice.nodes.getSelectedNodes().length > 0 ||
+            vm.modelservice.edges.getSelectedEdges().length > 0
+    }
+
+    function deleteSelected() {
+        vm.modelservice.deleteSelected();
+    }
+
+    function triggerResize() {
+        var w = angular.element($window);
+        w.triggerHandler('resize');
+    }
+}
+
+/*@ngInject*/
+export function AddRuleNodeController($scope, $mdDialog, ruleNode, ruleChainId, helpLinks) {
+
+    var vm = this;
+
+    vm.helpLinks = helpLinks;
+    vm.ruleNode = ruleNode;
+    vm.ruleChainId = ruleChainId;
+
+    vm.add = add;
+    vm.cancel = cancel;
+
+    function cancel() {
+        $mdDialog.cancel();
+    }
+
+    function add() {
+        $scope.theForm.$setPristine();
+        $mdDialog.hide(vm.ruleNode);
+    }
+}
+
+/*@ngInject*/
+export function AddRuleNodeLinkController($scope, $mdDialog, link, labels, helpLinks) {
+
+    var vm = this;
+
+    vm.helpLinks = helpLinks;
+    vm.link = link;
+    vm.labels = labels;
+
+    vm.add = add;
+    vm.cancel = cancel;
+
+    function cancel() {
+        $mdDialog.cancel();
+    }
+
+    function add() {
+        $scope.theForm.$setPristine();
+        $mdDialog.hide(vm.link);
+    }
+}
diff --git a/ui/src/app/rulechain/rulechain.directive.js b/ui/src/app/rulechain/rulechain.directive.js
new file mode 100644
index 0000000..8d19229
--- /dev/null
+++ b/ui/src/app/rulechain/rulechain.directive.js
@@ -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.
+ */
+
+/* eslint-disable import/no-unresolved, import/default */
+
+import ruleChainFieldsetTemplate from './rulechain-fieldset.tpl.html';
+
+/* eslint-enable import/no-unresolved, import/default */
+
+/*@ngInject*/
+export default function RuleChainDirective($compile, $templateCache, $mdDialog, $document, $q, $translate, types, toast) {
+    var linker = function (scope, element) {
+        var template = $templateCache.get(ruleChainFieldsetTemplate);
+        element.html(template);
+
+        scope.onRuleChainIdCopied = function() {
+            toast.showSuccess($translate.instant('rulechain.idCopiedMessage'), 750, angular.element(element).parent().parent(), 'bottom left');
+        };
+
+        $compile(element.contents())(scope);
+    }
+    return {
+        restrict: "E",
+        link: linker,
+        scope: {
+            ruleChain: '=',
+            isEdit: '=',
+            isReadOnly: '=',
+            theForm: '=',
+            onSetRootRuleChain: '&',
+            onExportRuleChain: '&',
+            onDeleteRuleChain: '&'
+        }
+    };
+}
diff --git a/ui/src/app/rulechain/rulechain.routes.js b/ui/src/app/rulechain/rulechain.routes.js
new file mode 100644
index 0000000..f649f53
--- /dev/null
+++ b/ui/src/app/rulechain/rulechain.routes.js
@@ -0,0 +1,127 @@
+/*
+ * 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.
+ */
+/* eslint-disable import/no-unresolved, import/default */
+
+import ruleNodeTemplate from './rulenode.tpl.html';
+import ruleChainsTemplate from './rulechains.tpl.html';
+import ruleChainTemplate from './rulechain.tpl.html';
+
+/* eslint-enable import/no-unresolved, import/default */
+
+/*@ngInject*/
+export default function RuleChainRoutes($stateProvider, NodeTemplatePathProvider) {
+
+    NodeTemplatePathProvider.setTemplatePath(ruleNodeTemplate);
+
+    $stateProvider
+        .state('home.ruleChains', {
+            url: '/ruleChains',
+            params: {'topIndex': 0},
+            module: 'private',
+            auth: ['SYS_ADMIN', 'TENANT_ADMIN'],
+            views: {
+                "content@home": {
+                    templateUrl: ruleChainsTemplate,
+                    controllerAs: 'vm',
+                    controller: 'RuleChainsController'
+                }
+            },
+            data: {
+                searchEnabled: true,
+                pageTitle: 'rulechain.rulechains'
+            },
+            ncyBreadcrumb: {
+                label: '{"icon": "settings_ethernet", "label": "rulechain.rulechains"}'
+            }
+        }).state('home.ruleChains.ruleChain', {
+            url: '/:ruleChainId',
+            reloadOnSearch: false,
+            module: 'private',
+            auth: ['SYS_ADMIN', 'TENANT_ADMIN'],
+            views: {
+                "content@home": {
+                    templateUrl: ruleChainTemplate,
+                    controller: 'RuleChainController',
+                    controllerAs: 'vm'
+                }
+            },
+            resolve: {
+                ruleChain:
+                    /*@ngInject*/
+                    function($stateParams, ruleChainService) {
+                        return ruleChainService.getRuleChain($stateParams.ruleChainId);
+                    },
+                ruleChainMetaData:
+                /*@ngInject*/
+                    function($stateParams, ruleChainService) {
+                        return ruleChainService.getRuleChainMetaData($stateParams.ruleChainId);
+                    },
+                ruleNodeComponents:
+                /*@ngInject*/
+                    function($stateParams, ruleChainService) {
+                        return ruleChainService.getRuleNodeComponents();
+                    }
+            },
+            data: {
+                import: false,
+                searchEnabled: true,
+                pageTitle: 'rulechain.rulechain'
+            },
+            ncyBreadcrumb: {
+                label: '{"icon": "settings_ethernet", "label": "{{ vm.ruleChain.name + (vm.ruleChain.root ? (\' (\' + (\'rulechain.root\' | translate) + \')\') : \'\') }}", "translate": "false"}'
+            }
+    }).state('home.ruleChains.importRuleChain', {
+        url: '/ruleChain/import',
+        reloadOnSearch: false,
+        module: 'private',
+        auth: ['SYS_ADMIN', 'TENANT_ADMIN'],
+        views: {
+            "content@home": {
+                templateUrl: ruleChainTemplate,
+                controller: 'RuleChainController',
+                controllerAs: 'vm'
+            }
+        },
+        params: {
+            ruleChainImport: {}
+        },
+        resolve: {
+            ruleChain:
+            /*@ngInject*/
+                function($stateParams) {
+                    return $stateParams.ruleChainImport.ruleChain;
+                },
+            ruleChainMetaData:
+            /*@ngInject*/
+                function($stateParams) {
+                    return $stateParams.ruleChainImport.metadata;
+                },
+            ruleNodeComponents:
+            /*@ngInject*/
+                function($stateParams, ruleChainService) {
+                    return ruleChainService.getRuleNodeComponents();
+                }
+        },
+        data: {
+            import: true,
+            searchEnabled: true,
+            pageTitle: 'rulechain.rulechain'
+        },
+        ncyBreadcrumb: {
+            label: '{"icon": "settings_ethernet", "label": "{{ (\'rulechain.import\' | translate) + \': \'+ vm.ruleChain.name }}", "translate": "false"}'
+        }
+    });
+}
diff --git a/ui/src/app/rulechain/rulechain.scss b/ui/src/app/rulechain/rulechain.scss
new file mode 100644
index 0000000..c51a955
--- /dev/null
+++ b/ui/src/app/rulechain/rulechain.scss
@@ -0,0 +1,512 @@
+/**
+ * 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.
+ */
+
+.tb-rulechain {
+  .tb-fullscreen-button-style {
+    z-index: 1;
+  }
+  section.tb-header-buttons.tb-library-open {
+    pointer-events: none;
+    position: absolute;
+    left: 0px;
+    top: 0px;
+    z-index: 1;
+    .md-button.tb-btn-open-library {
+      left: 0px;
+      top: 0px;
+      line-height: 36px;
+      width: 36px;
+      height: 36px;
+      margin: 4px 0 0 4px;
+      opacity: 0.5;
+    }
+  }
+  .tb-rulechain-library {
+    width: 250px;
+    min-width: 250px;
+    z-index: 1;
+    md-toolbar {
+      min-height: 48px;
+      height: 48px;
+      .md-toolbar-tools>.md-button:last-child {
+        margin-right: 0px;
+      }
+      .md-toolbar-tools {
+        font-size: 14px;
+        padding: 0px 6px;
+        height: 48px;
+        .md-button.md-icon-button {
+          margin: 0px;
+          &.tb-small {
+            height: 32px;
+            min-height: 32px;
+            line-height: 20px;
+            padding: 6px;
+            width: 32px;
+            md-icon {
+              line-height: 20px;
+              font-size: 20px;
+              height: 20px;
+              width: 20px;
+              min-height: 20px;
+              min-width: 20px;
+            }
+          }
+        }
+      }
+    }
+    .tb-rulechain-library-panel-group {
+      overflow-y: auto;
+      overflow-x: hidden;
+      .tb-panel-title {
+        -webkit-user-select: none;
+        -moz-user-select: none;
+        -ms-user-select: none;
+        user-select: none;
+        min-width: 150px;
+      }
+      .fc-canvas {
+        background: #f9f9f9;
+      }
+      md-icon.md-expansion-panel-icon {
+        margin-right: 0px;
+      }
+      md-expansion-panel-collapsed, .md-expansion-panel-header-container {
+        background: #e6e6e6;
+        border-color: #909090;
+        position: static;
+      }
+      md-expansion-panel {
+        &.md-open {
+          margin-top: 0;
+          margin-bottom: 0;
+        }
+      }
+      md-expansion-panel-content {
+        padding: 0px;
+      }
+    }
+  }
+  .tb-rulechain-graph {
+    z-index: 0;
+    overflow: auto;
+  }
+}
+
+#tb-rule-chain-context-menu {
+  padding-top: 0px;
+  border-radius: 8px;
+  max-height: 404px;
+  .tb-context-menu-header {
+    padding: 8px 5px 5px;
+    font-size: 14px;
+    display: flex;
+    flex-direction: row;
+    height: 36px;
+    min-height: 36px;
+    &.tb-rulechain {
+      background-color: #aac7e4;
+    }
+    &.tb-link {
+      background-color: #aac7e4;
+    }
+    md-icon {
+      padding-left: 2px;
+      padding-right: 10px;
+    }
+    .tb-context-menu-title {
+      font-weight: 500;
+    }
+    .tb-context-menu-subtitle {
+      font-size: 12px;
+    }
+  }
+}
+
+.fc-canvas {
+  min-width: 100%;
+  min-height: 100%;
+  outline: none;
+  -webkit-touch-callout: none;
+  -webkit-user-select: none;
+  -khtml-user-select: none;
+  -moz-user-select: none;
+  -ms-user-select: none;
+  user-select: none;
+  svg {
+    display: block;
+  }
+}
+
+.tb-rule-node, #tb-rule-chain-context-menu .tb-context-menu-header {
+  &.tb-filter-type {
+    background-color: #f1e861;
+  }
+  &.tb-enrichment-type {
+    background-color: #cdf14e;
+  }
+  &.tb-transformation-type {
+    background-color: #79cef1;
+  }
+  &.tb-action-type {
+    background-color: #f1928f;
+  }
+  &.tb-external-type {
+    background-color: #fbc766;
+  }
+  &.tb-rule-chain-type {
+    background-color: #d6c4f1;
+  }
+}
+
+.tb-rule-node {
+  display: flex;
+  flex-direction: row;
+  min-width: 150px;
+  max-width: 150px;
+  min-height: 32px;
+  max-height: 32px;
+  height: 32px;
+  padding: 5px 10px;
+  border-radius: 5px;
+  background-color: #F15B26;
+  pointer-events: none;
+  color: #333;
+  border: solid 1px #777;
+  font-size: 12px;
+  line-height: 16px;
+  &.tb-rule-node-highlighted:not(.tb-rule-node-invalid) {
+    box-shadow: 0 0 10px 6px #51cbee;
+    .tb-node-title {
+      text-decoration: underline;
+      font-weight: bold;
+    }
+  }
+  &.tb-rule-node-invalid {
+    box-shadow: 0 0 10px 6px #ff5c50;
+  }
+  &.tb-input-type {
+    background-color: #a3eaa9;
+    user-select: none;
+  }
+  md-icon {
+    font-size: 20px;
+    width: 20px;
+    height: 20px;
+    min-height: 20px;
+    min-width: 20px;
+    padding-right: 4px;
+  }
+  .tb-node-type {
+
+  }
+  .tb-node-title {
+    font-weight: 500;
+  }
+  .tb-node-type, .tb-node-title {
+    overflow: hidden;
+    white-space: nowrap;
+    text-overflow: ellipsis;
+  }
+}
+
+.fc-node {
+  z-index: 1;
+  outline: none;
+  border-radius: 8px;
+  &.fc-dragging {
+    z-index: 10;
+  }
+  p {
+    padding: 0 15px;
+    text-align: center;
+  }
+  .fc-node-overlay {
+    position: absolute;
+    pointer-events: none;
+    left: 0;
+    top: 0;
+    right: 0;
+    bottom: 0;
+    background-color: #000;
+    opacity: 0;
+    border-radius: 5px;
+  }
+  &.fc-hover {
+    .fc-node-overlay {
+      opacity: 0.25;
+    }
+  }
+  &.fc-selected {
+    .fc-node-overlay {
+      opacity: 0.25;
+    }
+  }
+  &.fc-selected {
+    &:not(.fc-edit) {
+      border: solid 3px red;
+      margin: -3px;
+    }
+  }
+}
+
+.fc-leftConnectors, .fc-rightConnectors {
+  position: absolute;
+  top: 0;
+  height: 100%;
+
+  display: flex;
+  flex-direction: column;
+
+  z-index: 0;
+  .fc-magnet {
+    align-items: center;
+  }
+}
+
+.fc-leftConnectors {
+  left: -20px;
+}
+
+.fc-rightConnectors {
+  right: -20px;
+}
+
+.fc-magnet {
+  display: flex;
+  flex-grow: 1;
+  height: 60px;
+  justify-content: center;
+}
+
+.fc-connector {
+  width: 14px;
+  height: 14px;
+  border: 1px solid #333;
+  margin: 10px;
+  border-radius: 5px;
+  background-color: #ccc;
+  pointer-events: all;
+}
+
+.fc-connector.fc-hover {
+  background-color: #000;
+}
+
+.fc-arrow-marker {
+  polygon {
+    stroke: gray;
+    fill: gray;
+  }
+}
+
+.fc-arrow-marker-selected {
+  polygon {
+    stroke: red;
+    fill: red;
+  }
+}
+
+.fc-edge {
+  outline: none;
+  stroke: gray;
+  stroke-width: 4;
+  fill: transparent;
+  transition: stroke-width .2s;
+  &.fc-selected {
+    stroke: red;
+    stroke-width: 4;
+    fill: transparent;
+  }
+  &.fc-active {
+    animation: dash 3s linear infinite;
+    stroke-dasharray: 20;
+  }
+  &.fc-hover {
+    stroke: gray;
+    stroke-width: 6;
+    fill: transparent;
+  }
+  &.fc-dragging {
+    pointer-events: none;
+  }
+}
+
+.edge-endpoint {
+  fill: gray;
+}
+
+.fc-nodedelete {
+  display: none;
+  font-size: 18px;
+}
+
+.fc-nodeedit {
+  display: none;
+  font-size: 15px;
+}
+
+.fc-edit {
+  .fc-nodedelete, .fc-nodeedit {
+    outline: none;
+    display: block;
+    position: absolute;
+    border: solid 2px white;
+    border-radius: 50%;
+    font-weight: 600;
+    line-height: 20px;
+    height: 20px;
+    padding-top: 2px;
+    width: 22px;
+    background: #f83e05;
+    color: #fff;
+    text-align: center;
+    vertical-align: bottom;
+    cursor: pointer;
+  }
+
+  .fc-nodeedit {
+    top: -24px;
+    right: 16px;
+  }
+
+  .fc-nodedelete {
+    top: -24px;
+    right: -13px;
+  }
+
+}
+
+.fc-noselect {
+  -webkit-touch-callout: none; /* iOS Safari */
+  -webkit-user-select: none; /* Safari */
+  -khtml-user-select: none; /* Konqueror HTML */
+  -moz-user-select: none; /* Firefox */
+  -ms-user-select: none; /* Internet Explorer/Edge */
+  user-select: none; /* Non-prefixed version, currently
+                                  supported by Chrome and Opera */
+}
+
+.fc-edge-label {
+  position: absolute;
+  transition: transform .2s;
+//  opacity: 0.8;
+  &.ng-leave {
+    transition: 0s none;
+  }
+  &.fc-hover {
+    transform: scale(1.25);
+  }
+  &.fc-selected {
+    .fc-edge-label-text {
+      span {
+        border: solid red;
+        color: #fff;
+        font-weight: 600;
+        background-color: red;
+      }
+    }
+  }
+  .fc-nodeedit {
+    top: -30px;
+    right: 14px;
+  }
+  .fc-nodedelete {
+    top: -30px;
+    right: -13px;
+  }
+  &:focus {
+    outline: 0;
+  }
+}
+
+.fc-edge-label-text {
+  position: absolute;
+  -webkit-transform: translate(-50%, -50%);
+  transform: translate(-50%, -50%);
+  white-space: nowrap;
+  text-align: center;
+  font-size: 14px;
+  font-weight: 600;
+  span {
+    cursor: default;
+    border: solid 2px #003a79;
+    border-radius: 10px;
+    color: #003a79;
+    background-color: #fff;
+    padding: 3px 5px;
+  }
+}
+
+.fc-select-rectangle {
+  border: 2px dashed #5262ff;
+  position: absolute;
+  background: rgba(20,125,255,0.1);
+  z-index: 2;
+}
+
+@keyframes dash {
+  from {
+    stroke-dashoffset: 500;
+  }
+}
+
+.tb-rule-node-tooltip, .tb-rule-node-help {
+  color: #333;
+}
+
+.tb-rule-node-tooltip {
+  font-size: 14px;
+  max-width: 300px;
+  &.tb-lib-tooltip {
+    width: 300px;
+  }
+}
+
+.tb-rule-node-help {
+  font-size: 16px;
+}
+
+.tb-rule-node-error-tooltip {
+  font-size: 16px;
+  color: #ea0d0d;
+}
+
+.tb-rule-node-tooltip, .tb-rule-node-error-tooltip, .tb-rule-node-help {
+  #tb-node-content {
+    .tb-node-title {
+      font-weight: 600;
+    }
+    .tb-node-description {
+      font-style: italic;
+      color: #555;
+    }
+    .tb-node-details {
+      padding-top: 10px;
+      padding-bottom: 10px;
+    }
+    code {
+      padding: 0px 3px 2px 3px;
+      margin: 1px;
+      color: #AD1625;
+      white-space: nowrap;
+      background-color: #f7f7f9;
+      border: 1px solid #e1e1e8;
+      border-radius: 2px;
+      font-size: 12px;
+    }
+  }
+}
\ No newline at end of file
diff --git a/ui/src/app/rulechain/rulechain.tpl.html b/ui/src/app/rulechain/rulechain.tpl.html
new file mode 100644
index 0000000..a84df90
--- /dev/null
+++ b/ui/src/app/rulechain/rulechain.tpl.html
@@ -0,0 +1,246 @@
+<!--
+
+    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.
+
+-->
+
+<md-content flex tb-expand-fullscreen tb-confirm-on-exit is-dirty="vm.isConfirmOnExit"
+            expand-tooltip-direction="bottom" layout="column" class="tb-rulechain"
+            ng-keydown="vm.keyDown($event)"
+            ng-keyup="vm.keyUp($event)" on-fullscreen-changed="vm.isFullscreen = expanded">
+    <section class="tb-rulechain-container" flex layout="column">
+        <div class="tb-rulechain-layout" flex layout="row">
+            <section layout="row" layout-wrap
+                     class="tb-header-buttons md-fab tb-library-open">
+                <md-button ng-show="!vm.isLibraryOpen"
+                           class="tb-btn-header tb-btn-open-library md-primary md-fab md-fab-top-left"
+                           aria-label="{{ 'rulenode.open-node-library' | translate }}"
+                           ng-click="vm.isLibraryOpen = true">
+                    <md-tooltip md-direction="{{vm.isFullscreen ? 'bottom' : 'top'}}">
+                        {{ 'rulenode.open-node-library' | translate }}
+                    </md-tooltip>
+                    <ng-md-icon icon="menu"></ng-md-icon>
+                </md-button>
+            </section>
+            <md-sidenav class="tb-rulechain-library md-sidenav-left md-whiteframe-4dp"
+                        md-disable-backdrop
+                        md-is-locked-open="vm.isLibraryOpenReadonly"
+                        md-is-open="vm.isLibraryOpenReadonly"
+                        md-component-id="rulechain-library-sidenav" layout="column">
+                <md-toolbar>
+                    <div class="md-toolbar-tools">
+                        <md-button class="md-icon-button tb-small" aria-label="{{ 'action.search' | translate }}">
+                            <md-icon aria-label="{{ 'action.search' | translate }}" class="material-icons">search</md-icon>
+                            <md-tooltip md-direction="{{vm.isFullscreen ? 'bottom' : 'top'}}">
+                                {{'rulenode.search' | translate}}
+                            </md-tooltip>
+                        </md-button>
+                        <div layout="row" md-theme="tb-dark" flex>
+                            <md-input-container flex>
+                                <label>&nbsp;</label>
+                                <input ng-model="vm.ruleNodeSearch" placeholder="{{'rulenode.search' | translate}}"/>
+                            </md-input-container>
+                        </div>
+                        <md-button class="md-icon-button tb-small" aria-label="Close"
+                                   ng-show="vm.ruleNodeSearch"
+                                   ng-click="vm.ruleNodeSearch = ''">
+                            <md-icon aria-label="Close" class="material-icons">close</md-icon>
+                            <md-tooltip md-direction="{{vm.isFullscreen ? 'bottom' : 'top'}}">
+                                {{ 'action.clear-search' | translate }}
+                            </md-tooltip>
+                        </md-button>
+                        <md-button class="md-icon-button tb-small" aria-label="Close" ng-click="vm.isLibraryOpen = false">
+                            <md-icon aria-label="Close" class="material-icons">chevron_left</md-icon>
+                            <md-tooltip md-direction="{{vm.isFullscreen ? 'bottom' : 'top'}}">
+                                {{ 'action.close' | translate }}
+                            </md-tooltip>
+                        </md-button>
+                    </div>
+                </md-toolbar>
+                <md-expansion-panel-group flex
+                                          ng-if="vm.ruleChainLibraryLoaded" class="tb-rulechain-library-panel-group"
+                                          md-component-id="libraryPanelGroup" auto-expand="true" multiple>
+                    <md-expansion-panel md-component-id="{{typeId}}" id="{{typeId}}" ng-repeat="(typeId, typeModel) in vm.ruleNodeTypesModel">
+                        <md-expansion-panel-collapsed ng-mouseenter="vm.typeHeaderMouseEnter($event, typeId)"
+                                                      ng-mouseleave="vm.destroyTooltips()">
+                            <md-icon aria-label="node-type-icon"
+                                     class="material-icons" style="margin-right: 8px;">{{vm.types.ruleNodeType[typeId].icon}}</md-icon>
+                            <div class="tb-panel-title" translate>{{vm.types.ruleNodeType[typeId].name}}</div>
+                            <md-expansion-panel-icon></md-expansion-panel-icon>
+                        </md-expansion-panel-collapsed>
+                        <md-expansion-panel-expanded>
+                            <md-expansion-panel-header ng-mouseenter="vm.typeHeaderMouseEnter($event, typeId)"
+                                                       ng-mouseleave="vm.destroyTooltips()"
+                                                       ng-click="vm.$mdExpansionPanel(typeId).collapse()">
+                                <md-icon aria-label="node-type-icon"
+                                         class="material-icons" style="margin-right: 8px;">{{vm.types.ruleNodeType[typeId].icon}}</md-icon>
+                                <div class="tb-panel-title" translate>{{vm.types.ruleNodeType[typeId].name}}</div>
+                                <md-expansion-panel-icon></md-expansion-panel-icon>
+                            </md-expansion-panel-header>
+                            <md-expansion-panel-content>
+                                <fc-canvas id="tb-rulechain-{{typeId}}"
+                                           model="vm.ruleNodeTypesModel[typeId].model" selected-objects="vm.ruleNodeTypesModel[typeId].selectedObjects"
+                                           automatic-resize="false"
+                                           callbacks="vm.nodeLibCallbacks"
+                                           node-width="170"
+                                           node-height="50"
+                                           control="vm.ruleNodeTypesCanvasControl[typeId]"
+                                           drop-target-id="'tb-rulchain-canvas'"></fc-canvas>
+                            </md-expansion-panel-content>
+                        </md-expansion-panel-expanded>
+                    </md-expansion-panel>
+                </md-expansion-panel-group>
+            </md-sidenav>
+            <md-menu flex style="position: relative;" md-position-mode="target target" tb-offset-x="-20" tb-offset-y="-45" tb-mousepoint-menu>
+                <div class="tb-absolute-fill tb-rulechain-graph" ng-click="" tb-contextmenu="vm.openRuleChainContextMenu($event, $mdOpenMousepointMenu)">
+                    <fc-canvas id="tb-rulchain-canvas"
+                               model="vm.ruleChainModel"
+                               selected-objects="vm.selectedObjects"
+                               edge-style="curved"
+                               node-width="170"
+                               node-height="50"
+                               automatic-resize="true"
+                               control="vm.canvasControl"
+                               callbacks="vm.editCallbacks">
+                    </fc-canvas>
+                </div>
+                <md-menu-content id="tb-rule-chain-context-menu" width="4" ng-mouseleave="$mdCloseMousepointMenu()">
+                    <div class="tb-context-menu-header {{vm.contextInfo.headerClass}}">
+                        <md-icon ng-if="!vm.contextInfo.iconUrl" aria-label="node-type-icon"
+                                 class="material-icons">{{vm.contextInfo.icon}}</md-icon>
+                        <md-icon ng-if="vm.contextInfo.iconUrl" aria-label="node-type-icon"
+                                 md-svg-icon="{{vm.contextInfo.iconUrl}}"></md-icon>
+                        <div flex>
+                            <div class="tb-context-menu-title">{{vm.contextInfo.title}}</div>
+                            <div class="tb-context-menu-subtitle">{{vm.contextInfo.subtitle}}</div>
+                        </div>
+                    </div>
+                    <div ng-repeat="item in vm.contextInfo.items">
+                        <md-divider ng-if="item.divider"></md-divider>
+                        <md-menu-item ng-if="!item.divider">
+                            <md-button ng-disabled="!item.enabled" ng-click="item.action(vm.contextMenuEvent)">
+                                <span ng-if="item.shortcut" class="tb-alt-text"> {{ item.shortcut | keyboardShortcut }}</span>
+                                <md-icon ng-if="item.icon" md-menu-align-target aria-label="{{ item.value | translate }}" class="material-icons">{{item.icon}}</md-icon>
+                                <span translate>{{item.value}}</span>
+                            </md-button>
+                        </md-menu-item>
+                    </div>
+                </md-menu-content>
+            </md-menu>
+        </div>
+        <tb-details-sidenav class="tb-rulenode-details-sidenav"
+                            header-title="{{vm.editingRuleNode.name}}"
+                            header-subtitle="{{(vm.types.ruleNodeType[vm.editingRuleNode.component.type].name | translate)
+                            + ' - ' + vm.editingRuleNode.component.name}}"
+                            is-read-only="vm.selectedRuleNodeTabIndex > 0"
+                            is-open="vm.isEditingRuleNode"
+                            tb-enable-backdrop
+                            is-always-edit="true"
+                            on-close-details="vm.onEditRuleNodeClosed()"
+                            on-toggle-details-edit-mode="vm.onRevertRuleNodeEdit(vm.ruleNodeForm)"
+                            on-apply-details="vm.saveRuleNode(vm.ruleNodeForm)"
+                            the-form="vm.ruleNodeForm">
+            <details-buttons tb-help="vm.helpLinkIdForRuleNodeType()" help-container-id="help-container">
+                <div id="help-container"></div>
+            </details-buttons>
+            <md-tabs md-selected="vm.selectedRuleNodeTabIndex"
+                     id="ruleNodeTabs" md-border-bottom flex class="tb-absolute-fill" ng-if="vm.isEditingRuleNode">
+                <md-tab label="{{ 'rulenode.details' | translate }}">
+                    <form name="vm.ruleNodeForm">
+                        <tb-rule-node
+                                rule-node="vm.editingRuleNode"
+                                rule-chain-id="vm.ruleChain.id.id"
+                                is-edit="true"
+                                is-read-only="false"
+                                on-delete-rule-node="vm.deleteRuleNode(event, vm.editingRuleNode)"
+                                the-form="vm.ruleNodeForm">
+                        </tb-rule-node>
+                    </form>
+                </md-tab>
+                <md-tab ng-if="vm.isEditingRuleNode && vm.editingRuleNode.ruleNodeId"
+                        md-on-select="vm.triggerResize()" label="{{ 'rulenode.events' | translate }}">
+                    <tb-event-table flex entity-type="vm.types.entityType.rulenode"
+                                    entity-id="vm.editingRuleNode.ruleNodeId.id"
+                                    tenant-id="vm.ruleChain.tenantId.id"
+                                    debug-event-types="{{vm.types.debugEventType.debugRuleNode.value}}"
+                                    default-event-type="{{vm.types.debugEventType.debugRuleNode.value}}">
+                    </tb-event-table>
+                </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>
+        </tb-details-sidenav>
+        <tb-details-sidenav class="tb-rulenode-link-details-sidenav"
+                            header-title="{{vm.editingRuleNodeLink.label}}"
+                            header-subtitle="{{'rulenode.link-details' | translate}}"
+                            is-read-only="false"
+                            is-open="vm.isEditingRuleNodeLink"
+                            tb-enable-backdrop
+                            is-always-edit="true"
+                            on-close-details="vm.onEditRuleNodeLinkClosed()"
+                            on-toggle-details-edit-mode="vm.onRevertRuleNodeLinkEdit(vm.ruleNodeLinkForm)"
+                            on-apply-details="vm.saveRuleNodeLink(vm.ruleNodeLinkForm)"
+                            the-form="vm.ruleNodeLinkForm">
+            <details-buttons tb-help="'ruleEngine'" help-container-id="link-help-container">
+                <div id="link-help-container"></div>
+            </details-buttons>
+            <form name="vm.ruleNodeLinkForm" ng-if="vm.isEditingRuleNodeLink">
+                <tb-rule-node-link
+                        link="vm.editingRuleNodeLink"
+                        labels="vm.editingRuleNodeLinkLabels"
+                        is-edit="true"
+                        is-read-only="false"
+                        the-form="vm.ruleNodeLinkForm">
+                </tb-rule-node-link>
+            </form>
+        </tb-details-sidenav>
+    </section>
+    <section layout="row" layout-wrap class="tb-footer-buttons md-fab" layout-align="start end">
+        <md-button ng-disabled="$root.loading" ng-show="vm.objectsSelected()" class="tb-btn-footer md-accent md-hue-2 md-fab"
+                   ng-click="vm.deleteSelected()" aria-label="{{ 'action.delete' | translate }}">
+            <md-tooltip md-direction="top">
+                {{ 'rulenode.delete-selected-objects' | translate }}
+            </md-tooltip>
+            <ng-md-icon icon="delete"></ng-md-icon>
+        </md-button>
+        <md-button ng-disabled="$root.loading  || vm.isInvalid || (!vm.isDirty && !vm.isImport)"
+                   class="tb-btn-footer md-accent md-hue-2 md-fab"
+                   aria-label="{{ 'action.apply' | translate }}"
+                   ng-click="vm.saveRuleChain()">
+            <md-tooltip md-direction="top">
+                {{ 'action.apply-changes' | translate }}
+            </md-tooltip>
+            <ng-md-icon icon="done"></ng-md-icon>
+        </md-button>
+        <md-button ng-disabled="$root.loading || !vm.isDirty"
+                   class="tb-btn-footer md-accent md-hue-2 md-fab"
+                   aria-label="{{ 'action.decline-changes' | translate }}"
+                   ng-click="vm.revertRuleChain()">
+            <md-tooltip md-direction="top">
+                {{ 'action.decline-changes' | translate }}
+            </md-tooltip>
+            <ng-md-icon icon="close"></ng-md-icon>
+        </md-button>
+    </section>
+</md-content>
diff --git a/ui/src/app/rulechain/rulechain-card.tpl.html b/ui/src/app/rulechain/rulechain-card.tpl.html
new file mode 100644
index 0000000..a3ebfff
--- /dev/null
+++ b/ui/src/app/rulechain/rulechain-card.tpl.html
@@ -0,0 +1,18 @@
+<!--
+
+    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.
+
+-->
+<div ng-if="item && item.root" translate>rulechain.root</div>
diff --git a/ui/src/app/rulechain/rulechain-fieldset.tpl.html b/ui/src/app/rulechain/rulechain-fieldset.tpl.html
new file mode 100644
index 0000000..9f786ab
--- /dev/null
+++ b/ui/src/app/rulechain/rulechain-fieldset.tpl.html
@@ -0,0 +1,57 @@
+<!--
+
+    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.
+
+-->
+<md-button ng-click="onExportRuleChain({event: $event})"
+           ng-show="!isEdit"
+           class="md-raised md-primary">{{ 'rulechain.export' | translate }}</md-button>
+<md-button ng-click="onSetRootRuleChain({event: $event})"
+           ng-show="!isEdit && !ruleChain.root"
+           class="md-raised md-primary">{{ 'rulechain.set-root' | translate }}</md-button>
+<md-button ng-click="onDeleteRuleChain({event: $event})"
+           ng-show="!isEdit && !ruleChain.root"
+           class="md-raised md-primary">{{ 'rulechain.delete' | translate }}</md-button>
+
+<div layout="row">
+    <md-button ngclipboard data-clipboard-action="copy"
+               ngclipboard-success="onRuleChainIdCopied(e)"
+               data-clipboard-text="{{ruleChain.id.id}}" ng-show="!isEdit"
+               class="md-raised">
+        <md-icon md-svg-icon="mdi:clipboard-arrow-left"></md-icon>
+        <span translate>rulechain.copyId</span>
+    </md-button>
+</div>
+
+<md-content class="md-padding tb-rulechain-fieldset" layout="column">
+    <fieldset ng-disabled="$root.loading || !isEdit || isReadOnly">
+        <md-input-container class="md-block">
+            <label translate>rulechain.name</label>
+            <input required name="name" ng-model="ruleChain.name">
+            <div ng-messages="theForm.name.$error">
+                <div translate ng-message="required">rulechain.name-required</div>
+            </div>
+        </md-input-container>
+        <md-input-container class="md-block">
+            <md-checkbox ng-disabled="$root.loading || !isEdit" aria-label="{{ 'rulechain.debug-mode' | translate }}"
+                         ng-model="ruleChain.debugMode">{{ 'rulechain.debug-mode' | translate }}
+            </md-checkbox>
+        </md-input-container>
+        <md-input-container class="md-block">
+            <label translate>rulechain.description</label>
+            <textarea ng-model="ruleChain.additionalInfo.description" rows="2"></textarea>
+        </md-input-container>
+    </fieldset>
+</md-content>
diff --git a/ui/src/app/rulechain/rulechains.controller.js b/ui/src/app/rulechain/rulechains.controller.js
new file mode 100644
index 0000000..3da5a51
--- /dev/null
+++ b/ui/src/app/rulechain/rulechains.controller.js
@@ -0,0 +1,215 @@
+/*
+ * 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.
+ */
+/* eslint-disable import/no-unresolved, import/default */
+
+import addRuleChainTemplate from './add-rulechain.tpl.html';
+import ruleChainCard from './rulechain-card.tpl.html';
+
+/* eslint-enable import/no-unresolved, import/default */
+
+/*@ngInject*/
+export default function RuleChainsController(ruleChainService, userService, importExport, $state,
+                                             $stateParams, $filter, $translate, $mdDialog, types) {
+
+    var ruleChainActionsList = [
+        {
+            onAction: function ($event, item) {
+                vm.grid.openItem($event, item);
+            },
+            name: function() { return $translate.instant('rulechain.details') },
+            details: function() { return $translate.instant('rulechain.rulechain-details') },
+            icon: "edit"
+        },
+        {
+            onAction: function ($event, item) {
+                exportRuleChain($event, item);
+            },
+            name: function() { $translate.instant('action.export') },
+            details: function() { return $translate.instant('rulechain.export') },
+            icon: "file_download"
+        },
+        {
+            onAction: function ($event, item) {
+                setRootRuleChain($event, item);
+            },
+            name: function() { return $translate.instant('rulechain.set-root') },
+            details: function() { return $translate.instant('rulechain.set-root') },
+            icon: "flag",
+            isEnabled: isNonRootRuleChain
+        },
+        {
+            onAction: function ($event, item) {
+                vm.grid.deleteItem($event, item);
+            },
+            name: function() { return $translate.instant('action.delete') },
+            details: function() { return $translate.instant('rulechain.delete') },
+            icon: "delete",
+            isEnabled: isNonRootRuleChain
+        }
+    ];
+
+    var ruleChainAddItemActionsList = [
+        {
+            onAction: function ($event) {
+                vm.grid.addItem($event);
+            },
+            name: function() { return $translate.instant('action.create') },
+            details: function() { return $translate.instant('rulechain.create-new-rulechain') },
+            icon: "insert_drive_file"
+        },
+        {
+            onAction: function ($event) {
+                importExport.importRuleChain($event).then(
+                    function(ruleChainImport) {
+                        $state.go('home.ruleChains.importRuleChain', {ruleChainImport:ruleChainImport});
+                    }
+                );
+            },
+            name: function() { return $translate.instant('action.import') },
+            details: function() { return $translate.instant('rulechain.import') },
+            icon: "file_upload"
+        }
+    ];
+
+    var vm = this;
+
+    vm.types = types;
+
+    vm.ruleChainGridConfig = {
+
+        refreshParamsFunc: null,
+
+        deleteItemTitleFunc: deleteRuleChainTitle,
+        deleteItemContentFunc: deleteRuleChainText,
+        deleteItemsTitleFunc: deleteRuleChainsTitle,
+        deleteItemsActionTitleFunc: deleteRuleChainsActionTitle,
+        deleteItemsContentFunc: deleteRuleChainsText,
+
+        fetchItemsFunc: fetchRuleChains,
+        saveItemFunc: saveRuleChain,
+        clickItemFunc: openRuleChain,
+        deleteItemFunc: deleteRuleChain,
+
+        getItemTitleFunc: getRuleChainTitle,
+        itemCardTemplateUrl: ruleChainCard,
+        parentCtl: vm,
+
+        actionsList: ruleChainActionsList,
+        addItemActions: ruleChainAddItemActionsList,
+
+        onGridInited: gridInited,
+
+        addItemTemplateUrl: addRuleChainTemplate,
+
+        addItemText: function() { return $translate.instant('rulechain.add-rulechain-text') },
+        noItemsText: function() { return $translate.instant('rulechain.no-rulechains-text') },
+        itemDetailsText: function() { return $translate.instant('rulechain.rulechain-details') },
+        isSelectionEnabled: isNonRootRuleChain
+    };
+
+    if (angular.isDefined($stateParams.items) && $stateParams.items !== null) {
+        vm.ruleChainGridConfig.items = $stateParams.items;
+    }
+
+    if (angular.isDefined($stateParams.topIndex) && $stateParams.topIndex > 0) {
+        vm.ruleChainGridConfig.topIndex = $stateParams.topIndex;
+    }
+
+    vm.isRootRuleChain = isRootRuleChain;
+    vm.isNonRootRuleChain = isNonRootRuleChain;
+
+    vm.exportRuleChain = exportRuleChain;
+    vm.setRootRuleChain = setRootRuleChain;
+
+    function deleteRuleChainTitle(ruleChain) {
+        return $translate.instant('rulechain.delete-rulechain-title', {ruleChainName: ruleChain.name});
+    }
+
+    function deleteRuleChainText() {
+        return $translate.instant('rulechain.delete-rulechain-text');
+    }
+
+    function deleteRuleChainsTitle(selectedCount) {
+        return $translate.instant('rulechain.delete-rulechains-title', {count: selectedCount}, 'messageformat');
+    }
+
+    function deleteRuleChainsActionTitle(selectedCount) {
+        return $translate.instant('rulechain.delete-rulechains-action-title', {count: selectedCount}, 'messageformat');
+    }
+
+    function deleteRuleChainsText() {
+        return $translate.instant('rulechain.delete-rulechains-text');
+    }
+
+    function gridInited(grid) {
+        vm.grid = grid;
+    }
+
+    function fetchRuleChains(pageLink) {
+        return ruleChainService.getRuleChains(pageLink);
+    }
+
+    function saveRuleChain(ruleChain) {
+        return ruleChainService.saveRuleChain(ruleChain);
+    }
+
+    function openRuleChain($event, ruleChain) {
+        if ($event) {
+            $event.stopPropagation();
+        }
+        $state.go('home.ruleChains.ruleChain', {ruleChainId: ruleChain.id.id});
+    }
+
+    function deleteRuleChain(ruleChainId) {
+        return ruleChainService.deleteRuleChain(ruleChainId);
+    }
+
+    function getRuleChainTitle(ruleChain) {
+        return ruleChain ? ruleChain.name : '';
+    }
+
+    function isRootRuleChain(ruleChain) {
+        return ruleChain && ruleChain.root;
+    }
+
+    function isNonRootRuleChain(ruleChain) {
+        return ruleChain && !ruleChain.root;
+    }
+
+    function exportRuleChain($event, ruleChain) {
+        $event.stopPropagation();
+        importExport.exportRuleChain(ruleChain.id.id);
+    }
+
+    function setRootRuleChain($event, ruleChain) {
+        $event.stopPropagation();
+        var confirm = $mdDialog.confirm()
+            .targetEvent($event)
+            .title($translate.instant('rulechain.set-root-rulechain-title', {ruleChainName: ruleChain.name}))
+            .htmlContent($translate.instant('rulechain.set-root-rulechain-text'))
+            .ariaLabel($translate.instant('rulechain.set-root'))
+            .cancel($translate.instant('action.no'))
+            .ok($translate.instant('action.yes'));
+        $mdDialog.show(confirm).then(function () {
+            ruleChainService.setRootRuleChain(ruleChain.id.id).then(
+                () => {
+                    vm.grid.refreshList();
+                }
+            );
+        });
+
+    }
+}
diff --git a/ui/src/app/rulechain/rulechains.tpl.html b/ui/src/app/rulechain/rulechains.tpl.html
new file mode 100644
index 0000000..c780b46
--- /dev/null
+++ b/ui/src/app/rulechain/rulechains.tpl.html
@@ -0,0 +1,78 @@
+<!--
+
+    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.
+
+-->
+<tb-grid grid-configuration="vm.ruleChainGridConfig">
+    <details-buttons tb-help="'rulechains'" help-container-id="help-container">
+        <div id="help-container"></div>
+    </details-buttons>
+    <md-tabs ng-class="{'tb-headless': (vm.grid.detailsConfig.isDetailsEditMode || !vm.isRuleChainEditable(vm.grid.operatingItem()))}"
+             id="tabs" md-border-bottom flex class="tb-absolute-fill">
+        <md-tab label="{{ 'rulechain.details' | translate }}">
+            <tb-rule-chain rule-chain="vm.grid.operatingItem()"
+                     is-edit="vm.grid.detailsConfig.isDetailsEditMode"
+                     is-read-only="vm.grid.isDetailsReadOnly(vm.grid.operatingItem())"
+                     the-form="vm.grid.detailsForm"
+                     on-set-root-rule-chain="vm.setRootRuleChain(event, vm.grid.detailsConfig.currentItem)"
+                     on-export-rule-chain="vm.exportRuleChain(event, vm.grid.detailsConfig.currentItem)"
+                     on-delete-rule-chain="vm.grid.deleteItem(event, vm.grid.detailsConfig.currentItem)">
+            </tb-rule-chain>
+        </md-tab>
+        <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode && vm.isRuleChainEditable(vm.grid.operatingItem())" md-on-select="vm.grid.triggerResize()" label="{{ 'attribute.attributes' | translate }}">
+            <tb-attribute-table flex
+                                entity-id="vm.grid.operatingItem().id.id"
+                                entity-type="{{vm.types.entityType.rulechain}}"
+                                entity-name="vm.grid.operatingItem().name"
+                                default-attribute-scope="{{vm.types.attributesScope.server.value}}">
+            </tb-attribute-table>
+        </md-tab>
+        <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode && vm.isRuleChainEditable(vm.grid.operatingItem())" md-on-select="vm.grid.triggerResize()" label="{{ 'attribute.latest-telemetry' | translate }}">
+            <tb-attribute-table flex
+                                entity-id="vm.grid.operatingItem().id.id"
+                                entity-type="{{vm.types.entityType.rulechain}}"
+                                entity-name="vm.grid.operatingItem().name"
+                                default-attribute-scope="{{vm.types.latestTelemetry.value}}"
+                                disable-attribute-scope-selection="true">
+            </tb-attribute-table>
+        </md-tab>
+        <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode && vm.isRuleChainEditable(vm.grid.operatingItem())" md-on-select="vm.grid.triggerResize()" label="{{ 'alarm.alarms' | translate }}">
+            <tb-alarm-table flex entity-type="vm.types.entityType.rulechain"
+                            entity-id="vm.grid.operatingItem().id.id">
+            </tb-alarm-table>
+        </md-tab>
+        <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode && vm.isRuleChainEditable(vm.grid.operatingItem())" md-on-select="vm.grid.triggerResize()" label="{{ 'rulechain.events' | translate }}">
+            <tb-event-table flex entity-type="vm.types.entityType.rulechain"
+                            entity-id="vm.grid.operatingItem().id.id"
+                            tenant-id="vm.grid.operatingItem().tenantId.id"
+                            debug-event-types="{{vm.types.debugEventType.debugRuleChain.value}}"
+                            default-event-type="{{vm.types.debugEventType.debugRuleChain.value}}">
+            </tb-event-table>
+        </md-tab>
+        <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode && vm.isRuleChainEditable(vm.grid.operatingItem())" md-on-select="vm.grid.triggerResize()" label="{{ 'relation.relations' | translate }}">
+            <tb-relation-table flex
+                               entity-id="vm.grid.operatingItem().id.id"
+                               entity-type="{{vm.types.entityType.rulechain}}">
+            </tb-relation-table>
+        </md-tab>
+        <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode && vm.isRuleChainEditable(vm.grid.operatingItem()) && vm.grid.isTenantAdmin()"
+                md-on-select="vm.grid.triggerResize()" label="{{ 'audit-log.audit-logs' | translate }}">
+            <tb-audit-log-table flex entity-type="vm.types.entityType.rulechain"
+                                entity-id="vm.grid.operatingItem().id.id"
+                                audit-log-mode="{{vm.types.auditLogMode.entity}}">
+            </tb-audit-log-table>
+        </md-tab>
+    </md-tabs>
+</tb-grid>
diff --git a/ui/src/app/rulechain/rulenode.directive.js b/ui/src/app/rulechain/rulenode.directive.js
new file mode 100644
index 0000000..be3e9c3
--- /dev/null
+++ b/ui/src/app/rulechain/rulenode.directive.js
@@ -0,0 +1,81 @@
+/*
+ * 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.
+ */
+
+import './rulenode.scss';
+
+/* eslint-disable import/no-unresolved, import/default */
+
+import ruleNodeFieldsetTemplate from './rulenode-fieldset.tpl.html';
+
+/* eslint-enable import/no-unresolved, import/default */
+
+/*@ngInject*/
+export default function RuleNodeDirective($compile, $templateCache, ruleChainService, types) {
+    var linker = function (scope, element) {
+        var template = $templateCache.get(ruleNodeFieldsetTemplate);
+        element.html(template);
+
+        scope.types = types;
+
+        scope.params = {
+            targetRuleChainId: null
+        };
+
+        scope.$watch('ruleNode', function() {
+            if (scope.ruleNode && scope.ruleNode.component.type == types.ruleNodeType.RULE_CHAIN.value) {
+                scope.params.targetRuleChainId = scope.ruleNode.targetRuleChainId;
+                watchTargetRuleChain();
+            } else {
+                if (scope.targetRuleChainWatch) {
+                    scope.targetRuleChainWatch();
+                    scope.targetRuleChainWatch = null;
+                }
+            }
+        });
+
+        function watchTargetRuleChain() {
+            scope.targetRuleChainWatch = scope.$watch('params.targetRuleChainId',
+                function(targetRuleChainId) {
+                    if (scope.ruleNode.targetRuleChainId != targetRuleChainId) {
+                        scope.ruleNode.targetRuleChainId = targetRuleChainId;
+                        if (targetRuleChainId) {
+                            ruleChainService.getRuleChain(targetRuleChainId).then(
+                                (ruleChain) => {
+                                    scope.ruleNode.name = ruleChain.name;
+                                }
+                            );
+                        } else {
+                            scope.ruleNode.name = "";
+                        }
+                    }
+                }
+            );
+        }
+        $compile(element.contents())(scope);
+    }
+    return {
+        restrict: "E",
+        link: linker,
+        scope: {
+            ruleChainId: '=',
+            ruleNode: '=',
+            isEdit: '=',
+            isReadOnly: '=',
+            theForm: '=',
+            onDeleteRuleNode: '&'
+        }
+    };
+}
diff --git a/ui/src/app/rulechain/rulenode.tpl.html b/ui/src/app/rulechain/rulenode.tpl.html
new file mode 100644
index 0000000..6a82a7f
--- /dev/null
+++ b/ui/src/app/rulechain/rulenode.tpl.html
@@ -0,0 +1,54 @@
+<!--
+
+    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.
+
+-->
+<div
+        id="{{node.id}}"
+        ng-attr-style="position: absolute; top: {{ node.y }}px; left: {{ node.x }}px;"
+        ng-dblclick="callbacks.doubleClick($event, node)"
+        ng-mousedown="callbacks.mouseDown($event, node)"
+        ng-mouseenter="callbacks.mouseEnter($event, node)"
+        ng-mouseleave="callbacks.mouseLeave($event, node)">
+    <div class="{{flowchartConstants.nodeOverlayClass}}"></div>
+    <div class="tb-rule-node {{node.nodeClass}}" ng-class="{'tb-rule-node-highlighted' : node.highlighted, 'tb-rule-node-invalid': node.error }">
+        <md-icon ng-if="!node.iconUrl" aria-label="node-type-icon" flex="15"
+                 class="material-icons">{{node.icon}}</md-icon>
+        <md-icon ng-if="node.iconUrl" aria-label="node-type-icon" flex="15"
+                 md-svg-icon="{{node.iconUrl}}"></md-icon>
+        <div layout="column" flex="85" layout-align="center">
+            <span class="tb-node-type">{{ node.component.name }}</span>
+            <span class="tb-node-title" ng-if="node.name">{{ node.name }}</span>
+        </div>
+        <div class="{{flowchartConstants.leftConnectorClass}}">
+            <div fc-magnet
+                 ng-repeat="connector in modelservice.nodes.getConnectorsByType(node, flowchartConstants.leftConnectorType)">
+                <div fc-connector></div>
+            </div>
+        </div>
+        <div class="{{flowchartConstants.rightConnectorClass}}">
+            <div fc-magnet
+                 ng-repeat="connector in modelservice.nodes.getConnectorsByType(node, flowchartConstants.rightConnectorType)">
+                <div fc-connector></div>
+            </div>
+        </div>
+    </div>
+    <div ng-if="modelservice.isEditable() && !node.readonly" class="fc-nodeedit" ng-click="callbacks.nodeEdit($event, node)">
+        <i class="fa fa-pencil" aria-hidden="true"></i>
+    </div>
+    <div ng-if="modelservice.isEditable() && !node.readonly" class="fc-nodedelete" ng-click="modelservice.nodes.delete(node)">
+        &times;
+    </div>
+</div>
diff --git a/ui/src/app/rulechain/rulenode-config.directive.js b/ui/src/app/rulechain/rulenode-config.directive.js
new file mode 100644
index 0000000..cf13e96
--- /dev/null
+++ b/ui/src/app/rulechain/rulenode-config.directive.js
@@ -0,0 +1,79 @@
+/*
+ * 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.
+ */
+
+/* eslint-disable import/no-unresolved, import/default */
+
+import ruleNodeConfigTemplate from './rulenode-config.tpl.html';
+
+/* eslint-enable import/no-unresolved, import/default */
+
+/*@ngInject*/
+export default function RuleNodeConfigDirective($compile, $templateCache, $injector, $translate) {
+
+    var linker = function (scope, element, attrs, ngModelCtrl) {
+        var template = $templateCache.get(ruleNodeConfigTemplate);
+        element.html(template);
+
+        scope.$watch('configuration', function (newVal, prevVal) {
+            if (!angular.equals(newVal, prevVal)) {
+                ngModelCtrl.$setViewValue(scope.configuration);
+            }
+        });
+
+        ngModelCtrl.$render = function () {
+            scope.configuration = ngModelCtrl.$viewValue;
+        };
+
+        scope.useDefinedDirective = function() {
+            return scope.nodeDefinition &&
+                scope.nodeDefinition.configDirective && !scope.definedDirectiveError;
+        };
+
+        scope.$watch('nodeDefinition', () => {
+            if (scope.nodeDefinition) {
+                validateDefinedDirective();
+            }
+        });
+
+        function validateDefinedDirective() {
+            if (scope.nodeDefinition.uiResourceLoadError && scope.nodeDefinition.uiResourceLoadError.length) {
+                scope.definedDirectiveError = scope.nodeDefinition.uiResourceLoadError;
+            } else {
+                var definedDirective = scope.nodeDefinition.configDirective;
+                if (definedDirective && definedDirective.length) {
+                    if (!$injector.has(definedDirective + 'Directive')) {
+                        scope.definedDirectiveError = $translate.instant('rulenode.directive-is-not-loaded', {directiveName: definedDirective});
+                    }
+                }
+            }
+        }
+
+        $compile(element.contents())(scope);
+    };
+
+    return {
+        restrict: "E",
+        require: "^ngModel",
+        scope: {
+            ruleNodeId:'=',
+            nodeDefinition:'=',
+            required:'=ngRequired',
+            readonly:'=ngReadonly'
+        },
+        link: linker
+    };
+
+}
diff --git a/ui/src/app/rulechain/rulenode-config.tpl.html b/ui/src/app/rulechain/rulenode-config.tpl.html
new file mode 100644
index 0000000..3148c39
--- /dev/null
+++ b/ui/src/app/rulechain/rulenode-config.tpl.html
@@ -0,0 +1,33 @@
+<!--
+
+    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.
+
+-->
+
+<tb-rule-node-defined-config ng-if="useDefinedDirective()"
+                             rule-node-id="ruleNodeId"
+                             ng-model="configuration"
+                             rule-node-directive="{{nodeDefinition.configDirective}}"
+                             ng-required="required"
+                             ng-readonly="readonly">
+</tb-rule-node-defined-config>
+<div class="tb-rulenode-directive-error" ng-if="definedDirectiveError">{{definedDirectiveError}}</div>
+<tb-json-object-edit ng-if="!useDefinedDirective()"
+                     class="tb-rule-node-configuration-json"
+                     ng-model="configuration"
+                     label="{{ 'rulenode.configuration' | translate }}"
+                     ng-required="required"
+                     fill-height="true">
+</tb-json-object-edit>
diff --git a/ui/src/app/rulechain/rulenode-defined-config.directive.js b/ui/src/app/rulechain/rulenode-defined-config.directive.js
new file mode 100644
index 0000000..d358955
--- /dev/null
+++ b/ui/src/app/rulechain/rulenode-defined-config.directive.js
@@ -0,0 +1,68 @@
+/*
+ * 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.
+ */
+
+const SNAKE_CASE_REGEXP = /[A-Z]/g;
+
+/*@ngInject*/
+export default function RuleNodeDefinedConfigDirective($compile) {
+
+    var linker = function (scope, element, attrs, ngModelCtrl) {
+
+        attrs.$observe('ruleNodeDirective', function() {
+            loadTemplate();
+        });
+
+        scope.$watch('configuration', function (newVal, prevVal) {
+            if (!angular.equals(newVal, prevVal)) {
+                ngModelCtrl.$setViewValue(scope.configuration);
+            }
+        });
+
+        ngModelCtrl.$render = function () {
+            scope.configuration = ngModelCtrl.$viewValue;
+        };
+
+        function loadTemplate() {
+            if (scope.ruleNodeConfigScope) {
+                scope.ruleNodeConfigScope.$destroy();
+            }
+            var directive = snake_case(attrs.ruleNodeDirective, '-');
+            var template = `<${directive} rule-node-id="ruleNodeId" ng-model="configuration" ng-required="required" ng-readonly="readonly"></${directive}>`;
+            element.html(template);
+            scope.ruleNodeConfigScope = scope.$new();
+            $compile(element.contents())(scope.ruleNodeConfigScope);
+        }
+
+        function snake_case(name, separator) {
+            separator = separator || '_';
+            return name.replace(SNAKE_CASE_REGEXP, function(letter, pos) {
+                return (pos ? separator : '') + letter.toLowerCase();
+            });
+        }
+    };
+
+    return {
+        restrict: "E",
+        require: "^ngModel",
+        scope: {
+            ruleNodeId:'=',
+            required:'=ngRequired',
+            readonly:'=ngReadonly'
+        },
+        link: linker
+    };
+
+}
diff --git a/ui/src/app/rulechain/rulenode-fieldset.tpl.html b/ui/src/app/rulechain/rulenode-fieldset.tpl.html
new file mode 100644
index 0000000..12ff320
--- /dev/null
+++ b/ui/src/app/rulechain/rulenode-fieldset.tpl.html
@@ -0,0 +1,64 @@
+<!--
+
+    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.
+
+-->
+<md-button ng-click="onDeleteRuleNode({event: $event})"
+           ng-show="!isEdit && !isReadOnly"
+           class="md-raised md-primary">{{ 'rulenode.delete' | translate }}</md-button>
+
+<md-content class="md-padding tb-rulenode" layout="column">
+    <fieldset ng-disabled="$root.loading || !isEdit || isReadOnly">
+        <section ng-if="ruleNode.component.type != types.ruleNodeType.RULE_CHAIN.value">
+            <section layout="column" layout-gt-sm="row">
+                <md-input-container flex class="md-block">
+                    <label translate>rulenode.name</label>
+                    <input required name="name" ng-model="ruleNode.name">
+                    <div ng-messages="theForm.name.$error">
+                        <div translate ng-message="required">rulenode.name-required</div>
+                    </div>
+                </md-input-container>
+                <md-input-container class="md-block">
+                    <md-checkbox ng-disabled="$root.loading || !isEdit" aria-label="{{ 'rulenode.debug-mode' | translate }}"
+                                 ng-model="ruleNode.debugMode">{{ 'rulenode.debug-mode' | translate }}
+                    </md-checkbox>
+                </md-input-container>
+            </section>
+            <tb-rule-node-config ng-model="ruleNode.configuration"
+                                 rule-node-id="ruleNode.ruleNodeId.id"
+                                 ng-required="true"
+                                 node-definition="ruleNode.component.configurationDescriptor.nodeDefinition"
+                                 ng-readonly="$root.loading || !isEdit || isReadOnly">
+            </tb-rule-node-config>
+            <md-input-container class="md-block">
+                <label translate>rulenode.description</label>
+                <textarea ng-model="ruleNode.additionalInfo.description" rows="2"></textarea>
+            </md-input-container>
+        </section>
+        <section ng-if="ruleNode.component.type == types.ruleNodeType.RULE_CHAIN.value">
+            <tb-entity-autocomplete the-form="theForm"
+                                    ng-disabled="$root.loading || !isEdit || isReadOnly"
+                                    tb-required="true"
+                                    exclude-entity-ids="[ruleChainId]"
+                                    entity-type="types.entityType.rulechain"
+                                    ng-model="params.targetRuleChainId">
+            </tb-entity-autocomplete>
+            <md-input-container class="md-block">
+                <label translate>rulenode.description</label>
+                <textarea ng-model="ruleNode.additionalInfo.description" rows="2"></textarea>
+            </md-input-container>
+        </section>
+    </fieldset>
+</md-content>
diff --git a/ui/src/app/rulechain/script/node-script-test.controller.js b/ui/src/app/rulechain/script/node-script-test.controller.js
new file mode 100644
index 0000000..487d11f
--- /dev/null
+++ b/ui/src/app/rulechain/script/node-script-test.controller.js
@@ -0,0 +1,177 @@
+/*
+ * 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.
+ */
+
+import './node-script-test.scss';
+
+import Split from 'split.js';
+
+import beautify from 'js-beautify';
+
+const js_beautify = beautify.js;
+
+/*@ngInject*/
+export default function NodeScriptTestController($scope, $mdDialog, $window, $document, $timeout,
+                                                $q, $mdUtil, $translate, toast, types, utils,
+                                                 ruleChainService, onShowingCallback, msg, msgType, metadata,
+                                                 functionTitle, inputParams) {
+
+    var vm = this;
+
+    vm.types = types;
+    vm.functionTitle = functionTitle;
+    vm.inputParams = inputParams;
+    vm.inputParams.msg = js_beautify(angular.toJson(msg), {indent_size: 4});
+    vm.inputParams.metadata = metadata;
+    vm.inputParams.msgType = msgType;
+
+    vm.output = '';
+
+    vm.test = test;
+    vm.save = save;
+    vm.cancel = cancel;
+
+    $scope.$watch('theForm.metadataForm.$dirty', (newVal) => {
+        if (newVal) {
+            toast.hide();
+        }
+    });
+
+    onShowingCallback.onShowed = () => {
+        vm.nodeScriptTestDialogElement = angular.element('.tb-node-script-test-dialog');
+        var w = vm.nodeScriptTestDialogElement.width();
+        if (w > 0) {
+            initSplitLayout();
+        } else {
+            $scope.$watch(
+                function () {
+                    return vm.nodeScriptTestDialogElement[0].offsetWidth || parseInt(vm.nodeScriptTestDialogElement.css('width'), 10);
+                },
+                function (newSize) {
+                    if (newSize > 0) {
+                        initSplitLayout();
+                    }
+                }
+            );
+        }
+    };
+
+    function onDividerDrag() {
+        $scope.$broadcast('update-ace-editor-size');
+    }
+
+    function initSplitLayout() {
+        if (!vm.layoutInited) {
+            Split([angular.element('#top_panel', vm.nodeScriptTestDialogElement)[0], angular.element('#bottom_panel', vm.nodeScriptTestDialogElement)[0]], {
+                sizes: [35, 65],
+                gutterSize: 8,
+                cursor: 'row-resize',
+                direction: 'vertical',
+                onDrag: function () {
+                    onDividerDrag()
+                }
+            });
+
+            Split([angular.element('#top_left_panel', vm.nodeScriptTestDialogElement)[0], angular.element('#top_right_panel', vm.nodeScriptTestDialogElement)[0]], {
+                sizes: [50, 50],
+                gutterSize: 8,
+                cursor: 'col-resize',
+                onDrag: function () {
+                    onDividerDrag()
+                }
+            });
+
+            Split([angular.element('#bottom_left_panel', vm.nodeScriptTestDialogElement)[0], angular.element('#bottom_right_panel', vm.nodeScriptTestDialogElement)[0]], {
+                sizes: [50, 50],
+                gutterSize: 8,
+                cursor: 'col-resize',
+                onDrag: function () {
+                    onDividerDrag()
+                }
+            });
+
+            onDividerDrag();
+
+            $scope.$applyAsync(function () {
+                vm.layoutInited = true;
+                var w = angular.element($window);
+                $timeout(function () {
+                    w.triggerHandler('resize')
+                });
+            });
+
+        }
+    }
+
+    function test() {
+        testNodeScript().then(
+            (output) => {
+                vm.output = js_beautify(output, {indent_size: 4});
+            }
+        );
+    }
+
+    function checkInputParamErrors() {
+        $scope.theForm.metadataForm.$setPristine();
+        $scope.$broadcast('form-submit', 'validatePayload');
+        if (!$scope.theForm.payloadForm.$valid) {
+            return false;
+        } else if (!$scope.theForm.metadataForm.$valid) {
+            showMetadataError($translate.instant('rulenode.metadata-required'));
+            return false;
+        }
+        return true;
+    }
+
+    function showMetadataError(error) {
+        var toastParent = angular.element('#metadata-panel', vm.nodeScriptTestDialogElement);
+        toast.showError(error, toastParent, 'bottom left');
+    }
+
+    function testNodeScript() {
+        var deferred = $q.defer();
+        if (checkInputParamErrors()) {
+            $mdUtil.nextTick(() => {
+                ruleChainService.testScript(vm.inputParams).then(
+                    (result) => {
+                        if (result.error) {
+                            toast.showError(result.error);
+                            deferred.reject();
+                        } else {
+                            deferred.resolve(result.output);
+                        }
+                    },
+                    () => {
+                        deferred.reject();
+                    }
+                );
+            });
+        } else {
+            deferred.reject();
+        }
+        return deferred.promise;
+    }
+
+    function cancel() {
+        $mdDialog.cancel();
+    }
+
+    function save() {
+        testNodeScript().then(() => {
+            $scope.theForm.funcBodyForm.$setPristine();
+            $mdDialog.hide(vm.inputParams.script);
+        });
+    }
+}
diff --git a/ui/src/app/rulechain/script/node-script-test.scss b/ui/src/app/rulechain/script/node-script-test.scss
new file mode 100644
index 0000000..a75ae59
--- /dev/null
+++ b/ui/src/app/rulechain/script/node-script-test.scss
@@ -0,0 +1,115 @@
+/**
+ * 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.
+ */
+@import '../../../scss/constants';
+@import "~compass-sass-mixins/lib/compass";
+
+md-dialog.tb-node-script-test-dialog {
+  &.md-dialog-fullscreen {
+    min-height: 100%;
+    min-width: 100%;
+    max-height: 100%;
+    max-width: 100%;
+    width: 100%;
+    height: 100%;
+    border-radius: 0;
+  }
+
+  .tb-split {
+    @include box-sizing(border-box);
+    overflow-y: auto;
+    overflow-x: hidden;
+  }
+
+  .ace_editor {
+    font-size: 14px !important;
+  }
+
+  .tb-content {
+    border: 1px solid #C0C0C0;
+    padding-top: 5px;
+    padding-left: 5px;
+  }
+
+  .gutter {
+    background-color: #eeeeee;
+
+    background-repeat: no-repeat;
+    background-position: 50%;
+  }
+
+  .gutter.gutter-horizontal {
+    cursor: col-resize;
+    background-image: url('../../../../node_modules/split.js/grips/vertical.png');
+  }
+
+  .gutter.gutter-vertical {
+    cursor: row-resize;
+    background-image: url('../../../../node_modules/split.js/grips/horizontal.png');
+  }
+
+  .tb-split.tb-split-horizontal, .gutter.gutter-horizontal {
+    height: 100%;
+    float: left;
+  }
+
+  .tb-split.tb-split-vertical {
+    display: flex;
+    .tb-split.tb-content {
+      height: 100%;
+    }
+  }
+
+  div.tb-editor-area-title-panel {
+    position: absolute;
+    font-size: 0.800rem;
+    font-weight: 500;
+    top: 13px;
+    right: 40px;
+    z-index: 5;
+    &.tb-js-function {
+      right: 80px;
+    }
+    label {
+      color: #00acc1;
+      background: rgba(220, 220, 220, 0.35);
+      border-radius: 5px;
+      padding: 4px;
+      text-transform: uppercase;
+    }
+    .md-button {
+      color: #7B7B7B;
+      min-width: 32px;
+      min-height: 15px;
+      line-height: 15px;
+      font-size: 0.800rem;
+      margin: 0;
+      padding: 4px;
+      background: rgba(220, 220, 220, 0.35);
+    }
+  }
+
+  .tb-resize-container {
+    overflow-y: auto;
+    height: 100%;
+    width: 100%;
+    position: relative;
+
+    .ace_editor {
+      height: 100%;
+    }
+  }
+
+}
diff --git a/ui/src/app/rulechain/script/node-script-test.service.js b/ui/src/app/rulechain/script/node-script-test.service.js
new file mode 100644
index 0000000..81d81c1
--- /dev/null
+++ b/ui/src/app/rulechain/script/node-script-test.service.js
@@ -0,0 +1,140 @@
+/*
+ * 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.
+ */
+
+/* eslint-disable import/no-unresolved, import/default */
+
+import nodeScriptTestTemplate from './node-script-test.tpl.html';
+
+/* eslint-enable import/no-unresolved, import/default */
+
+/*@ngInject*/
+export default function NodeScriptTest($q, $mdDialog, $document, ruleChainService) {
+
+    var service = {
+        testNodeScript: testNodeScript
+    };
+
+    return service;
+
+    function testNodeScript($event, script, scriptType, functionTitle, functionName, argNames, ruleNodeId) {
+        var deferred = $q.defer();
+        if ($event) {
+            $event.stopPropagation();
+        }
+
+        var msg, metadata, msgType;
+        if (ruleNodeId) {
+            ruleChainService.getLatestRuleNodeDebugInput(ruleNodeId).then(
+                (debugIn) => {
+                    if (debugIn) {
+                        if (debugIn.data) {
+                            msg = angular.fromJson(debugIn.data);
+                        }
+                        if (debugIn.metadata) {
+                            metadata = angular.fromJson(debugIn.metadata);
+                        }
+                        msgType = debugIn.msgType;
+                    }
+                    openTestScriptDialog($event, script, scriptType, functionTitle,
+                                         functionName, argNames, msg, metadata, msgType).then(
+                        (script) => {
+                            deferred.resolve(script);
+                        },
+                        () => {
+                            deferred.reject();
+                        }
+                    );
+                },
+                () => {
+                    deferred.reject();
+                }
+            );
+        } else {
+            openTestScriptDialog($event, script, scriptType, functionTitle,
+                functionName, argNames).then(
+                (script) => {
+                    deferred.resolve(script);
+                },
+                () => {
+                    deferred.reject();
+                }
+            );
+        }
+        return deferred.promise;
+    }
+
+    function openTestScriptDialog($event, script, scriptType, functionTitle, functionName, argNames, msg, metadata, msgType) {
+        var deferred = $q.defer();
+        if (!msg) {
+            msg = {
+                temperature: 22.4,
+                humidity: 78
+            };
+        }
+        if (!metadata) {
+            metadata = {
+                deviceType: "default",
+                deviceName: "Test Device",
+                ts: new Date().getTime() + ""
+            };
+        }
+        if (!msgType) {
+            msgType = "POST_TELEMETRY_REQUEST";
+        }
+
+        var onShowingCallback = {
+            onShowed: () => {
+            }
+        };
+
+        var inputParams = {
+            script: script,
+            scriptType: scriptType,
+            functionName: functionName,
+            argNames: argNames
+        };
+
+        $mdDialog.show({
+            controller: 'NodeScriptTestController',
+            controllerAs: 'vm',
+            templateUrl: nodeScriptTestTemplate,
+            parent: angular.element($document[0].body),
+            locals: {
+                msg: msg,
+                metadata: metadata,
+                msgType: msgType,
+                functionTitle: functionTitle,
+                inputParams: inputParams,
+                onShowingCallback: onShowingCallback
+            },
+            fullscreen: true,
+            skipHide: true,
+            targetEvent: $event,
+            onComplete: () => {
+                onShowingCallback.onShowed();
+            }
+        }).then(
+            (script) => {
+                deferred.resolve(script);
+            },
+            () => {
+                deferred.reject();
+            }
+        );
+        return deferred.promise;
+    }
+
+}
\ No newline at end of file
diff --git a/ui/src/app/rulechain/script/node-script-test.tpl.html b/ui/src/app/rulechain/script/node-script-test.tpl.html
new file mode 100644
index 0000000..0ce57e8
--- /dev/null
+++ b/ui/src/app/rulechain/script/node-script-test.tpl.html
@@ -0,0 +1,119 @@
+<!--
+
+    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.
+
+-->
+<md-dialog class="tb-node-script-test-dialog"
+           aria-label="{{ 'rulenode.test-script-function' | translate }}" style="width: 800px;">
+    <form flex name="theForm" ng-submit="vm.save()">
+        <md-toolbar>
+            <div class="md-toolbar-tools">
+                <h2>{{ 'rulenode.test-script-function' | translate }}</h2>
+                <span flex></span>
+                <md-button class="md-icon-button" ng-click="vm.cancel()">
+                    <ng-md-icon icon="close" aria-label="{{ 'dialog.close' | translate }}"></ng-md-icon>
+                </md-button>
+            </div>
+        </md-toolbar>
+        <md-dialog-content flex style="position: relative;">
+            <div class="tb-absolute-fill">
+                <div id="top_panel" class="tb-split tb-split-vertical">
+                    <div id="top_left_panel" class="tb-split tb-content">
+                        <div class="tb-resize-container">
+                            <div class="tb-editor-area-title-panel">
+                                <label translate>rulenode.message</label>
+                            </div>
+                            <ng-form name="payloadForm">
+                                <div layout="column" style="height: 100%;">
+                                    <div layout="row">
+                                        <md-input-container class="md-block" style="margin-bottom: 0px; min-width: 300px;">
+                                            <label translate>rulenode.message-type</label>
+                                            <input required name="msgType" ng-model="vm.inputParams.msgType">
+                                            <div ng-messages="payloadForm.msgType.$error">
+                                                <div translate ng-message="required">rulenode.message-type-required</div>
+                                            </div>
+                                        </md-input-container>
+                                    </div>
+                                    <tb-json-content flex
+                                                     ng-model="vm.inputParams.msg"
+                                                     label="{{ 'rulenode.message' | translate }}"
+                                                     content-type="vm.types.contentType.JSON.value"
+                                                     validate-content="true"
+                                                     validation-trigger-arg="validatePayload"
+                                                     fill-height="true">
+                                    </tb-json-content>
+                                </div>
+                            </ng-form>
+                        </div>
+                    </div>
+                    <div id="top_right_panel" class="tb-split tb-content">
+                        <div class="tb-resize-container" id="metadata-panel">
+                            <div class="tb-editor-area-title-panel">
+                                <label translate>rulenode.metadata</label>
+                            </div>
+                            <ng-form name="metadataForm">
+                                <tb-key-val-map title-text="rulenode.metadata" ng-disabled="$root.loading"
+                                                key-val-map="vm.inputParams.metadata"></tb-key-val-map>
+                            </ng-form>
+                        </div>
+                    </div>
+                </div>
+                <div id="bottom_panel" class="tb-split tb-split-vertical">
+                    <div id="bottom_left_panel" class="tb-split tb-content">
+                        <div class="tb-resize-container">
+                            <div class="tb-editor-area-title-panel tb-js-function">
+                                <label>{{ vm.functionTitle }}</label>
+                            </div>
+                            <ng-form name="funcBodyForm">
+                                <tb-js-func id="funcBodyInput" ng-model="vm.inputParams.script"
+                                            function-name="{{vm.inputParams.functionName}}"
+                                            function-args="{{ vm.inputParams.argNames }}"
+                                            validation-args="{{ [[vm.inputParams.msg, vm.inputParams.metadata, vm.inputParams.msgType]] }}"
+                                            validation-trigger-arg="validateFuncBody"
+                                            result-type="object"
+                                            fill-height="true">
+                                </tb-js-func>
+                            </ng-form>
+                        </div>
+                    </div>
+                    <div id="bottom_right_panel" class="tb-split tb-content">
+                        <div class="tb-resize-container">
+                            <div class="tb-editor-area-title-panel">
+                                <label translate>rulenode.output</label>
+                            </div>
+                            <tb-json-content ng-model="vm.output"
+                                             label="{{ 'rulenode.output' | translate }}"
+                                             content-type="vm.types.contentType.JSON.value"
+                                             validate-content="false"
+                                             ng-readonly="true"
+                                             fill-height="true">
+                            </tb-json-content>
+                        </div>
+                    </div>
+                </div>
+            </div>
+        </md-dialog-content>
+        <md-dialog-actions layout="row">
+            <md-button ng-disabled="$root.loading" ng-click="vm.test()" class="md-raised md-primary">
+                {{ 'rulenode.test' | translate }}
+            </md-button>
+            <span flex></span>
+            <md-button ng-disabled="$root.loading || theForm.funcBodyForm.$invalid || !theForm.funcBodyForm.$dirty" type="submit" class="md-raised md-primary">
+                {{ 'action.save' | translate }}
+            </md-button>
+            <md-button ng-disabled="$root.loading" ng-click="vm.cancel()" style="margin-right:20px;">{{ 'action.cancel' | translate }}</md-button>
+        </md-dialog-actions>
+    </form>
+</md-dialog>
diff --git a/ui/src/app/services/item-buffer.service.js b/ui/src/app/services/item-buffer.service.js
index 9fce811..a9fe348 100644
--- a/ui/src/app/services/item-buffer.service.js
+++ b/ui/src/app/services/item-buffer.service.js
@@ -24,10 +24,11 @@ export default angular.module('thingsboard.itembuffer', [angularStorage])
     .name;
 
 /*@ngInject*/
-function ItemBuffer($q, bufferStore, types, utils, dashboardUtils) {
+function ItemBuffer($q, bufferStore, types, utils, dashboardUtils, ruleChainService) {
 
     const WIDGET_ITEM = "widget_item";
     const WIDGET_REFERENCE = "widget_reference";
+    const RULE_NODES = "rule_nodes";
 
     var service = {
         prepareWidgetItem: prepareWidgetItem,
@@ -37,7 +38,10 @@ function ItemBuffer($q, bufferStore, types, utils, dashboardUtils) {
         canPasteWidgetReference: canPasteWidgetReference,
         pasteWidget: pasteWidget,
         pasteWidgetReference: pasteWidgetReference,
-        addWidgetToDashboard: addWidgetToDashboard
+        addWidgetToDashboard: addWidgetToDashboard,
+        copyRuleNodes: copyRuleNodes,
+        hasRuleNodes: hasRuleNodes,
+        pasteRuleNodes: pasteRuleNodes
     }
 
     return service;
@@ -151,6 +155,81 @@ function ItemBuffer($q, bufferStore, types, utils, dashboardUtils) {
         };
     }
 
+    function copyRuleNodes(nodes, connections) {
+        var ruleNodes = {
+            nodes: [],
+            connections: []
+        };
+        var top = -1, left = -1, bottom = -1, right = -1;
+        for (var i=0;i<nodes.length;i++) {
+            var origNode = nodes[i];
+            var node = {
+                additionalInfo: origNode.additionalInfo,
+                configuration: origNode.configuration,
+                debugMode: origNode.debugMode,
+                x: origNode.x,
+                y: origNode.y,
+                name: origNode.name,
+                componentClazz: origNode.component.clazz,
+            };
+            if (origNode.targetRuleChainId) {
+                node.targetRuleChainId = origNode.targetRuleChainId;
+            }
+            if (origNode.error) {
+                node.error = origNode.error;
+            }
+            ruleNodes.nodes.push(node);
+            if (i==0) {
+                top = node.y;
+                left = node.x;
+                bottom = node.y + 50;
+                right = node.x + 170;
+            } else {
+                top = Math.min(top, node.y);
+                left = Math.min(left, node.x);
+                bottom = Math.max(bottom, node.y + 50);
+                right = Math.max(right, node.x + 170);
+            }
+        }
+        ruleNodes.originX = left + (right-left)/2;
+        ruleNodes.originY = top + (bottom-top)/2;
+        for (i=0;i<connections.length;i++) {
+            var connection = connections[i];
+            ruleNodes.connections.push(connection);
+        }
+        bufferStore.set(RULE_NODES, angular.toJson(ruleNodes));
+    }
+
+    function hasRuleNodes() {
+        return bufferStore.get(RULE_NODES);
+    }
+
+    function pasteRuleNodes(x, y) {
+        var ruleNodesJson = bufferStore.get(RULE_NODES);
+        if (ruleNodesJson) {
+            var ruleNodes = angular.fromJson(ruleNodesJson);
+            var deltaX = x - ruleNodes.originX;
+            var deltaY = y - ruleNodes.originY;
+            for (var i=0;i<ruleNodes.nodes.length;i++) {
+                var node = ruleNodes.nodes[i];
+                var component = ruleChainService.getRuleNodeComponentByClazz(node.componentClazz);
+                if (component) {
+                    delete node.componentClazz;
+                    node.component = component;
+                    node.nodeClass = types.ruleNodeType[component.type].nodeClass;
+                    node.icon = types.ruleNodeType[component.type].icon;
+                    node.connectors = [];
+                    node.x = Math.round(node.x + deltaX);
+                    node.y = Math.round(node.y + deltaY);
+                } else {
+                    return null;
+                }
+            }
+            return ruleNodes;
+        }
+        return null;
+    }
+
     function copyWidget(dashboard, sourceState, sourceLayout, widget) {
         var widgetItem = prepareWidgetItem(dashboard, sourceState, sourceLayout, widget);
         bufferStore.set(WIDGET_ITEM, angular.toJson(widgetItem));
diff --git a/ui/src/app/services/menu.service.js b/ui/src/app/services/menu.service.js
index 9dbddd9..1c33d1f 100644
--- a/ui/src/app/services/menu.service.js
+++ b/ui/src/app/services/menu.service.js
@@ -67,18 +67,6 @@ function Menu(userService, $state, $rootScope) {
                             icon: 'home'
                         },
                         {
-                            name: 'plugin.plugins',
-                            type: 'link',
-                            state: 'home.plugins',
-                            icon: 'extension'
-                        },
-                        {
-                            name: 'rule.rules',
-                            type: 'link',
-                            state: 'home.rules',
-                            icon: 'settings_ethernet'
-                        },
-                        {
                             name: 'tenant.tenants',
                             type: 'link',
                             state: 'home.tenants',
@@ -113,21 +101,6 @@ function Menu(userService, $state, $rootScope) {
                         }];
                     homeSections =
                         [{
-                            name: 'rule-plugin.management',
-                            places: [
-                                {
-                                    name: 'plugin.plugins',
-                                    icon: 'extension',
-                                    state: 'home.plugins'
-                                },
-                                {
-                                    name: 'rule.rules',
-                                    icon: 'settings_ethernet',
-                                    state: 'home.rules'
-                                }
-                            ]
-                        },
-                        {
                             name: 'tenant.management',
                             places: [
                                 {
@@ -171,15 +144,9 @@ function Menu(userService, $state, $rootScope) {
                             icon: 'home'
                         },
                         {
-                            name: 'plugin.plugins',
+                            name: 'rulechain.rulechains',
                             type: 'link',
-                            state: 'home.plugins',
-                            icon: 'extension'
-                        },
-                        {
-                            name: 'rule.rules',
-                            type: 'link',
-                            state: 'home.rules',
+                            state: 'home.ruleChains',
                             icon: 'settings_ethernet'
                         },
                         {
@@ -221,17 +188,12 @@ function Menu(userService, $state, $rootScope) {
 
                     homeSections =
                         [{
-                            name: 'rule-plugin.management',
+                            name: 'rulechain.management',
                             places: [
                                 {
-                                    name: 'plugin.plugins',
-                                    icon: 'extension',
-                                    state: 'home.plugins'
-                                },
-                                {
-                                    name: 'rule.rules',
+                                    name: 'rulechain.rulechains',
                                     icon: 'settings_ethernet',
-                                    state: 'home.rules'
+                                    state: 'home.ruleChains'
                                 }
                             ]
                         },
diff --git a/ui/src/scss/main.scss b/ui/src/scss/main.scss
index 93ff320..8fed892 100644
--- a/ui/src/scss/main.scss
+++ b/ui/src/scss/main.scss
@@ -203,6 +203,12 @@ md-sidenav {
  * THINGSBOARD SPECIFIC
  ***********************/
 
+$swift-ease-out-duration: 0.4s !default;
+$swift-ease-out-timing-function: cubic-bezier(0.25, 0.8, 0.25, 1) !default;
+
+$input-label-float-offset: 6px !default;
+$input-label-float-scale: 0.75 !default;
+
 label {
   &.tb-title {
     pointer-events: none;
@@ -213,6 +219,18 @@ label {
     &.no-padding {
       padding-bottom: 0px;
     }
+    &.tb-required:after {
+      content: ' *';
+      font-size: 13px;
+      vertical-align: top;
+      color: rgba(0,0,0,0.54);
+    }
+    &.tb-error {
+      color: rgb(221,44,0);
+      &.tb-required:after {
+        color: rgb(221,44,0);
+      }
+    }
   }
 }
 
@@ -249,6 +267,22 @@ div {
   }
 }
 
+.tb-hint {
+  font-size: 12px;
+  line-height: 14px;
+  transition: all 0.3s cubic-bezier(0.55, 0, 0.55, 0.2);
+  color: grey;
+  padding-bottom: 15px;
+  &.ng-hide, &.ng-enter, &.ng-leave.ng-leave-active {
+    bottom: 26px;
+    opacity: 0;
+  }
+  &.ng-leave, &.ng-enter.ng-enter-active {
+    bottom: 7px;
+    opacity: 1;
+  }
+}
+
 .md-caption {
   &.tb-required:after {
     content: ' *';