Browse Source

Merge remote-tracking branch 'upstream/master' into feature/input-password/toggle

pull/5012/head
Vladyslav_Prykhodko 5 years ago
parent
commit
9f9e613320
  1. 5
      application/pom.xml
  2. 8
      application/src/main/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessor.java
  3. 10
      application/src/main/java/org/thingsboard/server/controller/AdminController.java
  4. 1
      application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java
  5. 11
      application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/CustomOAuth2ClientMapper.java
  6. 4
      application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/Oauth2AuthenticationSuccessHandler.java
  7. 67
      application/src/main/java/org/thingsboard/server/service/state/DefaultDeviceStateService.java
  8. 9
      application/src/main/java/org/thingsboard/server/service/state/DeviceStateService.java
  9. 8
      application/src/main/java/org/thingsboard/server/service/subscription/DefaultSubscriptionManagerService.java
  10. 1
      application/src/main/resources/thingsboard.yml
  11. 3
      application/src/test/java/org/thingsboard/server/transport/lwm2m/AbstractLwM2MIntegrationTest.java
  12. 104
      application/src/test/java/org/thingsboard/server/transport/lwm2m/NoSecLwM2MIntegrationTest.java
  13. 41
      common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaProducerTemplate.java
  14. 11
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/LwM2MTransportBootstrapService.java
  15. 12
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/secure/LwM2MBootstrapConfig.java
  16. 27
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/secure/LwM2MInMemoryBootstrapConfigurationAdapter.java
  17. 29
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/secure/LwM2MServerBootstrap.java
  18. 15
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClientContextImpl.java
  19. 1
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/ota/LwM2MClientOtaInfo.java
  20. 13
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbRedisLwM2MClientStore.java
  21. 5
      common/transport/mqtt/pom.xml
  22. 9
      common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportContext.java
  23. 65
      common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java
  24. 20
      common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/DeviceSessionCtx.java
  25. 1
      common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/GatewayDeviceSessionCtx.java
  26. 35
      common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/GatewaySessionHandler.java
  27. 221
      common/transport/mqtt/src/test/java/org/thingsboard/server/transport/mqtt/MqttTransportHandlerTest.java
  28. 61
      common/transport/mqtt/src/test/java/org/thingsboard/server/transport/mqtt/session/GatewaySessionHandlerTest.java
  29. 1
      common/transport/snmp/src/main/java/org/thingsboard/server/transport/snmp/SnmpTransportContext.java
  30. 4
      common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/TransportService.java
  31. 5
      common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportService.java
  32. 2
      common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/session/DeviceAwareSessionContext.java
  33. 4
      dao/src/main/java/org/thingsboard/server/dao/settings/AdminSettingsServiceImpl.java
  34. 2
      dao/src/main/java/org/thingsboard/server/dao/sqlts/AbstractChunkedAggregationTimeseriesDao.java
  35. 2
      dao/src/main/java/org/thingsboard/server/dao/sqlts/timescale/TimescaleTimeseriesDao.java
  36. 2
      dao/src/main/java/org/thingsboard/server/dao/timeseries/BaseTimeseriesService.java
  37. 7
      dao/src/main/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesDao.java
  38. 2
      dao/src/main/java/org/thingsboard/server/dao/timeseries/TimeseriesDao.java
  39. 4
      dao/src/test/java/org/thingsboard/server/dao/nosql/CassandraPartitionsCacheTest.java
  40. 21
      pom.xml
  41. 2
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/AlarmState.java
  42. 2
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rpc/TbSendRPCRequestNode.java
  43. 30
      rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/profile/TbDeviceProfileNodeTest.java
  44. 1
      transport/mqtt/src/main/resources/tb-mqtt-transport.yml
  45. 2
      ui-ngx/src/app/core/api/widget-api.models.ts
  46. 16
      ui-ngx/src/app/core/http/attribute.service.ts
  47. 7
      ui-ngx/src/app/modules/home/components/entity/contact-based.component.ts
  48. 6
      ui-ngx/src/app/modules/home/components/entity/entity.component.ts
  49. 7
      ui-ngx/src/app/modules/home/components/profile/device-profile.component.ts
  50. 7
      ui-ngx/src/app/modules/home/components/profile/tenant-profile.component.ts
  51. 55
      ui-ngx/src/app/modules/home/components/widget/widget.component.ts
  52. 7
      ui-ngx/src/app/modules/home/pages/admin/mail-server.component.html
  53. 53
      ui-ngx/src/app/modules/home/pages/admin/mail-server.component.ts
  54. 11
      ui-ngx/src/app/modules/home/pages/admin/resource/resources-library.component.ts
  55. 7
      ui-ngx/src/app/modules/home/pages/asset/asset.component.ts
  56. 7
      ui-ngx/src/app/modules/home/pages/customer/customer.component.ts
  57. 7
      ui-ngx/src/app/modules/home/pages/dashboard/dashboard-form.component.ts
  58. 7
      ui-ngx/src/app/modules/home/pages/device/device.component.ts
  59. 7
      ui-ngx/src/app/modules/home/pages/edge/edge.component.ts
  60. 7
      ui-ngx/src/app/modules/home/pages/entity-view/entity-view.component.ts
  61. 7
      ui-ngx/src/app/modules/home/pages/ota-update/ota-update.component.ts
  62. 7
      ui-ngx/src/app/modules/home/pages/rulechain/rulechain.component.ts
  63. 7
      ui-ngx/src/app/modules/home/pages/tenant/tenant.component.ts
  64. 7
      ui-ngx/src/app/modules/home/pages/user/user.component.ts
  65. 7
      ui-ngx/src/app/modules/home/pages/widget/widgets-bundle.component.ts
  66. 4
      ui-ngx/src/app/shared/models/settings.models.ts
  67. 4
      ui-ngx/src/assets/locale/locale.constant-de_DE.json
  68. 1
      ui-ngx/src/assets/locale/locale.constant-en_US.json
  69. 24
      ui-ngx/src/assets/locale/locale.constant-es_ES.json
  70. 20
      ui-ngx/src/assets/locale/locale.constant-fr_FR.json

5
application/pom.xml

@ -289,6 +289,11 @@
<artifactId>junit</artifactId> <artifactId>junit</artifactId>
<scope>test</scope> <scope>test</scope>
</dependency> </dependency>
<dependency>
<groupId>org.awaitility</groupId>
<artifactId>awaitility</artifactId>
<scope>test</scope>
</dependency>
<dependency> <dependency>
<groupId>org.mockito</groupId> <groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId> <artifactId>mockito-core</artifactId>

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

