Browse Source

Merge branch 'develop/1.5' of https://github.com/thingsboard/thingsboard into develop/1.5

pull/773/merge
vparomskiy 9 years ago
parent
commit
81b8f0f40e
  1. 2
      application/src/main/data/upgrade/1.5.0/schema_update.cql
  2. 5
      application/src/main/data/upgrade/1.5.0/schema_update.sql
  3. 4
      application/src/main/java/org/thingsboard/server/controller/BaseController.java
  4. 35
      application/src/main/java/org/thingsboard/server/controller/DeviceController.java
  5. 3
      application/src/main/java/org/thingsboard/server/service/component/AnnotationComponentDiscoveryService.java
  6. 6
      application/src/main/java/org/thingsboard/server/service/state/DefaultDeviceStateService.java
  7. 123
      application/src/test/java/org/thingsboard/server/system/BaseDeviceOfflineTest.java
  8. 35
      application/src/test/java/org/thingsboard/server/system/BaseHttpDeviceApiTest.java
  9. 23
      application/src/test/java/org/thingsboard/server/system/nosql/DeviceOfflineNoSqlTest.java
  10. 23
      application/src/test/java/org/thingsboard/server/system/sql/DeviceOfflineSqlTest.java
  11. 22
      common/data/src/main/java/org/thingsboard/server/common/data/Device.java
  12. 40
      common/data/src/main/java/org/thingsboard/server/common/data/device/DeviceStatusQuery.java
  13. 2
      common/data/src/main/java/org/thingsboard/server/common/data/plugin/ComponentType.java
  14. 74
      dao/src/main/java/org/thingsboard/server/dao/device/CassandraDeviceDao.java
  15. 49
      dao/src/main/java/org/thingsboard/server/dao/device/DeviceDao.java
  16. 34
      dao/src/main/java/org/thingsboard/server/dao/device/DeviceOfflineService.java
  17. 62
      dao/src/main/java/org/thingsboard/server/dao/device/DeviceOfflineServiceImpl.java
  18. 2
      dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java
  19. 28
      dao/src/main/java/org/thingsboard/server/dao/model/nosql/DeviceEntity.java
  20. 13
      dao/src/main/java/org/thingsboard/server/dao/model/sql/DeviceEntity.java
  21. 24
      dao/src/main/java/org/thingsboard/server/dao/sql/device/DeviceRepository.java
  22. 71
      dao/src/main/java/org/thingsboard/server/dao/sql/device/JpaDeviceDao.java
  23. 2
      dao/src/main/resources/cassandra/schema.cql
  24. 4
      dao/src/main/resources/sql/schema.sql
  25. 2
      rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/NodeDefinition.java
  26. 4
      rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleNode.java
  27. 10
      rule-engine/rule-engine-components/pom.xml
  28. 4
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAlarmNode.java
  29. 4
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbLogNode.java
  30. 5
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/aws/sns/TbSnsNode.java
  31. 5
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/aws/sqs/TbSqsNode.java
  32. 3
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/debug/TbMsgGeneratorNode.java
  33. 6
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbMsgTypeFilterNodeConfiguration.java
  34. 5
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/kafka/TbKafkaNode.java
  35. 68
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mail/RuleVelocityUtils.java
  36. 48
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mail/TbMsgToEmailNode.java
  37. 7
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mail/TbMsgToEmailNodeConfiguration.java
  38. 71
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mail/TbSendEmailNode.java
  39. 16
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mail/TbSendEmailNodeConfiguration.java
  40. 5
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/TbMqttNode.java
  41. 5
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rabbitmq/TbRabbitMqNode.java
  42. 9
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbRestApiCallNode.java
  43. 3
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rpc/TbSendRPCReplyNode.java
  44. 3
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rpc/TbSendRPCRequestNode.java
  45. 3
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgAttributesNode.java
  46. 3
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgTimeseriesNode.java
  47. 4
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbChangeOriginatorNode.java
  48. 6
      rule-engine/rule-engine-components/src/main/resources/public/static/rulenode/rulenode-core-config.js
  49. 9
      rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/mail/TbMsgToEmailNodeTest.java
  50. 8
      transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportResource.java
  51. 6
      transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportService.java
  52. 28
      transport/coap/src/test/java/org/thingsboard/server/transport/coap/CoapServerTest.java
  53. 20
      transport/http/src/main/java/org/thingsboard/server/transport/http/DeviceApiController.java
  54. 25
      transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java
  55. 7
      transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportServerInitializer.java
  56. 6
      transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportService.java
  57. 13
      transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/GatewaySessionCtx.java
  58. 9
      ui/src/app/common/types.constant.js
  59. 2
      ui/src/app/locale/locale.constant.js
  60. 23
      ui/src/app/rulechain/rulechain.controller.js
  61. 5
      ui/src/app/rulechain/rulechain.scss
  62. 8
      ui/src/app/rulechain/rulechain.tpl.html
  63. 4
      ui/src/app/rulechain/rulenode.tpl.html
  64. 23
      ui/src/scss/main.scss

2
application/src/main/data/upgrade/1.5.0/schema_update.cql

@ -94,5 +94,3 @@ CREATE TABLE IF NOT EXISTS thingsboard.rule_node (
PRIMARY KEY (id) PRIMARY KEY (id)
); );
ALTER TABLE thingsboard.device ADD last_connect bigint;
ALTER TABLE thingsboard.device ADD last_update bigint;

5
application/src/main/data/upgrade/1.5.0/schema_update.sql

@ -35,7 +35,4 @@ CREATE TABLE IF NOT EXISTS rule_node (
name varchar(255), name varchar(255),
debug_mode boolean, debug_mode boolean,
search_text varchar(255) search_text varchar(255)
); );
ALTER TABLE device ADD COLUMN IF NOT EXISTS last_connect BIGINT;
ALTER TABLE device ADD COLUMN IF NOT EXISTS last_update BIGINT;

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

@ -45,7 +45,6 @@ import org.thingsboard.server.dao.audit.AuditLogService;
import org.thingsboard.server.dao.customer.CustomerService; import org.thingsboard.server.dao.customer.CustomerService;
import org.thingsboard.server.dao.dashboard.DashboardService; import org.thingsboard.server.dao.dashboard.DashboardService;
import org.thingsboard.server.dao.device.DeviceCredentialsService; import org.thingsboard.server.dao.device.DeviceCredentialsService;
import org.thingsboard.server.dao.device.DeviceOfflineService;
import org.thingsboard.server.dao.device.DeviceService; import org.thingsboard.server.dao.device.DeviceService;
import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.dao.exception.IncorrectParameterException; import org.thingsboard.server.dao.exception.IncorrectParameterException;
@ -135,9 +134,6 @@ public abstract class BaseController {
@Autowired @Autowired
protected AuditLogService auditLogService; protected AuditLogService auditLogService;
@Autowired
protected DeviceOfflineService offlineService;
@Autowired @Autowired
protected DeviceStateService deviceStateService; protected DeviceStateService deviceStateService;

35
application/src/main/java/org/thingsboard/server/controller/DeviceController.java

@ -25,9 +25,6 @@ import org.thingsboard.server.common.data.EntitySubtype;
import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.audit.ActionType;
import org.thingsboard.server.common.data.device.DeviceSearchQuery; import org.thingsboard.server.common.data.device.DeviceSearchQuery;
import org.thingsboard.server.common.data.device.DeviceStatusQuery;
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.CustomerId;
import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
@ -37,6 +34,8 @@ import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.common.data.security.DeviceCredentials; import org.thingsboard.server.common.data.security.DeviceCredentials;
import org.thingsboard.server.dao.exception.IncorrectParameterException; import org.thingsboard.server.dao.exception.IncorrectParameterException;
import org.thingsboard.server.dao.model.ModelConstants; import org.thingsboard.server.dao.model.ModelConstants;
import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.service.security.model.SecurityUser; import org.thingsboard.server.service.security.model.SecurityUser;
import java.util.ArrayList; import java.util.ArrayList;
@ -70,7 +69,7 @@ public class DeviceController extends BaseController {
device.setTenantId(getCurrentUser().getTenantId()); device.setTenantId(getCurrentUser().getTenantId());
if (getCurrentUser().getAuthority() == Authority.CUSTOMER_USER) { if (getCurrentUser().getAuthority() == Authority.CUSTOMER_USER) {
if (device.getId() == null || device.getId().isNullUid() || if (device.getId() == null || device.getId().isNullUid() ||
device.getCustomerId() == null || device.getCustomerId().isNullUid()) { device.getCustomerId() == null || device.getCustomerId().isNullUid()) {
throw new ThingsboardException("You don't have permission to perform this operation!", throw new ThingsboardException("You don't have permission to perform this operation!",
ThingsboardErrorCode.PERMISSION_DENIED); ThingsboardErrorCode.PERMISSION_DENIED);
} else { } else {
@ -374,32 +373,4 @@ public class DeviceController extends BaseController {
throw handleException(e); throw handleException(e);
} }
} }
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/device/offline", method = RequestMethod.GET)
@ResponseBody
public List<Device> getOfflineDevices(@RequestParam("contactType") DeviceStatusQuery.ContactType contactType,
@RequestParam("threshold") long threshold) throws ThingsboardException {
try {
TenantId tenantId = getCurrentUser().getTenantId();
ListenableFuture<List<Device>> offlineDevices = offlineService.findOfflineDevices(tenantId.getId(), contactType, threshold);
return checkNotNull(offlineDevices.get());
} catch (Exception e) {
throw handleException(e);
}
}
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/device/online", method = RequestMethod.GET)
@ResponseBody
public List<Device> getOnlineDevices(@RequestParam("contactType") DeviceStatusQuery.ContactType contactType,
@RequestParam("threshold") long threshold) throws ThingsboardException {
try {
TenantId tenantId = getCurrentUser().getTenantId();
ListenableFuture<List<Device>> offlineDevices = offlineService.findOnlineDevices(tenantId.getId(), contactType, threshold);
return checkNotNull(offlineDevices.get());
} catch (Exception e) {
throw handleException(e);
}
}
} }

3
application/src/main/java/org/thingsboard/server/service/component/AnnotationComponentDiscoveryService.java

@ -118,6 +118,7 @@ public class AnnotationComponentDiscoveryService implements ComponentDiscoverySe
case FILTER: case FILTER:
case TRANSFORMATION: case TRANSFORMATION:
case ACTION: case ACTION:
case EXTERNAL:
RuleNode ruleNodeAnnotation = clazz.getAnnotation(RuleNode.class); RuleNode ruleNodeAnnotation = clazz.getAnnotation(RuleNode.class);
scannedComponent.setName(ruleNodeAnnotation.name()); scannedComponent.setName(ruleNodeAnnotation.name());
scannedComponent.setScope(ruleNodeAnnotation.scope()); scannedComponent.setScope(ruleNodeAnnotation.scope());
@ -194,6 +195,8 @@ public class AnnotationComponentDiscoveryService implements ComponentDiscoverySe
nodeDefinition.setDefaultConfiguration(mapper.valueToTree(defaultConfiguration)); nodeDefinition.setDefaultConfiguration(mapper.valueToTree(defaultConfiguration));
nodeDefinition.setUiResources(nodeAnnotation.uiResources()); nodeDefinition.setUiResources(nodeAnnotation.uiResources());
nodeDefinition.setConfigDirective(nodeAnnotation.configDirective()); nodeDefinition.setConfigDirective(nodeAnnotation.configDirective());
nodeDefinition.setIcon(nodeAnnotation.icon());
nodeDefinition.setIconUrl(nodeAnnotation.iconUrl());
return nodeDefinition; return nodeDefinition;
} }

6
application/src/main/java/org/thingsboard/server/service/state/DefaultDeviceStateService.java