@ -381,11 +381,11 @@ class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcessor {
} }
private void reportSessionOpen() { private void reportSessionOpen() {
systemContext.getDeviceStateService().onDeviceConnect(deviceId); systemContext.getDeviceStateService().onDeviceConnect(tenantId, deviceId);
} }
private void reportSessionClose() { private void reportSessionClose() {
systemContext.getDeviceStateService().onDeviceDisconnect(deviceId); systemContext.getDeviceStateService().onDeviceDisconnect(tenantId, deviceId);
} }
private void handleGetAttributesRequest(TbActorCtx context, SessionInfoProto sessionInfo, GetAttributeRequestMsg request) { private void handleGetAttributesRequest(TbActorCtx context, SessionInfoProto sessionInfo, GetAttributeRequestMsg request) {
@ -590,7 +590,7 @@ class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcessor {
if (sessions.size() == 1) { if (sessions.size() == 1) {
reportSessionOpen(); reportSessionOpen();
} }
systemContext.getDeviceStateService().onDeviceActivity(deviceId, System.currentTimeMillis()); systemContext.getDeviceStateService().onDeviceActivity(tenantId, deviceId, System.currentTimeMillis());
dumpSessions(); dumpSessions();
} else if (msg.getEvent() == SessionEvent.CLOSED) { } else if (msg.getEvent() == SessionEvent.CLOSED) {
log.debug("[{}] Canceling subscriptions for closed session [{}]", deviceId, sessionId); log.debug("[{}] Canceling subscriptions for closed session [{}]", deviceId, sessionId);
@ -620,7 +620,7 @@ class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcessor {
if (subscriptionInfo.getRpcSubscription()) { if (subscriptionInfo.getRpcSubscription()) {
rpcSubscriptions.putIfAbsent(sessionId, sessionMD.getSessionInfo()); rpcSubscriptions.putIfAbsent(sessionId, sessionMD.getSessionInfo());
} }
systemContext.getDeviceStateService().onDeviceActivity(deviceId, subscriptionInfo.getLastActivityTime()); systemContext.getDeviceStateService().onDeviceActivity(tenantId, deviceId, subscriptionInfo.getLastActivityTime());
dumpSessions(); dumpSessions();
} }

10
application/src/main/java/org/thingsboard/server/controller/AdminController.java

@ -26,12 +26,12 @@ import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import org.thingsboard.rule.engine.api.MailService; import org.thingsboard.rule.engine.api.MailService;
import org.thingsboard.rule.engine.api.SmsService; import org.thingsboard.rule.engine.api.SmsService;
import org.thingsboard.server.common.data.sms.config.TestSmsRequest;
import org.thingsboard.server.common.data.AdminSettings; import org.thingsboard.server.common.data.AdminSettings;
import org.thingsboard.server.common.data.UpdateMessage; import org.thingsboard.server.common.data.UpdateMessage;
import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.security.model.SecuritySettings; import org.thingsboard.server.common.data.security.model.SecuritySettings;
import org.thingsboard.server.common.data.sms.config.TestSmsRequest;
import org.thingsboard.server.dao.settings.AdminSettingsService; import org.thingsboard.server.dao.settings.AdminSettingsService;
import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.security.permission.Operation; import org.thingsboard.server.service.security.permission.Operation;
@ -67,7 +67,7 @@ public class AdminController extends BaseController {
accessControlService.checkPermission(getCurrentUser(), Resource.ADMIN_SETTINGS, Operation.READ); accessControlService.checkPermission(getCurrentUser(), Resource.ADMIN_SETTINGS, Operation.READ);
AdminSettings adminSettings = checkNotNull(adminSettingsService.findAdminSettingsByKey(TenantId.SYS_TENANT_ID, key)); AdminSettings adminSettings = checkNotNull(adminSettingsService.findAdminSettingsByKey(TenantId.SYS_TENANT_ID, key));
if (adminSettings.getKey().equals("mail")) { if (adminSettings.getKey().equals("mail")) {
((ObjectNode) adminSettings.getJsonValue()).put("password", ""); ((ObjectNode) adminSettings.getJsonValue()).remove("password");
} }
return adminSettings; return adminSettings;
} catch (Exception e) { } catch (Exception e) {
@ -84,7 +84,7 @@ public class AdminController extends BaseController {
adminSettings = checkNotNull(adminSettingsService.saveAdminSettings(TenantId.SYS_TENANT_ID, adminSettings)); adminSettings = checkNotNull(adminSettingsService.saveAdminSettings(TenantId.SYS_TENANT_ID, adminSettings));
if (adminSettings.getKey().equals("mail")) { if (adminSettings.getKey().equals("mail")) {
mailService.updateMailConfiguration(); mailService.updateMailConfiguration();
((ObjectNode) adminSettings.getJsonValue()).put("password", ""); ((ObjectNode) adminSettings.getJsonValue()).remove("password");
} else if (adminSettings.getKey().equals("sms")) { } else if (adminSettings.getKey().equals("sms")) {
smsService.updateSmsConfiguration(); smsService.updateSmsConfiguration();
} }
@ -126,6 +126,10 @@ public class AdminController extends BaseController {
accessControlService.checkPermission(getCurrentUser(), Resource.ADMIN_SETTINGS, Operation.READ); accessControlService.checkPermission(getCurrentUser(), Resource.ADMIN_SETTINGS, Operation.READ);
adminSettings = checkNotNull(adminSettings); adminSettings = checkNotNull(adminSettings);
if (adminSettings.getKey().equals("mail")) { if (adminSettings.getKey().equals("mail")) {
if(!adminSettings.getJsonValue().has("password")) {
AdminSettings mailSettings = checkNotNull(adminSettingsService.findAdminSettingsByKey(TenantId.SYS_TENANT_ID, "mail"));
((ObjectNode) adminSettings.getJsonValue()).put("password", mailSettings.getJsonValue().get("password").asText());
}
String email = getCurrentUser().getEmail(); String email = getCurrentUser().getEmail();
mailService.sendTestMail(adminSettings.getJsonValue(), email); mailService.sendTestMail(adminSettings.getJsonValue(), email);
} }

1
application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java

@ -215,6 +215,7 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService {
node.put("password", ""); node.put("password", "");
node.put("tlsVersion", "TLSv1.2");//NOSONAR, key used to identify password field (not password value itself) node.put("tlsVersion", "TLSv1.2");//NOSONAR, key used to identify password field (not password value itself)
node.put("enableProxy", false); node.put("enableProxy", false);
node.put("showChangePassword", false);
mailSettings.setJsonValue(node); mailSettings.setJsonValue(node);
adminSettingsService.saveAdminSettings(TenantId.SYS_TENANT_ID, mailSettings); adminSettingsService.saveAdminSettings(TenantId.SYS_TENANT_ID, mailSettings);
} }

11
application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/CustomOAuth2ClientMapper.java

@ -17,6 +17,7 @@ package org.thingsboard.server.service.security.auth.oauth2;
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 com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.web.client.RestTemplateBuilder; import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken; import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
@ -29,6 +30,7 @@ import org.thingsboard.server.common.data.oauth2.OAuth2Registration;
import org.thingsboard.server.dao.oauth2.OAuth2User; import org.thingsboard.server.dao.oauth2.OAuth2User;
import org.thingsboard.server.service.security.model.SecurityUser; import org.thingsboard.server.service.security.model.SecurityUser;
import javax.annotation.PostConstruct;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
@Service(value = "customOAuth2ClientMapper") @Service(value = "customOAuth2ClientMapper")
@ -40,6 +42,15 @@ public class CustomOAuth2ClientMapper extends AbstractOAuth2ClientMapper impleme
private RestTemplateBuilder restTemplateBuilder = new RestTemplateBuilder(); private RestTemplateBuilder restTemplateBuilder = new RestTemplateBuilder();
@PostConstruct
public void init() {
// Register time module to parse Instant objects.
// com.fasterxml.jackson.databind.exc.InvalidDefinitionException:
// Java 8 date/time type `java.time.Instant` not supported by default:
// add Module "com.fasterxml.jackson.datatype:jackson-datatype-jsr310" to enable handling
json.registerModule(new JavaTimeModule());
}
@Override @Override
public SecurityUser getOrCreateUserByClientPrincipal(HttpServletRequest request, OAuth2AuthenticationToken token, String providerAccessToken, OAuth2Registration registration) { public SecurityUser getOrCreateUserByClientPrincipal(HttpServletRequest request, OAuth2AuthenticationToken token, String providerAccessToken, OAuth2Registration registration) {
OAuth2MapperConfig config = registration.getMapperConfig(); OAuth2MapperConfig config = registration.getMapperConfig();

4
application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/Oauth2AuthenticationSuccessHandler.java

@ -15,6 +15,7 @@
*/ */
package org.thingsboard.server.service.security.auth.oauth2; package org.thingsboard.server.service.security.auth.oauth2;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication; import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClient; import org.springframework.security.oauth2.client.OAuth2AuthorizedClient;
@ -42,6 +43,7 @@ import java.net.URLEncoder;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.UUID; import java.util.UUID;
@Slf4j
@Component(value = "oauth2AuthenticationSuccessHandler") @Component(value = "oauth2AuthenticationSuccessHandler")
public class Oauth2AuthenticationSuccessHandler extends SimpleUrlAuthenticationSuccessHandler { public class Oauth2AuthenticationSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {
@ -99,6 +101,8 @@ public class Oauth2AuthenticationSuccessHandler extends SimpleUrlAuthenticationS
clearAuthenticationAttributes(request, response); clearAuthenticationAttributes(request, response);
getRedirectStrategy().sendRedirect(request, response, baseUrl + "/?accessToken=" + accessToken.getToken() + "&refreshToken=" + refreshToken.getToken()); getRedirectStrategy().sendRedirect(request, response, baseUrl + "/?accessToken=" + accessToken.getToken() + "&refreshToken=" + refreshToken.getToken());
} catch (Exception e) { } catch (Exception e) {
log.debug("Error occurred during processing authentication success result. " +
"request [{}], response [{}], authentication [{}]", request, response, authentication, e);
clearAuthenticationAttributes(request, response); clearAuthenticationAttributes(request, response);
String errorPrefix; String errorPrefix;
if (!StringUtils.isEmpty(callbackUrlScheme)) { if (!StringUtils.isEmpty(callbackUrlScheme)) {

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

@ -137,7 +137,6 @@ public class DefaultDeviceStateService extends TbApplicationEventListener<Partit
private ExecutorService deviceStateExecutor; private ExecutorService deviceStateExecutor;
private final ConcurrentMap<TopicPartitionInfo, Set<DeviceId>> partitionedDevices = new ConcurrentHashMap<>(); private final ConcurrentMap<TopicPartitionInfo, Set<DeviceId>> partitionedDevices = new ConcurrentHashMap<>();
final ConcurrentMap<DeviceId, DeviceStateData> deviceStates = new ConcurrentHashMap<>(); final ConcurrentMap<DeviceId, DeviceStateData> deviceStates = new ConcurrentHashMap<>();
private final ConcurrentMap<DeviceId, Long> deviceLastSavedActivity = new ConcurrentHashMap<>();
final Queue<Set<TopicPartitionInfo>> subscribeQueue = new ConcurrentLinkedQueue<>(); final Queue<Set<TopicPartitionInfo>> subscribeQueue = new ConcurrentLinkedQueue<>();
@ -192,7 +191,7 @@ public class DefaultDeviceStateService extends TbApplicationEventListener<Partit
} }
@Override @Override
public void onDeviceConnect(DeviceId deviceId) { public void onDeviceConnect(TenantId tenantId, DeviceId deviceId) {
log.trace("on Device Connect [{}]", deviceId.getId()); log.trace("on Device Connect [{}]", deviceId.getId());
DeviceStateData stateData = getOrFetchDeviceStateData(deviceId); DeviceStateData stateData = getOrFetchDeviceStateData(deviceId);
long ts = System.currentTimeMillis(); long ts = System.currentTimeMillis();
@ -200,23 +199,23 @@ public class DefaultDeviceStateService extends TbApplicationEventListener<Partit
save(deviceId, LAST_CONNECT_TIME, ts); save(deviceId, LAST_CONNECT_TIME, ts);
pushRuleEngineMessage(stateData, CONNECT_EVENT); pushRuleEngineMessage(stateData, CONNECT_EVENT);
checkAndUpdateState(deviceId, stateData); checkAndUpdateState(deviceId, stateData);
cleanDeviceStateIfBelongsExternalPartition(tenantId, deviceId);
} }
@Override @Override
public void onDeviceActivity(DeviceId deviceId, long lastReportedActivity) { public void onDeviceActivity(TenantId tenantId, DeviceId deviceId, long lastReportedActivity) {
log.trace("on Device Activity [{}], lastReportedActivity [{}]", deviceId.getId(), lastReportedActivity); log.trace("on Device Activity [{}], lastReportedActivity [{}]", deviceId.getId(), lastReportedActivity);
long lastSavedActivity = deviceLastSavedActivity.getOrDefault(deviceId, 0L); final DeviceStateData stateData = getOrFetchDeviceStateData(deviceId);
if (lastReportedActivity > 0 && lastReportedActivity > lastSavedActivity) { if (lastReportedActivity > 0 && lastReportedActivity > stateData.getState().getLastActivityTime()) {
final DeviceStateData stateData = getOrFetchDeviceStateData(deviceId);
updateActivityState(deviceId, stateData, lastReportedActivity); updateActivityState(deviceId, stateData, lastReportedActivity);
} }
cleanDeviceStateIfBelongsExternalPartition(tenantId, deviceId);
} }
void updateActivityState(DeviceId deviceId, DeviceStateData stateData, long lastReportedActivity) { void updateActivityState(DeviceId deviceId, DeviceStateData stateData, long lastReportedActivity) {
log.trace("updateActivityState - fetched state {} for device {}, lastReportedActivity {}", stateData, deviceId, lastReportedActivity); log.trace("updateActivityState - fetched state {} for device {}, lastReportedActivity {}", stateData, deviceId, lastReportedActivity);
if (stateData != null) { if (stateData != null) {
save(deviceId, LAST_ACTIVITY_TIME, lastReportedActivity); save(deviceId, LAST_ACTIVITY_TIME, lastReportedActivity);
deviceLastSavedActivity.put(deviceId, lastReportedActivity);
DeviceState state = stateData.getState(); DeviceState state = stateData.getState();
state.setLastActivityTime(lastReportedActivity); state.setLastActivityTime(lastReportedActivity);
if (!state.isActive()) { if (!state.isActive()) {
@ -225,21 +224,23 @@ public class DefaultDeviceStateService extends TbApplicationEventListener<Partit
pushRuleEngineMessage(stateData, ACTIVITY_EVENT); pushRuleEngineMessage(stateData, ACTIVITY_EVENT);
} }
} else { } else {
log.warn("updateActivityState - fetched state IN NULL for device {}, lastReportedActivity {}", deviceId, lastReportedActivity); log.debug("updateActivityState - fetched state IN NULL for device {}, lastReportedActivity {}", deviceId, lastReportedActivity);
cleanUpDeviceStateMap(deviceId);
} }
} }
@Override @Override
public void onDeviceDisconnect(DeviceId deviceId) { public void onDeviceDisconnect(TenantId tenantId, DeviceId deviceId) {
DeviceStateData stateData = getOrFetchDeviceStateData(deviceId); DeviceStateData stateData = getOrFetchDeviceStateData(deviceId);
long ts = System.currentTimeMillis(); long ts = System.currentTimeMillis();
stateData.getState().setLastDisconnectTime(ts); stateData.getState().setLastDisconnectTime(ts);
save(deviceId, LAST_DISCONNECT_TIME, ts); save(deviceId, LAST_DISCONNECT_TIME, ts);
pushRuleEngineMessage(stateData, DISCONNECT_EVENT); pushRuleEngineMessage(stateData, DISCONNECT_EVENT);
cleanDeviceStateIfBelongsExternalPartition(tenantId, deviceId);
} }
@Override @Override
public void onDeviceInactivityTimeoutUpdate(DeviceId deviceId, long inactivityTimeout) { public void onDeviceInactivityTimeoutUpdate(TenantId tenantId, DeviceId deviceId, long inactivityTimeout) {
if (inactivityTimeout <= 0L) { if (inactivityTimeout <= 0L) {
return; return;
} }
@ -247,6 +248,7 @@ public class DefaultDeviceStateService extends TbApplicationEventListener<Partit
DeviceStateData stateData = getOrFetchDeviceStateData(deviceId); DeviceStateData stateData = getOrFetchDeviceStateData(deviceId);
stateData.getState().setInactivityTimeout(inactivityTimeout); stateData.getState().setInactivityTimeout(inactivityTimeout);
checkAndUpdateState(deviceId, stateData); checkAndUpdateState(deviceId, stateData);
cleanDeviceStateIfBelongsExternalPartition(tenantId, deviceId);
} }
@Override @Override
@ -283,12 +285,10 @@ public class DefaultDeviceStateService extends TbApplicationEventListener<Partit
}, deviceStateExecutor); }, deviceStateExecutor);
} else if (proto.getUpdated()) { } else if (proto.getUpdated()) {
DeviceStateData stateData = getOrFetchDeviceStateData(device.getId()); DeviceStateData stateData = getOrFetchDeviceStateData(device.getId());
if (stateData != null) { TbMsgMetaData md = new TbMsgMetaData();
TbMsgMetaData md = new TbMsgMetaData(); md.putValue("deviceName", device.getName());
md.putValue("deviceName", device.getName()); md.putValue("deviceType", device.getType());
md.putValue("deviceType", device.getType()); stateData.setMetaData(md);
stateData.setMetaData(md);
}
} }
} else { } else {
//Device was probably deleted while message was in queue; //Device was probably deleted while message was in queue;
@ -356,10 +356,7 @@ public class DefaultDeviceStateService extends TbApplicationEventListener<Partit
// We no longer manage current partition of devices; // We no longer manage current partition of devices;
removedPartitions.forEach(partition -> { removedPartitions.forEach(partition -> {
Set<DeviceId> devices = partitionedDevices.remove(partition); Set<DeviceId> devices = partitionedDevices.remove(partition);
devices.forEach(deviceId -> { devices.forEach(this::cleanUpDeviceStateMap);
deviceStates.remove(deviceId);
deviceLastSavedActivity.remove(deviceId);
});
}); });
addedPartitions.forEach(tpi -> partitionedDevices.computeIfAbsent(tpi, key -> ConcurrentHashMap.newKeySet())); addedPartitions.forEach(tpi -> partitionedDevices.computeIfAbsent(tpi, key -> ConcurrentHashMap.newKeySet()));
@ -463,11 +460,12 @@ public class DefaultDeviceStateService extends TbApplicationEventListener<Partit
void updateInactivityStateIfExpired() { void updateInactivityStateIfExpired() {
final long ts = System.currentTimeMillis(); final long ts = System.currentTimeMillis();
log.debug("Calculating state updates for {} devices", deviceStates.size()); partitionedDevices.forEach((tpi, deviceIds) -> {
Set<DeviceId> deviceIds = new HashSet<>(deviceStates.keySet()); log.debug("Calculating state updates. tpi {} for {} devices", tpi.getFullTopicName(), deviceIds.size());
for (DeviceId deviceId : deviceIds) { for (DeviceId deviceId : deviceIds) {
updateInactivityStateIfExpired(ts, deviceId); updateInactivityStateIfExpired(ts, deviceId);
} }
});
} }
void updateInactivityStateIfExpired(long ts, DeviceId deviceId) { void updateInactivityStateIfExpired(long ts, DeviceId deviceId) {
@ -488,8 +486,7 @@ public class DefaultDeviceStateService extends TbApplicationEventListener<Partit
} }
} else { } else {
log.debug("[{}] Device that belongs to other server is detected and removed.", deviceId); log.debug("[{}] Device that belongs to other server is detected and removed.", deviceId);
deviceStates.remove(deviceId); cleanUpDeviceStateMap(deviceId);
deviceLastSavedActivity.remove(deviceId);
} }
} }
@ -522,6 +519,15 @@ public class DefaultDeviceStateService extends TbApplicationEventListener<Partit
} }
} }
private void cleanDeviceStateIfBelongsExternalPartition(TenantId tenantId, final DeviceId deviceId) {
TopicPartitionInfo tpi = partitionService.resolve(ServiceType.TB_CORE, tenantId, deviceId);
if (!partitionedDevices.containsKey(tpi)) {
cleanUpDeviceStateMap(deviceId);
log.debug("[{}][{}] device belongs to external partition. Probably rebalancing is in progress. Topic: {}"
, tenantId, deviceId, tpi.getFullTopicName());
}
}
private void sendDeviceEvent(TenantId tenantId, DeviceId deviceId, boolean added, boolean updated, boolean deleted) { private void sendDeviceEvent(TenantId tenantId, DeviceId deviceId, boolean added, boolean updated, boolean deleted) {
TransportProtos.DeviceStateServiceMsgProto.Builder builder = TransportProtos.DeviceStateServiceMsgProto.newBuilder(); TransportProtos.DeviceStateServiceMsgProto.Builder builder = TransportProtos.DeviceStateServiceMsgProto.newBuilder();
builder.setTenantIdMSB(tenantId.getId().getMostSignificantBits()); builder.setTenantIdMSB(tenantId.getId().getMostSignificantBits());
@ -536,13 +542,16 @@ public class DefaultDeviceStateService extends TbApplicationEventListener<Partit
} }
private void onDeviceDeleted(TenantId tenantId, DeviceId deviceId) { private void onDeviceDeleted(TenantId tenantId, DeviceId deviceId) {
deviceStates.remove(deviceId); cleanUpDeviceStateMap(deviceId);
deviceLastSavedActivity.remove(deviceId);
TopicPartitionInfo tpi = partitionService.resolve(ServiceType.TB_CORE, tenantId, deviceId); TopicPartitionInfo tpi = partitionService.resolve(ServiceType.TB_CORE, tenantId, deviceId);
Set<DeviceId> deviceIdSet = partitionedDevices.get(tpi); Set<DeviceId> deviceIdSet = partitionedDevices.get(tpi);
deviceIdSet.remove(deviceId); deviceIdSet.remove(deviceId);
} }
private void cleanUpDeviceStateMap(DeviceId deviceId) {
deviceStates.remove(deviceId);
}
private ListenableFuture<DeviceStateData> fetchDeviceState(Device device) { private ListenableFuture<DeviceStateData> fetchDeviceState(Device device) {
ListenableFuture<DeviceStateData> future; ListenableFuture<DeviceStateData> future;
if (persistToTelemetry) { if (persistToTelemetry) {

9
application/src/main/java/org/thingsboard/server/service/state/DeviceStateService.java

@ -18,6 +18,7 @@ package org.thingsboard.server.service.state;
import org.springframework.context.ApplicationListener; import org.springframework.context.ApplicationListener;
import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.Device;
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.queue.discovery.event.PartitionChangeEvent; import org.thingsboard.server.queue.discovery.event.PartitionChangeEvent;
import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.gen.transport.TransportProtos;
import org.thingsboard.server.common.msg.queue.TbCallback; import org.thingsboard.server.common.msg.queue.TbCallback;
@ -33,13 +34,13 @@ public interface DeviceStateService extends ApplicationListener<PartitionChangeE
void onDeviceDeleted(Device device); void onDeviceDeleted(Device device);
void onDeviceConnect(DeviceId deviceId); void onDeviceConnect(TenantId tenantId, DeviceId deviceId);
void onDeviceActivity(DeviceId deviceId, long lastReportedActivityTime); void onDeviceActivity(TenantId tenantId, DeviceId deviceId, long lastReportedActivityTime);
void onDeviceDisconnect(DeviceId deviceId); void onDeviceDisconnect(TenantId tenantId, DeviceId deviceId);
void onDeviceInactivityTimeoutUpdate(DeviceId deviceId, long inactivityTimeout); void onDeviceInactivityTimeoutUpdate(TenantId tenantId, DeviceId deviceId, long inactivityTimeout);
void onQueueMsg(TransportProtos.DeviceStateServiceMsgProto proto, TbCallback bytes); void onQueueMsg(TransportProtos.DeviceStateServiceMsgProto proto, TbCallback bytes);

8
application/src/main/java/org/thingsboard/server/service/subscription/DefaultSubscriptionManagerService.java

@ -224,7 +224,7 @@ public class DefaultSubscriptionManagerService extends TbApplicationEventListene
return subscriptionUpdate; return subscriptionUpdate;
}); });
if (entityId.getEntityType() == EntityType.DEVICE) { if (entityId.getEntityType() == EntityType.DEVICE) {
updateDeviceInactivityTimeout(entityId, ts); updateDeviceInactivityTimeout(tenantId, entityId, ts);
} }
callback.onSuccess(); callback.onSuccess();
} }
@ -259,7 +259,7 @@ public class DefaultSubscriptionManagerService extends TbApplicationEventListene
}); });
if (entityId.getEntityType() == EntityType.DEVICE) { if (entityId.getEntityType() == EntityType.DEVICE) {
if (TbAttributeSubscriptionScope.SERVER_SCOPE.name().equalsIgnoreCase(scope)) { if (TbAttributeSubscriptionScope.SERVER_SCOPE.name().equalsIgnoreCase(scope)) {
updateDeviceInactivityTimeout(entityId, attributes); updateDeviceInactivityTimeout(tenantId, entityId, attributes);
} else if (TbAttributeSubscriptionScope.SHARED_SCOPE.name().equalsIgnoreCase(scope) && notifyDevice) { } else if (TbAttributeSubscriptionScope.SHARED_SCOPE.name().equalsIgnoreCase(scope) && notifyDevice) {
clusterService.pushMsgToCore(DeviceAttributesEventNotificationMsg.onUpdate(tenantId, clusterService.pushMsgToCore(DeviceAttributesEventNotificationMsg.onUpdate(tenantId,
new DeviceId(entityId.getId()), DataConstants.SHARED_SCOPE, new ArrayList<>(attributes)) new DeviceId(entityId.getId()), DataConstants.SHARED_SCOPE, new ArrayList<>(attributes))
@ -269,10 +269,10 @@ public class DefaultSubscriptionManagerService extends TbApplicationEventListene
callback.onSuccess(); callback.onSuccess();
} }
private void updateDeviceInactivityTimeout(EntityId entityId, List<? extends KvEntry> kvEntries) { private void updateDeviceInactivityTimeout(TenantId tenantId, EntityId entityId, List<? extends KvEntry> kvEntries) {
for (KvEntry kvEntry : kvEntries) { for (KvEntry kvEntry : kvEntries) {
if (kvEntry.getKey().equals(DefaultDeviceStateService.INACTIVITY_TIMEOUT)) { if (kvEntry.getKey().equals(DefaultDeviceStateService.INACTIVITY_TIMEOUT)) {
deviceStateService.onDeviceInactivityTimeoutUpdate(new DeviceId(entityId.getId()), kvEntry.getLongValue().orElse(0L)); deviceStateService.onDeviceInactivityTimeoutUpdate(tenantId, new DeviceId(entityId.getId()), kvEntry.getLongValue().orElse(0L));
} }
} }
} }

1
application/src/main/resources/thingsboard.yml

@ -588,6 +588,7 @@ transport:
bind_address: "${MQTT_BIND_ADDRESS:0.0.0.0}" bind_address: "${MQTT_BIND_ADDRESS:0.0.0.0}"
bind_port: "${MQTT_BIND_PORT:1883}" bind_port: "${MQTT_BIND_PORT:1883}"
timeout: "${MQTT_TIMEOUT:10000}" timeout: "${MQTT_TIMEOUT:10000}"
msg_queue_size_per_device_limit: "${MQTT_MSG_QUEUE_SIZE_PER_DEVICE_LIMIT:100}" # messages await in the queue before device connected state. This limit works on low level before TenantProfileLimits mechanism
netty: netty:
leak_detector_level: "${NETTY_LEAK_DETECTOR_LVL:DISABLED}" leak_detector_level: "${NETTY_LEAK_DETECTOR_LVL:DISABLED}"
boss_group_thread_count: "${NETTY_BOSS_GROUP_THREADS:1}" boss_group_thread_count: "${NETTY_BOSS_GROUP_THREADS:1}"

3
application/src/test/java/org/thingsboard/server/transport/lwm2m/AbstractLwM2MIntegrationTest.java

@ -27,6 +27,7 @@ import org.springframework.mock.web.MockMultipartFile;
import org.springframework.test.web.servlet.request.MockMultipartHttpServletRequestBuilder; import org.springframework.test.web.servlet.request.MockMultipartHttpServletRequestBuilder;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.common.util.ThingsBoardThreadFactory;
import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.DeviceProfile;
import org.thingsboard.server.common.data.DeviceProfileProvisionType; import org.thingsboard.server.common.data.DeviceProfileProvisionType;
@ -261,7 +262,7 @@ public class AbstractLwM2MIntegrationTest extends AbstractWebsocketTest {
@Before @Before
public void beforeTest() throws Exception { public void beforeTest() throws Exception {
executor = Executors.newScheduledThreadPool(10); executor = Executors.newScheduledThreadPool(10, ThingsBoardThreadFactory.forName("test-lwm2m-scheduled"));
loginTenantAdmin(); loginTenantAdmin();
String[] resources = new String[]{"1.xml", "2.xml", "3.xml", "5.xml", "9.xml"}; String[] resources = new String[]{"1.xml", "2.xml", "3.xml", "5.xml", "9.xml"};

104
application/src/test/java/org/thingsboard/server/transport/lwm2m/NoSecLwM2MIntegrationTest.java

@ -16,6 +16,8 @@
package org.thingsboard.server.transport.lwm2m; package org.thingsboard.server.transport.lwm2m;
import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.core.type.TypeReference;
import lombok.extern.slf4j.Slf4j;
import org.junit.After;
import org.junit.Assert; import org.junit.Assert;
import org.junit.Test; import org.junit.Test;
import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.Device;
@ -23,16 +25,18 @@ import org.thingsboard.server.common.data.device.credentials.lwm2m.NoSecClientCr
import org.thingsboard.server.common.data.kv.KvEntry; import org.thingsboard.server.common.data.kv.KvEntry;
import org.thingsboard.server.common.data.kv.TsKvEntry; import org.thingsboard.server.common.data.kv.TsKvEntry;
import org.thingsboard.server.common.data.ota.OtaPackageUpdateStatus; import org.thingsboard.server.common.data.ota.OtaPackageUpdateStatus;
import org.thingsboard.server.common.data.query.EntityKey;
import org.thingsboard.server.common.data.query.EntityKeyType;
import org.thingsboard.server.transport.lwm2m.client.LwM2MTestClient; import org.thingsboard.server.transport.lwm2m.client.LwM2MTestClient;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.Comparator; import java.util.Comparator;
import java.util.List; import java.util.List;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import static org.awaitility.Awaitility.await;
import static org.hamcrest.Matchers.is;
import static org.thingsboard.rest.client.utils.RestJsonConverter.toTimeseries; import static org.thingsboard.rest.client.utils.RestJsonConverter.toTimeseries;
import static org.thingsboard.server.common.data.ota.OtaPackageUpdateStatus.DOWNLOADED; import static org.thingsboard.server.common.data.ota.OtaPackageUpdateStatus.DOWNLOADED;
import static org.thingsboard.server.common.data.ota.OtaPackageUpdateStatus.DOWNLOADING; import static org.thingsboard.server.common.data.ota.OtaPackageUpdateStatus.DOWNLOADING;
@ -43,8 +47,10 @@ import static org.thingsboard.server.common.data.ota.OtaPackageUpdateStatus.UPDA
import static org.thingsboard.server.common.data.ota.OtaPackageUpdateStatus.UPDATING; import static org.thingsboard.server.common.data.ota.OtaPackageUpdateStatus.UPDATING;
import static org.thingsboard.server.common.data.ota.OtaPackageUpdateStatus.VERIFIED; import static org.thingsboard.server.common.data.ota.OtaPackageUpdateStatus.VERIFIED;
@Slf4j
public class NoSecLwM2MIntegrationTest extends AbstractLwM2MIntegrationTest { public class NoSecLwM2MIntegrationTest extends AbstractLwM2MIntegrationTest {
public static final int TIMEOUT = 30;
private final String OTA_TRANSPORT_CONFIGURATION = "{\n" + private final String OTA_TRANSPORT_CONFIGURATION = "{\n" +
" \"observeAttr\": {\n" + " \"observeAttr\": {\n" +
" \"keyName\": {\n" + " \"keyName\": {\n" +
@ -122,6 +128,15 @@ public class NoSecLwM2MIntegrationTest extends AbstractLwM2MIntegrationTest {
" \"type\": \"LWM2M\"\n" + " \"type\": \"LWM2M\"\n" +
"}"; "}";
LwM2MTestClient client = null;
@After
public void tearDown() {
if (client != null) {
client.destroy();
}
}
@Test @Test
public void testConnectAndObserveTelemetry() throws Exception { public void testConnectAndObserveTelemetry() throws Exception {
NoSecClientCredentials clientCredentials = new NoSecClientCredentials(); NoSecClientCredentials clientCredentials = new NoSecClientCredentials();
@ -196,37 +211,68 @@ public class NoSecLwM2MIntegrationTest extends AbstractLwM2MIntegrationTest {
} }
} }
/**
* This is the example how to use the AWAITILITY instead Thread.sleep()
* Test will finish as fast as possible, but will await until TIMEOUT if a build machine is busy or slow
* Check the detailed log output to learn how Awaitility polling the API and when exactly expected result appears
* */
@Test @Test
public void testSoftwareUpdateByObject9() throws Exception { public void testSoftwareUpdateByObject9() throws Exception {
LwM2MTestClient client = null; //given
try { final List<OtaPackageUpdateStatus> expectedStatuses = Collections.unmodifiableList(Arrays.asList(
createDeviceProfile(OTA_TRANSPORT_CONFIGURATION); QUEUED, INITIATED, DOWNLOADING, DOWNLOADING, DOWNLOADING, DOWNLOADED, VERIFIED, UPDATED));
NoSecClientCredentials clientCredentials = new NoSecClientCredentials();
clientCredentials.setEndpoint("OTA_" + ENDPOINT);
Device device = createDevice(clientCredentials);
device.setSoftwareId(createSoftware().getId());
device = doPost("/api/device", device, Device.class);
Thread.sleep(1000); createDeviceProfile(OTA_TRANSPORT_CONFIGURATION);
NoSecClientCredentials clientCredentials = new NoSecClientCredentials();
client = new LwM2MTestClient(executor, "OTA_" + ENDPOINT); clientCredentials.setEndpoint("OTA_" + ENDPOINT);
client.init(SECURITY, COAP_CONFIG); final Device device = createDevice(clientCredentials);
device.setSoftwareId(createSoftware().getId());
Thread.sleep(3000);
log.warn("Saving by API " + device);
List<TsKvEntry> ts = toTimeseries(doGetAsyncTyped("/api/plugins/telemetry/DEVICE/" + device.getId().getId() + "/values/timeseries?orderBy=ASC&keys=sw_state&startTs=0&endTs=" + System.currentTimeMillis(), new TypeReference<>() { final Device savedDevice = doPost("/api/device", device, Device.class);
})); Assert.assertNotNull(savedDevice);
log.warn("Device saved by API {}", savedDevice);
List<OtaPackageUpdateStatus> statuses = ts.stream().sorted(Comparator.comparingLong(TsKvEntry::getTs)).map(KvEntry::getValueAsString).map(OtaPackageUpdateStatus::valueOf).collect(Collectors.toList());
log.warn("AWAIT atMost {} SECONDS on get device by API...", TIMEOUT);
await()
.atMost(TIMEOUT, TimeUnit.SECONDS)
.until(() -> getDeviceFromAPI(device.getId().getId()), is(savedDevice));
log.warn("Got device by API.");
//when
log.warn("Init the client...");
client = new LwM2MTestClient(executor, "OTA_" + ENDPOINT);
client.init(SECURITY, COAP_CONFIG);
log.warn("Init done");
log.warn("AWAIT atMost {} SECONDS on timeseries List<TsKvEntry> by API with list size {}...", TIMEOUT, expectedStatuses.size());
await()
.atMost(30, TimeUnit.SECONDS)
.until(() -> getSwStateTelemetryFromAPI(device.getId().getId())
.size(), is(expectedStatuses.size()));
log.warn("Got an expected await condition!");
//then
log.warn("Fetching ts for the final asserts");
List<TsKvEntry> ts = getSwStateTelemetryFromAPI(device.getId().getId());
log.warn("Got an ts {}", ts);
List<OtaPackageUpdateStatus> statuses = ts.stream().sorted(Comparator.comparingLong(TsKvEntry::getTs)).map(KvEntry::getValueAsString).map(OtaPackageUpdateStatus::valueOf).collect(Collectors.toList());
log.warn("Converted ts to statuses {}", statuses);
Assert.assertEquals(expectedStatuses, statuses);
}
List<OtaPackageUpdateStatus> expectedStatuses = Arrays.asList(QUEUED, INITIATED, DOWNLOADING, DOWNLOADING, DOWNLOADING, DOWNLOADED, VERIFIED, UPDATED); private Device getDeviceFromAPI(UUID deviceId) throws Exception {
final Device device = doGet("/api/device/" + deviceId, Device.class);
log.warn("Fetched device by API for deviceId {}, device is {}", deviceId, device);
return device;
}
Assert.assertEquals(expectedStatuses, statuses); private List<TsKvEntry> getSwStateTelemetryFromAPI(UUID deviceId) throws Exception {
} finally { final List<TsKvEntry> tsKvEntries = toTimeseries(doGetAsyncTyped("/api/plugins/telemetry/DEVICE/" + deviceId + "/values/timeseries?orderBy=ASC&keys=sw_state&startTs=0&endTs=" + System.currentTimeMillis(), new TypeReference<>() {
if (client != null) { }));
client.destroy(); log.warn("Fetched telemetry by API for deviceId {}, list size {}, tsKvEntries {}", deviceId, tsKvEntries.size(), tsKvEntries);
} return tsKvEntries;
}
} }
} }

41
common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaProducerTemplate.java

@ -77,25 +77,34 @@ public class TbKafkaProducerTemplate<T extends TbQueueMsg> implements TbQueuePro
@Override @Override
public void send(TopicPartitionInfo tpi, T msg, TbQueueCallback callback) { public void send(TopicPartitionInfo tpi, T msg, TbQueueCallback callback) {
createTopicIfNotExist(tpi); try {
String key = msg.getKey().toString(); createTopicIfNotExist(tpi);
byte[] data = msg.getData(); String key = msg.getKey().toString();
ProducerRecord<String, byte[]> record; byte[] data = msg.getData();
Iterable<Header> headers = msg.getHeaders().getData().entrySet().stream().map(e -> new RecordHeader(e.getKey(), e.getValue())).collect(Collectors.toList()); ProducerRecord<String, byte[]> record;
record = new ProducerRecord<>(tpi.getFullTopicName(), null, key, data, headers); Iterable<Header> headers = msg.getHeaders().getData().entrySet().stream().map(e -> new RecordHeader(e.getKey(), e.getValue())).collect(Collectors.toList());
producer.send(record, (metadata, exception) -> { record = new ProducerRecord<>(tpi.getFullTopicName(), null, key, data, headers);
if (exception == null) { producer.send(record, (metadata, exception) -> {
if (callback != null) { if (exception == null) {
callback.onSuccess(new KafkaTbQueueMsgMetadata(metadata)); if (callback != null) {
} callback.onSuccess(new KafkaTbQueueMsgMetadata(metadata));
} else { }
if (callback != null) {
callback.onFailure(exception);
} else { } else {
log.warn("Producer template failure: {}", exception.getMessage(), exception); if (callback != null) {
callback.onFailure(exception);
} else {
log.warn("Producer template failure: {}", exception.getMessage(), exception);
}
} }
});
} catch (Exception e) {
if (callback != null) {
callback.onFailure(e);
} else {
log.warn("Producer template failure (send method wrapper): {}", e.getMessage(), e);
} }
}); throw e;
}
} }
private void createTopicIfNotExist(TopicPartitionInfo tpi) { private void createTopicIfNotExist(TopicPartitionInfo tpi) {

11
common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/LwM2MTransportBootstrapService.java

@ -19,6 +19,9 @@ import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.eclipse.californium.elements.util.SslContextUtil; import org.eclipse.californium.elements.util.SslContextUtil;
import org.eclipse.californium.scandium.config.DtlsConnectorConfig; import org.eclipse.californium.scandium.config.DtlsConnectorConfig;
import org.eclipse.leshan.core.model.ObjectLoader;
import org.eclipse.leshan.core.model.ObjectModel;
import org.eclipse.leshan.core.model.StaticModel;
import org.eclipse.leshan.server.bootstrap.BootstrapSessionManager; import org.eclipse.leshan.server.bootstrap.BootstrapSessionManager;
import org.eclipse.leshan.server.californium.bootstrap.LeshanBootstrapServer; import org.eclipse.leshan.server.californium.bootstrap.LeshanBootstrapServer;
import org.eclipse.leshan.server.californium.bootstrap.LeshanBootstrapServerBuilder; import org.eclipse.leshan.server.californium.bootstrap.LeshanBootstrapServerBuilder;
@ -26,6 +29,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.thingsboard.server.transport.lwm2m.bootstrap.secure.LwM2MBootstrapSecurityStore; import org.thingsboard.server.transport.lwm2m.bootstrap.secure.LwM2MBootstrapSecurityStore;
import org.thingsboard.server.transport.lwm2m.bootstrap.secure.LwM2MInMemoryBootstrapConfigStore; import org.thingsboard.server.transport.lwm2m.bootstrap.secure.LwM2MInMemoryBootstrapConfigStore;
import org.thingsboard.server.transport.lwm2m.bootstrap.secure.LwM2MInMemoryBootstrapConfigurationAdapter;
import org.thingsboard.server.transport.lwm2m.bootstrap.secure.LwM2mDefaultBootstrapSessionManager; import org.thingsboard.server.transport.lwm2m.bootstrap.secure.LwM2mDefaultBootstrapSessionManager;
import org.thingsboard.server.transport.lwm2m.config.LwM2MTransportBootstrapConfig; import org.thingsboard.server.transport.lwm2m.config.LwM2MTransportBootstrapConfig;
import org.thingsboard.server.transport.lwm2m.config.LwM2MTransportServerConfig; import org.thingsboard.server.transport.lwm2m.config.LwM2MTransportServerConfig;
@ -38,6 +42,7 @@ import java.security.KeyStoreException;
import java.security.PrivateKey; import java.security.PrivateKey;
import java.security.PublicKey; import java.security.PublicKey;
import java.security.cert.X509Certificate; import java.security.cert.X509Certificate;
import java.util.List;
import static org.thingsboard.server.transport.lwm2m.server.LwM2mNetworkConfig.getCoapConfig; import static org.thingsboard.server.transport.lwm2m.server.LwM2mNetworkConfig.getCoapConfig;
@ -79,12 +84,14 @@ public class LwM2MTransportBootstrapService {
builder.setCoapConfig(getCoapConfig(bootstrapConfig.getPort(), bootstrapConfig.getSecurePort(), serverConfig)); builder.setCoapConfig(getCoapConfig(bootstrapConfig.getPort(), bootstrapConfig.getSecurePort(), serverConfig));
/* Define model provider (Create Models )*/ /* Define model provider (Create Models )*/
List<ObjectModel> models = ObjectLoader.loadDefault();
builder.setModel(new StaticModel(models));
/* Create credentials */ /* Create credentials */
this.setServerWithCredentials(builder); this.setServerWithCredentials(builder);
// /** Set securityStore with new ConfigStore */ /* Set securityStore with new ConfigStore */
// builder.setConfigStore(lwM2MInMemoryBootstrapConfigStore); builder.setConfigStore(new LwM2MInMemoryBootstrapConfigurationAdapter(lwM2MInMemoryBootstrapConfigStore));
/* SecurityStore */ /* SecurityStore */
builder.setSecurityStore(lwM2MBootstrapSecurityStore); builder.setSecurityStore(lwM2MBootstrapSecurityStore);

12
common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/secure/LwM2MBootstrapConfig.java

@ -74,15 +74,19 @@ public class LwM2MBootstrapConfig implements Serializable {
configBs.servers.put(0, server0); configBs.servers.put(0, server0);
/* Security Configuration (object 0) as defined in LWM2M 1.0.x TS. Bootstrap instance = 0 */ /* Security Configuration (object 0) as defined in LWM2M 1.0.x TS. Bootstrap instance = 0 */
this.bootstrapServer.setBootstrapServerIs(true); this.bootstrapServer.setBootstrapServerIs(true);
configBs.security.put(0, setServerSecurity(this.bootstrapServer.getHost(), this.bootstrapServer.getPort(), this.bootstrapServer.isBootstrapServerIs(), this.bootstrapServer.getSecurityMode(), this.bootstrapServer.getClientPublicKeyOrId(), this.bootstrapServer.getServerPublicKey(), this.bootstrapServer.getClientSecretKey(), this.bootstrapServer.getServerId())); configBs.security.put(0, setServerSecurity(this.lwm2mServer.getHost(), this.lwm2mServer.getPort(), this.lwm2mServer.getSecurityHost(), this.lwm2mServer.getSecurityPort(), this.bootstrapServer.isBootstrapServerIs(), this.bootstrapServer.getSecurityMode(), this.bootstrapServer.getClientPublicKeyOrId(), this.bootstrapServer.getServerPublicKey(), this.bootstrapServer.getClientSecretKey(), this.bootstrapServer.getServerId()));
/* Security Configuration (object 0) as defined in LWM2M 1.0.x TS. Server instance = 1 */ /* Security Configuration (object 0) as defined in LWM2M 1.0.x TS. Server instance = 1 */
configBs.security.put(1, setServerSecurity(this.lwm2mServer.getHost(), this.lwm2mServer.getPort(), this.lwm2mServer.isBootstrapServerIs(), this.lwm2mServer.getSecurityMode(), this.lwm2mServer.getClientPublicKeyOrId(), this.lwm2mServer.getServerPublicKey(), this.lwm2mServer.getClientSecretKey(), this.lwm2mServer.getServerId())); configBs.security.put(1, setServerSecurity(this.lwm2mServer.getHost(), this.lwm2mServer.getPort(), this.lwm2mServer.getSecurityHost(), this.lwm2mServer.getSecurityPort(), this.lwm2mServer.isBootstrapServerIs(), this.lwm2mServer.getSecurityMode(), this.lwm2mServer.getClientPublicKeyOrId(), this.lwm2mServer.getServerPublicKey(), this.lwm2mServer.getClientSecretKey(), this.lwm2mServer.getServerId()));
return configBs; return configBs;
} }
private BootstrapConfig.ServerSecurity setServerSecurity(String host, Integer port, boolean bootstrapServer, SecurityMode securityMode, String clientPublicKey, String serverPublicKey, String secretKey, int serverId) { private BootstrapConfig.ServerSecurity setServerSecurity(String host, Integer port, String securityHost, Integer securityPort, boolean bootstrapServer, SecurityMode securityMode, String clientPublicKey, String serverPublicKey, String secretKey, int serverId) {
BootstrapConfig.ServerSecurity serverSecurity = new BootstrapConfig.ServerSecurity(); BootstrapConfig.ServerSecurity serverSecurity = new BootstrapConfig.ServerSecurity();
serverSecurity.uri = "coaps://" + host + ":" + Integer.toString(port); if (securityMode.equals(SecurityMode.NO_SEC)) {
serverSecurity.uri = "coap://" + host + ":" + Integer.toString(port);
} else {
serverSecurity.uri = "coaps://" + securityHost + ":" + Integer.toString(securityPort);
}
serverSecurity.bootstrapServer = bootstrapServer; serverSecurity.bootstrapServer = bootstrapServer;
serverSecurity.securityMode = securityMode; serverSecurity.securityMode = securityMode;
serverSecurity.publicKeyOrId = setPublicKeyOrId(clientPublicKey, securityMode); serverSecurity.publicKeyOrId = setPublicKeyOrId(clientPublicKey, securityMode);

27
common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/secure/LwM2MInMemoryBootstrapConfigurationAdapter.java

@ -0,0 +1,27 @@
/**
* Copyright © 2016-2021 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.transport.lwm2m.bootstrap.secure;
import org.eclipse.leshan.server.bootstrap.BootstrapConfigStore;
import org.eclipse.leshan.server.bootstrap.BootstrapConfigurationStoreAdapter;
public class LwM2MInMemoryBootstrapConfigurationAdapter extends BootstrapConfigurationStoreAdapter {
public LwM2MInMemoryBootstrapConfigurationAdapter(BootstrapConfigStore store) {
super(store);
}
}

29
common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/secure/LwM2MServerBootstrap.java

@ -31,24 +31,31 @@ public class LwM2MServerBootstrap {
String host = "0.0.0.0"; String host = "0.0.0.0";
Integer port = 0; Integer port = 0;
String securityHost = "0.0.0.0";
Integer securityPort = 0;
SecurityMode securityMode = SecurityMode.NO_SEC; SecurityMode securityMode = SecurityMode.NO_SEC;
Integer serverId = 123; Integer serverId = 123;
boolean bootstrapServerIs = false; boolean bootstrapServerIs = false;
public LwM2MServerBootstrap(){}; public LwM2MServerBootstrap() {
}
;
public LwM2MServerBootstrap(LwM2MServerBootstrap bootstrapFromCredential, LwM2MServerBootstrap profileServerBootstrap) { public LwM2MServerBootstrap(LwM2MServerBootstrap bootstrapFromCredential, LwM2MServerBootstrap profileServerBootstrap) {
this.clientPublicKeyOrId = bootstrapFromCredential.getClientPublicKeyOrId(); this.clientPublicKeyOrId = bootstrapFromCredential.getClientPublicKeyOrId();
this.clientSecretKey = bootstrapFromCredential.getClientSecretKey(); this.clientSecretKey = bootstrapFromCredential.getClientSecretKey();
this.serverPublicKey = profileServerBootstrap.getServerPublicKey(); this.serverPublicKey = profileServerBootstrap.getServerPublicKey();
this.clientHoldOffTime = profileServerBootstrap.getClientHoldOffTime(); this.clientHoldOffTime = profileServerBootstrap.getClientHoldOffTime();
this.bootstrapServerAccountTimeout = profileServerBootstrap.getBootstrapServerAccountTimeout(); this.bootstrapServerAccountTimeout = profileServerBootstrap.getBootstrapServerAccountTimeout();
this.host = (profileServerBootstrap.getHost().equals("0.0.0.0")) ? "localhost" : profileServerBootstrap.getHost(); this.host = (profileServerBootstrap.getHost().equals("0.0.0.0")) ? "localhost" : profileServerBootstrap.getHost();
this.port = profileServerBootstrap.getPort(); this.port = profileServerBootstrap.getPort();
this.securityMode = profileServerBootstrap.getSecurityMode(); this.securityHost = (profileServerBootstrap.getSecurityHost().equals("0.0.0.0")) ? "localhost" : profileServerBootstrap.getSecurityHost();
this.serverId = profileServerBootstrap.getServerId(); this.securityPort = profileServerBootstrap.getSecurityPort();
this.bootstrapServerIs = profileServerBootstrap.bootstrapServerIs; this.securityMode = profileServerBootstrap.getSecurityMode();
this.serverId = profileServerBootstrap.getServerId();
this.bootstrapServerIs = profileServerBootstrap.bootstrapServerIs;
} }
} }

15
common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClientContextImpl.java

@ -93,7 +93,12 @@ public class LwM2mClientContextImpl implements LwM2mClientContext {
log.debug("Fetched clients from store: {}", fetchedClients); log.debug("Fetched clients from store: {}", fetchedClients);
fetchedClients.forEach(client -> { fetchedClients.forEach(client -> {
lwM2mClientsByEndpoint.put(client.getEndpoint(), client); lwM2mClientsByEndpoint.put(client.getEndpoint(), client);
updateFetchedClient(nodeId, client); try {
client.lock();
updateFetchedClient(nodeId, client);
} finally {
client.unlock();
}
}); });
} }
@ -161,7 +166,7 @@ public class LwM2mClientContextImpl implements LwM2mClientContext {
this.lwM2mClientsByRegistrationId.put(registration.getId(), client); this.lwM2mClientsByRegistrationId.put(registration.getId(), client);
client.setState(LwM2MClientState.REGISTERED); client.setState(LwM2MClientState.REGISTERED);
onUplink(client); onUplink(client);
if(!compareAndSetSleepFlag(client, false)){ if (!compareAndSetSleepFlag(client, false)) {
clientStore.put(client); clientStore.put(client);
} }
} finally { } finally {
@ -311,7 +316,11 @@ public class LwM2mClientContextImpl implements LwM2mClientContext {
public void update(LwM2mClient client) { public void update(LwM2mClient client) {
client.lock(); client.lock();
try { try {
clientStore.put(client); if (client.getState().equals(LwM2MClientState.REGISTERED)) {
clientStore.put(client);
} else {
log.error("[{}] Client is in invalid state: {}!", client.getEndpoint(), client.getState());
}
} finally { } finally {
client.unlock(); client.unlock();
} }

1
common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/ota/LwM2MClientOtaInfo.java

@ -106,6 +106,7 @@ public abstract class LwM2MClientOtaInfo<Strategy, State, Result> {
public abstract OtaPackageType getType(); public abstract OtaPackageType getType();
@JsonIgnore
public String getTargetPackageId() { public String getTargetPackageId() {
return getPackageId(targetName, targetVersion); return getPackageId(targetName, targetVersion);
} }

13
common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbRedisLwM2MClientStore.java

@ -15,11 +15,13 @@
*/ */
package org.thingsboard.server.transport.lwm2m.server.store; package org.thingsboard.server.transport.lwm2m.server.store;
import lombok.extern.slf4j.Slf4j;
import org.nustaq.serialization.FSTConfiguration; import org.nustaq.serialization.FSTConfiguration;
import org.springframework.data.redis.connection.RedisClusterConnection; import org.springframework.data.redis.connection.RedisClusterConnection;
import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.Cursor; import org.springframework.data.redis.core.Cursor;
import org.springframework.data.redis.core.ScanOptions; import org.springframework.data.redis.core.ScanOptions;
import org.thingsboard.server.transport.lwm2m.server.client.LwM2MClientState;
import org.thingsboard.server.transport.lwm2m.server.client.LwM2mClient; import org.thingsboard.server.transport.lwm2m.server.client.LwM2mClient;
import java.util.ArrayList; import java.util.ArrayList;
@ -27,6 +29,7 @@ import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Set;
@Slf4j
public class TbRedisLwM2MClientStore implements TbLwM2MClientStore { public class TbRedisLwM2MClientStore implements TbLwM2MClientStore {
private static final String CLIENT_EP = "CLIENT#EP#"; private static final String CLIENT_EP = "CLIENT#EP#";
@ -76,9 +79,13 @@ public class TbRedisLwM2MClientStore implements TbLwM2MClientStore {
@Override @Override
public void put(LwM2mClient client) { public void put(LwM2mClient client) {
byte[] clientSerialized = serializer.asByteArray(client); if (client.getState().equals(LwM2MClientState.UNREGISTERED)) {
try (var connection = connectionFactory.getConnection()) { log.error("[{}] Client is in invalid state: {}!", client.getEndpoint(), client.getState(), new Exception());
connection.getSet(getKey(client.getEndpoint()), clientSerialized); } else {
byte[] clientSerialized = serializer.asByteArray(client);
try (var connection = connectionFactory.getConnection()) {
connection.getSet(getKey(client.getEndpoint()), clientSerialized);
}
} }
} }

5
common/transport/mqtt/pom.xml

@ -88,6 +88,11 @@
<artifactId>junit</artifactId> <artifactId>junit</artifactId>
<scope>test</scope> <scope>test</scope>
</dependency> </dependency>
<dependency>
<groupId>org.awaitility</groupId>
<artifactId>awaitility</artifactId>
<scope>test</scope>
</dependency>
<dependency> <dependency>
<groupId>org.mockito</groupId> <groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId> <artifactId>mockito-core</artifactId>

9
common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportContext.java

@ -23,10 +23,15 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.thingsboard.common.util.ThingsBoardExecutors;
import org.thingsboard.server.common.transport.TransportContext; import org.thingsboard.server.common.transport.TransportContext;
import org.thingsboard.server.transport.mqtt.adaptors.JsonMqttAdaptor; import org.thingsboard.server.transport.mqtt.adaptors.JsonMqttAdaptor;
import org.thingsboard.server.transport.mqtt.adaptors.ProtoMqttAdaptor; import org.thingsboard.server.transport.mqtt.adaptors.ProtoMqttAdaptor;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import java.util.concurrent.ExecutorService;
/** /**
* Created by ashvayka on 04.10.18. * Created by ashvayka on 04.10.18.
*/ */
@ -59,4 +64,8 @@ public class MqttTransportContext extends TransportContext {
@Setter @Setter
private SslHandler sslHandler; private SslHandler sslHandler;
@Getter
@Value("${transport.mqtt.msg_queue_size_per_device_limit:100}")
private int messageQueueSizePerDeviceLimit;
} }

65
common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java

@ -123,9 +123,9 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement
private final SslHandler sslHandler; private final SslHandler sslHandler;
private final ConcurrentMap<MqttTopicMatcher, Integer> mqttQoSMap; private final ConcurrentMap<MqttTopicMatcher, Integer> mqttQoSMap;
private final DeviceSessionCtx deviceSessionCtx; final DeviceSessionCtx deviceSessionCtx;
private volatile InetSocketAddress address; volatile InetSocketAddress address;
private volatile GatewaySessionHandler gatewaySessionHandler; volatile GatewaySessionHandler gatewaySessionHandler;
private final ConcurrentHashMap<String, String> otaPackSessions; private final ConcurrentHashMap<String, String> otaPackSessions;
private final ConcurrentHashMap<String, Integer> chunkSizes; private final ConcurrentHashMap<String, Integer> chunkSizes;
@ -164,8 +164,8 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement
} }
} }
private void processMqttMsg(ChannelHandlerContext ctx, MqttMessage msg) { void processMqttMsg(ChannelHandlerContext ctx, MqttMessage msg) {
address = (InetSocketAddress) ctx.channel().remoteAddress(); address = getAddress(ctx);
if (msg.fixedHeader() == null) { if (msg.fixedHeader() == null) {
log.info("[{}:{}] Invalid message received", address.getHostName(), address.getPort()); log.info("[{}:{}] Invalid message received", address.getHostName(), address.getPort());
processDisconnect(ctx); processDisconnect(ctx);
@ -177,10 +177,14 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement
} else if (deviceSessionCtx.isProvisionOnly()) { } else if (deviceSessionCtx.isProvisionOnly()) {
processProvisionSessionMsg(ctx, msg); processProvisionSessionMsg(ctx, msg);
} else { } else {
processRegularSessionMsg(ctx, msg); enqueueRegularSessionMsg(ctx, msg);
} }
} }
InetSocketAddress getAddress(ChannelHandlerContext ctx) {
return (InetSocketAddress) ctx.channel().remoteAddress();
}
private void processProvisionSessionMsg(ChannelHandlerContext ctx, MqttMessage msg) { private void processProvisionSessionMsg(ChannelHandlerContext ctx, MqttMessage msg) {
switch (msg.fixedHeader().messageType()) { switch (msg.fixedHeader().messageType()) {
case PUBLISH: case PUBLISH:
@ -223,7 +227,42 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement
} }
} }
private void processRegularSessionMsg(ChannelHandlerContext ctx, MqttMessage msg) { void enqueueRegularSessionMsg(ChannelHandlerContext ctx, MqttMessage msg) {
final int queueSize = deviceSessionCtx.getMsgQueueSize().incrementAndGet();
if (queueSize > context.getMessageQueueSizePerDeviceLimit()) {
log.warn("Closing current session because msq queue size for device {} exceed limit {} with msgQueueSize counter {} and actual queue size {}",
deviceSessionCtx.getDeviceId(), context.getMessageQueueSizePerDeviceLimit(), queueSize, deviceSessionCtx.getMsgQueue().size());
ctx.close();
return;
}
deviceSessionCtx.getMsgQueue().add(msg);
processMsgQueue(ctx); //Under the normal conditions the msg queue will contain 0 messages. Many messages will be processed on device connect event in separate thread pool
}
void processMsgQueue(ChannelHandlerContext ctx) {
if (!deviceSessionCtx.isConnected()) {
log.trace("[{}][{}] Postpone processing msg due to device is not connected. Msg queue size is {}", sessionId, deviceSessionCtx.getDeviceId(), deviceSessionCtx.getMsgQueue().size());
return;
}
while (!deviceSessionCtx.getMsgQueue().isEmpty()) {
if (deviceSessionCtx.getMsgQueueProcessorLock().tryLock()) {
try {
MqttMessage msg;
while ((msg = deviceSessionCtx.getMsgQueue().poll()) != null) {
deviceSessionCtx.getMsgQueueSize().decrementAndGet();
processRegularSessionMsg(ctx, msg);
}
} finally {
deviceSessionCtx.getMsgQueueProcessorLock().unlock();
}
} else {
return;
}
}
}
void processRegularSessionMsg(ChannelHandlerContext ctx, MqttMessage msg) {
switch (msg.fixedHeader().messageType()) { switch (msg.fixedHeader().messageType()) {
case PUBLISH: case PUBLISH:
processPublish(ctx, (MqttPublishMessage) msg); processPublish(ctx, (MqttPublishMessage) msg);
@ -304,6 +343,7 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement
} }
} catch (RuntimeException | AdaptorException e) { } catch (RuntimeException | AdaptorException e) {
log.warn("[{}] Failed to process publish msg [{}][{}]", sessionId, topicName, msgId, e); log.warn("[{}] Failed to process publish msg [{}][{}]", sessionId, topicName, msgId, e);
ctx.close();
} }
} }
@ -588,7 +628,7 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement
return new MqttMessage(mqttFixedHeader, mqttMessageIdVariableHeader); return new MqttMessage(mqttFixedHeader, mqttMessageIdVariableHeader);
} }
private void processConnect(ChannelHandlerContext ctx, MqttConnectMessage msg) { void processConnect(ChannelHandlerContext ctx, MqttConnectMessage msg) {
log.info("[{}] Processing connect msg for client: {}!", sessionId, msg.payload().clientIdentifier()); log.info("[{}] Processing connect msg for client: {}!", sessionId, msg.payload().clientIdentifier());
String userName = msg.payload().userName(); String userName = msg.payload().userName();
String clientId = msg.payload().clientIdentifier(); String clientId = msg.payload().clientIdentifier();
@ -674,7 +714,7 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement
return null; return null;
} }
private void processDisconnect(ChannelHandlerContext ctx) { void processDisconnect(ChannelHandlerContext ctx) {
ctx.close(); ctx.close();
log.info("[{}] Client disconnected!", sessionId); log.info("[{}] Client disconnected!", sessionId);
doDisconnect(); doDisconnect();
@ -761,6 +801,11 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement
} }
deviceSessionCtx.setDisconnected(); deviceSessionCtx.setDisconnected();
} }
if (!deviceSessionCtx.getMsgQueue().isEmpty()) {
log.warn("doDisconnect for device {} but unprocessed messages {} left in the msg queue", deviceSessionCtx.getDeviceId(), deviceSessionCtx.getMsgQueue().size());
deviceSessionCtx.getMsgQueue().clear();
}
} }
@ -778,7 +823,9 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement
SessionMetaData sessionMetaData = transportService.registerAsyncSession(deviceSessionCtx.getSessionInfo(), MqttTransportHandler.this); SessionMetaData sessionMetaData = transportService.registerAsyncSession(deviceSessionCtx.getSessionInfo(), MqttTransportHandler.this);
checkGatewaySession(sessionMetaData); checkGatewaySession(sessionMetaData);
ctx.writeAndFlush(createMqttConnAckMsg(CONNECTION_ACCEPTED, connectMessage)); ctx.writeAndFlush(createMqttConnAckMsg(CONNECTION_ACCEPTED, connectMessage));
deviceSessionCtx.setConnected(true);
log.info("[{}] Client connected!", sessionId); log.info("[{}] Client connected!", sessionId);
transportService.getCallbackExecutor().execute(() -> processMsgQueue(ctx)); //this callback will execute in Producer worker thread and hard or blocking work have to be submitted to the separate thread.
} }
@Override @Override

20
common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/DeviceSessionCtx.java

@ -18,6 +18,7 @@ package org.thingsboard.server.transport.mqtt.session;
import com.google.protobuf.Descriptors; import com.google.protobuf.Descriptors;
import com.google.protobuf.DynamicMessage; import com.google.protobuf.DynamicMessage;
import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.mqtt.MqttMessage;
import lombok.Getter; import lombok.Getter;
import lombok.Setter; import lombok.Setter;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
@ -35,8 +36,11 @@ import org.thingsboard.server.transport.mqtt.util.MqttTopicFilter;
import org.thingsboard.server.transport.mqtt.util.MqttTopicFilterFactory; import org.thingsboard.server.transport.mqtt.util.MqttTopicFilterFactory;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.ConcurrentMap; import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/** /**
* @author Andrew Shvayka * @author Andrew Shvayka
@ -45,13 +49,23 @@ import java.util.concurrent.atomic.AtomicInteger;
public class DeviceSessionCtx extends MqttDeviceAwareSessionContext { public class DeviceSessionCtx extends MqttDeviceAwareSessionContext {
@Getter @Getter
@Setter
private ChannelHandlerContext channel; private ChannelHandlerContext channel;
@Getter @Getter
private MqttTransportContext context; private final MqttTransportContext context;
private final AtomicInteger msgIdSeq = new AtomicInteger(0); private final AtomicInteger msgIdSeq = new AtomicInteger(0);
@Getter
private final ConcurrentLinkedQueue<MqttMessage> msgQueue = new ConcurrentLinkedQueue<>();
@Getter
private final Lock msgQueueProcessorLock = new ReentrantLock();
@Getter
private final AtomicInteger msgQueueSize = new AtomicInteger(0);
@Getter @Getter
@Setter @Setter
private boolean provisionOnly = false; private boolean provisionOnly = false;
@ -73,10 +87,6 @@ public class DeviceSessionCtx extends MqttDeviceAwareSessionContext {
this.context = context; this.context = context;
} }
public void setChannel(ChannelHandlerContext channel) {
this.channel = channel;
}
public int nextMsgId() { public int nextMsgId() {
return msgIdSeq.incrementAndGet(); return msgIdSeq.incrementAndGet();
} }

1
common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/GatewayDeviceSessionCtx.java

@ -60,6 +60,7 @@ public class GatewayDeviceSessionCtx extends MqttDeviceAwareSessionContext imple
.setDeviceProfileIdLSB(deviceInfo.getDeviceProfileId().getId().getLeastSignificantBits()) .setDeviceProfileIdLSB(deviceInfo.getDeviceProfileId().getId().getLeastSignificantBits())
.build()); .build());
setDeviceInfo(deviceInfo); setDeviceInfo(deviceInfo);
setConnected(true);
setDeviceProfile(deviceProfile); setDeviceProfile(deviceProfile);
this.transportService = transportService; this.transportService = transportService;
} }

35
common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/GatewaySessionHandler.java

@ -34,6 +34,7 @@ import io.netty.handler.codec.mqtt.MqttMessage;
import io.netty.handler.codec.mqtt.MqttPublishMessage; import io.netty.handler.codec.mqtt.MqttPublishMessage;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.util.CollectionUtils; import org.springframework.util.CollectionUtils;
import org.springframework.util.ConcurrentReferenceHashMap;
import org.springframework.util.StringUtils; import org.springframework.util.StringUtils;
import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.transport.TransportService; import org.thingsboard.server.common.transport.TransportService;
@ -66,6 +67,8 @@ import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock; import java.util.concurrent.locks.ReentrantLock;
import static org.springframework.util.ConcurrentReferenceHashMap.ReferenceType;
/** /**
* Created by ashvayka on 19.01.17. * Created by ashvayka on 19.01.17.
*/ */
@ -82,7 +85,7 @@ public class GatewaySessionHandler {
private final UUID sessionId; private final UUID sessionId;
private final ConcurrentMap<String, Lock> deviceCreationLockMap; private final ConcurrentMap<String, Lock> deviceCreationLockMap;
private final ConcurrentMap<String, GatewayDeviceSessionCtx> devices; private final ConcurrentMap<String, GatewayDeviceSessionCtx> devices;
private final ConcurrentMap<String, SettableFuture<GatewayDeviceSessionCtx>> deviceFutures; private final ConcurrentMap<String, ListenableFuture<GatewayDeviceSessionCtx>> deviceFutures;
private final ConcurrentMap<MqttTopicMatcher, Integer> mqttQoSMap; private final ConcurrentMap<MqttTopicMatcher, Integer> mqttQoSMap;
private final ChannelHandlerContext channel; private final ChannelHandlerContext channel;
private final DeviceSessionCtx deviceSessionCtx; private final DeviceSessionCtx deviceSessionCtx;
@ -95,11 +98,15 @@ public class GatewaySessionHandler {
this.sessionId = sessionId; this.sessionId = sessionId;
this.devices = new ConcurrentHashMap<>(); this.devices = new ConcurrentHashMap<>();
this.deviceFutures = new ConcurrentHashMap<>(); this.deviceFutures = new ConcurrentHashMap<>();
this.deviceCreationLockMap = new ConcurrentHashMap<>(); this.deviceCreationLockMap = createWeakMap();
this.mqttQoSMap = deviceSessionCtx.getMqttQoSMap(); this.mqttQoSMap = deviceSessionCtx.getMqttQoSMap();
this.channel = deviceSessionCtx.getChannel(); this.channel = deviceSessionCtx.getChannel();
} }
ConcurrentReferenceHashMap<String, Lock> createWeakMap() {
return new ConcurrentReferenceHashMap<>(16, ReferenceType.WEAK);
}
public void onDeviceConnect(MqttPublishMessage mqttMsg) throws AdaptorException { public void onDeviceConnect(MqttPublishMessage mqttMsg) throws AdaptorException {
if (isJsonPayloadType()) { if (isJsonPayloadType()) {
onDeviceConnectJson(mqttMsg); onDeviceConnectJson(mqttMsg);
@ -228,21 +235,22 @@ public class GatewaySessionHandler {
if (result == null) { if (result == null) {
return getDeviceCreationFuture(deviceName, deviceType); return getDeviceCreationFuture(deviceName, deviceType);
} else { } else {
return toCompletedFuture(result); return Futures.immediateFuture(result);
} }
} finally { } finally {
deviceCreationLock.unlock(); deviceCreationLock.unlock();
} }
} else { } else {
return toCompletedFuture(result); return Futures.immediateFuture(result);
} }
} }
private ListenableFuture<GatewayDeviceSessionCtx> getDeviceCreationFuture(String deviceName, String deviceType) { private ListenableFuture<GatewayDeviceSessionCtx> getDeviceCreationFuture(String deviceName, String deviceType) {
SettableFuture<GatewayDeviceSessionCtx> future = deviceFutures.get(deviceName); final SettableFuture<GatewayDeviceSessionCtx> futureToSet = SettableFuture.create();
if (future == null) { ListenableFuture<GatewayDeviceSessionCtx> future = deviceFutures.putIfAbsent(deviceName, futureToSet);
final SettableFuture<GatewayDeviceSessionCtx> futureToSet = SettableFuture.create(); if (future != null) {
deviceFutures.put(deviceName, futureToSet); return future;
}
try { try {
transportService.process(GetOrCreateDeviceFromGatewayRequestMsg.newBuilder() transportService.process(GetOrCreateDeviceFromGatewayRequestMsg.newBuilder()
.setDeviceName(deviceName) .setDeviceName(deviceName)
@ -282,15 +290,6 @@ public class GatewaySessionHandler {
deviceFutures.remove(deviceName); deviceFutures.remove(deviceName);
throw e; throw e;
} }
} else {
return future;
}
}
private ListenableFuture<GatewayDeviceSessionCtx> toCompletedFuture(GatewayDeviceSessionCtx result) {
SettableFuture<GatewayDeviceSessionCtx> future = SettableFuture.create();
future.set(result);
return future;
} }
private int getMsgId(MqttPublishMessage mqttMsg) { private int getMsgId(MqttPublishMessage mqttMsg) {
@ -353,6 +352,7 @@ public class GatewaySessionHandler {
processPostTelemetryMsg(deviceCtx, postTelemetryMsg, deviceName, msgId); processPostTelemetryMsg(deviceCtx, postTelemetryMsg, deviceName, msgId);
} catch (Throwable e) { } catch (Throwable e) {
log.warn("[{}][{}] Failed to convert telemetry: {}", gateway.getDeviceId(), deviceName, deviceEntry.getValue(), e); log.warn("[{}][{}] Failed to convert telemetry: {}", gateway.getDeviceId(), deviceName, deviceEntry.getValue(), e);
channel.close();
} }
} }
@ -384,6 +384,7 @@ public class GatewaySessionHandler {
processPostTelemetryMsg(deviceCtx, postTelemetryMsg, deviceName, msgId); processPostTelemetryMsg(deviceCtx, postTelemetryMsg, deviceName, msgId);
} catch (Throwable e) { } catch (Throwable e) {
log.warn("[{}][{}] Failed to convert telemetry: {}", gateway.getDeviceId(), deviceName, msg, e); log.warn("[{}][{}] Failed to convert telemetry: {}", gateway.getDeviceId(), deviceName, msg, e);
channel.close();
} }
} }