@ -262,7 +262,7 @@ public class DefaultDeviceStateService implements DeviceStateService {
state.setInactivityTimeout(inactivityTimeout); state.setInactivityTimeout(inactivityTimeout);
boolean oldActive = state.isActive(); boolean oldActive = state.isActive();
state.setActive(ts < state.getLastActivityTime() + state.getInactivityTimeout()); state.setActive(ts < state.getLastActivityTime() + state.getInactivityTimeout());
if (!oldActive && state.isActive()) { if (!oldActive && state.isActive() || oldActive && !state.isActive()) {
saveAttribute(deviceId, ACTIVITY_STATE, state.isActive()); saveAttribute(deviceId, ACTIVITY_STATE, state.isActive());
} }
} }
@ -333,10 +333,6 @@ public class DefaultDeviceStateService implements DeviceStateService {
}); });
} }
private long getLastPersistTime(List<AttributeKvEntry> attributes) {
return attributes.stream().map(AttributeKvEntry::getLastUpdateTs).max(Long::compare).orElse(0L);
}
private long getAttributeValue(List<AttributeKvEntry> attributes, String attributeName, long defaultValue) { private long getAttributeValue(List<AttributeKvEntry> attributes, String attributeName, long defaultValue) {
for (AttributeKvEntry attribute : attributes) { for (AttributeKvEntry attribute : attributes) {
if (attribute.getKey().equals(attributeName)) { if (attribute.getKey().equals(attributeName)) {

123
application/src/test/java/org/thingsboard/server/system/BaseDeviceOfflineTest.java

@ -1,123 +0,0 @@
/**
* Copyright © 2016-2018 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.system;
import com.fasterxml.jackson.core.type.TypeReference;
import com.google.common.collect.ImmutableMap;
import org.junit.Before;
import org.junit.Test;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.security.DeviceCredentials;
import org.thingsboard.server.controller.AbstractControllerTest;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import static org.junit.Assert.assertEquals;
public class BaseDeviceOfflineTest extends AbstractControllerTest {
private Device deviceA;
private Device deviceB;
private DeviceCredentials credA;
private DeviceCredentials credB;
@Before
public void before() throws Exception {
loginTenantAdmin();
deviceA = createDevice("DevA", "VMS");
credA = getCredentials(deviceA.getUuidId());
deviceB = createDevice("DevB", "SOLAR");
credB = getCredentials(deviceB.getUuidId());
}
@Test
public void offlineDevicesCanBeFoundByLastConnectField() throws Exception {
makeDeviceContact(credA);
Thread.sleep(1000);
makeDeviceContact(credB);
Thread.sleep(100);
List<Device> devices = doGetTyped("/api/device/offline?contactType=CONNECT&threshold=700", new TypeReference<List<Device>>() {
});
assertEquals(devices.toString(),1, devices.size());
assertEquals("DevA", devices.get(0).getName());
}
@Test
public void offlineDevicesCanBeFoundByLastUpdateField() throws Exception {
makeDeviceUpdate(credA);
Thread.sleep(1000);
makeDeviceUpdate(credB);
makeDeviceContact(credA);
Thread.sleep(100);
List<Device> devices = doGetTyped("/api/device/offline?contactType=UPLOAD&threshold=700", new TypeReference<List<Device>>() {
});
assertEquals(devices.toString(),1, devices.size());
assertEquals("DevA", devices.get(0).getName());
}
@Test
public void onlineDevicesCanBeFoundByLastConnectField() throws Exception {
makeDeviceContact(credB);
Thread.sleep(1000);
makeDeviceContact(credA);
Thread.sleep(100);
List<Device> devices = doGetTyped("/api/device/online?contactType=CONNECT&threshold=700", new TypeReference<List<Device>>() {
});
assertEquals(devices.toString(),1, devices.size());
assertEquals("DevA", devices.get(0).getName());
}
@Test
public void onlineDevicesCanBeFoundByLastUpdateField() throws Exception {
makeDeviceUpdate(credB);
Thread.sleep(1000);
makeDeviceUpdate(credA);
makeDeviceContact(credB);
Thread.sleep(100);
List<Device> devices = doGetTyped("/api/device/online?contactType=UPLOAD&threshold=700", new TypeReference<List<Device>>() {
});
assertEquals(devices.toString(),1, devices.size());
assertEquals("DevA", devices.get(0).getName());
}
private Device createDevice(String name, String type) throws Exception {
Device device = new Device();
device.setName(name);
device.setType(type);
long currentTime = System.currentTimeMillis() - TimeUnit.SECONDS.toMillis(10);
device.setLastConnectTs(currentTime);
device.setLastUpdateTs(currentTime);
return doPost("/api/device", device, Device.class);
}
private DeviceCredentials getCredentials(UUID deviceId) throws Exception {
return doGet("/api/device/" + deviceId.toString() + "/credentials", DeviceCredentials.class);
}
private void makeDeviceUpdate(DeviceCredentials credentials) throws Exception {
doPost("/api/v1/" + credentials.getCredentialsId() + "/attributes", ImmutableMap.of("keyA", "valueA"), new String[]{});
}
private void makeDeviceContact(DeviceCredentials credentials) throws Exception {
doGet("/api/v1/" + credentials.getCredentialsId() + "/attributes?clientKeys=keyA,keyB,keyC");
}
}

35
application/src/test/java/org/thingsboard/server/system/BaseHttpDeviceApiTest.java

@ -15,7 +15,6 @@
*/ */
package org.thingsboard.server.system; package org.thingsboard.server.system;
import com.google.common.collect.ImmutableMap;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.springframework.test.web.servlet.ResultActions; import org.springframework.test.web.servlet.ResultActions;
@ -29,9 +28,6 @@ import java.util.Map;
import java.util.Random; import java.util.Random;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.request; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.request;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@ -52,9 +48,6 @@ public abstract class BaseHttpDeviceApiTest extends AbstractControllerTest {
device = new Device(); device = new Device();
device.setName("My device"); device.setName("My device");
device.setType("default"); device.setType("default");
long currentTime = System.currentTimeMillis();
device.setLastConnectTs(currentTime);
device.setLastUpdateTs(currentTime);
device = doPost("/api/device", device, Device.class); device = doPost("/api/device", device, Device.class);
deviceCredentials = deviceCredentials =
@ -74,34 +67,6 @@ public abstract class BaseHttpDeviceApiTest extends AbstractControllerTest {
doGetAsync("/api/v1/" + deviceCredentials.getCredentialsId() + "/attributes?clientKeys=keyA,keyB,keyC").andExpect(status().isOk()); doGetAsync("/api/v1/" + deviceCredentials.getCredentialsId() + "/attributes?clientKeys=keyA,keyB,keyC").andExpect(status().isOk());
} }
@Test
public void deviceLastContactAndUpdateFieldsAreUpdated() throws Exception {
Device actualDevice = doGet("/api/device/" + this.device.getId(), Device.class);
Long initConnectTs = actualDevice.getLastConnectTs();
Long initUpdateTs = actualDevice.getLastUpdateTs();
assertNotNull(initConnectTs);
assertNotNull(initUpdateTs);
Thread.sleep(50);
doPost("/api/v1/" + deviceCredentials.getCredentialsId() + "/attributes", ImmutableMap.of("keyA", "valueA"), new String[]{});
actualDevice = doGet("/api/device/" + this.device.getId(), Device.class);
Long postConnectTs = actualDevice.getLastConnectTs();
Long postUpdateTs = actualDevice.getLastUpdateTs();
System.out.println(postConnectTs + " - " + postUpdateTs + " -> " + (postConnectTs - initConnectTs) + " : " + (postUpdateTs - initUpdateTs));
assertTrue(postConnectTs > initConnectTs);
assertEquals(postConnectTs, postUpdateTs);
Thread.sleep(50);
doGet("/api/v1/" + deviceCredentials.getCredentialsId() + "/attributes?clientKeys=keyA,keyB,keyC");
Thread.sleep(50);
actualDevice = doGet("/api/device/" + this.device.getId(), Device.class);
Long getConnectTs = actualDevice.getLastConnectTs();
Long getUpdateTs = actualDevice.getLastUpdateTs();
assertTrue(getConnectTs > postConnectTs);
assertEquals(getUpdateTs, postUpdateTs);
}
protected ResultActions doGetAsync(String urlTemplate, Object... urlVariables) throws Exception { protected ResultActions doGetAsync(String urlTemplate, Object... urlVariables) throws Exception {
MockHttpServletRequestBuilder getRequest; MockHttpServletRequestBuilder getRequest;
getRequest = get(urlTemplate, urlVariables); getRequest = get(urlTemplate, urlVariables);

23
application/src/test/java/org/thingsboard/server/system/nosql/DeviceOfflineNoSqlTest.java

@ -1,23 +0,0 @@
/**
* Copyright © 2016-2018 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.system.nosql;
import org.thingsboard.server.dao.service.DaoNoSqlTest;
import org.thingsboard.server.system.BaseDeviceOfflineTest;
@DaoNoSqlTest
public class DeviceOfflineNoSqlTest extends BaseDeviceOfflineTest {
}

23
application/src/test/java/org/thingsboard/server/system/sql/DeviceOfflineSqlTest.java

@ -1,23 +0,0 @@
/**
* Copyright © 2016-2018 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.system.sql;
import org.thingsboard.server.dao.service.DaoSqlTest;
import org.thingsboard.server.system.BaseDeviceOfflineTest;
@DaoSqlTest
public class DeviceOfflineSqlTest extends BaseDeviceOfflineTest {
}

22
common/data/src/main/java/org/thingsboard/server/common/data/Device.java

@ -31,8 +31,6 @@ public class Device extends SearchTextBasedWithAdditionalInfo<DeviceId> implemen
private CustomerId customerId; private CustomerId customerId;
private String name; private String name;
private String type; private String type;
private Long lastConnectTs;
private Long lastUpdateTs;
public Device() { public Device() {
super(); super();
@ -83,22 +81,6 @@ public class Device extends SearchTextBasedWithAdditionalInfo<DeviceId> implemen
this.type = type; this.type = type;
} }
public Long getLastConnectTs() {
return lastConnectTs;
}
public void setLastConnectTs(Long lastConnectTs) {
this.lastConnectTs = lastConnectTs;
}
public Long getLastUpdateTs() {
return lastUpdateTs;
}
public void setLastUpdateTs(Long lastUpdateTs) {
this.lastUpdateTs = lastUpdateTs;
}
@Override @Override
public String getSearchText() { public String getSearchText() {
return getName(); return getName();
@ -119,10 +101,6 @@ public class Device extends SearchTextBasedWithAdditionalInfo<DeviceId> implemen
builder.append(getAdditionalInfo()); builder.append(getAdditionalInfo());
builder.append(", createdTime="); builder.append(", createdTime=");
builder.append(createdTime); builder.append(createdTime);
builder.append(", lastUpdateTs=");
builder.append(lastUpdateTs);
builder.append(", lastConnectTs=");
builder.append(lastConnectTs);
builder.append(", id="); builder.append(", id=");
builder.append(id); builder.append(id);
builder.append("]"); builder.append("]");

40
common/data/src/main/java/org/thingsboard/server/common/data/device/DeviceStatusQuery.java

@ -1,40 +0,0 @@
/**
* Copyright © 2016-2018 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.common.data.device;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.ToString;
@Data
@AllArgsConstructor
@ToString
public class DeviceStatusQuery {
private Status status;
private ContactType contactType;
private long threshold;
public enum Status {
ONLINE, OFFLINE
}
public enum ContactType {
CONNECT, UPLOAD
}
}

2
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 { public enum ComponentType {
ENRICHMENT, FILTER, TRANSFORMATION, ACTION, OLD_ACTION, PLUGIN ENRICHMENT, FILTER, TRANSFORMATION, ACTION, EXTERNAL, OLD_ACTION, PLUGIN
} }

74
dao/src/main/java/org/thingsboard/server/dao/device/CassandraDeviceDao.java

@ -15,9 +15,9 @@
*/ */
package org.thingsboard.server.dao.device; package org.thingsboard.server.dao.device;
import com.datastax.driver.core.*; import com.datastax.driver.core.ResultSet;
import com.datastax.driver.core.querybuilder.Clause; import com.datastax.driver.core.ResultSetFuture;
import com.datastax.driver.core.querybuilder.QueryBuilder; import com.datastax.driver.core.Statement;
import com.datastax.driver.core.querybuilder.Select; import com.datastax.driver.core.querybuilder.Select;
import com.datastax.driver.mapping.Result; import com.datastax.driver.mapping.Result;
import com.google.common.base.Function; import com.google.common.base.Function;
@ -28,11 +28,9 @@ import org.springframework.stereotype.Component;
import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.EntitySubtype; import org.thingsboard.server.common.data.EntitySubtype;
import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.device.DeviceStatusQuery;
import org.thingsboard.server.common.data.page.TextPageLink; import org.thingsboard.server.common.data.page.TextPageLink;
import org.thingsboard.server.dao.DaoUtil; import org.thingsboard.server.dao.DaoUtil;
import org.thingsboard.server.dao.model.EntitySubtypeEntity; import org.thingsboard.server.dao.model.EntitySubtypeEntity;
import org.thingsboard.server.dao.model.ModelConstants;
import org.thingsboard.server.dao.model.nosql.DeviceEntity; import org.thingsboard.server.dao.model.nosql.DeviceEntity;
import org.thingsboard.server.dao.nosql.CassandraAbstractSearchTextDao; import org.thingsboard.server.dao.nosql.CassandraAbstractSearchTextDao;
import org.thingsboard.server.dao.util.NoSqlDao; import org.thingsboard.server.dao.util.NoSqlDao;
@ -159,7 +157,7 @@ public class CassandraDeviceDao extends CassandraAbstractSearchTextDao<DeviceEnt
if (result != null) { if (result != null) {
List<EntitySubtype> entitySubtypes = new ArrayList<>(); List<EntitySubtype> entitySubtypes = new ArrayList<>();
result.all().forEach((entitySubtypeEntity) -> result.all().forEach((entitySubtypeEntity) ->
entitySubtypes.add(entitySubtypeEntity.toEntitySubtype()) entitySubtypes.add(entitySubtypeEntity.toEntitySubtype())
); );
return entitySubtypes; return entitySubtypes;
} else { } else {
@ -169,68 +167,4 @@ public class CassandraDeviceDao extends CassandraAbstractSearchTextDao<DeviceEnt
}); });
} }
@Override
public ListenableFuture<List<Device>> findDevicesByTenantIdAndStatus(UUID tenantId, DeviceStatusQuery statusQuery) {
log.debug("Try to find [{}] devices by tenantId [{}]", statusQuery.getStatus(), tenantId);
Select select = select().from(DEVICE_BY_TENANT_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME).allowFiltering();
Select.Where query = select.where();
query.and(eq(DEVICE_TENANT_ID_PROPERTY, tenantId));
Clause clause = statusClause(statusQuery);
query.and(clause);
return findListByStatementAsync(query);
}
@Override
public ListenableFuture<List<Device>> findDevicesByTenantIdTypeAndStatus(UUID tenantId, String type, DeviceStatusQuery statusQuery) {
log.debug("Try to find [{}] devices by tenantId [{}] and type [{}]", statusQuery.getStatus(), tenantId, type);
Select select = select().from(DEVICE_BY_TENANT_BY_TYPE_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME).allowFiltering();
Select.Where query = select.where()
.and(eq(DEVICE_TENANT_ID_PROPERTY, tenantId))
.and(eq(DEVICE_TYPE_PROPERTY, type));
query.and(statusClause(statusQuery));
return findListByStatementAsync(query);
}
@Override
public void saveDeviceStatus(Device device) {
PreparedStatement statement = prepare("insert into " +
"device (id, tenant_id, customer_id, type, last_connect, last_update) values (?, ?, ?, ?, ?, ?)");
BoundStatement boundStatement = statement.bind(device.getUuidId(), device.getTenantId().getId(), device.getCustomerId().getId(),
device.getType(), device.getLastConnectTs(), device.getLastUpdateTs());
ResultSetFuture resultSetFuture = executeAsyncWrite(boundStatement);
Futures.withFallback(resultSetFuture, t -> {
log.error("Can't update device status for [{}]", device, t);
throw new IllegalArgumentException("Can't update device status for {" + device + "}", t);
});
}
private String getStatusProperty(DeviceStatusQuery statusQuery) {
switch (statusQuery.getContactType()) {
case UPLOAD:
return DEVICE_LAST_UPDATE_PROPERTY;
case CONNECT:
return DEVICE_LAST_CONNECT_PROPERTY;
}
return null;
}
private Clause statusClause(DeviceStatusQuery statusQuery) {
long minTime = System.currentTimeMillis() - statusQuery.getThreshold();
String statusProperty = getStatusProperty(statusQuery);
if (statusProperty != null) {
switch (statusQuery.getStatus()) {
case ONLINE:
return gt(statusProperty, minTime);
case OFFLINE:
return lt(statusProperty, minTime);
}
}
log.error("Could not build status query from [{}]", statusQuery);
throw new IllegalStateException("Could not build status query for device []");
}
} }

49
dao/src/main/java/org/thingsboard/server/dao/device/DeviceDao.java

@ -18,7 +18,6 @@ package org.thingsboard.server.dao.device;
import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListenableFuture;
import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.EntitySubtype; import org.thingsboard.server.common.data.EntitySubtype;
import org.thingsboard.server.common.data.device.DeviceStatusQuery;
import org.thingsboard.server.common.data.page.TextPageLink; import org.thingsboard.server.common.data.page.TextPageLink;
import org.thingsboard.server.dao.Dao; import org.thingsboard.server.dao.Dao;
@ -28,6 +27,7 @@ import java.util.UUID;
/** /**
* The Interface DeviceDao. * The Interface DeviceDao.
*
*/ */
public interface DeviceDao extends Dao<Device> { public interface DeviceDao extends Dao<Device> {
@ -52,7 +52,7 @@ public interface DeviceDao extends Dao<Device> {
* Find devices by tenantId, type and page link. * Find devices by tenantId, type and page link.
* *
* @param tenantId the tenantId * @param tenantId the tenantId
* @param type the type * @param type the type
* @param pageLink the page link * @param pageLink the page link
* @return the list of device objects * @return the list of device objects
*/ */
@ -61,7 +61,7 @@ public interface DeviceDao extends Dao<Device> {
/** /**
* Find devices by tenantId and devices Ids. * Find devices by tenantId and devices Ids.
* *
* @param tenantId the tenantId * @param tenantId the tenantId
* @param deviceIds the device Ids * @param deviceIds the device Ids
* @return the list of device objects * @return the list of device objects
*/ */
@ -70,9 +70,9 @@ public interface DeviceDao extends Dao<Device> {
/** /**
* Find devices by tenantId, customerId and page link. * Find devices by tenantId, customerId and page link.
* *
* @param tenantId the tenantId * @param tenantId the tenantId
* @param customerId the customerId * @param customerId the customerId
* @param pageLink the page link * @param pageLink the page link
* @return the list of device objects * @return the list of device objects
*/ */
List<Device> findDevicesByTenantIdAndCustomerId(UUID tenantId, UUID customerId, TextPageLink pageLink); List<Device> findDevicesByTenantIdAndCustomerId(UUID tenantId, UUID customerId, TextPageLink pageLink);
@ -80,10 +80,10 @@ public interface DeviceDao extends Dao<Device> {
/** /**
* Find devices by tenantId, customerId, type and page link. * Find devices by tenantId, customerId, type and page link.
* *
* @param tenantId the tenantId * @param tenantId the tenantId
* @param customerId the customerId * @param customerId the customerId
* @param type the type * @param type the type
* @param pageLink the page link * @param pageLink the page link
* @return the list of device objects * @return the list of device objects
*/ */
List<Device> findDevicesByTenantIdAndCustomerIdAndType(UUID tenantId, UUID customerId, String type, TextPageLink pageLink); List<Device> findDevicesByTenantIdAndCustomerIdAndType(UUID tenantId, UUID customerId, String type, TextPageLink pageLink);
@ -92,9 +92,9 @@ public interface DeviceDao extends Dao<Device> {
/** /**
* Find devices by tenantId, customerId and devices Ids. * Find devices by tenantId, customerId and devices Ids.
* *
* @param tenantId the tenantId * @param tenantId the tenantId
* @param customerId the customerId * @param customerId the customerId
* @param deviceIds the device Ids * @param deviceIds the device Ids
* @return the list of device objects * @return the list of device objects
*/ */
ListenableFuture<List<Device>> findDevicesByTenantIdCustomerIdAndIdsAsync(UUID tenantId, UUID customerId, List<UUID> deviceIds); ListenableFuture<List<Device>> findDevicesByTenantIdCustomerIdAndIdsAsync(UUID tenantId, UUID customerId, List<UUID> deviceIds);
@ -103,7 +103,7 @@ public interface DeviceDao extends Dao<Device> {
* Find devices by tenantId and device name. * Find devices by tenantId and device name.
* *
* @param tenantId the tenantId * @param tenantId the tenantId
* @param name the device name * @param name the device name
* @return the optional device object * @return the optional device object
*/ */
Optional<Device> findDeviceByTenantIdAndName(UUID tenantId, String name); Optional<Device> findDeviceByTenantIdAndName(UUID tenantId, String name);
@ -114,31 +114,4 @@ public interface DeviceDao extends Dao<Device> {
* @return the list of tenant device type objects * @return the list of tenant device type objects
*/ */
ListenableFuture<List<EntitySubtype>> findTenantDeviceTypesAsync(UUID tenantId); ListenableFuture<List<EntitySubtype>> findTenantDeviceTypesAsync(UUID tenantId);
/**
* Find devices by tenantId, statusQuery and page link.
*
* @param tenantId the tenantId
* @param statusQuery the page link
* @return the list of device objects
*/
ListenableFuture<List<Device>> findDevicesByTenantIdAndStatus(UUID tenantId, DeviceStatusQuery statusQuery);
/**
* Find devices by tenantId, type, statusQuery and page link.
*
* @param tenantId the tenantId
* @param type the type
* @param statusQuery the page link
* @return the list of device objects
*/
ListenableFuture<List<Device>> findDevicesByTenantIdTypeAndStatus(UUID tenantId, String type, DeviceStatusQuery statusQuery);
/**
* Update device last contact and update timestamp async
*
* @param device the device object
*/
void saveDeviceStatus(Device device);
} }

34
dao/src/main/java/org/thingsboard/server/dao/device/DeviceOfflineService.java

@ -1,34 +0,0 @@
/**
* Copyright © 2016-2018 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.dao.device;
import com.google.common.util.concurrent.ListenableFuture;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.device.DeviceStatusQuery;
import java.util.List;
import java.util.UUID;
public interface DeviceOfflineService {
void online(Device device, boolean isUpdate);
void offline(Device device);
ListenableFuture<List<Device>> findOfflineDevices(UUID tenantId, DeviceStatusQuery.ContactType contactType, long offlineThreshold);
ListenableFuture<List<Device>> findOnlineDevices(UUID tenantId, DeviceStatusQuery.ContactType contactType, long offlineThreshold);
}

62
dao/src/main/java/org/thingsboard/server/dao/device/DeviceOfflineServiceImpl.java

@ -1,62 +0,0 @@
/**
* Copyright © 2016-2018 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.dao.device;
import com.google.common.util.concurrent.ListenableFuture;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.device.DeviceStatusQuery;
import java.util.List;
import java.util.UUID;
import static org.thingsboard.server.common.data.device.DeviceStatusQuery.Status.OFFLINE;
import static org.thingsboard.server.common.data.device.DeviceStatusQuery.Status.ONLINE;
@Service
public class DeviceOfflineServiceImpl implements DeviceOfflineService {
@Autowired
private DeviceDao deviceDao;
@Override
public void online(Device device, boolean isUpdate) {
long current = System.currentTimeMillis();
device.setLastConnectTs(current);
if(isUpdate) {
device.setLastUpdateTs(current);
}
deviceDao.saveDeviceStatus(device);
}
@Override
public void offline(Device device) {
online(device, false);
}
@Override
public ListenableFuture<List<Device>> findOfflineDevices(UUID tenantId, DeviceStatusQuery.ContactType contactType, long offlineThreshold) {
DeviceStatusQuery statusQuery = new DeviceStatusQuery(OFFLINE, contactType, offlineThreshold);
return deviceDao.findDevicesByTenantIdAndStatus(tenantId, statusQuery);
}
@Override
public ListenableFuture<List<Device>> findOnlineDevices(UUID tenantId, DeviceStatusQuery.ContactType contactType, long offlineThreshold) {
DeviceStatusQuery statusQuery = new DeviceStatusQuery(ONLINE, contactType, offlineThreshold);
return deviceDao.findDevicesByTenantIdAndStatus(tenantId, statusQuery);
}
}

2
dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java

@ -133,8 +133,6 @@ public class ModelConstants {
public static final String DEVICE_NAME_PROPERTY = "name"; public static final String DEVICE_NAME_PROPERTY = "name";
public static final String DEVICE_TYPE_PROPERTY = "type"; public static final String DEVICE_TYPE_PROPERTY = "type";
public static final String DEVICE_ADDITIONAL_INFO_PROPERTY = ADDITIONAL_INFO_PROPERTY; public static final String DEVICE_ADDITIONAL_INFO_PROPERTY = ADDITIONAL_INFO_PROPERTY;
public static final String DEVICE_LAST_CONNECT_PROPERTY = "last_connect";
public static final String DEVICE_LAST_UPDATE_PROPERTY = "last_update";
public static final String DEVICE_BY_TENANT_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME = "device_by_tenant_and_search_text"; public static final String DEVICE_BY_TENANT_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME = "device_by_tenant_and_search_text";
public static final String DEVICE_BY_TENANT_BY_TYPE_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME = "device_by_tenant_by_type_and_search_text"; public static final String DEVICE_BY_TENANT_BY_TYPE_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME = "device_by_tenant_by_type_and_search_text";

28
dao/src/main/java/org/thingsboard/server/dao/model/nosql/DeviceEntity.java

@ -63,12 +63,6 @@ public final class DeviceEntity implements SearchTextEntity<Device> {
@Column(name = DEVICE_ADDITIONAL_INFO_PROPERTY, codec = JsonCodec.class) @Column(name = DEVICE_ADDITIONAL_INFO_PROPERTY, codec = JsonCodec.class)
private JsonNode additionalInfo; private JsonNode additionalInfo;
@Column(name = DEVICE_LAST_CONNECT_PROPERTY)
private Long lastConnectTs;
@Column(name = DEVICE_LAST_UPDATE_PROPERTY)
private Long lastUpdateTs;
public DeviceEntity() { public DeviceEntity() {
super(); super();
} }
@ -86,8 +80,6 @@ public final class DeviceEntity implements SearchTextEntity<Device> {
this.name = device.getName(); this.name = device.getName();
this.type = device.getType(); this.type = device.getType();
this.additionalInfo = device.getAdditionalInfo(); this.additionalInfo = device.getAdditionalInfo();
this.lastConnectTs = device.getLastConnectTs();
this.lastUpdateTs = device.getLastUpdateTs();
} }
public UUID getId() { public UUID getId() {
@ -137,23 +129,7 @@ public final class DeviceEntity implements SearchTextEntity<Device> {
public void setAdditionalInfo(JsonNode additionalInfo) { public void setAdditionalInfo(JsonNode additionalInfo) {
this.additionalInfo = additionalInfo; this.additionalInfo = additionalInfo;
} }
public Long getLastConnectTs() {
return lastConnectTs;
}
public void setLastConnectTs(Long lastConnectTs) {
this.lastConnectTs = lastConnectTs;
}
public Long getLastUpdateTs() {
return lastUpdateTs;
}
public void setLastUpdateTs(Long lastUpdateTs) {
this.lastUpdateTs = lastUpdateTs;
}
@Override @Override
public String getSearchTextSource() { public String getSearchTextSource() {
return getName(); return getName();
@ -181,8 +157,6 @@ public final class DeviceEntity implements SearchTextEntity<Device> {
device.setName(name); device.setName(name);
device.setType(type); device.setType(type);
device.setAdditionalInfo(additionalInfo); device.setAdditionalInfo(additionalInfo);
device.setLastConnectTs(lastConnectTs);
device.setLastUpdateTs(lastUpdateTs);
return device; return device;
} }

13
dao/src/main/java/org/thingsboard/server/dao/model/sql/DeviceEntity.java

@ -34,9 +34,6 @@ import javax.persistence.Column;
import javax.persistence.Entity; import javax.persistence.Entity;
import javax.persistence.Table; import javax.persistence.Table;
import static org.thingsboard.server.dao.model.ModelConstants.DEVICE_LAST_CONNECT_PROPERTY;
import static org.thingsboard.server.dao.model.ModelConstants.DEVICE_LAST_UPDATE_PROPERTY;
@Data @Data
@EqualsAndHashCode(callSuper = true) @EqualsAndHashCode(callSuper = true)
@Entity @Entity
@ -63,12 +60,6 @@ public final class DeviceEntity extends BaseSqlEntity<Device> implements SearchT
@Column(name = ModelConstants.DEVICE_ADDITIONAL_INFO_PROPERTY) @Column(name = ModelConstants.DEVICE_ADDITIONAL_INFO_PROPERTY)
private JsonNode additionalInfo; private JsonNode additionalInfo;
@Column(name = DEVICE_LAST_CONNECT_PROPERTY)
private Long lastConnectTs;
@Column(name = DEVICE_LAST_UPDATE_PROPERTY)
private Long lastUpdateTs;
public DeviceEntity() { public DeviceEntity() {
super(); super();
} }
@ -86,8 +77,6 @@ public final class DeviceEntity extends BaseSqlEntity<Device> implements SearchT
this.name = device.getName(); this.name = device.getName();
this.type = device.getType(); this.type = device.getType();
this.additionalInfo = device.getAdditionalInfo(); this.additionalInfo = device.getAdditionalInfo();
this.lastConnectTs = device.getLastConnectTs();
this.lastUpdateTs = device.getLastUpdateTs();
} }
@Override @Override
@ -113,8 +102,6 @@ public final class DeviceEntity extends BaseSqlEntity<Device> implements SearchT
device.setName(name); device.setName(name);
device.setType(type); device.setType(type);
device.setAdditionalInfo(additionalInfo); device.setAdditionalInfo(additionalInfo);
device.setLastConnectTs(lastConnectTs);
device.setLastUpdateTs(lastUpdateTs);
return device; return device;
} }
} }

24
dao/src/main/java/org/thingsboard/server/dao/sql/device/DeviceRepository.java

@ -79,28 +79,4 @@ public interface DeviceRepository extends CrudRepository<DeviceEntity, String> {
List<DeviceEntity> findDevicesByTenantIdAndCustomerIdAndIdIn(String tenantId, String customerId, List<String> deviceIds); List<DeviceEntity> findDevicesByTenantIdAndCustomerIdAndIdIn(String tenantId, String customerId, List<String> deviceIds);
List<DeviceEntity> findDevicesByTenantIdAndIdIn(String tenantId, List<String> deviceIds); List<DeviceEntity> findDevicesByTenantIdAndIdIn(String tenantId, List<String> deviceIds);
@Query("SELECT d FROM DeviceEntity d WHERE d.tenantId = :tenantId AND d.lastConnectTs > :time")
List<DeviceEntity> findConnectOnlineByTenantId(@Param("tenantId") String tenantId, @Param("time") long time);
@Query("SELECT d FROM DeviceEntity d WHERE d.tenantId = :tenantId AND d.lastConnectTs < :time")
List<DeviceEntity> findConnectOfflineByTenantId(@Param("tenantId") String tenantId, @Param("time") long time);
@Query("SELECT d FROM DeviceEntity d WHERE d.tenantId = :tenantId AND d.lastUpdateTs > :time")
List<DeviceEntity> findUpdateOnlineByTenantId(@Param("tenantId") String tenantId, @Param("time") long time);
@Query("SELECT d FROM DeviceEntity d WHERE d.tenantId = :tenantId AND d.lastUpdateTs < :time")
List<DeviceEntity> findUpdateOfflineByTenantId(@Param("tenantId") String tenantId, @Param("time") long time);
@Query("SELECT d FROM DeviceEntity d WHERE d.tenantId = :tenantId AND d.lastConnectTs > :time AND d.type = :type")
List<DeviceEntity> findConnectOnlineByTenantIdAndType(@Param("tenantId") String tenantId, @Param("time") long time, @Param("type") String type);
@Query("SELECT d FROM DeviceEntity d WHERE d.tenantId = :tenantId AND d.lastConnectTs < :time AND d.type = :type")
List<DeviceEntity> findConnectOfflineByTenantIdAndType(@Param("tenantId") String tenantId, @Param("time") long time, @Param("type") String type);
@Query("SELECT d FROM DeviceEntity d WHERE d.tenantId = :tenantId AND d.lastUpdateTs > :time AND d.type = :type")
List<DeviceEntity> findUpdateOnlineByTenantIdAndType(@Param("tenantId") String tenantId, @Param("time") long time, @Param("type") String type);
@Query("SELECT d FROM DeviceEntity d WHERE d.tenantId = :tenantId AND d.lastUpdateTs < :time AND d.type = :type")
List<DeviceEntity> findUpdateOfflineByTenantIdAndType(@Param("tenantId") String tenantId, @Param("time") long time, @Param("type") String type);
} }

71
dao/src/main/java/org/thingsboard/server/dao/sql/device/JpaDeviceDao.java

@ -15,9 +15,7 @@
*/ */
package org.thingsboard.server.dao.sql.device; package org.thingsboard.server.dao.sql.device;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListenableFuture;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.PageRequest;
import org.springframework.data.repository.CrudRepository; import org.springframework.data.repository.CrudRepository;
@ -26,7 +24,6 @@ import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.EntitySubtype; import org.thingsboard.server.common.data.EntitySubtype;
import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.UUIDConverter; import org.thingsboard.server.common.data.UUIDConverter;
import org.thingsboard.server.common.data.device.DeviceStatusQuery;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.TextPageLink; import org.thingsboard.server.common.data.page.TextPageLink;
import org.thingsboard.server.dao.DaoUtil; import org.thingsboard.server.dao.DaoUtil;
@ -46,7 +43,6 @@ import static org.thingsboard.server.dao.model.ModelConstants.NULL_UUID_STR;
*/ */
@Component @Component
@SqlDao @SqlDao
@Slf4j
public class JpaDeviceDao extends JpaAbstractSearchTextDao<DeviceEntity, Device> implements DeviceDao { public class JpaDeviceDao extends JpaAbstractSearchTextDao<DeviceEntity, Device> implements DeviceDao {
@Autowired @Autowired
@ -128,73 +124,6 @@ public class JpaDeviceDao extends JpaAbstractSearchTextDao<DeviceEntity, Device>
return service.submit(() -> convertTenantDeviceTypesToDto(tenantId, deviceRepository.findTenantDeviceTypes(fromTimeUUID(tenantId)))); return service.submit(() -> convertTenantDeviceTypesToDto(tenantId, deviceRepository.findTenantDeviceTypes(fromTimeUUID(tenantId))));
} }
@Override
public ListenableFuture<List<Device>> findDevicesByTenantIdAndStatus(UUID tenantId, DeviceStatusQuery statusQuery) {
String strTenantId = fromTimeUUID(tenantId);
long minTime = System.currentTimeMillis() - statusQuery.getThreshold();
switch (statusQuery.getStatus()) {
case OFFLINE: {
switch (statusQuery.getContactType()) {
case UPLOAD:
return service.submit(() -> DaoUtil.convertDataList(deviceRepository.findUpdateOfflineByTenantId(strTenantId, minTime)));
case CONNECT:
return service.submit(() -> DaoUtil.convertDataList(deviceRepository.findConnectOfflineByTenantId(strTenantId, minTime)));
}
break;
}
case ONLINE: {
switch (statusQuery.getContactType()) {
case UPLOAD:
return service.submit(() -> DaoUtil.convertDataList(deviceRepository.findUpdateOnlineByTenantId(strTenantId, minTime)));
case CONNECT:
return service.submit(() -> DaoUtil.convertDataList(deviceRepository.findConnectOnlineByTenantId(strTenantId, minTime)));
}
break;
}
}
log.error("Could not build status query from [{}]", statusQuery);
throw new IllegalStateException("Could not build status query for device []");
}
@Override
public ListenableFuture<List<Device>> findDevicesByTenantIdTypeAndStatus(UUID tenantId, String type, DeviceStatusQuery statusQuery) {
String strTenantId = fromTimeUUID(tenantId);
long minTime = System.currentTimeMillis() - statusQuery.getThreshold();
switch (statusQuery.getStatus()) {
case OFFLINE: {
switch (statusQuery.getContactType()) {
case UPLOAD:
return service.submit(() -> DaoUtil.convertDataList(deviceRepository.findUpdateOfflineByTenantIdAndType(strTenantId, minTime, type)));
case CONNECT:
return service.submit(() -> DaoUtil.convertDataList(deviceRepository.findConnectOfflineByTenantIdAndType(strTenantId, minTime, type)));
}
break;
}
case ONLINE: {
switch (statusQuery.getContactType()) {
case UPLOAD:
return service.submit(() -> DaoUtil.convertDataList(deviceRepository.findUpdateOnlineByTenantIdAndType(strTenantId, minTime, type)));
case CONNECT:
return service.submit(() -> DaoUtil.convertDataList(deviceRepository.findConnectOnlineByTenantIdAndType(strTenantId, minTime, type)));
}
break;
}
}
log.error("Could not build status query from [{}]", statusQuery);
throw new IllegalStateException("Could not build status query for device []");
}
@Override
public void saveDeviceStatus(Device device) {
ListenableFuture<Device> future = service.submit(() -> save(device));
Futures.withFallback(future, t -> {
log.error("Can't update device status for [{}]", device, t);
throw new IllegalArgumentException("Can't update device status for {" + device + "}", t);
});
}
private List<EntitySubtype> convertTenantDeviceTypesToDto(UUID tenantId, List<String> types) { private List<EntitySubtype> convertTenantDeviceTypesToDto(UUID tenantId, List<String> types) {
List<EntitySubtype> list = Collections.emptyList(); List<EntitySubtype> list = Collections.emptyList();
if (types != null && !types.isEmpty()) { if (types != null && !types.isEmpty()) {

2
dao/src/main/resources/cassandra/schema.cql

@ -159,8 +159,6 @@ CREATE TABLE IF NOT EXISTS thingsboard.device (
type text, type text,
search_text text, search_text text,
additional_info text, additional_info text,
last_connect bigint,
last_update bigint,
PRIMARY KEY (id, tenant_id, customer_id, type) PRIMARY KEY (id, tenant_id, customer_id, type)
); );

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

@ -118,9 +118,7 @@ CREATE TABLE IF NOT EXISTS device (
type varchar(255), type varchar(255),
name varchar(255), name varchar(255),
search_text varchar(255), search_text varchar(255),
tenant_id varchar(31), tenant_id varchar(31)
last_connect bigint,
last_update bigint
); );
CREATE TABLE IF NOT EXISTS device_credentials ( CREATE TABLE IF NOT EXISTS device_credentials (

2
rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/NodeDefinition.java

@ -31,5 +31,7 @@ public class NodeDefinition {
JsonNode defaultConfiguration; JsonNode defaultConfiguration;
String[] uiResources; String[] uiResources;
String configDirective; String configDirective;
String icon;
String iconUrl;
} }

4
rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleNode.java

@ -49,6 +49,10 @@ public @interface RuleNode {
String configDirective() default ""; String configDirective() default "";
String icon() default "";
String iconUrl() default "";
boolean customRelations() default false; boolean customRelations() default false;
} }

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

@ -72,16 +72,6 @@
<groupId>com.google.guava</groupId> <groupId>com.google.guava</groupId>
<artifactId>guava</artifactId> <artifactId>guava</artifactId>
</dependency> </dependency>
<dependency>
<groupId>org.apache.velocity</groupId>
<artifactId>velocity</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.apache.velocity</groupId>
<artifactId>velocity-tools</artifactId>
<scope>provided</scope>
</dependency>
<dependency> <dependency>
<groupId>org.springframework</groupId> <groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId> <artifactId>spring-web</artifactId>

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

@ -47,7 +47,9 @@ import static org.thingsboard.rule.engine.DonAsynchron.withCallback;
"Message payload can be accessed via <code>msg</code> property. For example <code>'temperature = ' + msg.temperature ;</code>" + "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>", "Message metadata can be accessed via <code>metadata</code> property. For example <code>'name = ' + metadata.customerName;</code>",
uiResources = {"static/rulenode/rulenode-core-config.js"}, uiResources = {"static/rulenode/rulenode-core-config.js"},
configDirective = "tbActionNodeAlarmConfig") configDirective = "tbActionNodeAlarmConfig",
icon = "notifications_active"
)
public class TbAlarmNode implements TbNode { public class TbAlarmNode implements TbNode {

4
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbLogNode.java

@ -34,7 +34,9 @@ import static org.thingsboard.rule.engine.api.TbRelationTypes.SUCCESS;
"Message payload can be accessed via <code>msg</code> property. For example <code>'temperature = ' + msg.temperature ;</code>" + "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>", "Message metadata can be accessed via <code>metadata</code> property. For example <code>'name = ' + metadata.customerName;</code>",
uiResources = {"static/rulenode/rulenode-core-config.js"}, uiResources = {"static/rulenode/rulenode-core-config.js"},
configDirective = "tbActionNodeLogConfig") configDirective = "tbActionNodeLogConfig",
icon = "menu"
)
public class TbLogNode implements TbNode { public class TbLogNode implements TbNode {

5
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/aws/sns/TbSnsNode.java

@ -38,13 +38,14 @@ import static org.thingsboard.rule.engine.DonAsynchron.withCallback;
@Slf4j @Slf4j
@RuleNode( @RuleNode(
type = ComponentType.ACTION, type = ComponentType.EXTERNAL,
name = "aws sns", name = "aws sns",
configClazz = TbSnsNodeConfiguration.class, configClazz = TbSnsNodeConfiguration.class,
nodeDescription = "Publish messages to AWS SNS", nodeDescription = "Publish messages to AWS SNS",
nodeDetails = "Expects messages with any message type. Will publish message to AWS SNS topic.", nodeDetails = "Expects messages with any message type. Will publish message to AWS SNS topic.",
uiResources = {"static/rulenode/rulenode-core-config.js"}, uiResources = {"static/rulenode/rulenode-core-config.js"},
configDirective = "tbActionNodeSnsConfig" configDirective = "tbActionNodeSnsConfig",
iconUrl = "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgd2lkdGg9IjQ4IiBoZWlnaHQ9IjQ4Ij48cGF0aCBkPSJNMTMuMjMgMTAuNTZWMTBjLTEuOTQgMC0zLjk5LjM5LTMuOTkgMi42NyAwIDEuMTYuNjEgMS45NSAxLjYzIDEuOTUuNzYgMCAxLjQzLS40NyAxLjg2LTEuMjIuNTItLjkzLjUtMS44LjUtMi44NG0yLjcgNi41M2MtLjE4LjE2LS40My4xNy0uNjMuMDYtLjg5LS43NC0xLjA1LTEuMDgtMS41NC0xLjc5LTEuNDcgMS41LTIuNTEgMS45NS00LjQyIDEuOTUtMi4yNSAwLTQuMDEtMS4zOS00LjAxLTQuMTcgMC0yLjE4IDEuMTctMy42NCAyLjg2LTQuMzggMS40Ni0uNjQgMy40OS0uNzYgNS4wNC0uOTNWNy41YzAtLjY2LjA1LTEuNDEtLjMzLTEuOTYtLjMyLS40OS0uOTUtLjctMS41LS43LTEuMDIgMC0xLjkzLjUzLTIuMTUgMS42MS0uMDUuMjQtLjI1LjQ4LS40Ny40OWwtMi42LS4yOGMtLjIyLS4wNS0uNDYtLjIyLS40LS41Ni42LTMuMTUgMy40NS00LjEgNi00LjEgMS4zIDAgMyAuMzUgNC4wMyAxLjMzQzE3LjExIDQuNTUgMTcgNi4xOCAxNyA3Ljk1djQuMTdjMCAxLjI1LjUgMS44MSAxIDIuNDguMTcuMjUuMjEuNTQgMCAuNzFsLTIuMDYgMS43OGgtLjAxIj48L3BhdGg+PHBhdGggZD0iTTIwLjE2IDE5LjU0QzE4IDIxLjE0IDE0LjgyIDIyIDEyLjEgMjJjLTMuODEgMC03LjI1LTEuNDEtOS44NS0zLjc2LS4yLS4xOC0uMDItLjQzLjI1LS4yOSAyLjc4IDEuNjMgNi4yNSAyLjYxIDkuODMgMi42MSAyLjQxIDAgNS4wNy0uNSA3LjUxLTEuNTMuMzctLjE2LjY2LjI0LjMyLjUxIj48L3BhdGg+PHBhdGggZD0iTTIxLjA3IDE4LjVjLS4yOC0uMzYtMS44NS0uMTctMi41Ny0uMDgtLjE5LjAyLS4yMi0uMTYtLjAzLS4zIDEuMjQtLjg4IDMuMjktLjYyIDMuNTMtLjMzLjI0LjMtLjA3IDIuMzUtMS4yNCAzLjMyLS4xOC4xNi0uMzUuMDctLjI2LS4xMS4yNi0uNjcuODUtMi4xNC41Ny0yLjV6Ij48L3BhdGg+PC9zdmc+"
) )
public class TbSnsNode implements TbNode { public class TbSnsNode implements TbNode {

5
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/aws/sqs/TbSqsNode.java

@ -41,13 +41,14 @@ import static org.thingsboard.rule.engine.DonAsynchron.withCallback;
@Slf4j @Slf4j
@RuleNode( @RuleNode(
type = ComponentType.ACTION, type = ComponentType.EXTERNAL,
name = "aws sqs", name = "aws sqs",
configClazz = TbSqsNodeConfiguration.class, configClazz = TbSqsNodeConfiguration.class,
nodeDescription = "Publish messages to AWS SQS", nodeDescription = "Publish messages to AWS SQS",
nodeDetails = "Expects messages with any message type. Will publish message to AWS SQS queue.", nodeDetails = "Expects messages with any message type. Will publish message to AWS SQS queue.",
uiResources = {"static/rulenode/rulenode-core-config.js"}, uiResources = {"static/rulenode/rulenode-core-config.js"},
configDirective = "tbActionNodeSqsConfig" configDirective = "tbActionNodeSqsConfig",
iconUrl = "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgd2lkdGg9IjQ4IiBoZWlnaHQ9IjQ4Ij48cGF0aCBkPSJNMTMuMjMgMTAuNTZWMTBjLTEuOTQgMC0zLjk5LjM5LTMuOTkgMi42NyAwIDEuMTYuNjEgMS45NSAxLjYzIDEuOTUuNzYgMCAxLjQzLS40NyAxLjg2LTEuMjIuNTItLjkzLjUtMS44LjUtMi44NG0yLjcgNi41M2MtLjE4LjE2LS40My4xNy0uNjMuMDYtLjg5LS43NC0xLjA1LTEuMDgtMS41NC0xLjc5LTEuNDcgMS41LTIuNTEgMS45NS00LjQyIDEuOTUtMi4yNSAwLTQuMDEtMS4zOS00LjAxLTQuMTcgMC0yLjE4IDEuMTctMy42NCAyLjg2LTQuMzggMS40Ni0uNjQgMy40OS0uNzYgNS4wNC0uOTNWNy41YzAtLjY2LjA1LTEuNDEtLjMzLTEuOTYtLjMyLS40OS0uOTUtLjctMS41LS43LTEuMDIgMC0xLjkzLjUzLTIuMTUgMS42MS0uMDUuMjQtLjI1LjQ4LS40Ny40OWwtMi42LS4yOGMtLjIyLS4wNS0uNDYtLjIyLS40LS41Ni42LTMuMTUgMy40NS00LjEgNi00LjEgMS4zIDAgMyAuMzUgNC4wMyAxLjMzQzE3LjExIDQuNTUgMTcgNi4xOCAxNyA3Ljk1djQuMTdjMCAxLjI1LjUgMS44MSAxIDIuNDguMTcuMjUuMjEuNTQgMCAuNzFsLTIuMDYgMS43OGgtLjAxIj48L3BhdGg+PHBhdGggZD0iTTIwLjE2IDE5LjU0QzE4IDIxLjE0IDE0LjgyIDIyIDEyLjEgMjJjLTMuODEgMC03LjI1LTEuNDEtOS44NS0zLjc2LS4yLS4xOC0uMDItLjQzLjI1LS4yOSAyLjc4IDEuNjMgNi4yNSAyLjYxIDkuODMgMi42MSAyLjQxIDAgNS4wNy0uNSA3LjUxLTEuNTMuMzctLjE2LjY2LjI0LjMyLjUxIj48L3BhdGg+PHBhdGggZD0iTTIxLjA3IDE4LjVjLS4yOC0uMzYtMS44NS0uMTctMi41Ny0uMDgtLjE5LjAyLS4yMi0uMTYtLjAzLS4zIDEuMjQtLjg4IDMuMjktLjYyIDMuNTMtLjMzLjI0LjMtLjA3IDIuMzUtMS4yNCAzLjMyLS4xOC4xNi0uMzUuMDctLjI2LS4xMS4yNi0uNjcuODUtMi4xNC41Ny0yLjV6Ij48L3BhdGg+PC9zdmc+"
) )
public class TbSqsNode implements TbNode { public class TbSqsNode implements TbNode {

3
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/debug/TbMsgGeneratorNode.java

@ -41,7 +41,8 @@ import static org.thingsboard.rule.engine.api.TbRelationTypes.SUCCESS;
nodeDetails = "Generates messages with configurable period. ", nodeDetails = "Generates messages with configurable period. ",
inEnabled = false, inEnabled = false,
uiResources = {"static/rulenode/rulenode-core-config.js", "static/rulenode/rulenode-core-config.css"}, uiResources = {"static/rulenode/rulenode-core-config.js", "static/rulenode/rulenode-core-config.css"},
configDirective = "tbActionNodeGeneratorConfig" configDirective = "tbActionNodeGeneratorConfig",
icon = "repeat"
) )
public class TbMsgGeneratorNode implements TbNode { public class TbMsgGeneratorNode implements TbNode {

6
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbMsgTypeFilterNodeConfiguration.java

@ -17,6 +17,7 @@ package org.thingsboard.rule.engine.filter;
import lombok.Data; import lombok.Data;
import org.thingsboard.rule.engine.api.NodeConfiguration; import org.thingsboard.rule.engine.api.NodeConfiguration;
import org.thingsboard.server.common.msg.session.SessionMsgType;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
@ -33,7 +34,10 @@ public class TbMsgTypeFilterNodeConfiguration implements NodeConfiguration<TbMsg
@Override @Override
public TbMsgTypeFilterNodeConfiguration defaultConfiguration() { public TbMsgTypeFilterNodeConfiguration defaultConfiguration() {
TbMsgTypeFilterNodeConfiguration configuration = new TbMsgTypeFilterNodeConfiguration(); TbMsgTypeFilterNodeConfiguration configuration = new TbMsgTypeFilterNodeConfiguration();
configuration.setMessageTypes(Arrays.asList("POST_ATTRIBUTES","POST_TELEMETRY","RPC_REQUEST")); configuration.setMessageTypes(Arrays.asList(
SessionMsgType.POST_ATTRIBUTES_REQUEST.name(),
SessionMsgType.POST_TELEMETRY_REQUEST.name(),
SessionMsgType.TO_SERVER_RPC_REQUEST.name()));
return configuration; return configuration;
} }
} }

5
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/kafka/TbKafkaNode.java

@ -28,13 +28,14 @@ import java.util.concurrent.ExecutionException;
@Slf4j @Slf4j
@RuleNode( @RuleNode(
type = ComponentType.ACTION, type = ComponentType.EXTERNAL,
name = "kafka", name = "kafka",
configClazz = TbKafkaNodeConfiguration.class, configClazz = TbKafkaNodeConfiguration.class,
nodeDescription = "Publish messages to Kafka server", nodeDescription = "Publish messages to Kafka server",
nodeDetails = "Expects messages with any message type. Will send record via Kafka producer to Kafka server.", nodeDetails = "Expects messages with any message type. Will send record via Kafka producer to Kafka server.",
uiResources = {"static/rulenode/rulenode-core-config.js"}, uiResources = {"static/rulenode/rulenode-core-config.js"},
configDirective = "tbActionNodeKafkaConfig" configDirective = "tbActionNodeKafkaConfig",
iconUrl = "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTUzOCIgaGVpZ2h0PSIyNTAwIiB2aWV3Qm94PSIwIDAgMjU2IDQxNiIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiBwcmVzZXJ2ZUFzcGVjdFJhdGlvPSJ4TWlkWU1pZCI+PHBhdGggZD0iTTIwMS44MTYgMjMwLjIxNmMtMTYuMTg2IDAtMzAuNjk3IDcuMTcxLTQwLjYzNCAxOC40NjFsLTI1LjQ2My0xOC4wMjZjMi43MDMtNy40NDIgNC4yNTUtMTUuNDMzIDQuMjU1LTIzLjc5NyAwLTguMjE5LTEuNDk4LTE2LjA3Ni00LjExMi0yMy40MDhsMjUuNDA2LTE3LjgzNWM5LjkzNiAxMS4yMzMgMjQuNDA5IDE4LjM2NSA0MC41NDggMTguMzY1IDI5Ljg3NSAwIDU0LjE4NC0yNC4zMDUgNTQuMTg0LTU0LjE4NCAwLTI5Ljg3OS0yNC4zMDktNTQuMTg0LTU0LjE4NC01NC4xODQtMjkuODc1IDAtNTQuMTg0IDI0LjMwNS01NC4xODQgNTQuMTg0IDAgNS4zNDguODA4IDEwLjUwNSAyLjI1OCAxNS4zODlsLTI1LjQyMyAxNy44NDRjLTEwLjYyLTEzLjE3NS0yNS45MTEtMjIuMzc0LTQzLjMzMy0yNS4xODJ2LTMwLjY0YzI0LjU0NC01LjE1NSA0My4wMzctMjYuOTYyIDQzLjAzNy01My4wMTlDMTI0LjE3MSAyNC4zMDUgOTkuODYyIDAgNjkuOTg3IDAgNDAuMTEyIDAgMTUuODAzIDI0LjMwNSAxNS44MDMgNTQuMTg0YzAgMjUuNzA4IDE4LjAxNCA0Ny4yNDYgNDIuMDY3IDUyLjc2OXYzMS4wMzhDMjUuMDQ0IDE0My43NTMgMCAxNzIuNDAxIDAgMjA2Ljg1NGMwIDM0LjYyMSAyNS4yOTIgNjMuMzc0IDU4LjM1NSA2OC45NHYzMi43NzRjLTI0LjI5OSA1LjM0MS00Mi41NTIgMjcuMDExLTQyLjU1MiA1Mi44OTQgMCAyOS44NzkgMjQuMzA5IDU0LjE4NCA1NC4xODQgNTQuMTg0IDI5Ljg3NSAwIDU0LjE4NC0yNC4zMDUgNTQuMTg0LTU0LjE4NCAwLTI1Ljg4My0xOC4yNTMtNDcuNTUzLTQyLjU1Mi01Mi44OTR2LTMyLjc3NWE2OS45NjUgNjkuOTY1IDAgMCAwIDQyLjYtMjQuNzc2bDI1LjYzMyAxOC4xNDNjLTEuNDIzIDQuODQtMi4yMiA5Ljk0Ni0yLjIyIDE1LjI0IDAgMjkuODc5IDI0LjMwOSA1NC4xODQgNTQuMTg0IDU0LjE4NCAyOS44NzUgMCA1NC4xODQtMjQuMzA1IDU0LjE4NC01NC4xODQgMC0yOS44NzktMjQuMzA5LTU0LjE4NC01NC4xODQtNTQuMTg0em0wLTEyNi42OTVjMTQuNDg3IDAgMjYuMjcgMTEuNzg4IDI2LjI3IDI2LjI3MXMtMTEuNzgzIDI2LjI3LTI2LjI3IDI2LjI3LTI2LjI3LTExLjc4Ny0yNi4yNy0yNi4yN2MwLTE0LjQ4MyAxMS43ODMtMjYuMjcxIDI2LjI3LTI2LjI3MXptLTE1OC4xLTQ5LjMzN2MwLTE0LjQ4MyAxMS43ODQtMjYuMjcgMjYuMjcxLTI2LjI3czI2LjI3IDExLjc4NyAyNi4yNyAyNi4yN2MwIDE0LjQ4My0xMS43ODMgMjYuMjctMjYuMjcgMjYuMjdzLTI2LjI3MS0xMS43ODctMjYuMjcxLTI2LjI3em01Mi41NDEgMzA3LjI3OGMwIDE0LjQ4My0xMS43ODMgMjYuMjctMjYuMjcgMjYuMjdzLTI2LjI3MS0xMS43ODctMjYuMjcxLTI2LjI3YzAtMTQuNDgzIDExLjc4NC0yNi4yNyAyNi4yNzEtMjYuMjdzMjYuMjcgMTEuNzg3IDI2LjI3IDI2LjI3em0tMjYuMjcyLTExNy45N2MtMjAuMjA1IDAtMzYuNjQyLTE2LjQzNC0zNi42NDItMzYuNjM4IDAtMjAuMjA1IDE2LjQzNy0zNi42NDIgMzYuNjQyLTM2LjY0MiAyMC4yMDQgMCAzNi42NDEgMTYuNDM3IDM2LjY0MSAzNi42NDIgMCAyMC4yMDQtMTYuNDM3IDM2LjYzOC0zNi42NDEgMzYuNjM4em0xMzEuODMxIDY3LjE3OWMtMTQuNDg3IDAtMjYuMjctMTEuNzg4LTI2LjI3LTI2LjI3MXMxMS43ODMtMjYuMjcgMjYuMjctMjYuMjcgMjYuMjcgMTEuNzg3IDI2LjI3IDI2LjI3YzAgMTQuNDgzLTExLjc4MyAyNi4yNzEtMjYuMjcgMjYuMjcxeiIvPjwvc3ZnPg=="
) )
public class TbKafkaNode implements TbNode { public class TbKafkaNode implements TbNode {

68
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mail/RuleVelocityUtils.java

@ -1,68 +0,0 @@
/**
* Copyright © 2016-2018 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.rule.engine.mail;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.velocity.Template;
import org.apache.velocity.VelocityContext;
import org.apache.velocity.runtime.RuntimeServices;
import org.apache.velocity.runtime.RuntimeSingleton;
import org.apache.velocity.runtime.parser.ParseException;
import org.apache.velocity.runtime.parser.node.SimpleNode;
import org.thingsboard.server.common.msg.TbMsg;
import java.io.IOException;
import java.io.StringReader;
import java.io.StringWriter;
import java.util.Map;
import static org.thingsboard.server.common.msg.TbMsgDataType.JSON;
public class RuleVelocityUtils {
public static VelocityContext createContext(TbMsg msg) throws IOException {
VelocityContext context = new VelocityContext();
context.put("originator", msg.getOriginator());
context.put("type", msg.getType());
context.put("metadata", msg.getMetaData().values());
if (msg.getDataType() == JSON) {
Map map = new ObjectMapper().readValue(msg.getData(), Map.class);
context.put("msg", map);
} else {
context.put("msg", msg.getData());
}
return context;
}
public static String merge(Template template, VelocityContext context) {
StringWriter writer = new StringWriter();
template.merge(context, writer);
return writer.toString();
}
public static Template create(String source, String templateName) throws ParseException {
RuntimeServices runtimeServices = RuntimeSingleton.getRuntimeServices();
StringReader reader = new StringReader(source);
SimpleNode node = runtimeServices.parse(reader, templateName);
Template template = new Template();
template.setRuntimeServices(runtimeServices);
template.setData(node);
template.initDocument();
return template;
}
}

48
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mail/TbMsgToEmailNode.java

@ -18,17 +18,14 @@ package org.thingsboard.rule.engine.mail;
import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.velocity.Template;
import org.apache.velocity.VelocityContext;
import org.apache.velocity.runtime.parser.ParseException;
import org.springframework.util.StringUtils; import org.springframework.util.StringUtils;
import org.thingsboard.rule.engine.TbNodeUtils; import org.thingsboard.rule.engine.TbNodeUtils;
import org.thingsboard.rule.engine.api.*; import org.thingsboard.rule.engine.api.*;
import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.common.data.plugin.ComponentType;
import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.common.msg.TbMsgMetaData;
import java.io.IOException; import java.io.IOException;
import java.util.Optional;
import static org.thingsboard.rule.engine.api.TbRelationTypes.SUCCESS; import static org.thingsboard.rule.engine.api.TbRelationTypes.SUCCESS;
import static org.thingsboard.rule.engine.mail.TbSendEmailNode.SEND_EMAIL_TYPE; import static org.thingsboard.rule.engine.mail.TbSendEmailNode.SEND_EMAIL_TYPE;
@ -42,34 +39,18 @@ import static org.thingsboard.rule.engine.mail.TbSendEmailNode.SEND_EMAIL_TYPE;
nodeDetails = "Related Entity found using configured relation direction and Relation Type. " + 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. ", "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"}, uiResources = {"static/rulenode/rulenode-core-config.js"},
configDirective = "tbTransformationNodeToEmailConfig") configDirective = "tbTransformationNodeToEmailConfig",
icon = "email"
)
public class TbMsgToEmailNode implements TbNode { public class TbMsgToEmailNode implements TbNode {
private static final ObjectMapper MAPPER = new ObjectMapper(); private static final ObjectMapper MAPPER = new ObjectMapper();
private TbMsgToEmailNodeConfiguration config; private TbMsgToEmailNodeConfiguration config;
private Optional<Template> fromTemplate;
private Optional<Template> toTemplate;
private Optional<Template> ccTemplate;
private Optional<Template> bccTemplate;
private Optional<Template> subjectTemplate;
private Optional<Template> bodyTemplate;
@Override @Override
public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException { public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
this.config = TbNodeUtils.convert(configuration, TbMsgToEmailNodeConfiguration.class); this.config = TbNodeUtils.convert(configuration, TbMsgToEmailNodeConfiguration.class);
try {
fromTemplate = toTemplate(config.getFromTemplate(), "From Template");
toTemplate = toTemplate(config.getToTemplate(), "To Template");
ccTemplate = toTemplate(config.getCcTemplate(), "Cc Template");
bccTemplate = toTemplate(config.getBccTemplate(), "Bcc Template");
subjectTemplate = toTemplate(config.getSubjectTemplate(), "Subject Template");
bodyTemplate = toTemplate(config.getBodyTemplate(), "Body Template");
} catch (ParseException e) {
log.error("Failed to create templates based on provided configuration!", e);
throw new TbNodeException(e);
}
} }
@Override @Override
@ -91,21 +72,20 @@ public class TbMsgToEmailNode implements TbNode {
private EmailPojo convert(TbMsg msg) throws IOException { private EmailPojo convert(TbMsg msg) throws IOException {
EmailPojo.EmailPojoBuilder builder = EmailPojo.builder(); EmailPojo.EmailPojoBuilder builder = EmailPojo.builder();
VelocityContext context = RuleVelocityUtils.createContext(msg); builder.from(fromTemplate(this.config.getFromTemplate(), msg.getMetaData()));
fromTemplate.ifPresent(t -> builder.from(RuleVelocityUtils.merge(t, context))); builder.to(fromTemplate(this.config.getToTemplate(), msg.getMetaData()));
toTemplate.ifPresent(t -> builder.to(RuleVelocityUtils.merge(t, context))); builder.cc(fromTemplate(this.config.getCcTemplate(), msg.getMetaData()));
ccTemplate.ifPresent(t -> builder.cc(RuleVelocityUtils.merge(t, context))); builder.bcc(fromTemplate(this.config.getBccTemplate(), msg.getMetaData()));
bccTemplate.ifPresent(t -> builder.bcc(RuleVelocityUtils.merge(t, context))); builder.subject(fromTemplate(this.config.getSubjectTemplate(), msg.getMetaData()));
subjectTemplate.ifPresent(t -> builder.subject(RuleVelocityUtils.merge(t, context))); builder.body(fromTemplate(this.config.getBodyTemplate(), msg.getMetaData()));
bodyTemplate.ifPresent(t -> builder.body(RuleVelocityUtils.merge(t, context)));
return builder.build(); return builder.build();
} }
private Optional<Template> toTemplate(String source, String name) throws ParseException { private String fromTemplate(String template, TbMsgMetaData metaData) {
if (!StringUtils.isEmpty(source)) { if (!StringUtils.isEmpty(template)) {
return Optional.of(RuleVelocityUtils.create(source, name)); return TbNodeUtils.processPattern(template, metaData);
} else { } else {
return Optional.empty(); return null;
} }
} }

7
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mail/TbMsgToEmailNodeConfiguration.java

@ -32,10 +32,9 @@ public class TbMsgToEmailNodeConfiguration implements NodeConfiguration {
public TbMsgToEmailNodeConfiguration defaultConfiguration() { public TbMsgToEmailNodeConfiguration defaultConfiguration() {
TbMsgToEmailNodeConfiguration configuration = new TbMsgToEmailNodeConfiguration(); TbMsgToEmailNodeConfiguration configuration = new TbMsgToEmailNodeConfiguration();
configuration.fromTemplate = "info@testmail.org"; configuration.fromTemplate = "info@testmail.org";
configuration.toTemplate = "$metadata.userEmail"; configuration.toTemplate = "${userEmail}";
configuration.subjectTemplate = "Device $deviceType temperature high"; configuration.subjectTemplate = "Device ${deviceType} temperature high";
configuration.bodyTemplate = "Device $metadata.deviceName has high temperature $msg.temp"; configuration.bodyTemplate = "Device ${deviceName} has high temperature ${temp}";
return configuration; return configuration;
} }
} }

71
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mail/TbSendEmailNode.java

@ -15,38 +15,57 @@
*/ */
package org.thingsboard.rule.engine.mail; package org.thingsboard.rule.engine.mail;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.util.concurrent.ListenableFuture;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.springframework.mail.javamail.JavaMailSenderImpl;
import org.springframework.mail.javamail.MimeMessageHelper;
import org.thingsboard.rule.engine.TbNodeUtils; import org.thingsboard.rule.engine.TbNodeUtils;
import org.thingsboard.rule.engine.api.*; import org.thingsboard.rule.engine.api.*;
import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.common.data.plugin.ComponentType;
import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsg;
import javax.mail.internet.MimeMessage;
import java.io.IOException; import java.io.IOException;
import java.util.Properties;
import static org.thingsboard.rule.engine.DonAsynchron.withCallback; import static org.thingsboard.rule.engine.DonAsynchron.withCallback;
import static org.thingsboard.rule.engine.api.TbRelationTypes.SUCCESS; import static org.thingsboard.rule.engine.api.TbRelationTypes.SUCCESS;
@Slf4j @Slf4j
@RuleNode( @RuleNode(
type = ComponentType.ACTION, type = ComponentType.EXTERNAL,
name = "send email", name = "send email",
configClazz = TbSendEmailNodeConfiguration.class, configClazz = TbSendEmailNodeConfiguration.class,
nodeDescription = "Log incoming messages using JS script for transformation Message into String", nodeDescription = "Log incoming messages using JS script for transformation Message into String",
nodeDetails = "Transform incoming Message with configured JS condition to String and log final value. " + nodeDetails = "Transform incoming Message with configured JS condition to String and log final value. " +
"Message payload can be accessed via <code>msg</code> property. For example <code>'temperature = ' + msg.temperature ;</code>" + "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>") "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 = "tbActionNodeSendEmailConfig",
icon = "send"
)
public class TbSendEmailNode implements TbNode { public class TbSendEmailNode implements TbNode {
private static final String MAIL_PROP = "mail.";
static final String SEND_EMAIL_TYPE = "SEND_EMAIL"; static final String SEND_EMAIL_TYPE = "SEND_EMAIL";
private static final ObjectMapper MAPPER = new ObjectMapper(); private static final ObjectMapper MAPPER = new ObjectMapper();
private TbSendEmailNodeConfiguration config; private TbSendEmailNodeConfiguration config;
private JavaMailSenderImpl mailSender;
@Override @Override
public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException { public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
this.config = TbNodeUtils.convert(configuration, TbSendEmailNodeConfiguration.class); try {
this.config = TbNodeUtils.convert(configuration, TbSendEmailNodeConfiguration.class);
if (!this.config.isUseSystemSmtpSettings()) {
mailSender = createMailSender();
}
} catch (Exception e) {
throw new TbNodeException(e);
}
} }
@Override @Override
@ -55,8 +74,7 @@ public class TbSendEmailNode implements TbNode {
validateType(msg.getType()); validateType(msg.getType());
EmailPojo email = getEmail(msg); EmailPojo email = getEmail(msg);
withCallback(ctx.getMailExecutor().executeAsync(() -> { withCallback(ctx.getMailExecutor().executeAsync(() -> {
ctx.getMailService().send(email.getFrom(), email.getTo(), email.getCc(), sendEmail(ctx, email);
email.getBcc(), email.getSubject(), email.getBody());
return null; return null;
}), }),
ok -> ctx.tellNext(msg, SUCCESS), ok -> ctx.tellNext(msg, SUCCESS),
@ -66,6 +84,27 @@ public class TbSendEmailNode implements TbNode {
} }
} }
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 { private EmailPojo getEmail(TbMsg msg) throws IOException {
EmailPojo email = MAPPER.readValue(msg.getData(), EmailPojo.class); EmailPojo email = MAPPER.readValue(msg.getData(), EmailPojo.class);
if (StringUtils.isBlank(email.getTo())) { if (StringUtils.isBlank(email.getTo())) {
@ -84,4 +123,26 @@ public class TbSendEmailNode implements TbNode {
@Override @Override
public void destroy() { 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;
}
} }

16
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mail/TbSendEmailNodeConfiguration.java

@ -21,12 +21,24 @@ import org.thingsboard.rule.engine.api.NodeConfiguration;
@Data @Data
public class TbSendEmailNodeConfiguration implements NodeConfiguration { public class TbSendEmailNodeConfiguration implements NodeConfiguration {
private String tmp; 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 @Override
public TbSendEmailNodeConfiguration defaultConfiguration() { public TbSendEmailNodeConfiguration defaultConfiguration() {
TbSendEmailNodeConfiguration configuration = new TbSendEmailNodeConfiguration(); TbSendEmailNodeConfiguration configuration = new TbSendEmailNodeConfiguration();
configuration.tmp = ""; configuration.setUseSystemSmtpSettings(true);
configuration.setSmtpHost("localhost");
configuration.setSmtpProtocol("smtp");
configuration.setSmtpPort(25);
configuration.setTimeout(10000);
configuration.setEnableTls(false);
return configuration; return configuration;
} }
} }

5
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/TbMqttNode.java

@ -43,13 +43,14 @@ import java.util.concurrent.TimeoutException;
@Slf4j @Slf4j
@RuleNode( @RuleNode(
type = ComponentType.ACTION, type = ComponentType.EXTERNAL,
name = "mqtt", name = "mqtt",
configClazz = TbMqttNodeConfiguration.class, configClazz = TbMqttNodeConfiguration.class,
nodeDescription = "Publish messages to MQTT broker", nodeDescription = "Publish messages to MQTT broker",
nodeDetails = "Expects messages with any message type. Will publish message to MQTT broker.", nodeDetails = "Expects messages with any message type. Will publish message to MQTT broker.",
uiResources = {"static/rulenode/rulenode-core-config.js", "static/rulenode/rulenode-core-config.css"}, uiResources = {"static/rulenode/rulenode-core-config.js", "static/rulenode/rulenode-core-config.css"},
configDirective = "tbActionNodeMqttConfig" configDirective = "tbActionNodeMqttConfig",
icon = "call_split"
) )
public class TbMqttNode implements TbNode { public class TbMqttNode implements TbNode {

5
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rabbitmq/TbRabbitMqNode.java

@ -33,13 +33,14 @@ import static org.thingsboard.rule.engine.DonAsynchron.withCallback;
@Slf4j @Slf4j
@RuleNode( @RuleNode(
type = ComponentType.ACTION, type = ComponentType.EXTERNAL,
name = "rabbitmq", name = "rabbitmq",
configClazz = TbRabbitMqNodeConfiguration.class, configClazz = TbRabbitMqNodeConfiguration.class,
nodeDescription = "Publish messages to RabbitMQ", nodeDescription = "Publish messages to RabbitMQ",
nodeDetails = "Expects messages with any message type. Will publish message to RabbitMQ queue.", nodeDetails = "Expects messages with any message type. Will publish message to RabbitMQ queue.",
uiResources = {"static/rulenode/rulenode-core-config.js"}, uiResources = {"static/rulenode/rulenode-core-config.js"},
configDirective = "tbActionNodeRabbitMqConfig" configDirective = "tbActionNodeRabbitMqConfig",
iconUrl = "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbDpzcGFjZT0icHJlc2VydmUiIHZlcnNpb249IjEuMSIgeT0iMHB4IiB4PSIwcHgiIHZpZXdCb3g9IjAgMCAxMDAwIDEwMDAiPjxwYXRoIHN0cm9rZS13aWR0aD0iLjg0OTU2IiBkPSJtODYwLjQ3IDQxNi4zMmgtMjYyLjAxYy0xMi45MTMgMC0yMy42MTgtMTAuNzA0LTIzLjYxOC0yMy42MTh2LTI3Mi43MWMwLTIwLjMwNS0xNi4yMjctMzYuMjc2LTM2LjI3Ni0zNi4yNzZoLTkzLjc5MmMtMjAuMzA1IDAtMzYuMjc2IDE2LjIyNy0zNi4yNzYgMzYuMjc2djI3MC44NGMtMC4yNTQ4NyAxNC4xMDMtMTEuNDY5IDI1LjU3Mi0yNS43NDIgMjUuNTcybC04NS42MzYgMC42Nzk2NWMtMTQuMTAzIDAtMjUuNTcyLTExLjQ2OS0yNS41NzItMjUuNTcybDAuNjc5NjUtMjcxLjUyYzAtMjAuMzA1LTE2LjIyNy0zNi4yNzYtMzYuMjc2LTM2LjI3NmgtOTMuNTM3Yy0yMC4zMDUgMC0zNi4yNzYgMTYuMjI3LTM2LjI3NiAzNi4yNzZ2NzYzLjg0YzAgMTguMDk2IDE0Ljc4MiAzMi40NTMgMzIuNDUzIDMyLjQ1M2g3MjIuODFjMTguMDk2IDAgMzIuNDUzLTE0Ljc4MiAzMi40NTMtMzIuNDUzdi00MzUuMzFjLTEuMTg5NC0xOC4xODEtMTUuMjkyLTMyLjE5OC0zMy4zODgtMzIuMTk4em0tMTIyLjY4IDI4Ny4wN2MwIDIzLjYxOC0xOC44NiA0Mi40NzgtNDIuNDc4IDQyLjQ3OGgtNzMuOTk3Yy0yMy42MTggMC00Mi40NzgtMTguODYtNDIuNDc4LTQyLjQ3OHYtNzQuMjUyYzAtMjMuNjE4IDE4Ljg2LTQyLjQ3OCA0Mi40NzgtNDIuNDc4aDczLjk5N2MyMy42MTggMCA0Mi40NzggMTguODYgNDIuNDc4IDQyLjQ3OHoiLz48L3N2Zz4="
) )
public class TbRabbitMqNode implements TbNode { public class TbRabbitMqNode implements TbNode {

9
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbRestApiCallNode.java

@ -26,7 +26,8 @@ import org.springframework.http.ResponseEntity;
import org.springframework.http.client.Netty4ClientHttpRequestFactory; import org.springframework.http.client.Netty4ClientHttpRequestFactory;
import org.springframework.util.concurrent.ListenableFuture; import org.springframework.util.concurrent.ListenableFuture;
import org.springframework.util.concurrent.ListenableFutureCallback; import org.springframework.util.concurrent.ListenableFutureCallback;
import org.springframework.web.client.*; import org.springframework.web.client.AsyncRestTemplate;
import org.springframework.web.client.HttpClientErrorException;
import org.thingsboard.rule.engine.TbNodeUtils; import org.thingsboard.rule.engine.TbNodeUtils;
import org.thingsboard.rule.engine.api.*; import org.thingsboard.rule.engine.api.*;
import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.common.data.plugin.ComponentType;
@ -34,19 +35,19 @@ import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.common.msg.TbMsgMetaData;
import javax.net.ssl.SSLException; import javax.net.ssl.SSLException;
import java.util.Map;
import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
@Slf4j @Slf4j
@RuleNode( @RuleNode(
type = ComponentType.ACTION, type = ComponentType.EXTERNAL,
name = "rest api call", name = "rest api call",
configClazz = TbRestApiCallNodeConfiguration.class, configClazz = TbRestApiCallNodeConfiguration.class,
nodeDescription = "Invoke REST API calls to external REST server", nodeDescription = "Invoke REST API calls to external REST server",
nodeDetails = "Expects messages with any message type. Will invoke REST API call to external REST server.", nodeDetails = "Expects messages with any message type. Will invoke REST API call to external REST server.",
uiResources = {"static/rulenode/rulenode-core-config.js"}, uiResources = {"static/rulenode/rulenode-core-config.js"},
configDirective = "tbActionNodeRestApiCallConfig" configDirective = "tbActionNodeRestApiCallConfig",
iconUrl = "data:image/svg+xml;base64,PHN2ZyBzdHlsZT0iZW5hYmxlLWJhY2tncm91bmQ6bmV3IDAgMCA1MTIgNTEyIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbDpzcGFjZT0icHJlc2VydmUiIHZpZXdCb3g9IjAgMCA1MTIgNTEyIiB2ZXJzaW9uPSIxLjEiIHk9IjBweCIgeD0iMHB4Ij48ZyB0cmFuc2Zvcm09Im1hdHJpeCguOTQ5NzUgMCAwIC45NDk3NSAxNy4xMiAyNi40OTIpIj48cGF0aCBkPSJtMTY5LjExIDEwOC41NGMtOS45MDY2IDAuMDczNC0xOS4wMTQgNi41NzI0LTIyLjAxNCAxNi40NjlsLTY5Ljk5MyAyMzEuMDhjLTMuNjkwNCAxMi4xODEgMy4yODkyIDI1LjIyIDE1LjQ2OSAyOC45MSAyLjIyNTkgMC42NzQ4MSA0LjQ5NjkgMSA2LjcyODUgMSA5Ljk3MjEgMCAxOS4xNjUtNi41MTUzIDIyLjE4Mi0xNi40NjdhNi41MjI0IDYuNTIyNCAwIDAgMCAwLjAwMiAtMC4wMDJsNjkuOTktMjMxLjA3YTYuNTIyNCA2LjUyMjQgMCAwIDAgMCAtMC4wMDJjMy42ODU1LTEyLjE4MS0zLjI4Ny0yNS4yMjUtMTUuNDcxLTI4LjkxMi0yLjI4MjUtMC42OTE0NS00LjYxMTYtMS4wMTY5LTYuODk4NC0xem04NC45ODggMGMtOS45MDQ4IDAuMDczNC0xOS4wMTggNi41Njc1LTIyLjAxOCAxNi40NjlsLTY5Ljk4NiAyMzEuMDhjLTMuNjg5OCAxMi4xNzkgMy4yODUzIDI1LjIxNyAxNS40NjUgMjguOTA4IDIuMjI5NyAwLjY3NjQ3IDQuNTAwOCAxLjAwMiA2LjczMjQgMS4wMDIgOS45NzIxIDAgMTkuMTY1LTYuNTE1MyAyMi4xODItMTYuNDY3YTYuNTIyNCA2LjUyMjQgMCAwIDAgMC4wMDIgLTAuMDAybDY5Ljk4OC0yMzEuMDdjMy42OTA4LTEyLjE4MS0zLjI4NTItMjUuMjIzLTE1LjQ2Ny0yOC45MTItMi4yODE0LTAuNjkyMzEtNC42MTA4LTEuMDE4OS02Ljg5ODQtMS4wMDJ6bS0yMTcuMjkgNDIuMjNjLTEyLjcyOS0wLjAwMDg3LTIzLjE4OCAxMC40NTYtMjMuMTg4IDIzLjE4NiAwLjAwMSAxMi43MjggMTAuNDU5IDIzLjE4NiAyMy4xODggMjMuMTg2IDEyLjcyNy0wLjAwMSAyMy4xODMtMTAuNDU5IDIzLjE4NC0yMy4xODYgMC4wMDA4NzYtMTIuNzI4LTEwLjQ1Ni0yMy4xODUtMjMuMTg0LTIzLjE4NnptMCAxNDYuNjRjLTEyLjcyNy0wLjAwMDg3LTIzLjE4NiAxMC40NTUtMjMuMTg4IDIzLjE4NC0wLjAwMDg3MyAxMi43MjkgMTAuNDU4IDIzLjE4OCAyMy4xODggMjMuMTg4IDEyLjcyOC0wLjAwMSAyMy4xODQtMTAuNDYgMjMuMTg0LTIzLjE4OC0wLjAwMS0xMi43MjYtMTAuNDU3LTIzLjE4My0yMy4xODQtMjMuMTg0em0yNzAuNzkgNDIuMjExYy0xMi43MjcgMC0yMy4xODQgMTAuNDU3LTIzLjE4NCAyMy4xODRzMTAuNDU1IDIzLjE4OCAyMy4xODQgMjMuMTg4aDE1NC45OGMxMi43MjkgMCAyMy4xODYtMTAuNDYgMjMuMTg2LTIzLjE4OCAwLjAwMS0xMi43MjgtMTAuNDU4LTIzLjE4NC0yMy4xODYtMjMuMTg0eiIgdHJhbnNmb3JtPSJtYXRyaXgoMS4wMzc2IDAgMCAxLjAzNzYgLTcuNTY3NiAtMTQuOTI1KSIgc3Ryb2tlLXdpZHRoPSIxLjI2OTMiLz48L2c+PC9zdmc+"
) )
public class TbRestApiCallNode implements TbNode { public class TbRestApiCallNode implements TbNode {

3
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rpc/TbSendRPCReplyNode.java

@ -36,7 +36,8 @@ import org.thingsboard.server.common.msg.TbMsg;
nodeDescription = "Sends reply to the RPC call from device", nodeDescription = "Sends reply to the RPC call from device",
nodeDetails = "Expects messages with any message type. Will forward message body to the device.", nodeDetails = "Expects messages with any message type. Will forward message body to the device.",
uiResources = {"static/rulenode/rulenode-core-config.js"}, uiResources = {"static/rulenode/rulenode-core-config.js"},
configDirective = "tbActionNodeRpcReplyConfig" configDirective = "tbActionNodeRpcReplyConfig",
icon = "call_merge"
) )
public class TbSendRPCReplyNode implements TbNode { public class TbSendRPCReplyNode implements TbNode {

3
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rpc/TbSendRPCRequestNode.java

@ -43,7 +43,8 @@ import java.util.concurrent.TimeUnit;
nodeDescription = "Sends one-way RPC call to device", nodeDescription = "Sends one-way RPC call to device",
nodeDetails = "Expects messages with \"method\" and \"params\". Will forward response from device to next nodes.", nodeDetails = "Expects messages with \"method\" and \"params\". Will forward response from device to next nodes.",
uiResources = {"static/rulenode/rulenode-core-config.js"}, uiResources = {"static/rulenode/rulenode-core-config.js"},
configDirective = "tbActionNodeRpcRequestConfig" configDirective = "tbActionNodeRpcRequestConfig",
icon = "call_made"
) )
public class TbSendRPCRequestNode implements TbNode { public class TbSendRPCRequestNode implements TbNode {

3
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgAttributesNode.java

@ -48,7 +48,8 @@ import java.util.Set;
nodeDescription = "Saves attributes data", nodeDescription = "Saves attributes data",
nodeDetails = "Saves entity attributes based on configurable scope parameter. Expects messages with 'POST_ATTRIBUTES_REQUEST' message type", 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"}, uiResources = {"static/rulenode/rulenode-core-config.js", "static/rulenode/rulenode-core-config.css"},
configDirective = "tbActionNodeAttributesConfig" configDirective = "tbActionNodeAttributesConfig",
icon = "file_upload"
) )
public class TbMsgAttributesNode implements TbNode { public class TbMsgAttributesNode implements TbNode {

3
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgTimeseriesNode.java

@ -45,7 +45,8 @@ import java.util.Map;
nodeDescription = "Saves timeseries data", nodeDescription = "Saves timeseries data",
nodeDetails = "Saves timeseries telemetry data based on configurable TTL parameter. Expects messages with 'POST_TELEMETRY_REQUEST' message type", 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"}, uiResources = {"static/rulenode/rulenode-core-config.js", "static/rulenode/rulenode-core-config.css"},
configDirective = "tbActionNodeTimeseriesConfig" configDirective = "tbActionNodeTimeseriesConfig",
icon = "file_upload"
) )
public class TbMsgTimeseriesNode implements TbNode { public class TbMsgTimeseriesNode implements TbNode {

4
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbChangeOriginatorNode.java

@ -40,7 +40,9 @@ import java.util.HashSet;
nodeDetails = "Related Entity found using configured relation direction and Relation Type. " + 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. ", "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"}, uiResources = {"static/rulenode/rulenode-core-config.js", "static/rulenode/rulenode-core-config.css"},
configDirective = "tbTransformationNodeChangeOriginatorConfig") configDirective = "tbTransformationNodeChangeOriginatorConfig",
icon = "find_replace"
)
public class TbChangeOriginatorNode extends TbAbstractTransformNode { public class TbChangeOriginatorNode extends TbAbstractTransformNode {
protected static final String CUSTOMER_SOURCE = "CUSTOMER"; protected static final String CUSTOMER_SOURCE = "CUSTOMER";

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

File diff suppressed because one or more lines are too long

9
rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/mail/TbMsgToEmailNodeTest.java

@ -59,6 +59,9 @@ public class TbMsgToEmailNodeTest {
initWithScript(); initWithScript();
metaData.putValue("username", "oreo"); metaData.putValue("username", "oreo");
metaData.putValue("userEmail", "user@email.io"); 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); TbMsg msg = new TbMsg(UUIDs.timeBased(), "USER", originator, metaData, rawJson, ruleChainId, ruleNodeId, 0L);
emailNode.onMsg(ctx, msg); emailNode.onMsg(ctx, msg);
@ -91,9 +94,9 @@ public class TbMsgToEmailNodeTest {
try { try {
TbMsgToEmailNodeConfiguration config = new TbMsgToEmailNodeConfiguration(); TbMsgToEmailNodeConfiguration config = new TbMsgToEmailNodeConfiguration();
config.setFromTemplate("test@mail.org"); config.setFromTemplate("test@mail.org");
config.setToTemplate("$metadata.userEmail"); config.setToTemplate("${userEmail}");
config.setSubjectTemplate("Hi $metadata.username there"); config.setSubjectTemplate("Hi ${username} there");
config.setBodyTemplate("$msg.name is to high. Current $msg.passed and $msg.complex.count"); config.setBodyTemplate("${name} is to high. Current ${passed} and ${count}");
ObjectMapper mapper = new ObjectMapper(); ObjectMapper mapper = new ObjectMapper();
TbNodeConfiguration nodeConfiguration = new TbNodeConfiguration(mapper.valueToTree(config)); TbNodeConfiguration nodeConfiguration = new TbNodeConfiguration(mapper.valueToTree(config));

8
transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportResource.java

@ -35,7 +35,6 @@ import org.thingsboard.server.common.transport.SessionMsgProcessor;
import org.thingsboard.server.common.transport.adaptor.AdaptorException; import org.thingsboard.server.common.transport.adaptor.AdaptorException;
import org.thingsboard.server.common.transport.auth.DeviceAuthService; import org.thingsboard.server.common.transport.auth.DeviceAuthService;
import org.thingsboard.server.common.transport.quota.QuotaService; import org.thingsboard.server.common.transport.quota.QuotaService;
import org.thingsboard.server.dao.device.DeviceOfflineService;
import org.thingsboard.server.transport.coap.adaptors.CoapTransportAdaptor; import org.thingsboard.server.transport.coap.adaptors.CoapTransportAdaptor;
import org.thingsboard.server.transport.coap.session.CoapExchangeObserverProxy; import org.thingsboard.server.transport.coap.session.CoapExchangeObserverProxy;
import org.thingsboard.server.transport.coap.session.CoapSessionCtx; import org.thingsboard.server.transport.coap.session.CoapSessionCtx;
@ -54,17 +53,15 @@ public class CoapTransportResource extends CoapResource {
private final SessionMsgProcessor processor; private final SessionMsgProcessor processor;
private final DeviceAuthService authService; private final DeviceAuthService authService;
private final QuotaService quotaService; private final QuotaService quotaService;
private final DeviceOfflineService offlineService;
private final Field observerField; private final Field observerField;
private final long timeout; private final long timeout;
public CoapTransportResource(SessionMsgProcessor processor, DeviceAuthService authService, CoapTransportAdaptor adaptor, String name, public CoapTransportResource(SessionMsgProcessor processor, DeviceAuthService authService, CoapTransportAdaptor adaptor, String name,
long timeout, QuotaService quotaService, DeviceOfflineService offlineService) { long timeout, QuotaService quotaService) {
super(name); super(name);
this.processor = processor; this.processor = processor;
this.authService = authService; this.authService = authService;
this.quotaService = quotaService; this.quotaService = quotaService;
this.offlineService = offlineService;
this.adaptor = adaptor; this.adaptor = adaptor;
this.timeout = timeout; this.timeout = timeout;
// This is important to turn off existing observable logic in // This is important to turn off existing observable logic in
@ -171,7 +168,6 @@ public class CoapTransportResource extends CoapResource {
case TO_SERVER_RPC_REQUEST: case TO_SERVER_RPC_REQUEST:
ctx.setSessionType(SessionType.SYNC); ctx.setSessionType(SessionType.SYNC);
msg = adaptor.convertToActorMsg(ctx, type, request); msg = adaptor.convertToActorMsg(ctx, type, request);
offlineService.online(ctx.getDevice(), true);
break; break;
case SUBSCRIBE_ATTRIBUTES_REQUEST: case SUBSCRIBE_ATTRIBUTES_REQUEST:
case SUBSCRIBE_RPC_COMMANDS_REQUEST: case SUBSCRIBE_RPC_COMMANDS_REQUEST:
@ -179,13 +175,11 @@ public class CoapTransportResource extends CoapResource {
advanced.setObserver(new CoapExchangeObserverProxy(systemObserver, ctx)); advanced.setObserver(new CoapExchangeObserverProxy(systemObserver, ctx));
ctx.setSessionType(SessionType.ASYNC); ctx.setSessionType(SessionType.ASYNC);
msg = adaptor.convertToActorMsg(ctx, type, request); msg = adaptor.convertToActorMsg(ctx, type, request);
offlineService.online(ctx.getDevice(), false);
break; break;
case UNSUBSCRIBE_ATTRIBUTES_REQUEST: case UNSUBSCRIBE_ATTRIBUTES_REQUEST:
case UNSUBSCRIBE_RPC_COMMANDS_REQUEST: case UNSUBSCRIBE_RPC_COMMANDS_REQUEST:
ctx.setSessionType(SessionType.ASYNC); ctx.setSessionType(SessionType.ASYNC);
msg = adaptor.convertToActorMsg(ctx, type, request); msg = adaptor.convertToActorMsg(ctx, type, request);
offlineService.online(ctx.getDevice(), false);
break; break;
default: default:
log.trace("[{}] Unsupported msg type: {}", ctx.getSessionId(), type); log.trace("[{}] Unsupported msg type: {}", ctx.getSessionId(), type);

6
transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportService.java

@ -27,7 +27,6 @@ import org.springframework.stereotype.Service;
import org.thingsboard.server.common.transport.SessionMsgProcessor; import org.thingsboard.server.common.transport.SessionMsgProcessor;
import org.thingsboard.server.common.transport.auth.DeviceAuthService; import org.thingsboard.server.common.transport.auth.DeviceAuthService;
import org.thingsboard.server.common.transport.quota.QuotaService; import org.thingsboard.server.common.transport.quota.QuotaService;
import org.thingsboard.server.dao.device.DeviceOfflineService;
import org.thingsboard.server.transport.coap.adaptors.CoapTransportAdaptor; import org.thingsboard.server.transport.coap.adaptors.CoapTransportAdaptor;
import javax.annotation.PostConstruct; import javax.annotation.PostConstruct;
@ -58,9 +57,6 @@ public class CoapTransportService {
@Autowired(required = false) @Autowired(required = false)
private QuotaService quotaService; private QuotaService quotaService;
@Autowired(required = false)
private DeviceOfflineService offlineService;
@Value("${coap.bind_address}") @Value("${coap.bind_address}")
private String host; private String host;
@ -90,7 +86,7 @@ public class CoapTransportService {
private void createResources() { private void createResources() {
CoapResource api = new CoapResource(API); CoapResource api = new CoapResource(API);
api.add(new CoapTransportResource(processor, authService, adaptor, V1, timeout, quotaService, offlineService)); api.add(new CoapTransportResource(processor, authService, adaptor, V1, timeout, quotaService));
server.add(api); server.add(api);
} }

28
transport/coap/src/test/java/org/thingsboard/server/transport/coap/CoapServerTest.java

@ -15,7 +15,6 @@
*/ */
package org.thingsboard.server.transport.coap; package org.thingsboard.server.transport.coap;
import com.google.common.util.concurrent.ListenableFuture;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.eclipse.californium.core.CoapClient; import org.eclipse.californium.core.CoapClient;
import org.eclipse.californium.core.CoapResponse; import org.eclipse.californium.core.CoapResponse;
@ -32,7 +31,6 @@ import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.annotation.DirtiesContext.ClassMode; import org.springframework.test.annotation.DirtiesContext.ClassMode;
import org.springframework.test.context.junit4.SpringRunner; import org.springframework.test.context.junit4.SpringRunner;
import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.device.DeviceStatusQuery;
import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
@ -53,7 +51,6 @@ import org.thingsboard.server.common.transport.SessionMsgProcessor;
import org.thingsboard.server.common.transport.auth.DeviceAuthResult; import org.thingsboard.server.common.transport.auth.DeviceAuthResult;
import org.thingsboard.server.common.transport.auth.DeviceAuthService; import org.thingsboard.server.common.transport.auth.DeviceAuthService;
import org.thingsboard.server.common.transport.quota.QuotaService; import org.thingsboard.server.common.transport.quota.QuotaService;
import org.thingsboard.server.dao.device.DeviceOfflineService;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@ -140,31 +137,6 @@ public class CoapServerTest {
public static QuotaService quotaService() { public static QuotaService quotaService() {
return key -> false; return key -> false;
} }
@Bean
public static DeviceOfflineService offlineService() {
return new DeviceOfflineService() {
@Override
public void online(Device device, boolean isUpdate) {
}
@Override
public void offline(Device device) {
}
@Override
public ListenableFuture<List<Device>> findOfflineDevices(UUID tenantId, DeviceStatusQuery.ContactType contactType, long offlineThreshold) {
return null;
}
@Override
public ListenableFuture<List<Device>> findOnlineDevices(UUID tenantId, DeviceStatusQuery.ContactType contactType, long offlineThreshold) {
return null;
}
};
}
} }
@Autowired @Autowired

20
transport/http/src/main/java/org/thingsboard/server/transport/http/DeviceApiController.java

@ -26,7 +26,6 @@ import org.springframework.http.ResponseEntity;
import org.springframework.util.StringUtils; import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import org.springframework.web.context.request.async.DeferredResult; import org.springframework.web.context.request.async.DeferredResult;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.security.DeviceTokenCredentials; import org.thingsboard.server.common.data.security.DeviceTokenCredentials;
import org.thingsboard.server.common.msg.core.*; import org.thingsboard.server.common.msg.core.*;
import org.thingsboard.server.common.msg.session.AdaptorToSessionActorMsg; import org.thingsboard.server.common.msg.session.AdaptorToSessionActorMsg;
@ -37,7 +36,6 @@ import org.thingsboard.server.common.transport.SessionMsgProcessor;
import org.thingsboard.server.common.transport.adaptor.JsonConverter; import org.thingsboard.server.common.transport.adaptor.JsonConverter;
import org.thingsboard.server.common.transport.auth.DeviceAuthService; import org.thingsboard.server.common.transport.auth.DeviceAuthService;
import org.thingsboard.server.common.transport.quota.QuotaService; import org.thingsboard.server.common.transport.quota.QuotaService;
import org.thingsboard.server.dao.device.DeviceOfflineService;
import org.thingsboard.server.transport.http.session.HttpSessionCtx; import org.thingsboard.server.transport.http.session.HttpSessionCtx;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
@ -65,9 +63,6 @@ public class DeviceApiController {
@Autowired(required = false) @Autowired(required = false)
private QuotaService quotaService; private QuotaService quotaService;
@Autowired(required = false)
private DeviceOfflineService offlineService;
@RequestMapping(value = "/{deviceToken}/attributes", method = RequestMethod.GET, produces = "application/json") @RequestMapping(value = "/{deviceToken}/attributes", method = RequestMethod.GET, produces = "application/json")
public DeferredResult<ResponseEntity> getDeviceAttributes(@PathVariable("deviceToken") String deviceToken, public DeferredResult<ResponseEntity> getDeviceAttributes(@PathVariable("deviceToken") String deviceToken,
@RequestParam(value = "clientKeys", required = false, defaultValue = "") String clientKeys, @RequestParam(value = "clientKeys", required = false, defaultValue = "") String clientKeys,
@ -87,7 +82,7 @@ public class DeviceApiController {
Set<String> sharedKeySet = !StringUtils.isEmpty(sharedKeys) ? new HashSet<>(Arrays.asList(sharedKeys.split(","))) : null; Set<String> sharedKeySet = !StringUtils.isEmpty(sharedKeys) ? new HashSet<>(Arrays.asList(sharedKeys.split(","))) : null;
request = new BasicGetAttributesRequest(0, clientKeySet, sharedKeySet); request = new BasicGetAttributesRequest(0, clientKeySet, sharedKeySet);
} }
process(ctx, request, false); process(ctx, request);
} else { } else {
responseWriter.setResult(new ResponseEntity<>(HttpStatus.UNAUTHORIZED)); responseWriter.setResult(new ResponseEntity<>(HttpStatus.UNAUTHORIZED));
} }
@ -105,7 +100,7 @@ public class DeviceApiController {
HttpSessionCtx ctx = getHttpSessionCtx(responseWriter); HttpSessionCtx ctx = getHttpSessionCtx(responseWriter);
if (ctx.login(new DeviceTokenCredentials(deviceToken))) { if (ctx.login(new DeviceTokenCredentials(deviceToken))) {
try { try {
process(ctx, JsonConverter.convertToAttributes(new JsonParser().parse(json)), true); process(ctx, JsonConverter.convertToAttributes(new JsonParser().parse(json)));
} catch (IllegalStateException | JsonSyntaxException ex) { } catch (IllegalStateException | JsonSyntaxException ex) {
responseWriter.setResult(new ResponseEntity<>(HttpStatus.BAD_REQUEST)); responseWriter.setResult(new ResponseEntity<>(HttpStatus.BAD_REQUEST));
} }
@ -125,7 +120,7 @@ public class DeviceApiController {
HttpSessionCtx ctx = getHttpSessionCtx(responseWriter); HttpSessionCtx ctx = getHttpSessionCtx(responseWriter);
if (ctx.login(new DeviceTokenCredentials(deviceToken))) { if (ctx.login(new DeviceTokenCredentials(deviceToken))) {
try { try {
process(ctx, JsonConverter.convertToTelemetry(new JsonParser().parse(json)), true); process(ctx, JsonConverter.convertToTelemetry(new JsonParser().parse(json)));
} catch (IllegalStateException | JsonSyntaxException ex) { } catch (IllegalStateException | JsonSyntaxException ex) {
responseWriter.setResult(new ResponseEntity<>(HttpStatus.BAD_REQUEST)); responseWriter.setResult(new ResponseEntity<>(HttpStatus.BAD_REQUEST));
} }
@ -155,7 +150,7 @@ public class DeviceApiController {
if (ctx.login(new DeviceTokenCredentials(deviceToken))) { if (ctx.login(new DeviceTokenCredentials(deviceToken))) {
try { try {
JsonObject response = new JsonParser().parse(json).getAsJsonObject(); JsonObject response = new JsonParser().parse(json).getAsJsonObject();
process(ctx, new ToDeviceRpcResponseMsg(requestId, response.toString()), true); process(ctx, new ToDeviceRpcResponseMsg(requestId, response.toString()));
} catch (IllegalStateException | JsonSyntaxException ex) { } catch (IllegalStateException | JsonSyntaxException ex) {
responseWriter.setResult(new ResponseEntity<>(HttpStatus.BAD_REQUEST)); responseWriter.setResult(new ResponseEntity<>(HttpStatus.BAD_REQUEST));
} }
@ -178,7 +173,7 @@ public class DeviceApiController {
JsonObject request = new JsonParser().parse(json).getAsJsonObject(); JsonObject request = new JsonParser().parse(json).getAsJsonObject();
process(ctx, new ToServerRpcRequestMsg(0, process(ctx, new ToServerRpcRequestMsg(0,
request.get("method").getAsString(), request.get("method").getAsString(),
request.get("params").toString()), true); request.get("params").toString()));
} catch (IllegalStateException | JsonSyntaxException ex) { } catch (IllegalStateException | JsonSyntaxException ex) {
responseWriter.setResult(new ResponseEntity<>(HttpStatus.BAD_REQUEST)); responseWriter.setResult(new ResponseEntity<>(HttpStatus.BAD_REQUEST));
} }
@ -204,7 +199,7 @@ public class DeviceApiController {
HttpSessionCtx ctx = getHttpSessionCtx(responseWriter, timeout); HttpSessionCtx ctx = getHttpSessionCtx(responseWriter, timeout);
if (ctx.login(new DeviceTokenCredentials(deviceToken))) { if (ctx.login(new DeviceTokenCredentials(deviceToken))) {
try { try {
process(ctx, msg, false); process(ctx, msg);
} catch (IllegalStateException | JsonSyntaxException ex) { } catch (IllegalStateException | JsonSyntaxException ex) {
responseWriter.setResult(new ResponseEntity<>(HttpStatus.BAD_REQUEST)); responseWriter.setResult(new ResponseEntity<>(HttpStatus.BAD_REQUEST));
} }
@ -222,10 +217,9 @@ public class DeviceApiController {
return new HttpSessionCtx(processor, authService, responseWriter, timeout != 0 ? timeout : defaultTimeout); return new HttpSessionCtx(processor, authService, responseWriter, timeout != 0 ? timeout : defaultTimeout);
} }
private void process(HttpSessionCtx ctx, FromDeviceMsg request, boolean isUpdate) { private void process(HttpSessionCtx ctx, FromDeviceMsg request) {
AdaptorToSessionActorMsg msg = new BasicAdaptorToSessionActorMsg(ctx, request); AdaptorToSessionActorMsg msg = new BasicAdaptorToSessionActorMsg(ctx, request);
processor.process(new BasicToDeviceActorSessionMsg(ctx.getDevice(), msg)); processor.process(new BasicToDeviceActorSessionMsg(ctx.getDevice(), msg));
offlineService.online(ctx.getDevice(), isUpdate);
} }
private boolean quotaExceeded(HttpServletRequest request, DeferredResult<ResponseEntity> responseWriter) { private boolean quotaExceeded(HttpServletRequest request, DeferredResult<ResponseEntity> responseWriter) {

25
transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java

@ -37,7 +37,6 @@ import org.thingsboard.server.common.transport.adaptor.AdaptorException;
import org.thingsboard.server.common.transport.auth.DeviceAuthService; import org.thingsboard.server.common.transport.auth.DeviceAuthService;
import org.thingsboard.server.common.transport.quota.QuotaService; import org.thingsboard.server.common.transport.quota.QuotaService;
import org.thingsboard.server.dao.EncryptionUtil; import org.thingsboard.server.dao.EncryptionUtil;
import org.thingsboard.server.dao.device.DeviceOfflineService;
import org.thingsboard.server.dao.device.DeviceService; import org.thingsboard.server.dao.device.DeviceService;
import org.thingsboard.server.dao.relation.RelationService; import org.thingsboard.server.dao.relation.RelationService;
import org.thingsboard.server.transport.mqtt.adaptors.MqttTransportAdaptor; import org.thingsboard.server.transport.mqtt.adaptors.MqttTransportAdaptor;
@ -73,14 +72,13 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement
private final DeviceAuthService authService; private final DeviceAuthService authService;
private final RelationService relationService; private final RelationService relationService;
private final QuotaService quotaService; private final QuotaService quotaService;
private final DeviceOfflineService offlineService;
private final SslHandler sslHandler; private final SslHandler sslHandler;
private volatile boolean connected; private volatile boolean connected;
private volatile InetSocketAddress address; private volatile InetSocketAddress address;
private volatile GatewaySessionCtx gatewaySessionCtx; private volatile GatewaySessionCtx gatewaySessionCtx;
public MqttTransportHandler(SessionMsgProcessor processor, DeviceService deviceService, DeviceAuthService authService, RelationService relationService, public MqttTransportHandler(SessionMsgProcessor processor, DeviceService deviceService, DeviceAuthService authService, RelationService relationService,
MqttTransportAdaptor adaptor, SslHandler sslHandler, QuotaService quotaService, DeviceOfflineService offlineService) { MqttTransportAdaptor adaptor, SslHandler sslHandler, QuotaService quotaService) {
this.processor = processor; this.processor = processor;
this.deviceService = deviceService; this.deviceService = deviceService;
this.relationService = relationService; this.relationService = relationService;
@ -90,7 +88,6 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement
this.sessionId = deviceSessionCtx.getSessionId().toUidStr(); this.sessionId = deviceSessionCtx.getSessionId().toUidStr();
this.sslHandler = sslHandler; this.sslHandler = sslHandler;
this.quotaService = quotaService; this.quotaService = quotaService;
this.offlineService = offlineService;
} }
@Override @Override
@ -132,13 +129,11 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement
case PINGREQ: case PINGREQ:
if (checkConnected(ctx)) { if (checkConnected(ctx)) {
ctx.writeAndFlush(new MqttMessage(new MqttFixedHeader(PINGRESP, false, AT_MOST_ONCE, false, 0))); ctx.writeAndFlush(new MqttMessage(new MqttFixedHeader(PINGRESP, false, AT_MOST_ONCE, false, 0)));
offlineService.online(deviceSessionCtx.getDevice(), false);
} }
break; break;
case DISCONNECT: case DISCONNECT:
if (checkConnected(ctx)) { if (checkConnected(ctx)) {
processDisconnect(ctx); processDisconnect(ctx);
offlineService.offline(deviceSessionCtx.getDevice());
} }
break; break;
default: default:
@ -190,28 +185,23 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement
try { try {
if (topicName.equals(DEVICE_TELEMETRY_TOPIC)) { if (topicName.equals(DEVICE_TELEMETRY_TOPIC)) {
msg = adaptor.convertToActorMsg(deviceSessionCtx, POST_TELEMETRY_REQUEST, mqttMsg); msg = adaptor.convertToActorMsg(deviceSessionCtx, POST_TELEMETRY_REQUEST, mqttMsg);
offlineService.online(deviceSessionCtx.getDevice(), true);
} else if (topicName.equals(DEVICE_ATTRIBUTES_TOPIC)) { } else if (topicName.equals(DEVICE_ATTRIBUTES_TOPIC)) {
msg = adaptor.convertToActorMsg(deviceSessionCtx, POST_ATTRIBUTES_REQUEST, mqttMsg); msg = adaptor.convertToActorMsg(deviceSessionCtx, POST_ATTRIBUTES_REQUEST, mqttMsg);
offlineService.online(deviceSessionCtx.getDevice(), true);
} else if (topicName.startsWith(DEVICE_ATTRIBUTES_REQUEST_TOPIC_PREFIX)) { } else if (topicName.startsWith(DEVICE_ATTRIBUTES_REQUEST_TOPIC_PREFIX)) {
msg = adaptor.convertToActorMsg(deviceSessionCtx, GET_ATTRIBUTES_REQUEST, mqttMsg); msg = adaptor.convertToActorMsg(deviceSessionCtx, GET_ATTRIBUTES_REQUEST, mqttMsg);
if (msgId >= 0) { if (msgId >= 0) {
ctx.writeAndFlush(createMqttPubAckMsg(msgId)); ctx.writeAndFlush(createMqttPubAckMsg(msgId));
} }
offlineService.online(deviceSessionCtx.getDevice(), false);
} else if (topicName.startsWith(DEVICE_RPC_RESPONSE_TOPIC)) { } else if (topicName.startsWith(DEVICE_RPC_RESPONSE_TOPIC)) {
msg = adaptor.convertToActorMsg(deviceSessionCtx, TO_DEVICE_RPC_RESPONSE, mqttMsg); msg = adaptor.convertToActorMsg(deviceSessionCtx, TO_DEVICE_RPC_RESPONSE, mqttMsg);
if (msgId >= 0) { if (msgId >= 0) {
ctx.writeAndFlush(createMqttPubAckMsg(msgId)); ctx.writeAndFlush(createMqttPubAckMsg(msgId));
} }
offlineService.online(deviceSessionCtx.getDevice(), true);
} else if (topicName.startsWith(DEVICE_RPC_REQUESTS_TOPIC)) { } else if (topicName.startsWith(DEVICE_RPC_REQUESTS_TOPIC)) {
msg = adaptor.convertToActorMsg(deviceSessionCtx, TO_SERVER_RPC_REQUEST, mqttMsg); msg = adaptor.convertToActorMsg(deviceSessionCtx, TO_SERVER_RPC_REQUEST, mqttMsg);
if (msgId >= 0) { if (msgId >= 0) {
ctx.writeAndFlush(createMqttPubAckMsg(msgId)); ctx.writeAndFlush(createMqttPubAckMsg(msgId));
} }
offlineService.online(deviceSessionCtx.getDevice(), true);
} }
} catch (AdaptorException e) { } catch (AdaptorException e) {
log.warn("[{}] Failed to process publish msg [{}][{}]", sessionId, topicName, msgId, e); log.warn("[{}] Failed to process publish msg [{}][{}]", sessionId, topicName, msgId, e);
@ -260,7 +250,6 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement
} }
} }
ctx.writeAndFlush(createSubAckMessage(mqttMsg.variableHeader().messageId(), grantedQoSList)); ctx.writeAndFlush(createSubAckMessage(mqttMsg.variableHeader().messageId(), grantedQoSList));
offlineService.online(deviceSessionCtx.getDevice(), false);
} }
private void processUnsubscribe(ChannelHandlerContext ctx, MqttUnsubscribeMessage mqttMsg) { private void processUnsubscribe(ChannelHandlerContext ctx, MqttUnsubscribeMessage mqttMsg) {
@ -284,7 +273,6 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement
} }
} }
ctx.writeAndFlush(createUnSubAckMessage(mqttMsg.variableHeader().messageId())); ctx.writeAndFlush(createUnSubAckMessage(mqttMsg.variableHeader().messageId()));
offlineService.online(deviceSessionCtx.getDevice(), false);
} }
private MqttMessage createUnSubAckMessage(int msgId) { private MqttMessage createUnSubAckMessage(int msgId) {
@ -316,7 +304,6 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement
ctx.writeAndFlush(createMqttConnAckMsg(CONNECTION_ACCEPTED)); ctx.writeAndFlush(createMqttConnAckMsg(CONNECTION_ACCEPTED));
connected = true; connected = true;
checkGatewaySession(); checkGatewaySession();
offlineService.online(deviceSessionCtx.getDevice(), false);
} }
} }
@ -328,7 +315,6 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement
ctx.writeAndFlush(createMqttConnAckMsg(CONNECTION_ACCEPTED)); ctx.writeAndFlush(createMqttConnAckMsg(CONNECTION_ACCEPTED));
connected = true; connected = true;
checkGatewaySession(); checkGatewaySession();
offlineService.online(deviceSessionCtx.getDevice(), false);
} else { } else {
ctx.writeAndFlush(createMqttConnAckMsg(CONNECTION_REFUSED_NOT_AUTHORIZED)); ctx.writeAndFlush(createMqttConnAckMsg(CONNECTION_REFUSED_NOT_AUTHORIZED));
ctx.close(); ctx.close();
@ -379,9 +365,6 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
log.error("[{}] Unexpected Exception", sessionId, cause); log.error("[{}] Unexpected Exception", sessionId, cause);
ctx.close(); ctx.close();
if(deviceSessionCtx.getDevice() != null) {
offlineService.offline(deviceSessionCtx.getDevice());
}
} }
private static MqttSubAckMessage createSubAckMessage(Integer msgId, List<Integer> grantedQoSList) { private static MqttSubAckMessage createSubAckMessage(Integer msgId, List<Integer> grantedQoSList) {
@ -420,8 +403,7 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement
if (infoNode != null) { if (infoNode != null) {
JsonNode gatewayNode = infoNode.get("gateway"); JsonNode gatewayNode = infoNode.get("gateway");
if (gatewayNode != null && gatewayNode.asBoolean()) { if (gatewayNode != null && gatewayNode.asBoolean()) {
gatewaySessionCtx = new GatewaySessionCtx(processor, deviceService, authService, gatewaySessionCtx = new GatewaySessionCtx(processor, deviceService, authService, relationService, deviceSessionCtx);
relationService, deviceSessionCtx, offlineService);
} }
} }
} }
@ -429,8 +411,5 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement
@Override @Override
public void operationComplete(Future<? super Void> future) throws Exception { public void operationComplete(Future<? super Void> future) throws Exception {
processor.process(SessionCloseMsg.onError(deviceSessionCtx.getSessionId())); processor.process(SessionCloseMsg.onError(deviceSessionCtx.getSessionId()));
if(deviceSessionCtx.getDevice() != null) {
offlineService.offline(deviceSessionCtx.getDevice());
}
} }
} }

7
transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportServerInitializer.java

@ -24,7 +24,6 @@ import io.netty.handler.ssl.SslHandler;
import org.thingsboard.server.common.transport.SessionMsgProcessor; import org.thingsboard.server.common.transport.SessionMsgProcessor;
import org.thingsboard.server.common.transport.auth.DeviceAuthService; import org.thingsboard.server.common.transport.auth.DeviceAuthService;
import org.thingsboard.server.common.transport.quota.QuotaService; import org.thingsboard.server.common.transport.quota.QuotaService;
import org.thingsboard.server.dao.device.DeviceOfflineService;
import org.thingsboard.server.dao.device.DeviceService; import org.thingsboard.server.dao.device.DeviceService;
import org.thingsboard.server.dao.relation.RelationService; import org.thingsboard.server.dao.relation.RelationService;
import org.thingsboard.server.transport.mqtt.adaptors.MqttTransportAdaptor; import org.thingsboard.server.transport.mqtt.adaptors.MqttTransportAdaptor;
@ -43,11 +42,10 @@ public class MqttTransportServerInitializer extends ChannelInitializer<SocketCha
private final MqttTransportAdaptor adaptor; private final MqttTransportAdaptor adaptor;
private final MqttSslHandlerProvider sslHandlerProvider; private final MqttSslHandlerProvider sslHandlerProvider;
private final QuotaService quotaService; private final QuotaService quotaService;
private final DeviceOfflineService offlineService;
public MqttTransportServerInitializer(SessionMsgProcessor processor, DeviceService deviceService, DeviceAuthService authService, RelationService relationService, public MqttTransportServerInitializer(SessionMsgProcessor processor, DeviceService deviceService, DeviceAuthService authService, RelationService relationService,
MqttTransportAdaptor adaptor, MqttSslHandlerProvider sslHandlerProvider, MqttTransportAdaptor adaptor, MqttSslHandlerProvider sslHandlerProvider,
QuotaService quotaService, DeviceOfflineService offlineService) { QuotaService quotaService) {
this.processor = processor; this.processor = processor;
this.deviceService = deviceService; this.deviceService = deviceService;
this.authService = authService; this.authService = authService;
@ -55,7 +53,6 @@ public class MqttTransportServerInitializer extends ChannelInitializer<SocketCha
this.adaptor = adaptor; this.adaptor = adaptor;
this.sslHandlerProvider = sslHandlerProvider; this.sslHandlerProvider = sslHandlerProvider;
this.quotaService = quotaService; this.quotaService = quotaService;
this.offlineService = offlineService;
} }
@Override @Override
@ -70,7 +67,7 @@ public class MqttTransportServerInitializer extends ChannelInitializer<SocketCha
pipeline.addLast("encoder", MqttEncoder.INSTANCE); pipeline.addLast("encoder", MqttEncoder.INSTANCE);
MqttTransportHandler handler = new MqttTransportHandler(processor, deviceService, authService, relationService, MqttTransportHandler handler = new MqttTransportHandler(processor, deviceService, authService, relationService,
adaptor, sslHandler, quotaService, offlineService); adaptor, sslHandler, quotaService);
pipeline.addLast(handler); pipeline.addLast(handler);
ch.closeFuture().addListener(handler); ch.closeFuture().addListener(handler);

6
transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportService.java

@ -30,7 +30,6 @@ import org.springframework.stereotype.Service;
import org.thingsboard.server.common.transport.SessionMsgProcessor; import org.thingsboard.server.common.transport.SessionMsgProcessor;
import org.thingsboard.server.common.transport.auth.DeviceAuthService; import org.thingsboard.server.common.transport.auth.DeviceAuthService;
import org.thingsboard.server.common.transport.quota.QuotaService; import org.thingsboard.server.common.transport.quota.QuotaService;
import org.thingsboard.server.dao.device.DeviceOfflineService;
import org.thingsboard.server.dao.device.DeviceService; import org.thingsboard.server.dao.device.DeviceService;
import org.thingsboard.server.dao.relation.RelationService; import org.thingsboard.server.dao.relation.RelationService;
import org.thingsboard.server.transport.mqtt.adaptors.MqttTransportAdaptor; import org.thingsboard.server.transport.mqtt.adaptors.MqttTransportAdaptor;
@ -70,9 +69,6 @@ public class MqttTransportService {
@Autowired(required = false) @Autowired(required = false)
private QuotaService quotaService; private QuotaService quotaService;
@Autowired(required = false)
private DeviceOfflineService offlineService;
@Value("${mqtt.bind_address}") @Value("${mqtt.bind_address}")
private String host; private String host;
@Value("${mqtt.bind_port}") @Value("${mqtt.bind_port}")
@ -110,7 +106,7 @@ public class MqttTransportService {
b.group(bossGroup, workerGroup) b.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class) .channel(NioServerSocketChannel.class)
.childHandler(new MqttTransportServerInitializer(processor, deviceService, authService, relationService, .childHandler(new MqttTransportServerInitializer(processor, deviceService, authService, relationService,
adaptor, sslHandlerProvider, quotaService, offlineService)); adaptor, sslHandlerProvider, quotaService));
serverChannel = b.bind(host, port).sync().channel(); serverChannel = b.bind(host, port).sync().channel();
log.info("Mqtt transport started!"); log.info("Mqtt transport started!");

13
transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/GatewaySessionCtx.java

@ -36,7 +36,6 @@ import org.thingsboard.server.common.transport.SessionMsgProcessor;
import org.thingsboard.server.common.transport.adaptor.AdaptorException; import org.thingsboard.server.common.transport.adaptor.AdaptorException;
import org.thingsboard.server.common.transport.adaptor.JsonConverter; import org.thingsboard.server.common.transport.adaptor.JsonConverter;
import org.thingsboard.server.common.transport.auth.DeviceAuthService; import org.thingsboard.server.common.transport.auth.DeviceAuthService;
import org.thingsboard.server.dao.device.DeviceOfflineService;
import org.thingsboard.server.dao.device.DeviceService; import org.thingsboard.server.dao.device.DeviceService;
import org.thingsboard.server.dao.relation.RelationService; import org.thingsboard.server.dao.relation.RelationService;
import org.thingsboard.server.transport.mqtt.MqttTransportHandler; import org.thingsboard.server.transport.mqtt.MqttTransportHandler;
@ -62,17 +61,14 @@ public class GatewaySessionCtx {
private final DeviceService deviceService; private final DeviceService deviceService;
private final DeviceAuthService authService; private final DeviceAuthService authService;
private final RelationService relationService; private final RelationService relationService;
private final DeviceOfflineService offlineService;
private final Map<String, GatewayDeviceSessionCtx> devices; private final Map<String, GatewayDeviceSessionCtx> devices;
private ChannelHandlerContext channel; private ChannelHandlerContext channel;
public GatewaySessionCtx(SessionMsgProcessor processor, DeviceService deviceService, DeviceAuthService authService, public GatewaySessionCtx(SessionMsgProcessor processor, DeviceService deviceService, DeviceAuthService authService, RelationService relationService, DeviceSessionCtx gatewaySessionCtx) {
RelationService relationService, DeviceSessionCtx gatewaySessionCtx, DeviceOfflineService offlineService) {
this.processor = processor; this.processor = processor;
this.deviceService = deviceService; this.deviceService = deviceService;
this.authService = authService; this.authService = authService;
this.relationService = relationService; this.relationService = relationService;
this.offlineService = offlineService;
this.gateway = gatewaySessionCtx.getDevice(); this.gateway = gatewaySessionCtx.getDevice();
this.gatewaySessionId = gatewaySessionCtx.getSessionId(); this.gatewaySessionId = gatewaySessionCtx.getSessionId();
this.devices = new HashMap<>(); this.devices = new HashMap<>();
@ -102,7 +98,6 @@ public class GatewaySessionCtx {
log.debug("[{}] Added device [{}] to the gateway session", gatewaySessionId, deviceName); 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 AttributesSubscribeMsg())));
processor.process(new BasicToDeviceActorSessionMsg(device, new BasicAdaptorToSessionActorMsg(ctx, new RpcSubscribeMsg()))); processor.process(new BasicToDeviceActorSessionMsg(device, new BasicAdaptorToSessionActorMsg(ctx, new RpcSubscribeMsg())));
offlineService.online(device, false);
} }
} }
@ -112,7 +107,6 @@ public class GatewaySessionCtx {
if (deviceSessionCtx != null) { if (deviceSessionCtx != null) {
processor.process(SessionCloseMsg.onDisconnect(deviceSessionCtx.getSessionId())); processor.process(SessionCloseMsg.onDisconnect(deviceSessionCtx.getSessionId()));
deviceSessionCtx.setClosed(true); deviceSessionCtx.setClosed(true);
offlineService.offline(deviceSessionCtx.getDevice());
log.debug("[{}] Removed device [{}] from the gateway session", gatewaySessionId, deviceName); log.debug("[{}] Removed device [{}] from the gateway session", gatewaySessionId, deviceName);
} else { } else {
log.debug("[{}] Device [{}] was already removed from the gateway session", gatewaySessionId, deviceName); log.debug("[{}] Device [{}] was already removed from the gateway session", gatewaySessionId, deviceName);
@ -123,7 +117,6 @@ public class GatewaySessionCtx {
public void onGatewayDisconnect() { public void onGatewayDisconnect() {
devices.forEach((k, v) -> { devices.forEach((k, v) -> {
processor.process(SessionCloseMsg.onDisconnect(v.getSessionId())); processor.process(SessionCloseMsg.onDisconnect(v.getSessionId()));
offlineService.offline(v.getDevice());
}); });
} }
@ -145,7 +138,6 @@ public class GatewaySessionCtx {
GatewayDeviceSessionCtx deviceSessionCtx = devices.get(deviceName); GatewayDeviceSessionCtx deviceSessionCtx = devices.get(deviceName);
processor.process(new BasicToDeviceActorSessionMsg(deviceSessionCtx.getDevice(), processor.process(new BasicToDeviceActorSessionMsg(deviceSessionCtx.getDevice(),
new BasicAdaptorToSessionActorMsg(deviceSessionCtx, request))); new BasicAdaptorToSessionActorMsg(deviceSessionCtx, request)));
offlineService.online(deviceSessionCtx.getDevice(), true);
} }
} else { } else {
throw new JsonSyntaxException(CAN_T_PARSE_VALUE + json); throw new JsonSyntaxException(CAN_T_PARSE_VALUE + json);
@ -162,7 +154,6 @@ public class GatewaySessionCtx {
GatewayDeviceSessionCtx deviceSessionCtx = devices.get(deviceName); GatewayDeviceSessionCtx deviceSessionCtx = devices.get(deviceName);
processor.process(new BasicToDeviceActorSessionMsg(deviceSessionCtx.getDevice(), processor.process(new BasicToDeviceActorSessionMsg(deviceSessionCtx.getDevice(),
new BasicAdaptorToSessionActorMsg(deviceSessionCtx, new ToDeviceRpcResponseMsg(requestId, data)))); new BasicAdaptorToSessionActorMsg(deviceSessionCtx, new ToDeviceRpcResponseMsg(requestId, data))));
offlineService.online(deviceSessionCtx.getDevice(), true);
} else { } else {
throw new JsonSyntaxException(CAN_T_PARSE_VALUE + json); throw new JsonSyntaxException(CAN_T_PARSE_VALUE + json);
} }
@ -185,7 +176,6 @@ public class GatewaySessionCtx {
GatewayDeviceSessionCtx deviceSessionCtx = devices.get(deviceName); GatewayDeviceSessionCtx deviceSessionCtx = devices.get(deviceName);
processor.process(new BasicToDeviceActorSessionMsg(deviceSessionCtx.getDevice(), processor.process(new BasicToDeviceActorSessionMsg(deviceSessionCtx.getDevice(),
new BasicAdaptorToSessionActorMsg(deviceSessionCtx, request))); new BasicAdaptorToSessionActorMsg(deviceSessionCtx, request)));
offlineService.online(deviceSessionCtx.getDevice(), true);
} }
} else { } else {
throw new JsonSyntaxException(CAN_T_PARSE_VALUE + json); throw new JsonSyntaxException(CAN_T_PARSE_VALUE + json);
@ -220,7 +210,6 @@ public class GatewaySessionCtx {
processor.process(new BasicToDeviceActorSessionMsg(deviceSessionCtx.getDevice(), processor.process(new BasicToDeviceActorSessionMsg(deviceSessionCtx.getDevice(),
new BasicAdaptorToSessionActorMsg(deviceSessionCtx, request))); new BasicAdaptorToSessionActorMsg(deviceSessionCtx, request)));
ack(msg); ack(msg);
offlineService.online(deviceSessionCtx.getDevice(), false);
} else { } else {
throw new JsonSyntaxException(CAN_T_PARSE_VALUE + json); throw new JsonSyntaxException(CAN_T_PARSE_VALUE + json);
} }

9
ui/src/app/common/types.constant.js

@ -485,7 +485,7 @@ export default angular.module('thingsboard.types', [])
clientSide: false clientSide: false
} }
}, },
ruleNodeTypeComponentTypes: ["FILTER", "ENRICHMENT", "TRANSFORMATION", "ACTION"], ruleNodeTypeComponentTypes: ["FILTER", "ENRICHMENT", "TRANSFORMATION", "ACTION", "EXTERNAL"],
ruleChainNodeComponent: { ruleChainNodeComponent: {
type: 'RULE_CHAIN', type: 'RULE_CHAIN',
name: 'rule chain', name: 'rule chain',
@ -536,6 +536,13 @@ export default angular.module('thingsboard.types', [])
nodeClass: "tb-action-type", nodeClass: "tb-action-type",
icon: "flash_on" icon: "flash_on"
}, },
EXTERNAL: {
value: "EXTERNAL",
name: "rulenode.type-external",
details: "rulenode.type-external-details",
nodeClass: "tb-external-type",
icon: "cloud_upload"
},
RULE_CHAIN: { RULE_CHAIN: {
value: "RULE_CHAIN", value: "RULE_CHAIN",
name: "rulenode.type-rule-chain", name: "rulenode.type-rule-chain",

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

@ -1228,6 +1228,8 @@ export default angular.module('thingsboard.locale', [])
"type-transformation-details": "Change Message payload and Metadata", "type-transformation-details": "Change Message payload and Metadata",
"type-action": "Action", "type-action": "Action",
"type-action-details": "Perform special 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": "Rule Chain",
"type-rule-chain-details": "Forwards incoming messages to specified Rule Chain", "type-rule-chain-details": "Forwards incoming messages to specified Rule Chain",
"directive-is-not-loaded": "Defined configuration directive '{{directiveName}}' is not available.", "directive-is-not-loaded": "Defined configuration directive '{{directiveName}}' is not available.",

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

@ -246,6 +246,7 @@ export function RuleChainController($state, $scope, $compile, $q, $mdUtil, $time
var contextInfo = { var contextInfo = {
headerClass: node.nodeClass, headerClass: node.nodeClass,
icon: node.icon, icon: node.icon,
iconUrl: node.iconUrl,
title: node.name, title: node.name,
subtitle: node.component.name subtitle: node.component.name
}; };
@ -805,12 +806,21 @@ export function RuleChainController($state, $scope, $compile, $q, $mdUtil, $time
var ruleNodeComponent = ruleNodeComponents[i]; var ruleNodeComponent = ruleNodeComponents[i];
componentType = ruleNodeComponent.type; componentType = ruleNodeComponent.type;
var model = vm.ruleNodeTypesModel[componentType].model; 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 = { var node = {
id: 'node-lib-' + componentType + '-' + model.nodes.length, id: 'node-lib-' + componentType + '-' + model.nodes.length,
component: ruleNodeComponent, component: ruleNodeComponent,
name: '', name: '',
nodeClass: vm.types.ruleNodeType[componentType].nodeClass, nodeClass: vm.types.ruleNodeType[componentType].nodeClass,
icon: vm.types.ruleNodeType[componentType].icon, icon: icon,
iconUrl: iconUrl,
x: 30, x: 30,
y: 10+50*model.nodes.length, y: 10+50*model.nodes.length,
connectors: [] connectors: []
@ -904,6 +914,14 @@ export function RuleChainController($state, $scope, $compile, $q, $mdUtil, $time
var ruleNode = vm.ruleChainMetaData.nodes[i]; var ruleNode = vm.ruleChainMetaData.nodes[i];
var component = ruleChainService.getRuleNodeComponentByClazz(ruleNode.type); var component = ruleChainService.getRuleNodeComponentByClazz(ruleNode.type);
if (component) { 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 = { var node = {
id: 'rule-chain-node-' + vm.nextNodeID++, id: 'rule-chain-node-' + vm.nextNodeID++,
ruleNodeId: ruleNode.id, ruleNodeId: ruleNode.id,
@ -915,7 +933,8 @@ export function RuleChainController($state, $scope, $compile, $q, $mdUtil, $time
component: component, component: component,
name: ruleNode.name, name: ruleNode.name,
nodeClass: vm.types.ruleNodeType[component.type].nodeClass, nodeClass: vm.types.ruleNodeType[component.type].nodeClass,
icon: vm.types.ruleNodeType[component.type].icon, icon: icon,
iconUrl: iconUrl,
connectors: [] connectors: []
}; };
if (component.configurationDescriptor.nodeDefinition.inEnabled) { if (component.configurationDescriptor.nodeDefinition.inEnabled) {

5
ui/src/app/rulechain/rulechain.scss

@ -76,7 +76,7 @@
-moz-user-select: none; -moz-user-select: none;
-ms-user-select: none; -ms-user-select: none;
user-select: none; user-select: none;
min-width: 180px; min-width: 150px;
} }
.fc-canvas { .fc-canvas {
background: #f9f9f9; background: #f9f9f9;
@ -161,6 +161,9 @@
&.tb-action-type { &.tb-action-type {
background-color: #f1928f; background-color: #f1928f;
} }
&.tb-external-type {
background-color: #fbc766;
}
&.tb-rule-chain-type { &.tb-rule-chain-type {
background-color: #d6c4f1; background-color: #d6c4f1;
} }

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

@ -75,6 +75,8 @@
<md-expansion-panel md-component-id="{{typeId}}" id="{{typeId}}" ng-repeat="(typeId, typeModel) in vm.ruleNodeTypesModel"> <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)" <md-expansion-panel-collapsed ng-mouseenter="vm.typeHeaderMouseEnter($event, typeId)"
ng-mouseleave="vm.destroyTooltips()"> 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> <div class="tb-panel-title" translate>{{vm.types.ruleNodeType[typeId].name}}</div>
<md-expansion-panel-icon></md-expansion-panel-icon> <md-expansion-panel-icon></md-expansion-panel-icon>
</md-expansion-panel-collapsed> </md-expansion-panel-collapsed>
@ -82,6 +84,8 @@
<md-expansion-panel-header ng-mouseenter="vm.typeHeaderMouseEnter($event, typeId)" <md-expansion-panel-header ng-mouseenter="vm.typeHeaderMouseEnter($event, typeId)"
ng-mouseleave="vm.destroyTooltips()" ng-mouseleave="vm.destroyTooltips()"
ng-click="vm.$mdExpansionPanel(typeId).collapse()"> 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> <div class="tb-panel-title" translate>{{vm.types.ruleNodeType[typeId].name}}</div>
<md-expansion-panel-icon></md-expansion-panel-icon> <md-expansion-panel-icon></md-expansion-panel-icon>
</md-expansion-panel-header> </md-expansion-panel-header>
@ -114,8 +118,10 @@
</div> </div>
<md-menu-content id="tb-rule-chain-context-menu" width="4" ng-mouseleave="$mdCloseMousepointMenu()"> <md-menu-content id="tb-rule-chain-context-menu" width="4" ng-mouseleave="$mdCloseMousepointMenu()">
<div class="tb-context-menu-header {{vm.contextInfo.headerClass}}"> <div class="tb-context-menu-header {{vm.contextInfo.headerClass}}">
<md-icon aria-label="node-type-icon" <md-icon ng-if="!vm.contextInfo.iconUrl" aria-label="node-type-icon"
class="material-icons">{{vm.contextInfo.icon}}</md-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 flex>
<div class="tb-context-menu-title">{{vm.contextInfo.title}}</div> <div class="tb-context-menu-title">{{vm.contextInfo.title}}</div>
<div class="tb-context-menu-subtitle">{{vm.contextInfo.subtitle}}</div> <div class="tb-context-menu-subtitle">{{vm.contextInfo.subtitle}}</div>

4
ui/src/app/rulechain/rulenode.tpl.html

@ -24,8 +24,10 @@
ng-mouseleave="callbacks.mouseLeave($event, node)"> ng-mouseleave="callbacks.mouseLeave($event, node)">
<div class="{{flowchartConstants.nodeOverlayClass}}"></div> <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 }"> <div class="tb-rule-node {{node.nodeClass}}" ng-class="{'tb-rule-node-highlighted' : node.highlighted, 'tb-rule-node-invalid': node.error }">
<md-icon aria-label="node-type-icon" flex="15" <md-icon ng-if="!node.iconUrl" aria-label="node-type-icon" flex="15"
class="material-icons">{{node.icon}}</md-icon> 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"> <div layout="column" flex="85" layout-align="center">
<span class="tb-node-type">{{ node.component.name }}</span> <span class="tb-node-type">{{ node.component.name }}</span>
<span class="tb-node-title" ng-if="node.name">{{ node.name }}</span> <span class="tb-node-title" ng-if="node.name">{{ node.name }}</span>

23
ui/src/scss/main.scss

@ -267,6 +267,29 @@ 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-input-container {
.tk-hint {
padding-top: 40px;
}
}
.md-caption { .md-caption {
&.tb-required:after { &.tb-required:after {
content: ' *'; content: ' *';

Loading…
Cancel
Save