221
common/transport/mqtt/src/test/java/org/thingsboard/server/transport/mqtt/MqttTransportHandlerTest.java

@ -0,0 +1,221 @@
/**
* Copyright © 2016-2021 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.transport.mqtt;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.EmptyByteBuf;
import io.netty.buffer.PooledByteBufAllocator;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.mqtt.MqttConnectMessage;
import io.netty.handler.codec.mqtt.MqttConnectPayload;
import io.netty.handler.codec.mqtt.MqttConnectVariableHeader;
import io.netty.handler.codec.mqtt.MqttFixedHeader;
import io.netty.handler.codec.mqtt.MqttMessage;
import io.netty.handler.codec.mqtt.MqttMessageType;
import io.netty.handler.codec.mqtt.MqttPublishMessage;
import io.netty.handler.codec.mqtt.MqttPublishVariableHeader;
import io.netty.handler.codec.mqtt.MqttQoS;
import io.netty.handler.ssl.SslHandler;
import lombok.extern.slf4j.Slf4j;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import org.thingsboard.common.util.ThingsBoardThreadFactory;
import java.net.InetSocketAddress;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.contains;
import static org.hamcrest.Matchers.empty;
import static org.hamcrest.Matchers.greaterThan;
import static org.hamcrest.Matchers.is;
import static org.junit.Assert.fail;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.willDoNothing;
import static org.mockito.BDDMockito.willReturn;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
@Slf4j
@RunWith(MockitoJUnitRunner.class)
public class MqttTransportHandlerTest {
public static final int MSG_QUEUE_LIMIT = 10;
public static final InetSocketAddress IP_ADDR = new InetSocketAddress("127.0.0.1", 9876);
public static final int TIMEOUT = 30;
@Mock
MqttTransportContext context;
@Mock
SslHandler sslHandler;
@Mock
ChannelHandlerContext ctx;
AtomicInteger packedId = new AtomicInteger();
ExecutorService executor;
MqttTransportHandler handler;
@Before
public void setUp() throws Exception {
willReturn(MSG_QUEUE_LIMIT).given(context).getMessageQueueSizePerDeviceLimit();
handler = spy(new MqttTransportHandler(context, sslHandler));
willReturn(IP_ADDR).given(handler).getAddress(any());
}
@After
public void tearDown() {
if (executor != null) {
executor.shutdownNow();
}
}
MqttConnectMessage getMqttConnectMessage() {
MqttFixedHeader mqttFixedHeader = new MqttFixedHeader(MqttMessageType.CONNECT, true, MqttQoS.AT_LEAST_ONCE, false, 123);
MqttConnectVariableHeader variableHeader = new MqttConnectVariableHeader("device", packedId.incrementAndGet(), true, true, true, 1, true, false, 60);
MqttConnectPayload payload = new MqttConnectPayload("clientId", "topic", "message".getBytes(StandardCharsets.UTF_8), "username", "password".getBytes(StandardCharsets.UTF_8));
return new MqttConnectMessage(mqttFixedHeader, variableHeader, payload);
}
MqttPublishMessage getMqttPublishMessage() {
MqttFixedHeader mqttFixedHeader = new MqttFixedHeader(MqttMessageType.PUBLISH, true, MqttQoS.AT_LEAST_ONCE, false, 123);
MqttPublishVariableHeader variableHeader = new MqttPublishVariableHeader("v1/gateway/telemetry", packedId.incrementAndGet());
ByteBuf payload = new EmptyByteBuf(new PooledByteBufAllocator());
return new MqttPublishMessage(mqttFixedHeader, variableHeader, payload);
}
@Test
public void givenMessageWithoutFixedHeader_whenProcessMqttMsg_thenProcessDisconnect() {
MqttFixedHeader mqttFixedHeader = null;
MqttMessage msg = new MqttMessage(mqttFixedHeader);
willDoNothing().given(handler).processDisconnect(ctx);
handler.processMqttMsg(ctx, msg);
assertThat(handler.address, is(IP_ADDR));
verify(handler, times(1)).processDisconnect(ctx);
}
@Test
public void givenMqttConnectMessage_whenProcessMqttMsg_thenProcessConnect() {
MqttConnectMessage msg = getMqttConnectMessage();
willDoNothing().given(handler).processConnect(ctx, msg);
handler.processMqttMsg(ctx, msg);
assertThat(handler.address, is(IP_ADDR));
assertThat(handler.deviceSessionCtx.getChannel(), is(ctx));
verify(handler, never()).processDisconnect(any());
verify(handler, times(1)).processConnect(ctx, msg);
}
@Test
public void givenQueueLimit_whenEnqueueRegularSessionMsgOverLimit_thenOK() {
List<MqttPublishMessage> messages = Stream.generate(this::getMqttPublishMessage).limit(MSG_QUEUE_LIMIT).collect(Collectors.toList());
messages.forEach(msg -> handler.enqueueRegularSessionMsg(ctx, msg));
assertThat(handler.deviceSessionCtx.getMsgQueueSize().get(), is(MSG_QUEUE_LIMIT));
assertThat(handler.deviceSessionCtx.getMsgQueue(), contains(messages.toArray()));
}
@Test
public void givenQueueLimit_whenEnqueueRegularSessionMsgOverLimit_thenCtxClose() {
final int limit = MSG_QUEUE_LIMIT + 1;
willDoNothing().given(handler).processMsgQueue(ctx);
List<MqttPublishMessage> messages = Stream.generate(this::getMqttPublishMessage).limit(limit).collect(Collectors.toList());
messages.forEach((msg) -> handler.enqueueRegularSessionMsg(ctx, msg));
assertThat(handler.deviceSessionCtx.getMsgQueueSize().get(), is(limit));
verify(handler, times(limit)).enqueueRegularSessionMsg(any(), any());
verify(handler, times(MSG_QUEUE_LIMIT)).processMsgQueue(any());
verify(ctx, times(1)).close();
}
@Test
public void givenMqttConnectMessageAndPublishImmediately_whenProcessMqttMsg_thenEnqueueRegularSessionMsg() {
givenMqttConnectMessage_whenProcessMqttMsg_thenProcessConnect();
List<MqttPublishMessage> messages = Stream.generate(this::getMqttPublishMessage).limit(MSG_QUEUE_LIMIT).collect(Collectors.toList());
messages.forEach((msg) -> handler.processMqttMsg(ctx, msg));
assertThat(handler.address, is(IP_ADDR));
assertThat(handler.deviceSessionCtx.getChannel(), is(ctx));
assertThat(handler.deviceSessionCtx.isConnected(), is(false));
assertThat(handler.deviceSessionCtx.getMsgQueueSize().get(), is(MSG_QUEUE_LIMIT));
assertThat(handler.deviceSessionCtx.getMsgQueue(), contains(messages.toArray()));
verify(handler, never()).processDisconnect(any());
verify(handler, times(1)).processConnect(any(), any());
verify(handler, times(MSG_QUEUE_LIMIT)).enqueueRegularSessionMsg(any(), any());
verify(handler, never()).processRegularSessionMsg(any(), any());
messages.forEach((msg) -> verify(handler, times(1)).enqueueRegularSessionMsg(ctx, msg));
}
@Test
public void givenMessageQueue_whenProcessMqttMsgConcurrently_thenEnqueueRegularSessionMsg() throws InterruptedException {
//given
assertThat(handler.deviceSessionCtx.isConnected(), is(false));
assertThat(MSG_QUEUE_LIMIT, greaterThan(2));
List<MqttPublishMessage> messages = Stream.generate(this::getMqttPublishMessage).limit(MSG_QUEUE_LIMIT).collect(Collectors.toList());
messages.forEach((msg) -> handler.enqueueRegularSessionMsg(ctx, msg));
willDoNothing().given(handler).processRegularSessionMsg(any(), any());
executor = Executors.newCachedThreadPool(ThingsBoardThreadFactory.forName(getClass().getName()));
CountDownLatch readyLatch = new CountDownLatch(MSG_QUEUE_LIMIT);
CountDownLatch startLatch = new CountDownLatch(1);
CountDownLatch finishLatch = new CountDownLatch(MSG_QUEUE_LIMIT);
Stream.iterate(0, i -> i + 1).limit(MSG_QUEUE_LIMIT).forEach(x ->
executor.submit(() -> {
try {
readyLatch.countDown();
assertThat(startLatch.await(TIMEOUT, TimeUnit.SECONDS), is(true));
handler.processMsgQueue(ctx);
finishLatch.countDown();
} catch (Exception e) {
log.error("Failed to run processMsgQueue", e);
fail("Failed to run processMsgQueue");
}
}));
//when
assertThat(readyLatch.await(TIMEOUT, TimeUnit.SECONDS), is(true));
handler.deviceSessionCtx.setConnected(true);
startLatch.countDown();
assertThat(finishLatch.await(TIMEOUT, TimeUnit.SECONDS), is(true));
//then
assertThat(handler.deviceSessionCtx.getMsgQueueSize().get(), is(0));
assertThat(handler.deviceSessionCtx.getMsgQueue(), empty());
verify(handler, times(MSG_QUEUE_LIMIT)).processRegularSessionMsg(any(), any());
messages.forEach((msg) -> verify(handler, times(1)).processRegularSessionMsg(ctx, msg));
}
}

61
common/transport/mqtt/src/test/java/org/thingsboard/server/transport/mqtt/session/GatewaySessionHandlerTest.java

@ -0,0 +1,61 @@
/**
* Copyright © 2016-2021 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.transport.mqtt.session;
import org.junit.Test;
import java.util.WeakHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import static org.awaitility.Awaitility.await;
import static org.junit.Assert.assertTrue;
import static org.mockito.BDDMockito.willCallRealMethod;
import static org.mockito.Mockito.mock;
public class GatewaySessionHandlerTest {
@Test
public void givenWeakHashMap_WhenGC_thenMapIsEmpty() {
WeakHashMap<String, Lock> map = new WeakHashMap<>();
String deviceName = new String("device"); //constants are static and doesn't affected by GC, so use new instead
map.put(deviceName, new ReentrantLock());
assertTrue(map.containsKey(deviceName));
deviceName = null;
System.gc();
await().atMost(10, TimeUnit.SECONDS).until(() -> !map.containsKey("device"));
}
@Test
public void givenConcurrentReferenceHashMap_WhenGC_thenMapIsEmpty() {
GatewaySessionHandler gsh = mock(GatewaySessionHandler.class);
willCallRealMethod().given(gsh).createWeakMap();
ConcurrentMap<String, Lock> map = gsh.createWeakMap();
map.put("device", new ReentrantLock());
assertTrue(map.containsKey("device"));
System.gc();
await().atMost(10, TimeUnit.SECONDS).until(() -> !map.containsKey("device"));
}
}

1
common/transport/snmp/src/main/java/org/thingsboard/server/transport/snmp/SnmpTransportContext.java

@ -188,6 +188,7 @@ public class SnmpTransportContext extends TransportContext {
deviceSessionContext.setSessionInfo(sessionInfo); deviceSessionContext.setSessionInfo(sessionInfo);
deviceSessionContext.setDeviceInfo(msg.getDeviceInfo()); deviceSessionContext.setDeviceInfo(msg.getDeviceInfo());
deviceSessionContext.setConnected(true);
} else { } else {
log.warn("[{}] Failed to process device auth", deviceSessionContext.getDeviceId()); log.warn("[{}] Failed to process device auth", deviceSessionContext.getDeviceId());
} }

4
common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/TransportService.java

@ -56,6 +56,8 @@ import org.thingsboard.server.gen.transport.TransportProtos.ValidateDeviceLwM2MC
import org.thingsboard.server.gen.transport.TransportProtos.ValidateDeviceTokenRequestMsg; import org.thingsboard.server.gen.transport.TransportProtos.ValidateDeviceTokenRequestMsg;
import org.thingsboard.server.gen.transport.TransportProtos.ValidateDeviceX509CertRequestMsg; import org.thingsboard.server.gen.transport.TransportProtos.ValidateDeviceX509CertRequestMsg;
import java.util.concurrent.ExecutorService;
/** /**
* Created by ashvayka on 04.10.18. * Created by ashvayka on 04.10.18.
*/ */
@ -131,4 +133,6 @@ public interface TransportService {
void log(SessionInfoProto sessionInfo, String msg); void log(SessionInfoProto sessionInfo, String msg);
void notifyAboutUplink(SessionInfoProto sessionInfo, TransportProtos.UplinkNotificationMsg build, TransportServiceCallback<Void> empty); void notifyAboutUplink(SessionInfoProto sessionInfo, TransportProtos.UplinkNotificationMsg build, TransportServiceCallback<Void> empty);
ExecutorService getCallbackExecutor();
} }

5
common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportService.java

@ -1141,4 +1141,9 @@ public class DefaultTransportService implements TransportService {
callback.onError(e); callback.onError(e);
} }
} }
@Override
public ExecutorService getCallbackExecutor() {
return transportCallbackExecutor;
}
} }

2
common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/session/DeviceAwareSessionContext.java

@ -46,6 +46,7 @@ public abstract class DeviceAwareSessionContext implements SessionContext {
@Setter @Setter
private volatile TransportProtos.SessionInfoProto sessionInfo; private volatile TransportProtos.SessionInfoProto sessionInfo;
@Setter
private volatile boolean connected; private volatile boolean connected;
public DeviceId getDeviceId() { public DeviceId getDeviceId() {
@ -54,7 +55,6 @@ public abstract class DeviceAwareSessionContext implements SessionContext {
public void setDeviceInfo(TransportDeviceInfo deviceInfo) { public void setDeviceInfo(TransportDeviceInfo deviceInfo) {
this.deviceInfo = deviceInfo; this.deviceInfo = deviceInfo;
this.connected = true;
this.deviceId = deviceInfo.getDeviceId(); this.deviceId = deviceInfo.getDeviceId();
} }

4
dao/src/main/java/org/thingsboard/server/dao/settings/AdminSettingsServiceImpl.java

@ -52,7 +52,7 @@ public class AdminSettingsServiceImpl implements AdminSettingsService {
public AdminSettings saveAdminSettings(TenantId tenantId, AdminSettings adminSettings) { public AdminSettings saveAdminSettings(TenantId tenantId, AdminSettings adminSettings) {
log.trace("Executing saveAdminSettings [{}]", adminSettings); log.trace("Executing saveAdminSettings [{}]", adminSettings);
adminSettingsValidator.validate(adminSettings, data -> tenantId); adminSettingsValidator.validate(adminSettings, data -> tenantId);
if (adminSettings.getKey().equals("mail") && "".equals(adminSettings.getJsonValue().get("password").asText())) { if(adminSettings.getKey().equals("mail") && !adminSettings.getJsonValue().has("password")) {
AdminSettings mailSettings = findAdminSettingsByKey(tenantId, "mail"); AdminSettings mailSettings = findAdminSettingsByKey(tenantId, "mail");
if (mailSettings != null) { if (mailSettings != null) {
((ObjectNode) adminSettings.getJsonValue()).put("password", mailSettings.getJsonValue().get("password").asText()); ((ObjectNode) adminSettings.getJsonValue()).put("password", mailSettings.getJsonValue().get("password").asText());
@ -61,7 +61,7 @@ public class AdminSettingsServiceImpl implements AdminSettingsService {
return adminSettingsDao.save(tenantId, adminSettings); return adminSettingsDao.save(tenantId, adminSettings);
} }
private DataValidator<AdminSettings> adminSettingsValidator = private DataValidator<AdminSettings> adminSettingsValidator =
new DataValidator<AdminSettings>() { new DataValidator<AdminSettings>() {

2
dao/src/main/java/org/thingsboard/server/dao/sqlts/AbstractChunkedAggregationTimeseriesDao.java

@ -100,7 +100,7 @@ public abstract class AbstractChunkedAggregationTimeseriesDao extends AbstractSq
} }
@Override @Override
public ListenableFuture<Integer> savePartition(TenantId tenantId, EntityId entityId, long tsKvEntryTs, String key, long ttl) { public ListenableFuture<Integer> savePartition(TenantId tenantId, EntityId entityId, long tsKvEntryTs, String key) {
return Futures.immediateFuture(null); return Futures.immediateFuture(null);
} }

2
dao/src/main/java/org/thingsboard/server/dao/sqlts/timescale/TimescaleTimeseriesDao.java

@ -124,7 +124,7 @@ public class TimescaleTimeseriesDao extends AbstractSqlTimeseriesDao implements
} }
@Override @Override
public ListenableFuture<Integer> savePartition(TenantId tenantId, EntityId entityId, long tsKvEntryTs, String key, long ttl) { public ListenableFuture<Integer> savePartition(TenantId tenantId, EntityId entityId, long tsKvEntryTs, String key) {
return Futures.immediateFuture(0); return Futures.immediateFuture(0);
} }

2
dao/src/main/java/org/thingsboard/server/dao/timeseries/BaseTimeseriesService.java

@ -170,7 +170,7 @@ public class BaseTimeseriesService implements TimeseriesService {
if (entityId.getEntityType().equals(EntityType.ENTITY_VIEW)) { if (entityId.getEntityType().equals(EntityType.ENTITY_VIEW)) {
throw new IncorrectParameterException("Telemetry data can't be stored for entity view. Read only"); throw new IncorrectParameterException("Telemetry data can't be stored for entity view. Read only");
} }
futures.add(timeseriesDao.savePartition(tenantId, entityId, tsKvEntry.getTs(), tsKvEntry.getKey(), ttl)); futures.add(timeseriesDao.savePartition(tenantId, entityId, tsKvEntry.getTs(), tsKvEntry.getKey()));
futures.add(Futures.transform(timeseriesLatestDao.saveLatest(tenantId, entityId, tsKvEntry), v -> 0, MoreExecutors.directExecutor())); futures.add(Futures.transform(timeseriesLatestDao.saveLatest(tenantId, entityId, tsKvEntry), v -> 0, MoreExecutors.directExecutor()));
futures.add(timeseriesDao.save(tenantId, entityId, tsKvEntry, ttl)); futures.add(timeseriesDao.save(tenantId, entityId, tsKvEntry, ttl));
} }

7
dao/src/main/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesDao.java

@ -181,11 +181,14 @@ public class CassandraBaseTimeseriesDao extends AbstractCassandraBaseTimeseriesD
} }
@Override @Override
public ListenableFuture<Integer> savePartition(TenantId tenantId, EntityId entityId, long tsKvEntryTs, String key, long ttl) { public ListenableFuture<Integer> savePartition(TenantId tenantId, EntityId entityId, long tsKvEntryTs, String key) {
if (isFixedPartitioning()) { if (isFixedPartitioning()) {
return Futures.immediateFuture(null); return Futures.immediateFuture(null);
} }
ttl = computeTtl(ttl); // DO NOT apply custom TTL to partition, otherwise, short TTL will remove partition too early
// partitions must remain in the DB forever or be removed only by systemTtl
// removal of empty partition is too expensive (we need to scan all data keys for these partitions with ALLOW FILTERING)
long ttl = computeTtl(0);
long partition = toPartitionTs(tsKvEntryTs); long partition = toPartitionTs(tsKvEntryTs);
if (cassandraTsPartitionsCache == null) { if (cassandraTsPartitionsCache == null) {
return doSavePartition(tenantId, entityId, key, ttl, partition); return doSavePartition(tenantId, entityId, key, ttl, partition);

2
dao/src/main/java/org/thingsboard/server/dao/timeseries/TimeseriesDao.java

@ -33,7 +33,7 @@ public interface TimeseriesDao {
ListenableFuture<Integer> save(TenantId tenantId, EntityId entityId, TsKvEntry tsKvEntry, long ttl); ListenableFuture<Integer> save(TenantId tenantId, EntityId entityId, TsKvEntry tsKvEntry, long ttl);
ListenableFuture<Integer> savePartition(TenantId tenantId, EntityId entityId, long tsKvEntryTs, String key, long ttl); ListenableFuture<Integer> savePartition(TenantId tenantId, EntityId entityId, long tsKvEntryTs, String key);
ListenableFuture<Void> remove(TenantId tenantId, EntityId entityId, DeleteTsKvQuery query); ListenableFuture<Void> remove(TenantId tenantId, EntityId entityId, DeleteTsKvQuery query);

4
dao/src/test/java/org/thingsboard/server/dao/nosql/CassandraPartitionsCacheTest.java

@ -100,10 +100,10 @@ public class CassandraPartitionsCacheTest {
long tsKvEntryTs = System.currentTimeMillis(); long tsKvEntryTs = System.currentTimeMillis();
for (int i = 0; i < 50000; i++) { for (int i = 0; i < 50000; i++) {
cassandraBaseTimeseriesDao.savePartition(tenantId, tenantId, tsKvEntryTs, "test" + i, 0); cassandraBaseTimeseriesDao.savePartition(tenantId, tenantId, tsKvEntryTs, "test" + i);
} }
for (int i = 0; i < 60000; i++) { for (int i = 0; i < 60000; i++) {
cassandraBaseTimeseriesDao.savePartition(tenantId, tenantId, tsKvEntryTs, "test" + i, 0); cassandraBaseTimeseriesDao.savePartition(tenantId, tenantId, tsKvEntryTs, "test" + i);
} }
verify(cassandraBaseTimeseriesDao, times(60000)).executeAsyncWrite(any(TenantId.class), any(Statement.class)); verify(cassandraBaseTimeseriesDao, times(60000)).executeAsyncWrite(any(TenantId.class), any(Statement.class));
} }

21
pom.xml

@ -42,13 +42,14 @@
<spring-boot.version>2.3.12.RELEASE</spring-boot.version> <spring-boot.version>2.3.12.RELEASE</spring-boot.version>
<spring.version>5.2.16.RELEASE</spring.version> <spring.version>5.2.16.RELEASE</spring.version>
<spring-redis.version>5.2.11.RELEASE</spring-redis.version> <spring-redis.version>5.2.11.RELEASE</spring-redis.version>
<spring-security.version>5.4.1</spring-security.version> <spring-security.version>5.4.4</spring-security.version>
<spring-data-redis.version>2.4.3</spring-data-redis.version> <spring-data-redis.version>2.4.3</spring-data-redis.version>
<jedis.version>3.3.0</jedis.version> <jedis.version>3.3.0</jedis.version>
<jjwt.version>0.7.0</jjwt.version> <jjwt.version>0.7.0</jjwt.version>
<json-path.version>2.2.0</json-path.version> <json-path.version>2.2.0</json-path.version>
<junit.version>4.12</junit.version> <junit.version>4.12</junit.version>
<jupiter.version>5.7.1</jupiter.version> <jupiter.version>5.7.1</jupiter.version>
<awaitility.version>4.1.0</awaitility.version>
<hamcrest.version>2.2</hamcrest.version> <hamcrest.version>2.2</hamcrest.version>
<slf4j.version>1.7.7</slf4j.version> <slf4j.version>1.7.7</slf4j.version>
<logback.version>1.2.3</logback.version> <logback.version>1.2.3</logback.version>
@ -1382,6 +1383,12 @@
<groupId>io.grpc</groupId> <groupId>io.grpc</groupId>
<artifactId>grpc-netty</artifactId> <artifactId>grpc-netty</artifactId>
<version>${grpc.version}</version> <version>${grpc.version}</version>
<exclusions>
<exclusion>
<groupId>io.netty</groupId>
<artifactId>*</artifactId>
</exclusion>
</exclusions>
</dependency> </dependency>
<dependency> <dependency>
<groupId>io.grpc</groupId> <groupId>io.grpc</groupId>
@ -1437,6 +1444,12 @@
<version>${junit.version}</version> <version>${junit.version}</version>
<scope>test</scope> <scope>test</scope>
</dependency> </dependency>
<dependency>
<groupId>org.awaitility</groupId>
<artifactId>awaitility</artifactId>
<version>${awaitility.version}</version>
<scope>test</scope>
</dependency>
<dependency> <dependency>
<groupId>org.hamcrest</groupId> <groupId>org.hamcrest</groupId>
<artifactId>hamcrest</artifactId> <artifactId>hamcrest</artifactId>
@ -1580,6 +1593,12 @@
<groupId>com.microsoft.azure</groupId> <groupId>com.microsoft.azure</groupId>
<artifactId>azure-servicebus</artifactId> <artifactId>azure-servicebus</artifactId>
<version>${azure-servicebus.version}</version> <version>${azure-servicebus.version}</version>
<exclusions>
<exclusion>
<groupId>io.netty</groupId>
<artifactId>*</artifactId>
</exclusion>
</exclusions>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.passay</groupId> <groupId>org.passay</groupId>

2
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/AlarmState.java

@ -188,7 +188,7 @@ class AlarmState {
setAlarmConditionMetadata(ruleState, metaData); setAlarmConditionMetadata(ruleState, metaData);
TbMsg newMsg = ctx.newMsg(lastMsgQueueName != null ? lastMsgQueueName : ServiceQueue.MAIN, "ALARM", TbMsg newMsg = ctx.newMsg(lastMsgQueueName != null ? lastMsgQueueName : ServiceQueue.MAIN, "ALARM",
originator, msg != null ? msg.getCustomerId() : null, metaData, data); originator, msg != null ? msg.getCustomerId() : null, metaData, data);
ctx.tellNext(newMsg, relationType); ctx.enqueueForTellNext(newMsg, relationType);
} }
protected void setAlarmConditionMetadata(AlarmRuleState ruleState, TbMsgMetaData metaData) { protected void setAlarmConditionMetadata(AlarmRuleState ruleState, TbMsgMetaData metaData) {

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

@ -120,7 +120,7 @@ public class TbSendRPCRequestNode implements TbNode {
ctx.enqueueForTellNext(next, TbRelationTypes.SUCCESS); ctx.enqueueForTellNext(next, TbRelationTypes.SUCCESS);
} else { } else {
TbMsg next = ctx.newMsg(msg.getQueueName(), msg.getType(), msg.getOriginator(), msg.getCustomerId(), msg.getMetaData(), wrap("error", ruleEngineDeviceRpcResponse.getError().get().name())); TbMsg next = ctx.newMsg(msg.getQueueName(), msg.getType(), msg.getOriginator(), msg.getCustomerId(), msg.getMetaData(), wrap("error", ruleEngineDeviceRpcResponse.getError().get().name()));
ctx.tellFailure(next, new RuntimeException(ruleEngineDeviceRpcResponse.getError().get().name())); ctx.enqueueForTellFailure(next, ruleEngineDeviceRpcResponse.getError().get().name());
} }
}); });
ctx.ack(msg); ctx.ack(msg);

30
rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/profile/TbDeviceProfileNodeTest.java

@ -196,7 +196,7 @@ public class TbDeviceProfileNodeTest {
TbMsgDataType.JSON, mapper.writeValueAsString(data), null, null); TbMsgDataType.JSON, mapper.writeValueAsString(data), null, null);
node.onMsg(ctx, msg); node.onMsg(ctx, msg);
verify(ctx).tellSuccess(msg); verify(ctx).tellSuccess(msg);
verify(ctx).tellNext(theMsg, "Alarm Created"); verify(ctx).enqueueForTellNext(theMsg, "Alarm Created");
verify(ctx, Mockito.never()).tellFailure(Mockito.any(), Mockito.any()); verify(ctx, Mockito.never()).tellFailure(Mockito.any(), Mockito.any());
TbMsg theMsg2 = TbMsg.newMsg("ALARM", deviceId, new TbMsgMetaData(), "2"); TbMsg theMsg2 = TbMsg.newMsg("ALARM", deviceId, new TbMsgMetaData(), "2");
@ -207,7 +207,7 @@ public class TbDeviceProfileNodeTest {
TbMsgDataType.JSON, mapper.writeValueAsString(data), null, null); TbMsgDataType.JSON, mapper.writeValueAsString(data), null, null);
node.onMsg(ctx, msg2); node.onMsg(ctx, msg2);
verify(ctx).tellSuccess(msg2); verify(ctx).tellSuccess(msg2);
verify(ctx).tellNext(theMsg2, "Alarm Updated"); verify(ctx).enqueueForTellNext(theMsg2, "Alarm Updated");
} }
@ -289,7 +289,7 @@ public class TbDeviceProfileNodeTest {
node.onMsg(ctx, msg); node.onMsg(ctx, msg);
verify(ctx).tellSuccess(msg); verify(ctx).tellSuccess(msg);
verify(ctx).tellNext(theMsg, "Alarm Created"); verify(ctx).enqueueForTellNext(theMsg, "Alarm Created");
verify(ctx, Mockito.never()).tellFailure(Mockito.any(), Mockito.any()); verify(ctx, Mockito.never()).tellFailure(Mockito.any(), Mockito.any());
} }
@ -376,7 +376,7 @@ public class TbDeviceProfileNodeTest {
node.onMsg(ctx, msg); node.onMsg(ctx, msg);
verify(ctx).tellSuccess(msg); verify(ctx).tellSuccess(msg);
verify(ctx).tellNext(theMsg, "Alarm Created"); verify(ctx).enqueueForTellNext(theMsg, "Alarm Created");
verify(ctx, Mockito.never()).tellFailure(Mockito.any(), Mockito.any()); verify(ctx, Mockito.never()).tellFailure(Mockito.any(), Mockito.any());
} }
@ -445,7 +445,7 @@ public class TbDeviceProfileNodeTest {
node.onMsg(ctx, msg); node.onMsg(ctx, msg);
verify(ctx).tellSuccess(msg); verify(ctx).tellSuccess(msg);
verify(ctx).tellNext(theMsg, "Alarm Created"); verify(ctx).enqueueForTellNext(theMsg, "Alarm Created");
verify(ctx, Mockito.never()).tellFailure(Mockito.any(), Mockito.any()); verify(ctx, Mockito.never()).tellFailure(Mockito.any(), Mockito.any());
} }
@ -554,7 +554,7 @@ public class TbDeviceProfileNodeTest {
node.onMsg(ctx, msg2); node.onMsg(ctx, msg2);
verify(ctx).tellSuccess(msg2); verify(ctx).tellSuccess(msg2);
verify(ctx).tellNext(theMsg, "Alarm Created"); verify(ctx).enqueueForTellNext(theMsg, "Alarm Created");
verify(ctx, Mockito.never()).tellFailure(Mockito.any(), Mockito.any()); verify(ctx, Mockito.never()).tellFailure(Mockito.any(), Mockito.any());
} }
@ -678,7 +678,7 @@ public class TbDeviceProfileNodeTest {
node.onMsg(ctx, msg2); node.onMsg(ctx, msg2);
verify(ctx).tellSuccess(msg2); verify(ctx).tellSuccess(msg2);
verify(ctx).tellNext(theMsg, "Alarm Created"); verify(ctx).enqueueForTellNext(theMsg, "Alarm Created");
verify(ctx, Mockito.never()).tellFailure(Mockito.any(), Mockito.any()); verify(ctx, Mockito.never()).tellFailure(Mockito.any(), Mockito.any());
} }
@ -781,7 +781,7 @@ public class TbDeviceProfileNodeTest {
node.onMsg(ctx, msg2); node.onMsg(ctx, msg2);
verify(ctx).tellSuccess(msg2); verify(ctx).tellSuccess(msg2);
verify(ctx).tellNext(theMsg, "Alarm Created"); verify(ctx).enqueueForTellNext(theMsg, "Alarm Created");
verify(ctx, Mockito.never()).tellFailure(Mockito.any(), Mockito.any()); verify(ctx, Mockito.never()).tellFailure(Mockito.any(), Mockito.any());
} }
@ -897,7 +897,7 @@ public class TbDeviceProfileNodeTest {
node.onMsg(ctx, msg2); node.onMsg(ctx, msg2);
verify(ctx).tellSuccess(msg2); verify(ctx).tellSuccess(msg2);
verify(ctx).tellNext(theMsg, "Alarm Created"); verify(ctx).enqueueForTellNext(theMsg, "Alarm Created");
verify(ctx, Mockito.never()).tellFailure(Mockito.any(), Mockito.any()); verify(ctx, Mockito.never()).tellFailure(Mockito.any(), Mockito.any());
} }
@ -999,7 +999,7 @@ public class TbDeviceProfileNodeTest {
node.onMsg(ctx, msg2); node.onMsg(ctx, msg2);
verify(ctx).tellSuccess(msg2); verify(ctx).tellSuccess(msg2);
verify(ctx).tellNext(theMsg, "Alarm Created"); verify(ctx).enqueueForTellNext(theMsg, "Alarm Created");
verify(ctx, Mockito.never()).tellFailure(Mockito.any(), Mockito.any()); verify(ctx, Mockito.never()).tellFailure(Mockito.any(), Mockito.any());
} }
@ -1082,7 +1082,7 @@ public class TbDeviceProfileNodeTest {
node.onMsg(ctx, msg); node.onMsg(ctx, msg);
verify(ctx).tellSuccess(msg); verify(ctx).tellSuccess(msg);
verify(ctx).tellNext(theMsg, "Alarm Created"); verify(ctx).enqueueForTellNext(theMsg, "Alarm Created");
verify(ctx, Mockito.never()).tellFailure(Mockito.any(), Mockito.any()); verify(ctx, Mockito.never()).tellFailure(Mockito.any(), Mockito.any());
} }
@ -1163,7 +1163,7 @@ public class TbDeviceProfileNodeTest {
node.onMsg(ctx, msg); node.onMsg(ctx, msg);
verify(ctx).tellSuccess(msg); verify(ctx).tellSuccess(msg);
verify(ctx).tellNext(theMsg, "Alarm Created"); verify(ctx).enqueueForTellNext(theMsg, "Alarm Created");
verify(ctx, Mockito.never()).tellFailure(Mockito.any(), Mockito.any()); verify(ctx, Mockito.never()).tellFailure(Mockito.any(), Mockito.any());
} }
@ -1237,7 +1237,7 @@ public class TbDeviceProfileNodeTest {
node.onMsg(ctx, msg); node.onMsg(ctx, msg);
verify(ctx).tellSuccess(msg); verify(ctx).tellSuccess(msg);
verify(ctx).tellNext(theMsg, "Alarm Created"); verify(ctx).enqueueForTellNext(theMsg, "Alarm Created");
verify(ctx, Mockito.never()).tellFailure(Mockito.any(), Mockito.any()); verify(ctx, Mockito.never()).tellFailure(Mockito.any(), Mockito.any());
} }
@ -1321,7 +1321,7 @@ public class TbDeviceProfileNodeTest {
node.onMsg(ctx, msg); node.onMsg(ctx, msg);
verify(ctx).tellSuccess(msg); verify(ctx).tellSuccess(msg);
verify(ctx).tellNext(theMsg, "Alarm Created"); verify(ctx).enqueueForTellNext(theMsg, "Alarm Created");
verify(ctx, Mockito.never()).tellFailure(Mockito.any(), Mockito.any()); verify(ctx, Mockito.never()).tellFailure(Mockito.any(), Mockito.any());
} }
@ -1407,7 +1407,7 @@ public class TbDeviceProfileNodeTest {
node.onMsg(ctx, msg); node.onMsg(ctx, msg);
verify(ctx).tellSuccess(msg); verify(ctx).tellSuccess(msg);
verify(ctx).tellNext(theMsg, "Alarm Created"); verify(ctx).enqueueForTellNext(theMsg, "Alarm Created");
verify(ctx, Mockito.never()).tellFailure(Mockito.any(), Mockito.any()); verify(ctx, Mockito.never()).tellFailure(Mockito.any(), Mockito.any());
} }

1
transport/mqtt/src/main/resources/tb-mqtt-transport.yml

@ -89,6 +89,7 @@ transport:
bind_address: "${MQTT_BIND_ADDRESS:0.0.0.0}" bind_address: "${MQTT_BIND_ADDRESS:0.0.0.0}"
bind_port: "${MQTT_BIND_PORT:1883}" bind_port: "${MQTT_BIND_PORT:1883}"
timeout: "${MQTT_TIMEOUT:10000}" timeout: "${MQTT_TIMEOUT:10000}"
msg_queue_size_per_device_limit: "${MQTT_MSG_QUEUE_SIZE_PER_DEVICE_LIMIT:100}" # messages await in the queue before device connected state. This limit works on low level before TenantProfileLimits mechanism
netty: netty:
leak_detector_level: "${NETTY_LEAK_DETECTOR_LVL:DISABLED}" leak_detector_level: "${NETTY_LEAK_DETECTOR_LVL:DISABLED}"
boss_group_thread_count: "${NETTY_BOSS_GROUP_THREADS:1}" boss_group_thread_count: "${NETTY_BOSS_GROUP_THREADS:1}"

2
ui-ngx/src/app/core/api/widget-api.models.ts

@ -84,6 +84,8 @@ export interface WidgetActionsApi {
entityId?: EntityId, entityName?: string, additionalParams?: any, entityLabel?: string) => void; entityId?: EntityId, entityName?: string, additionalParams?: any, entityLabel?: string) => void;
elementClick: ($event: Event) => void; elementClick: ($event: Event) => void;
getActiveEntityInfo: () => SubscriptionEntityInfo; getActiveEntityInfo: () => SubscriptionEntityInfo;
openDashboardStateInSeparateDialog: (targetDashboardStateId: string, params?: StateParams, dialogTitle?: string,
hideDashboardToolbar?: boolean, dialogWidth?: number, dialogHeight?: number) => void;
} }
export interface AliasInfo { export interface AliasInfo {

16
ui-ngx/src/app/core/http/attribute.service.ts

@ -50,11 +50,17 @@ export class AttributeService {
} }
public deleteEntityTimeseries(entityId: EntityId, timeseries: Array<AttributeData>, deleteAllDataForKeys = false, public deleteEntityTimeseries(entityId: EntityId, timeseries: Array<AttributeData>, deleteAllDataForKeys = false,
config?: RequestConfig): Observable<any> { startTs?: number, endTs?: number, config?: RequestConfig): Observable<any> {
const keys = timeseries.map(attribute => encodeURI(attribute.key)).join(','); const keys = timeseries.map(attribute => encodeURI(attribute.key)).join(',');
return this.http.delete(`/api/plugins/telemetry/${entityId.entityType}/${entityId.id}/timeseries/delete` + let url = `/api/plugins/telemetry/${entityId.entityType}/${entityId.id}/timeseries/delete` +
`?keys=${keys}&deleteAllDataForKeys=${deleteAllDataForKeys}`, `?keys=${keys}&deleteAllDataForKeys=${deleteAllDataForKeys}`;
defaultHttpOptionsFromConfig(config)); if (isDefinedAndNotNull(startTs)) {
url += `&startTs=${startTs}`;
}
if (isDefinedAndNotNull(endTs)) {
url += `&endTs=${endTs}`;
}
return this.http.delete(url, defaultHttpOptionsFromConfig(config));
} }
public saveEntityAttributes(entityId: EntityId, attributeScope: AttributeScope, attributes: Array<AttributeData>, public saveEntityAttributes(entityId: EntityId, attributeScope: AttributeScope, attributes: Array<AttributeData>,
@ -97,7 +103,7 @@ export class AttributeService {
}); });
let deleteEntityTimeseriesObservable: Observable<any>; let deleteEntityTimeseriesObservable: Observable<any>;
if (deleteTimeseries.length) { if (deleteTimeseries.length) {
deleteEntityTimeseriesObservable = this.deleteEntityTimeseries(entityId, deleteTimeseries, true, config); deleteEntityTimeseriesObservable = this.deleteEntityTimeseries(entityId, deleteTimeseries, true, null, null, config);
} else { } else {
deleteEntityTimeseriesObservable = of(null); deleteEntityTimeseriesObservable = of(null);
} }

7
ui-ngx/src/app/modules/home/components/entity/contact-based.component.ts

@ -18,7 +18,7 @@ import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state'; import { AppState } from '@core/core.state';
import { FormBuilder, FormGroup, ValidatorFn, Validators } from '@angular/forms'; import { FormBuilder, FormGroup, ValidatorFn, Validators } from '@angular/forms';
import { ContactBased } from '@shared/models/contact-based.model'; import { ContactBased } from '@shared/models/contact-based.model';
import { AfterViewInit, Directive } from '@angular/core'; import { AfterViewInit, ChangeDetectorRef, Directive } from '@angular/core';
import { POSTAL_CODE_PATTERNS } from '@home/models/contact.models'; import { POSTAL_CODE_PATTERNS } from '@home/models/contact.models';
import { HasId } from '@shared/models/base-data'; import { HasId } from '@shared/models/base-data';
import { EntityComponent } from './entity.component'; import { EntityComponent } from './entity.component';
@ -30,8 +30,9 @@ export abstract class ContactBasedComponent<T extends ContactBased<HasId>> exten
protected constructor(protected store: Store<AppState>, protected constructor(protected store: Store<AppState>,
protected fb: FormBuilder, protected fb: FormBuilder,
protected entityValue: T, protected entityValue: T,
protected entitiesTableConfigValue: EntityTableConfig<T>) { protected entitiesTableConfigValue: EntityTableConfig<T>,
super(store, fb, entityValue, entitiesTableConfigValue); protected cd: ChangeDetectorRef) {
super(store, fb, entityValue, entitiesTableConfigValue, cd);
} }
buildForm(entity: T): FormGroup { buildForm(entity: T): FormGroup {

6
ui-ngx/src/app/modules/home/components/entity/entity.component.ts

@ -17,7 +17,7 @@
import { BaseData, HasId } from '@shared/models/base-data'; import { BaseData, HasId } from '@shared/models/base-data';
import { FormBuilder, FormGroup } from '@angular/forms'; import { FormBuilder, FormGroup } from '@angular/forms';
import { PageComponent } from '@shared/components/page.component'; import { PageComponent } from '@shared/components/page.component';
import { Directive, EventEmitter, Input, OnInit, Output } from '@angular/core'; import { ChangeDetectorRef, Directive, EventEmitter, Input, OnInit, Output } from '@angular/core';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state'; import { AppState } from '@core/core.state';
import { EntityAction } from '@home/models/entity/entity-component.models'; import { EntityAction } from '@home/models/entity/entity-component.models';
@ -50,6 +50,7 @@ export abstract class EntityComponent<T extends BaseData<HasId>,
@Input() @Input()
set isEdit(isEdit: boolean) { set isEdit(isEdit: boolean) {
this.isEditValue = isEdit; this.isEditValue = isEdit;
this.cd.markForCheck();
this.updateFormState(); this.updateFormState();
} }
@ -80,7 +81,8 @@ export abstract class EntityComponent<T extends BaseData<HasId>,
protected constructor(protected store: Store<AppState>, protected constructor(protected store: Store<AppState>,
protected fb: FormBuilder, protected fb: FormBuilder,
protected entityValue: T, protected entityValue: T,
protected entitiesTableConfigValue: C) { protected entitiesTableConfigValue: C,
protected cd: ChangeDetectorRef) {
super(store); super(store);
this.entityForm = this.buildForm(this.entityValue); this.entityForm = this.buildForm(this.entityValue);
} }

7
ui-ngx/src/app/modules/home/components/profile/device-profile.component.ts

@ -14,7 +14,7 @@
/// limitations under the License. /// limitations under the License.
/// ///
import { Component, Inject, Input, Optional } from '@angular/core'; import { ChangeDetectorRef, Component, Inject, Input, Optional } from '@angular/core';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state'; import { AppState } from '@core/core.state';
import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { FormBuilder, FormGroup, Validators } from '@angular/forms';
@ -77,8 +77,9 @@ export class DeviceProfileComponent extends EntityComponent<DeviceProfile> {
protected translate: TranslateService, protected translate: TranslateService,
@Optional() @Inject('entity') protected entityValue: DeviceProfile, @Optional() @Inject('entity') protected entityValue: DeviceProfile,
@Optional() @Inject('entitiesTableConfig') protected entitiesTableConfigValue: EntityTableConfig<DeviceProfile>, @Optional() @Inject('entitiesTableConfig') protected entitiesTableConfigValue: EntityTableConfig<DeviceProfile>,
protected fb: FormBuilder) { protected fb: FormBuilder,
super(store, fb, entityValue, entitiesTableConfigValue); protected cd: ChangeDetectorRef) {
super(store, fb, entityValue, entitiesTableConfigValue, cd);
} }
hideDelete() { hideDelete() {

7
ui-ngx/src/app/modules/home/components/profile/tenant-profile.component.ts

@ -14,7 +14,7 @@
/// limitations under the License. /// limitations under the License.
/// ///
import { Component, Inject, Input, Optional } from '@angular/core'; import { ChangeDetectorRef, Component, Inject, Input, Optional } from '@angular/core';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state'; import { AppState } from '@core/core.state';
import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { FormBuilder, FormGroup, Validators } from '@angular/forms';
@ -43,8 +43,9 @@ export class TenantProfileComponent extends EntityComponent<TenantProfile> {
protected translate: TranslateService, protected translate: TranslateService,
@Optional() @Inject('entity') protected entityValue: TenantProfile, @Optional() @Inject('entity') protected entityValue: TenantProfile,
@Optional() @Inject('entitiesTableConfig') protected entitiesTableConfigValue: EntityTableConfig<TenantProfile>, @Optional() @Inject('entitiesTableConfig') protected entitiesTableConfigValue: EntityTableConfig<TenantProfile>,
protected fb: FormBuilder) { protected fb: FormBuilder,
super(store, fb, entityValue, entitiesTableConfigValue); protected cd: ChangeDetectorRef) {
super(store, fb, entityValue, entitiesTableConfigValue, cd);
} }
hideDelete() { hideDelete() {

55
ui-ngx/src/app/modules/home/components/widget/widget.component.ts

@ -284,7 +284,8 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
getActionDescriptors: this.getActionDescriptors.bind(this), getActionDescriptors: this.getActionDescriptors.bind(this),
handleWidgetAction: this.handleWidgetAction.bind(this), handleWidgetAction: this.handleWidgetAction.bind(this),
elementClick: this.elementClick.bind(this), elementClick: this.elementClick.bind(this),
getActiveEntityInfo: this.getActiveEntityInfo.bind(this) getActiveEntityInfo: this.getActiveEntityInfo.bind(this),
openDashboardStateInSeparateDialog: this.openDashboardStateInSeparateDialog.bind(this)
}; };
this.widgetContext.customHeaderActions = []; this.widgetContext.customHeaderActions = [];
@ -1025,7 +1026,8 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
this.updateEntityParams(params, targetEntityParamName, targetEntityId, entityName, entityLabel); this.updateEntityParams(params, targetEntityParamName, targetEntityId, entityName, entityLabel);
if (type === WidgetActionType.openDashboardState) { if (type === WidgetActionType.openDashboardState) {
if (descriptor.openInSeparateDialog) { if (descriptor.openInSeparateDialog) {
this.openDashboardStateInDialog(descriptor, entityId, entityName, additionalParams, entityLabel); this.openDashboardStateInSeparateDialog(descriptor.targetDashboardStateId, params, descriptor.dialogTitle,
descriptor.dialogHideDashboardToolbar, descriptor.dialogWidth, descriptor.dialogHeight);
} else { } else {
this.widgetContext.stateController.openState(targetDashboardStateId, params, descriptor.openRightLayout); this.widgetContext.stateController.openState(targetDashboardStateId, params, descriptor.openRightLayout);
} }
@ -1276,22 +1278,15 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
} }
} }
private openDashboardStateInDialog(descriptor: WidgetActionDescriptor, private openDashboardStateInSeparateDialog(targetDashboardStateId: string, params?: StateParams, dialogTitle?: string,
entityId?: EntityId, entityName?: string, additionalParams?: any, entityLabel?: string) { hideDashboardToolbar = true, dialogWidth?: number, dialogHeight?: number) {
const dashboard = deepClone(this.widgetContext.stateController.dashboardCtrl.dashboardCtx.getDashboard()); const dashboard = deepClone(this.widgetContext.stateController.dashboardCtrl.dashboardCtx.getDashboard());
const stateObject: StateObject = {}; const stateObject: StateObject = {};
stateObject.params = {}; stateObject.params = params;
const targetEntityParamName = descriptor.stateEntityParamName;
const targetDashboardStateId = descriptor.targetDashboardStateId;
let targetEntityId: EntityId;
if (descriptor.setEntityId) {
targetEntityId = entityId;
}
this.updateEntityParams(stateObject.params, targetEntityParamName, targetEntityId, entityName, entityLabel);
if (targetDashboardStateId) { if (targetDashboardStateId) {
stateObject.id = targetDashboardStateId; stateObject.id = targetDashboardStateId;
} }
let title = descriptor.dialogTitle; let title = dialogTitle;
if (!title) { if (!title) {
if (targetDashboardStateId && dashboard.configuration.states) { if (targetDashboardStateId && dashboard.configuration.states) {
const dashboardState = dashboard.configuration.states[targetDashboardStateId]; const dashboardState = dashboard.configuration.states[targetDashboardStateId];
@ -1304,7 +1299,6 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
title = dashboard.title; title = dashboard.title;
} }
title = this.utils.customTranslation(title, title); title = this.utils.customTranslation(title, title);
const params = stateObject.params;
const paramsEntityName = params && params.entityName ? params.entityName : ''; const paramsEntityName = params && params.entityName ? params.entityName : '';
const paramsEntityLabel = params && params.entityLabel ? params.entityLabel : ''; const paramsEntityLabel = params && params.entityLabel ? params.entityLabel : '';
title = insertVariable(title, 'entityName', paramsEntityName); title = insertVariable(title, 'entityName', paramsEntityName);
@ -1324,28 +1318,27 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
dashboard, dashboard,
state: objToBase64([ stateObject ]), state: objToBase64([ stateObject ]),
title, title,
hideToolbar: descriptor.dialogHideDashboardToolbar, hideToolbar: hideDashboardToolbar,
width: descriptor.dialogWidth, width: dialogWidth,
height: descriptor.dialogHeight height: dialogHeight
} }
}); });
} }
private elementClick($event: Event) { private elementClick($event: Event) {
const e = ($event.target || $event.srcElement) as Element; const elementClicked = ($event.target || $event.srcElement) as Element;
if (e.id) { const descriptors = this.getActionDescriptors('elementClick');
const descriptors = this.getActionDescriptors('elementClick'); if (descriptors.length) {
if (descriptors.length) { const idsList = descriptors.map(descriptor => `#${descriptor.name}`).join(',');
descriptors.forEach((descriptor) => { const targetElement = $(elementClicked).closest(idsList, this.widgetContext.$container[0]);
if (descriptor.name === e.id) { if (targetElement.length && targetElement[0].id) {
$event.stopPropagation(); $event.stopPropagation();
const entityInfo = this.getActiveEntityInfo(); const descriptor = descriptors.find(descriptorInfo => descriptorInfo.name === targetElement[0].id);
const entityId = entityInfo ? entityInfo.entityId : null; const entityInfo = this.getActiveEntityInfo();
const entityName = entityInfo ? entityInfo.entityName : null; const entityId = entityInfo ? entityInfo.entityId : null;
const entityLabel = entityInfo && entityInfo.entityLabel ? entityInfo.entityLabel : null; const entityName = entityInfo ? entityInfo.entityName : null;
this.handleWidgetAction($event, descriptor, entityId, entityName, null, entityLabel); const entityLabel = entityInfo && entityInfo.entityLabel ? entityInfo.entityLabel : null;
} this.handleWidgetAction($event, descriptor, entityId, entityName, null, entityLabel);
});
} }
} }
} }

7
ui-ngx/src/app/modules/home/pages/admin/mail-server.component.html

@ -39,7 +39,7 @@
</mat-form-field> </mat-form-field>
<mat-form-field class="mat-block"> <mat-form-field class="mat-block">
<mat-label translate>admin.smtp-protocol</mat-label> <mat-label translate>admin.smtp-protocol</mat-label>
<mat-select matInput formControlName="smtpProtocol"> <mat-select formControlName="smtpProtocol">
<mat-option *ngFor="let protocol of smtpProtocols" [value]="protocol"> <mat-option *ngFor="let protocol of smtpProtocols" [value]="protocol">
{{protocol.toUpperCase()}} {{protocol.toUpperCase()}}
</mat-option> </mat-option>
@ -127,7 +127,10 @@
<input matInput formControlName="username" placeholder="{{ 'common.enter-username' | translate }}" <input matInput formControlName="username" placeholder="{{ 'common.enter-username' | translate }}"
autocomplete="new-username"/> autocomplete="new-username"/>
</mat-form-field> </mat-form-field>
<mat-form-field class="mat-block"> <mat-checkbox *ngIf="showChangePassword" formControlName="changePassword" style="padding-bottom: 16px;">
{{ 'admin.change-password' | translate }}
</mat-checkbox>
<mat-form-field class="mat-block" *ngIf="mailSettings.get('changePassword').value || !showChangePassword">
<mat-label translate>common.password</mat-label> <mat-label translate>common.password</mat-label>
<input matInput formControlName="password" type="password" <input matInput formControlName="password" type="password"
placeholder="{{ 'common.enter-password' | translate }}" autocomplete="new-password"/> placeholder="{{ 'common.enter-password' | translate }}" autocomplete="new-password"/>

53
ui-ngx/src/app/modules/home/pages/admin/mail-server.component.ts

@ -14,7 +14,7 @@
/// limitations under the License. /// limitations under the License.
/// ///
import { Component, OnInit } from '@angular/core'; import { Component, OnDestroy, OnInit } from '@angular/core';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state'; import { AppState } from '@core/core.state';
import { PageComponent } from '@shared/components/page.component'; import { PageComponent } from '@shared/components/page.component';
@ -25,21 +25,26 @@ import { AdminService } from '@core/http/admin.service';
import { ActionNotificationShow } from '@core/notification/notification.actions'; import { ActionNotificationShow } from '@core/notification/notification.actions';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { HasConfirmForm } from '@core/guards/confirm-on-exit.guard'; import { HasConfirmForm } from '@core/guards/confirm-on-exit.guard';
import { isString } from '@core/utils'; import { isDefinedAndNotNull, isString } from '@core/utils';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
@Component({ @Component({
selector: 'tb-mail-server', selector: 'tb-mail-server',
templateUrl: './mail-server.component.html', templateUrl: './mail-server.component.html',
styleUrls: ['./mail-server.component.scss', './settings-card.scss'] styleUrls: ['./mail-server.component.scss', './settings-card.scss']
}) })
export class MailServerComponent extends PageComponent implements OnInit, HasConfirmForm { export class MailServerComponent extends PageComponent implements OnInit, OnDestroy, HasConfirmForm {
mailSettings: FormGroup; mailSettings: FormGroup;
adminSettings: AdminSettings<MailServerSettings>; adminSettings: AdminSettings<MailServerSettings>;
smtpProtocols = ['smtp', 'smtps']; smtpProtocols = ['smtp', 'smtps'];
showChangePassword = false;
tlsVersions = ['TLSv1', 'TLSv1.1', 'TLSv1.2', 'TLSv1.3']; tlsVersions = ['TLSv1', 'TLSv1.1', 'TLSv1.2', 'TLSv1.3'];
private destroy$ = new Subject();
constructor(protected store: Store<AppState>, constructor(protected store: Store<AppState>,
private router: Router, private router: Router,
private adminService: AdminService, private adminService: AdminService,
@ -56,12 +61,22 @@ export class MailServerComponent extends PageComponent implements OnInit, HasCon
if (this.adminSettings.jsonValue && isString(this.adminSettings.jsonValue.enableTls)) { if (this.adminSettings.jsonValue && isString(this.adminSettings.jsonValue.enableTls)) {
this.adminSettings.jsonValue.enableTls = (this.adminSettings.jsonValue.enableTls as any) === 'true'; this.adminSettings.jsonValue.enableTls = (this.adminSettings.jsonValue.enableTls as any) === 'true';
} }
this.showChangePassword =
isDefinedAndNotNull(this.adminSettings.jsonValue.showChangePassword) ? this.adminSettings.jsonValue.showChangePassword : true ;
delete this.adminSettings.jsonValue.showChangePassword;
this.mailSettings.reset(this.adminSettings.jsonValue); this.mailSettings.reset(this.adminSettings.jsonValue);
this.enableMailPassword(!this.showChangePassword);
this.enableProxyChanged(); this.enableProxyChanged();
} }
); );
} }
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
super.ngOnDestroy();
}
buildMailServerSettingsForm() { buildMailServerSettingsForm() {
this.mailSettings = this.fb.group({ this.mailSettings = this.fb.group({
mailFrom: ['', [Validators.required]], mailFrom: ['', [Validators.required]],
@ -81,14 +96,23 @@ export class MailServerComponent extends PageComponent implements OnInit, HasCon
proxyUser: [''], proxyUser: [''],
proxyPassword: [''], proxyPassword: [''],
username: [''], username: [''],
changePassword: [false],
password: [''] password: ['']
}); });
this.registerDisableOnLoadFormControl(this.mailSettings.get('smtpProtocol')); this.registerDisableOnLoadFormControl(this.mailSettings.get('smtpProtocol'));
this.registerDisableOnLoadFormControl(this.mailSettings.get('enableTls')); this.registerDisableOnLoadFormControl(this.mailSettings.get('enableTls'));
this.registerDisableOnLoadFormControl(this.mailSettings.get('enableProxy')); this.registerDisableOnLoadFormControl(this.mailSettings.get('enableProxy'));
this.mailSettings.get('enableProxy').valueChanges.subscribe(() => { this.registerDisableOnLoadFormControl(this.mailSettings.get('changePassword'));
this.mailSettings.get('enableProxy').valueChanges.pipe(
takeUntil(this.destroy$)
).subscribe(() => {
this.enableProxyChanged(); this.enableProxyChanged();
}); });
this.mailSettings.get('changePassword').valueChanges.pipe(
takeUntil(this.destroy$)
).subscribe((value) => {
this.enableMailPassword(value);
});
} }
enableProxyChanged(): void { enableProxyChanged(): void {
@ -102,8 +126,16 @@ export class MailServerComponent extends PageComponent implements OnInit, HasCon
} }
} }
enableMailPassword(enable: boolean) {
if (enable) {
this.mailSettings.get('password').enable({emitEvent: false});
} else {
this.mailSettings.get('password').disable({emitEvent: false});
}
}
sendTestMail(): void { sendTestMail(): void {
this.adminSettings.jsonValue = {...this.adminSettings.jsonValue, ...this.mailSettings.value}; this.adminSettings.jsonValue = {...this.adminSettings.jsonValue, ...this.mailSettingsFormValue};
this.adminService.sendTestMail(this.adminSettings).subscribe( this.adminService.sendTestMail(this.adminSettings).subscribe(
() => { () => {
this.store.dispatch(new ActionNotificationShow({ message: this.translate.instant('admin.test-mail-sent'), this.store.dispatch(new ActionNotificationShow({ message: this.translate.instant('admin.test-mail-sent'),
@ -113,13 +145,11 @@ export class MailServerComponent extends PageComponent implements OnInit, HasCon
} }
save(): void { save(): void {
this.adminSettings.jsonValue = {...this.adminSettings.jsonValue, ...this.mailSettings.value}; this.adminSettings.jsonValue = {...this.adminSettings.jsonValue, ...this.mailSettingsFormValue};
this.adminService.saveAdminSettings(this.adminSettings).subscribe( this.adminService.saveAdminSettings(this.adminSettings).subscribe(
(adminSettings) => { (adminSettings) => {
if (!adminSettings.jsonValue.password) {
adminSettings.jsonValue.password = this.mailSettings.value.password;
}
this.adminSettings = adminSettings; this.adminSettings = adminSettings;
this.showChangePassword = true;
this.mailSettings.reset(this.adminSettings.jsonValue); this.mailSettings.reset(this.adminSettings.jsonValue);
} }
); );
@ -129,4 +159,9 @@ export class MailServerComponent extends PageComponent implements OnInit, HasCon
return this.mailSettings; return this.mailSettings;
} }
private get mailSettingsFormValue(): MailServerSettings {
const formValue = this.mailSettings.value;
delete formValue.changePassword;
return formValue;
}
} }

11
ui-ngx/src/app/modules/home/pages/admin/resource/resources-library.component.ts

@ -14,7 +14,7 @@
/// limitations under the License. /// limitations under the License.
/// ///
import { Component, Inject, OnDestroy, OnInit } from '@angular/core'; import { ChangeDetectorRef, Component, Inject, OnDestroy, OnInit } from '@angular/core';
import { Subject } from 'rxjs'; import { Subject } from 'rxjs';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state'; import { AppState } from '@core/core.state';
@ -30,7 +30,7 @@ import {
ResourceTypeTranslationMap ResourceTypeTranslationMap
} from '@shared/models/resource.models'; } from '@shared/models/resource.models';
import { pairwise, startWith, takeUntil } from 'rxjs/operators'; import { pairwise, startWith, takeUntil } from 'rxjs/operators';
import { ActionNotificationShow } from "@core/notification/notification.actions"; import { ActionNotificationShow } from '@core/notification/notification.actions';
@Component({ @Component({
selector: 'tb-resources-library', selector: 'tb-resources-library',
@ -48,8 +48,9 @@ export class ResourcesLibraryComponent extends EntityComponent<Resource> impleme
protected translate: TranslateService, protected translate: TranslateService,
@Inject('entity') protected entityValue: Resource, @Inject('entity') protected entityValue: Resource,
@Inject('entitiesTableConfig') protected entitiesTableConfigValue: EntityTableConfig<Resource>, @Inject('entitiesTableConfig') protected entitiesTableConfigValue: EntityTableConfig<Resource>,
public fb: FormBuilder) { public fb: FormBuilder,
super(store, fb, entityValue, entitiesTableConfigValue); protected cd: ChangeDetectorRef) {
super(store, fb, entityValue, entitiesTableConfigValue, cd);
} }
ngOnInit() { ngOnInit() {
@ -102,7 +103,7 @@ export class ResourcesLibraryComponent extends EntityComponent<Resource> impleme
if (this.isAdd) { if (this.isAdd) {
form.addControl('data', this.fb.control(null, Validators.required)); form.addControl('data', this.fb.control(null, Validators.required));
} }
return form return form;
} }
updateForm(entity: Resource) { updateForm(entity: Resource) {

7
ui-ngx/src/app/modules/home/pages/asset/asset.component.ts

@ -14,7 +14,7 @@
/// limitations under the License. /// limitations under the License.
/// ///
import { Component, Inject } from '@angular/core'; import { ChangeDetectorRef, Component, Inject } from '@angular/core';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state'; import { AppState } from '@core/core.state';
import { EntityComponent } from '../../components/entity/entity.component'; import { EntityComponent } from '../../components/entity/entity.component';
@ -41,8 +41,9 @@ export class AssetComponent extends EntityComponent<AssetInfo> {
protected translate: TranslateService, protected translate: TranslateService,
@Inject('entity') protected entityValue: AssetInfo, @Inject('entity') protected entityValue: AssetInfo,
@Inject('entitiesTableConfig') protected entitiesTableConfigValue: EntityTableConfig<AssetInfo>, @Inject('entitiesTableConfig') protected entitiesTableConfigValue: EntityTableConfig<AssetInfo>,
public fb: FormBuilder) { public fb: FormBuilder,
super(store, fb, entityValue, entitiesTableConfigValue); protected cd: ChangeDetectorRef) {
super(store, fb, entityValue, entitiesTableConfigValue, cd);
} }
ngOnInit() { ngOnInit() {

7
ui-ngx/src/app/modules/home/pages/customer/customer.component.ts

@ -14,7 +14,7 @@
/// limitations under the License. /// limitations under the License.
/// ///
import { Component, Inject } from '@angular/core'; import { ChangeDetectorRef, Component, Inject } from '@angular/core';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state'; import { AppState } from '@core/core.state';
import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { FormBuilder, FormGroup, Validators } from '@angular/forms';
@ -42,8 +42,9 @@ export class CustomerComponent extends ContactBasedComponent<Customer> {
protected translate: TranslateService, protected translate: TranslateService,
@Inject('entity') protected entityValue: Customer, @Inject('entity') protected entityValue: Customer,
@Inject('entitiesTableConfig') protected entitiesTableConfigValue: EntityTableConfig<Customer>, @Inject('entitiesTableConfig') protected entitiesTableConfigValue: EntityTableConfig<Customer>,
protected fb: FormBuilder) { protected fb: FormBuilder,
super(store, fb, entityValue, entitiesTableConfigValue); protected cd: ChangeDetectorRef) {
super(store, fb, entityValue, entitiesTableConfigValue, cd);
} }
hideDelete() { hideDelete() {

7
ui-ngx/src/app/modules/home/pages/dashboard/dashboard-form.component.ts

@ -14,7 +14,7 @@
/// limitations under the License. /// limitations under the License.
/// ///
import { Component, Inject } from '@angular/core'; import { ChangeDetectorRef, Component, Inject } from '@angular/core';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state'; import { AppState } from '@core/core.state';
import { EntityComponent } from '../../components/entity/entity.component'; import { EntityComponent } from '../../components/entity/entity.component';
@ -49,8 +49,9 @@ export class DashboardFormComponent extends EntityComponent<Dashboard> {
private dashboardService: DashboardService, private dashboardService: DashboardService,
@Inject('entity') protected entityValue: Dashboard, @Inject('entity') protected entityValue: Dashboard,
@Inject('entitiesTableConfig') protected entitiesTableConfigValue: EntityTableConfig<Dashboard>, @Inject('entitiesTableConfig') protected entitiesTableConfigValue: EntityTableConfig<Dashboard>,
public fb: FormBuilder) { public fb: FormBuilder,
super(store, fb, entityValue, entitiesTableConfigValue); protected cd: ChangeDetectorRef) {
super(store, fb, entityValue, entitiesTableConfigValue, cd);
} }
ngOnInit() { ngOnInit() {

7
ui-ngx/src/app/modules/home/pages/device/device.component.ts

@ -14,7 +14,7 @@
/// limitations under the License. /// limitations under the License.
/// ///
import { Component, Inject } from '@angular/core'; import { ChangeDetectorRef, Component, Inject } from '@angular/core';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state'; import { AppState } from '@core/core.state';
import { EntityComponent } from '../../components/entity/entity.component'; import { EntityComponent } from '../../components/entity/entity.component';
@ -56,8 +56,9 @@ export class DeviceComponent extends EntityComponent<DeviceInfo> {
protected translate: TranslateService, protected translate: TranslateService,
@Inject('entity') protected entityValue: DeviceInfo, @Inject('entity') protected entityValue: DeviceInfo,
@Inject('entitiesTableConfig') protected entitiesTableConfigValue: EntityTableConfig<DeviceInfo>, @Inject('entitiesTableConfig') protected entitiesTableConfigValue: EntityTableConfig<DeviceInfo>,
public fb: FormBuilder) { public fb: FormBuilder,
super(store, fb, entityValue, entitiesTableConfigValue); protected cd: ChangeDetectorRef) {
super(store, fb, entityValue, entitiesTableConfigValue, cd);
} }
ngOnInit() { ngOnInit() {

7
ui-ngx/src/app/modules/home/pages/edge/edge.component.ts

@ -14,7 +14,7 @@
/// limitations under the License. /// limitations under the License.
/// ///
import { Component, Inject } from '@angular/core'; import { ChangeDetectorRef, Component, Inject } from '@angular/core';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state'; import { AppState } from '@core/core.state';
import { EntityComponent } from '@home/components/entity/entity.component'; import { EntityComponent } from '@home/components/entity/entity.component';
@ -42,8 +42,9 @@ export class EdgeComponent extends EntityComponent<EdgeInfo> {
protected translate: TranslateService, protected translate: TranslateService,
@Inject('entity') protected entityValue: EdgeInfo, @Inject('entity') protected entityValue: EdgeInfo,
@Inject('entitiesTableConfig') protected entitiesTableConfigValue: EntityTableConfig<EdgeInfo>, @Inject('entitiesTableConfig') protected entitiesTableConfigValue: EntityTableConfig<EdgeInfo>,
public fb: FormBuilder) { public fb: FormBuilder,
super(store, fb, entityValue, entitiesTableConfigValue); protected cd: ChangeDetectorRef) {
super(store, fb, entityValue, entitiesTableConfigValue, cd);
} }
ngOnInit() { ngOnInit() {

7
ui-ngx/src/app/modules/home/pages/entity-view/entity-view.component.ts

@ -14,7 +14,7 @@
/// limitations under the License. /// limitations under the License.
/// ///
import { Component, Inject } from '@angular/core'; import { ChangeDetectorRef, Component, Inject } from '@angular/core';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state'; import { AppState } from '@core/core.state';
import { EntityComponent } from '../../components/entity/entity.component'; import { EntityComponent } from '../../components/entity/entity.component';
@ -53,8 +53,9 @@ export class EntityViewComponent extends EntityComponent<EntityViewInfo> {
protected translate: TranslateService, protected translate: TranslateService,
@Inject('entity') protected entityValue: EntityViewInfo, @Inject('entity') protected entityValue: EntityViewInfo,
@Inject('entitiesTableConfig') protected entitiesTableConfigValue: EntityTableConfig<EntityViewInfo>, @Inject('entitiesTableConfig') protected entitiesTableConfigValue: EntityTableConfig<EntityViewInfo>,
public fb: FormBuilder) { public fb: FormBuilder,
super(store, fb, entityValue, entitiesTableConfigValue); protected cd: ChangeDetectorRef) {
super(store, fb, entityValue, entitiesTableConfigValue, cd);
} }
ngOnInit() { ngOnInit() {

7
ui-ngx/src/app/modules/home/pages/ota-update/ota-update.component.ts

@ -14,7 +14,7 @@
/// limitations under the License. /// limitations under the License.
/// ///
import { Component, Inject, OnDestroy, OnInit } from '@angular/core'; import { ChangeDetectorRef, Component, Inject, OnDestroy, OnInit } from '@angular/core';
import { combineLatest, Subject } from 'rxjs'; import { combineLatest, Subject } from 'rxjs';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state'; import { AppState } from '@core/core.state';
@ -50,8 +50,9 @@ export class OtaUpdateComponent extends EntityComponent<OtaPackage> implements O
protected translate: TranslateService, protected translate: TranslateService,
@Inject('entity') protected entityValue: OtaPackage, @Inject('entity') protected entityValue: OtaPackage,
@Inject('entitiesTableConfig') protected entitiesTableConfigValue: EntityTableConfig<OtaPackage>, @Inject('entitiesTableConfig') protected entitiesTableConfigValue: EntityTableConfig<OtaPackage>,
public fb: FormBuilder) { public fb: FormBuilder,
super(store, fb, entityValue, entitiesTableConfigValue); protected cd: ChangeDetectorRef) {
super(store, fb, entityValue, entitiesTableConfigValue, cd);
} }
ngOnInit() { ngOnInit() {

7
ui-ngx/src/app/modules/home/pages/rulechain/rulechain.component.ts

@ -14,7 +14,7 @@
/// limitations under the License. /// limitations under the License.
/// ///
import { Component, Inject } from '@angular/core'; import { ChangeDetectorRef, Component, Inject } from '@angular/core';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state'; import { AppState } from '@core/core.state';
import { EntityComponent } from '../../components/entity/entity.component'; import { EntityComponent } from '../../components/entity/entity.component';
@ -37,8 +37,9 @@ export class RuleChainComponent extends EntityComponent<RuleChain> {
protected translate: TranslateService, protected translate: TranslateService,
@Inject('entity') protected entityValue: RuleChain, @Inject('entity') protected entityValue: RuleChain,
@Inject('entitiesTableConfig') protected entitiesTableConfigValue: EntityTableConfig<RuleChain>, @Inject('entitiesTableConfig') protected entitiesTableConfigValue: EntityTableConfig<RuleChain>,
public fb: FormBuilder) { public fb: FormBuilder,
super(store, fb, entityValue, entitiesTableConfigValue); protected cd: ChangeDetectorRef) {
super(store, fb, entityValue, entitiesTableConfigValue, cd);
} }
ngOnInit() { ngOnInit() {

7
ui-ngx/src/app/modules/home/pages/tenant/tenant.component.ts

@ -14,7 +14,7 @@
/// limitations under the License. /// limitations under the License.
/// ///
import { Component, Inject } from '@angular/core'; import { ChangeDetectorRef, Component, Inject } from '@angular/core';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state'; import { AppState } from '@core/core.state';
import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { FormBuilder, FormGroup, Validators } from '@angular/forms';
@ -36,8 +36,9 @@ export class TenantComponent extends ContactBasedComponent<TenantInfo> {
protected translate: TranslateService, protected translate: TranslateService,
@Inject('entity') protected entityValue: TenantInfo, @Inject('entity') protected entityValue: TenantInfo,
@Inject('entitiesTableConfig') protected entitiesTableConfigValue: EntityTableConfig<TenantInfo>, @Inject('entitiesTableConfig') protected entitiesTableConfigValue: EntityTableConfig<TenantInfo>,
protected fb: FormBuilder) { protected fb: FormBuilder,
super(store, fb, entityValue, entitiesTableConfigValue); protected cd: ChangeDetectorRef) {
super(store, fb, entityValue, entitiesTableConfigValue, cd);
} }
hideDelete() { hideDelete() {

7
ui-ngx/src/app/modules/home/pages/user/user.component.ts

@ -14,7 +14,7 @@
/// limitations under the License. /// limitations under the License.
/// ///
import { Component, Inject, Optional } from '@angular/core'; import { ChangeDetectorRef, Component, Inject, Optional } from '@angular/core';
import { select, Store } from '@ngrx/store'; import { select, Store } from '@ngrx/store';
import { AppState } from '@core/core.state'; import { AppState } from '@core/core.state';
import { EntityComponent } from '../../components/entity/entity.component'; import { EntityComponent } from '../../components/entity/entity.component';
@ -43,8 +43,9 @@ export class UserComponent extends EntityComponent<User> {
constructor(protected store: Store<AppState>, constructor(protected store: Store<AppState>,
@Optional() @Inject('entity') protected entityValue: User, @Optional() @Inject('entity') protected entityValue: User,
@Optional() @Inject('entitiesTableConfig') protected entitiesTableConfigValue: EntityTableConfig<User>, @Optional() @Inject('entitiesTableConfig') protected entitiesTableConfigValue: EntityTableConfig<User>,
public fb: FormBuilder) { public fb: FormBuilder,
super(store, fb, entityValue, entitiesTableConfigValue); protected cd: ChangeDetectorRef) {
super(store, fb, entityValue, entitiesTableConfigValue, cd);
} }
hideDelete() { hideDelete() {

7
ui-ngx/src/app/modules/home/pages/widget/widgets-bundle.component.ts

@ -14,7 +14,7 @@
/// limitations under the License. /// limitations under the License.
/// ///
import { Component, Inject } from '@angular/core'; import { ChangeDetectorRef, Component, Inject } from '@angular/core';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state'; import { AppState } from '@core/core.state';
import { EntityComponent } from '../../components/entity/entity.component'; import { EntityComponent } from '../../components/entity/entity.component';
@ -32,8 +32,9 @@ export class WidgetsBundleComponent extends EntityComponent<WidgetsBundle> {
constructor(protected store: Store<AppState>, constructor(protected store: Store<AppState>,
@Inject('entity') protected entityValue: WidgetsBundle, @Inject('entity') protected entityValue: WidgetsBundle,
@Inject('entitiesTableConfig') protected entitiesTableConfigValue: EntityTableConfig<WidgetsBundle>, @Inject('entitiesTableConfig') protected entitiesTableConfigValue: EntityTableConfig<WidgetsBundle>,
public fb: FormBuilder) { public fb: FormBuilder,
super(store, fb, entityValue, entitiesTableConfigValue); protected cd: ChangeDetectorRef) {
super(store, fb, entityValue, entitiesTableConfigValue, cd);
} }
hideDelete() { hideDelete() {

4
ui-ngx/src/app/shared/models/settings.models.ts

@ -27,6 +27,7 @@ export interface AdminSettings<T> {
export declare type SmtpProtocol = 'smtp' | 'smtps'; export declare type SmtpProtocol = 'smtp' | 'smtps';
export interface MailServerSettings { export interface MailServerSettings {
showChangePassword: boolean;
mailFrom: string; mailFrom: string;
smtpProtocol: SmtpProtocol; smtpProtocol: SmtpProtocol;
smtpHost: string; smtpHost: string;
@ -34,7 +35,8 @@ export interface MailServerSettings {
timeout: number; timeout: number;
enableTls: boolean; enableTls: boolean;
username: string; username: string;
password: string; changePassword?: boolean;
password?: string;
enableProxy: boolean; enableProxy: boolean;
proxyHost: string; proxyHost: string;
proxyPort: number; proxyPort: number;

4
ui-ngx/src/assets/locale/locale.constant-de_DE.json

@ -803,7 +803,7 @@
"rulechain-templates": "Regelkettenvorlagen", "rulechain-templates": "Regelkettenvorlagen",
"rulechains": "Rand Regelketten", "rulechains": "Rand Regelketten",
"search": "Kanten durchsuchen", "search": "Kanten durchsuchen",
"selected-edges": "{Anzahl, Plural, 1 {1 Kante} andere {# Kanten}} ausgewählt", "selected-edges": "{count, plural, 1 {1 Rand} other {# Rand} } ausgewählt",
"any-edge": "Beliebige Kante", "any-edge": "Beliebige Kante",
"no-edge-types-matching": "Es wurden keine Kantentypen gefunden, die mit '{{entitySubtype}}' übereinstimmen.", "no-edge-types-matching": "Es wurden keine Kantentypen gefunden, die mit '{{entitySubtype}}' übereinstimmen.",
"edge-type-list-empty": "Keine Kantentypen ausgewählt.", "edge-type-list-empty": "Keine Kantentypen ausgewählt.",
@ -1452,7 +1452,7 @@
"unset-auto-assign-to-edge-text": "Nach der Bestätigung wird die Kantenregelkette bei der Erstellung nicht mehr automatisch den Kanten zugewiesen.", "unset-auto-assign-to-edge-text": "Nach der Bestätigung wird die Kantenregelkette bei der Erstellung nicht mehr automatisch den Kanten zugewiesen.",
"edge-template-root": "Vorlagenstamm", "edge-template-root": "Vorlagenstamm",
"search": "Suchen Sie nach Regelketten", "search": "Suchen Sie nach Regelketten",
"selected-rulechains": "{count, plural, 1 {1 Regelkette} andere {# Regelketten}} ausgewählt", "selected-rulechains": "{count, plural, 1 {1 Regelkette} other {# Regelketten} } ausgewählt",
"open-rulechain": "Regelkette öffnen", "open-rulechain": "Regelkette öffnen",
"assign-to-edge": "Rand zuweisen", "assign-to-edge": "Rand zuweisen",
"edge-rulechain": "Kantenregelkette" "edge-rulechain": "Kantenregelkette"

1
ui-ngx/src/assets/locale/locale.constant-en_US.json

@ -104,6 +104,7 @@
"proxy-port-range": "Proxy port should be in a range from 1 to 65535.", "proxy-port-range": "Proxy port should be in a range from 1 to 65535.",
"proxy-user": "Proxy user", "proxy-user": "Proxy user",
"proxy-password": "Proxy password", "proxy-password": "Proxy password",
"change-password": "Change password",
"send-test-mail": "Send test mail", "send-test-mail": "Send test mail",
"sms-provider": "SMS provider", "sms-provider": "SMS provider",
"sms-provider-settings": "SMS provider settings", "sms-provider-settings": "SMS provider settings",

24
ui-ngx/src/assets/locale/locale.constant-es_ES.json

@ -413,8 +413,8 @@
"unassign-asset-from-edge": "Anular activo de bodre", "unassign-asset-from-edge": "Anular activo de bodre",
"unassign-asset-from-edge-title": "¿Está seguro de que desea desasignar el activo '{{assetName}}'?", "unassign-asset-from-edge-title": "¿Está seguro de que desea desasignar el activo '{{assetName}}'?",
"unassign-asset-from-edge-text": "Después de la confirmación, el activo no será asignado y el borde no podrá acceder a él", "unassign-asset-from-edge-text": "Después de la confirmación, el activo no será asignado y el borde no podrá acceder a él",
"unassign-assets-from-edge-action-title": "Anular asignación {count, plural, 1 {1 activo} other {# activos}} desde el borde", "unassign-assets-from-edge-action-title": "Anular asignación {count, plural, 1 {1 activo} other {# activos} } desde el borde",
"unassign-assets-from-edge-title": "¿Está seguro de que desea desasignar {count, plural, 1 {1 activo} other {# activos}}?", "unassign-assets-from-edge-title": "¿Está seguro de que desea desasignar {count, plural, 1 {1 activo} other {# activos} }?",
"unassign-assets-from-edge-text": "Después de la confirmación, todos los activos seleccionados quedarán sin asignar y el borde no podrá acceder a ellos." "unassign-assets-from-edge-text": "Después de la confirmación, todos los activos seleccionados quedarán sin asignar y el borde no podrá acceder a ellos."
}, },
"attribute": { "attribute": {
@ -950,7 +950,7 @@
"assign-device-to-edge-text": "Seleccione los dispositivos para asignar al borde", "assign-device-to-edge-text": "Seleccione los dispositivos para asignar al borde",
"unassign-device-from-edge-title": "¿Está seguro de que desea desasignar el dispositivo '{{deviceName}}'?", "unassign-device-from-edge-title": "¿Está seguro de que desea desasignar el dispositivo '{{deviceName}}'?",
"unassign-device-from-edge-text": "Después de la confirmación, el dispositivo no será asignado y el borde no podrá acceder a él", "unassign-device-from-edge-text": "Después de la confirmación, el dispositivo no será asignado y el borde no podrá acceder a él",
"unassign-devices-from-edge-title": "¿Está seguro de que desea desasignar {count, plural, 1 {1 dispositivo} other {# dispositivos}}?", "unassign-devices-from-edge-title": "¿Está seguro de que desea desasignar {count, plural, 1 {1 dispositivo} other {# dispositivos} }?",
"unassign-devices-from-edge-text": "Después de la confirmación, todos los dispositivos seleccionados quedarán sin asignar y el borde no podrá acceder a ellos." "unassign-devices-from-edge-text": "Después de la confirmación, todos los dispositivos seleccionados quedarán sin asignar y el borde no podrá acceder a ellos."
}, },
"device-profile": { "device-profile": {
@ -1123,7 +1123,7 @@
"delete": "Eliminar borde", "delete": "Eliminar borde",
"delete-edge-title": "¿Está seguro de que desea eliminar el borde '{{edgeName}}'?", "delete-edge-title": "¿Está seguro de que desea eliminar el borde '{{edgeName}}'?",
"delete-edge-text": "Tenga cuidado, después de la confirmación, el borde y todos los datos relacionados serán irrecuperables", "delete-edge-text": "Tenga cuidado, después de la confirmación, el borde y todos los datos relacionados serán irrecuperables",
"delete-edges-title": "¿Está seguro de que desea edge {count, plural, 1 {1 borde} other {# bordes}}?", "delete-edges-title": "¿Está seguro de que desea edge {count, plural, 1 {1 borde} other {# bordes} }?",
"delete-edges-text": "Tenga cuidado, después de la confirmación se eliminarán todos los bordes seleccionados y todos los datos relacionados se volverán irrecuperables", "delete-edges-text": "Tenga cuidado, después de la confirmación se eliminarán todos los bordes seleccionados y todos los datos relacionados se volverán irrecuperables",
"name": "Nombre", "name": "Nombre",
"name-starts-with": "Edge name starts with", "name-starts-with": "Edge name starts with",
@ -1156,7 +1156,7 @@
"unassign-from-customer": "Anular asignación del cliente", "unassign-from-customer": "Anular asignación del cliente",
"unassign-edge-title": "¿Está seguro de que desea desasignar el borde '{{edgeName}}'?", "unassign-edge-title": "¿Está seguro de que desea desasignar el borde '{{edgeName}}'?",
"unassign-edge-text": "Después de la confirmación, el borde quedará sin asignar y el cliente no podrá acceder a él", "unassign-edge-text": "Después de la confirmación, el borde quedará sin asignar y el cliente no podrá acceder a él",
"unassign-edges-title": "¿Está seguro de que desea anular la asignación de {count, plural, 1 {1 borde} other {# bordes}}?", "unassign-edges-title": "¿Está seguro de que desea anular la asignación de {count, plural, 1 {1 borde} other {# bordes} }?",
"unassign-edges-text": "Después de la confirmación de todos los bordes seleccionados, se anulará la asignación y el cliente no podrá acceder a ellos.", "unassign-edges-text": "Después de la confirmación de todos los bordes seleccionados, se anulará la asignación y el cliente no podrá acceder a ellos.",
"make-public": "Hacer público el borde", "make-public": "Hacer público el borde",
"make-public-edge-title": "¿Estás seguro de que quieres hacer público el edge '{{edgeName}}'?", "make-public-edge-title": "¿Estás seguro de que quieres hacer público el edge '{{edgeName}}'?",
@ -1189,14 +1189,14 @@
"rulechain-templates": "Plantillas, de cadena de reglas", "rulechain-templates": "Plantillas, de cadena de reglas",
"rulechains": "Cadenas de regla de borde", "rulechains": "Cadenas de regla de borde",
"search": "Bordes de búsqueda", "search": "Bordes de búsqueda",
"selected-edges": "{count, plural, 1 {1 borde} other {# bordes}} seleccionados", "selected-edges": "{count, plural, 1 {1 borde} other {# bordes} } seleccionadas",
"any-edge": "Cualquier bordee", "any-edge": "Cualquier bordee",
"no-edge-types-matching": "No se encontraron tipos de aristas que coincidan con '{{entitySubtype}}'.", "no-edge-types-matching": "No se encontraron tipos de aristas que coincidan con '{{entitySubtype}}'.",
"edge-type-list-empty": "No se seleccionó ningún tipo de borde.", "edge-type-list-empty": "No se seleccionó ningún tipo de borde.",
"edge-types": "Tipos de bordes", "edge-types": "Tipos de bordes",
"enter-edge-type": "Ingrese el tipo de borde", "enter-edge-type": "Ingrese el tipo de borde",
"deployed": "Desplegada", "deployed": "Desplegada",
"pending": "Pending", "pending": "Pendiente",
"downlinks": "Enlaces descendentes", "downlinks": "Enlaces descendentes",
"no-downlinks-prompt": "No se encontraron enlaces descendentes", "no-downlinks-prompt": "No se encontraron enlaces descendentes",
"sync-process-started-successfully": "¡El proceso de sincronización se inició correctamente!", "sync-process-started-successfully": "¡El proceso de sincronización se inició correctamente!",
@ -1356,7 +1356,7 @@
"type-api-usage-state": "Estado de uso de la API", "type-api-usage-state": "Estado de uso de la API",
"type-edge": "Borde", "type-edge": "Borde",
"type-edges": "Bordes", "type-edges": "Bordes",
"list-of-edges": "{cuenta, plural, 1 {Un borde} other {Lista de # bordes}}", "list-of-edges": "{count, plural, 1 {Un borde} other {Lista de # bordes} }",
"edge-name-starts-with": "Bordes cuyos nombres comienzan con '{{prefijo}}'" "edge-name-starts-with": "Bordes cuyos nombres comienzan con '{{prefijo}}'"
}, },
"entity-field": { "entity-field": {
@ -1481,9 +1481,9 @@
"assign-entity-view-to-edge-text": "Seleccione las vistas de entidad para asignar al borde", "assign-entity-view-to-edge-text": "Seleccione las vistas de entidad para asignar al borde",
"unassign-entity-view-from-edge-title": "¿Está seguro de que desea anular la asignación de la vista de entidad '{{entityViewName}}'?", "unassign-entity-view-from-edge-title": "¿Está seguro de que desea anular la asignación de la vista de entidad '{{entityViewName}}'?",
"unassign-entity-view-from-edge-text": "Después de la confirmación, la vista de entidad quedará sin asignar y el borde no podrá acceder a ella", "unassign-entity-view-from-edge-text": "Después de la confirmación, la vista de entidad quedará sin asignar y el borde no podrá acceder a ella",
"unassign-entity-views-from-edge-action-title": "Anular asignación {recuento, plural, 1 {1 vista de entidad} otras {# vistas de entidad}} del borde", "unassign-entity-views-from-edge-action-title": "Anular asignación {count, plural, 1 {1 vista de entidad} other {# vistas de entidad} } del borde",
"unassign-entity-view-from-edge": "Anular asignación de vista de entidad", "unassign-entity-view-from-edge": "Anular asignación de vista de entidad",
"unassign-entity-views-from-edge-title": "¿Está seguro de que desea desasignar {count, plural, 1 {1 vista de entidad} other {# vistas de entidad}}?", "unassign-entity-views-from-edge-title": "¿Está seguro de que desea desasignar {count, plural, 1 {1 vista de entidad} other {# vistas de entidad} }?",
"unassign-entity-views-from-edge-text": "Después de la confirmación, todas las vistas de entidad seleccionadas no serán asignadas y el borde no podrá acceder a ellas" "unassign-entity-views-from-edge-text": "Después de la confirmación, todas las vistas de entidad seleccionadas no serán asignadas y el borde no podrá acceder a ellas"
}, },
"event": { "event": {
@ -2074,9 +2074,9 @@
"delete-rulechains": "Eliminar cadenas de reglas", "delete-rulechains": "Eliminar cadenas de reglas",
"unassign-rulechain": "Anular asignación de cadena de reglas", "unassign-rulechain": "Anular asignación de cadena de reglas",
"unassign-rulechains": "Anular asignación de cadenas de reglas", "unassign-rulechains": "Anular asignación de cadenas de reglas",
"unassign-rulechain-title": "¿Está seguro de que desea desasignar la cadena de reglas '{{ruleChainTitle}}'?", "unassign-rulechain-title": "¿Está seguro de que desea desasignar la cadena de reglas '{{ruleChainName}}'?",
"unassign-rulechain-from-edge-text": "Después de la confirmación, la cadena de reglas quedará sin asignar y el borde no podrá acceder a ella", "unassign-rulechain-from-edge-text": "Después de la confirmación, la cadena de reglas quedará sin asignar y el borde no podrá acceder a ella",
"unassign-rulechains-from-edge-action-title": "Anular asignación {count, plural, 1 {1 cadena de reglas} other {# cadenas de reglas}} des bordes", "unassign-rulechains-from-edge-action-title": "Anular asignación {count, plural, 1 {1 cadena de reglas} other {# cadenas de reglas} } des bordes",
"unassign-rulechains-from-edge-text": "Después de la confirmación, todas las cadenas de reglas seleccionadas quedarán sin asignar y el borde no podrá acceder a ellas", "unassign-rulechains-from-edge-text": "Después de la confirmación, todas las cadenas de reglas seleccionadas quedarán sin asignar y el borde no podrá acceder a ellas",
"assign-rulechain-to-edge-title": "Asignar cadena (s) de reglas a borde", "assign-rulechain-to-edge-title": "Asignar cadena (s) de reglas a borde",
"assign-rulechain-to-edge-text": "Seleccione las cadenas de reglas para asignar al borde", "assign-rulechain-to-edge-text": "Seleccione las cadenas de reglas para asignar al borde",

20
ui-ngx/src/assets/locale/locale.constant-fr_FR.json

@ -736,7 +736,7 @@
"assign-device-to-edge-text":"Veuillez sélectionner la bordure pour attribuer le ou les dispositifs", "assign-device-to-edge-text":"Veuillez sélectionner la bordure pour attribuer le ou les dispositifs",
"unassign-device-from-edge-title": "Êtes-vous sûr de vouloir annuler l'affection du dispositif {{deviceName}} '?", "unassign-device-from-edge-title": "Êtes-vous sûr de vouloir annuler l'affection du dispositif {{deviceName}} '?",
"unassign-device-from-edge-text": "Après la confirmation, dispositif sera non attribué et ne sera pas accessible a la bordure.", "unassign-device-from-edge-text": "Après la confirmation, dispositif sera non attribué et ne sera pas accessible a la bordure.",
"unassign-devices-from-edge-title": "Voulez-vous vraiment annuler l'affectation de {count, plural, 1 {1 device} other {# devices}}?", "unassign-devices-from-edge-title": "Voulez-vous vraiment annuler l'affectation de {count, plural, 1 {1 device} other {# devices} }?",
"unassign-devices-from-edge-text": "Après la confirmation, tous les dispositifs sélectionnés ne seront pas attribues et ne seront pas accessibles par la bordure." "unassign-devices-from-edge-text": "Après la confirmation, tous les dispositifs sélectionnés ne seront pas attribues et ne seront pas accessibles par la bordure."
}, },
"dialog": { "dialog": {
@ -755,7 +755,7 @@
"delete": "Supprimer la bordure", "delete": "Supprimer la bordure",
"delete-edge-title": "Êtes-vous sûr de vouloir supprimer la bordure '{{edgeName}}'?", "delete-edge-title": "Êtes-vous sûr de vouloir supprimer la bordure '{{edgeName}}'?",
"delete-edge-text": "Faites attention, après la confirmation, la bordure et toutes les données associées deviendront irrécupérables", "delete-edge-text": "Faites attention, après la confirmation, la bordure et toutes les données associées deviendront irrécupérables",
"delete-edges-title": "Êtes-vous sûr de vouloir supprimer {count, plural, 1 {1 bordure} other {# bordure}}?", "delete-edges-title": "Êtes-vous sûr de vouloir supprimer {count, plural, 1 {1 bordure} other {# bordure} }?",
"delete-edges-text": "Faites attention, après la confirmation, tous les bordures sélectionnés seront supprimés et toutes les données associées deviendront irrécupérables.", "delete-edges-text": "Faites attention, après la confirmation, tous les bordures sélectionnés seront supprimés et toutes les données associées deviendront irrécupérables.",
"name": "Nom", "name": "Nom",
"name-starts-with": "Le nom du bord commence par", "name-starts-with": "Le nom du bord commence par",
@ -788,7 +788,7 @@
"unassign-from-customer": "Retirer du client", "unassign-from-customer": "Retirer du client",
"unassign-edge-title": "Êtes-vous sûr de vouloir annuler l'affection du dispositif {{edgeName}}", "unassign-edge-title": "Êtes-vous sûr de vouloir annuler l'affection du dispositif {{edgeName}}",
"unassign-edge-text": "Après la confirmation, le dispositif ne sera pas attribué et ne sera pas accessible au client", "unassign-edge-text": "Après la confirmation, le dispositif ne sera pas attribué et ne sera pas accessible au client",
"unassign-edges-title": "Voulez-vous vraiment annuler l'attribution de {count, plural, 1 {1 bordure} other {# bordures}}?", "unassign-edges-title": "Voulez-vous vraiment annuler l'attribution de {count, plural, 1 {1 bordure} other {# bordures} }?",
"unassign-edges-text": "Après la confirmation, tous les bordures sélectionnés ne seront plus attribués et ne seront pas accessibles par le client.", "unassign-edges-text": "Après la confirmation, tous les bordures sélectionnés ne seront plus attribués et ne seront pas accessibles par le client.",
"make-public": "Make edge public", "make-public": "Make edge public",
"make-public-edge-title": "Are you sure you want to make the edge '{{edgeName}}' public?", "make-public-edge-title": "Are you sure you want to make the edge '{{edgeName}}' public?",
@ -821,7 +821,7 @@
"rulechain-templates": "Modèles de chaîne de règles", "rulechain-templates": "Modèles de chaîne de règles",
"rulechains": "Chaînes de règles de la bordure", "rulechains": "Chaînes de règles de la bordure",
"search": "Rechercher les bords", "search": "Rechercher les bords",
"selected-edges": "{count, plural, 1 {1 edge} other {# bords}} sélectionné", "selected-edges": "{count, plural, 1 {1 bordure} other {# bords} } sélectionné",
"any-edge": "Tout bord", "any-edge": "Tout bord",
"no-edge-types-matching": "Aucun type d'arête correspondant à \"{{entitySubtype}}\" n'a été trouvé.", "no-edge-types-matching": "Aucun type d'arête correspondant à \"{{entitySubtype}}\" n'a été trouvé.",
"edge-type-list-empty": "Aucun type d'arête sélectionné.", "edge-type-list-empty": "Aucun type d'arête sélectionné.",
@ -1479,9 +1479,9 @@
"delete-rulechains": "Supprimer une chaînes de règles", "delete-rulechains": "Supprimer une chaînes de règles",
"unassign-rulechain": "Retirer chaîne de règles", "unassign-rulechain": "Retirer chaîne de règles",
"unassign-rulechains": "Retirer chaînes de règles", "unassign-rulechains": "Retirer chaînes de règles",
"unassign-rulechain-title": "AÊtes-vous sûr de vouloir retirer l'attribution de chaînes de règles '{{ruleChainTitle}}'?", "unassign-rulechain-title": "AÊtes-vous sûr de vouloir retirer l'attribution de chaînes de règles '{{ruleChainName}}'?",
"unassign-rulechain-from-edge-text": "Après la confirmation, l'actif sera non attribué et ne sera pas accessible a la bordure.", "unassign-rulechain-from-edge-text": "Après la confirmation, l'actif sera non attribué et ne sera pas accessible a la bordure.",
"unassign-rulechains-from-edge-action-title": "Retirer {count, plural, 1 {1 chaîne de règles} other {# chaînes de règles}} de la bordure", "unassign-rulechains-from-edge-action-title": "Retirer {count, plural, 1 {1 chaîne de règles} other {# chaînes de règles} } de la bordure",
"unassign-rulechains-from-edge-text": "Après la confirmation, tous les chaînes de règles sélectionnés ne seront pas attribués et ne seront pas accessibles a la bordure.", "unassign-rulechains-from-edge-text": "Après la confirmation, tous les chaînes de règles sélectionnés ne seront pas attribués et ne seront pas accessibles a la bordure.",
"assign-rulechain-to-edge-title": "Attribuer les chaînes de règles a la bordure", "assign-rulechain-to-edge-title": "Attribuer les chaînes de règles a la bordure",
"assign-rulechain-to-edge-text": "Veuillez sélectionner la bordure pour attribuer le ou les chaînes de règles", "assign-rulechain-to-edge-text": "Veuillez sélectionner la bordure pour attribuer le ou les chaînes de règles",
@ -1497,11 +1497,11 @@
"unset-auto-assign-to-edge-text": "Après la confirmation, la chaîne de règles d'arêtes ne sera plus automatiquement affectée aux arêtes lors de la création.", "unset-auto-assign-to-edge-text": "Après la confirmation, la chaîne de règles d'arêtes ne sera plus automatiquement affectée aux arêtes lors de la création.",
"edge-template-root": "Racine du modèle", "edge-template-root": "Racine du modèle",
"search": "Rechercher des chaînes de règles", "search": "Rechercher des chaînes de règles",
"selected-rulechains": "{count, plural, 1 {1 rule chain} other {# rule chains}} sélectionné", "selected-rulechains": "{count, plural, 1 {1 rule chain} other {# rule chains} } sélectionné",
"open-rulechain": "Chaîne de règles ouverte", "open-rulechain": "Chaîne de règles ouverte",
"assign-to-edge": "Attribuer à Edge", "assign-to-edge": "Attribuer à Bordure",
"edge-rulechain": "Chaîne de règles Edge", "edge-rulechain": "Chaîne de règles Bordure",
"unassign-rulechains-from-edge-title": "Voulez-vous vraiment annuler l'attribution de {count, plural, 1 {1 rulechain} other {# rulechains}}?" "unassign-rulechains-from-edge-title": "Voulez-vous vraiment annuler l'attribution de {count, plural, 1 {1 rulechain} other {# rulechains} }?"
}, },
"rulenode": { "rulenode": {
"add": "Ajouter un noeud de règle", "add": "Ajouter un noeud de règle",

Loading…
Cancel
Save