Commit 9f9e613320f435ada5142ee048afb881fbad5540

Authored by Vladyslav_Prykhodko
2 parents 8f0b1e88 33887ecb

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

Showing 70 changed files with 895 additions and 294 deletions
@@ -290,6 +290,11 @@ @@ -290,6 +290,11 @@
290 <scope>test</scope> 290 <scope>test</scope>
291 </dependency> 291 </dependency>
292 <dependency> 292 <dependency>
  293 + <groupId>org.awaitility</groupId>
  294 + <artifactId>awaitility</artifactId>
  295 + <scope>test</scope>
  296 + </dependency>
  297 + <dependency>
293 <groupId>org.mockito</groupId> 298 <groupId>org.mockito</groupId>
294 <artifactId>mockito-core</artifactId> 299 <artifactId>mockito-core</artifactId>
295 <scope>test</scope> 300 <scope>test</scope>
@@ -381,11 +381,11 @@ class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcessor { @@ -381,11 +381,11 @@ class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcessor {
381 } 381 }
382 382
383 private void reportSessionOpen() { 383 private void reportSessionOpen() {
384 - systemContext.getDeviceStateService().onDeviceConnect(deviceId); 384 + systemContext.getDeviceStateService().onDeviceConnect(tenantId, deviceId);
385 } 385 }
386 386
387 private void reportSessionClose() { 387 private void reportSessionClose() {
388 - systemContext.getDeviceStateService().onDeviceDisconnect(deviceId); 388 + systemContext.getDeviceStateService().onDeviceDisconnect(tenantId, deviceId);
389 } 389 }
390 390
391 private void handleGetAttributesRequest(TbActorCtx context, SessionInfoProto sessionInfo, GetAttributeRequestMsg request) { 391 private void handleGetAttributesRequest(TbActorCtx context, SessionInfoProto sessionInfo, GetAttributeRequestMsg request) {
@@ -590,7 +590,7 @@ class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcessor { @@ -590,7 +590,7 @@ class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcessor {
590 if (sessions.size() == 1) { 590 if (sessions.size() == 1) {
591 reportSessionOpen(); 591 reportSessionOpen();
592 } 592 }
593 - systemContext.getDeviceStateService().onDeviceActivity(deviceId, System.currentTimeMillis()); 593 + systemContext.getDeviceStateService().onDeviceActivity(tenantId, deviceId, System.currentTimeMillis());
594 dumpSessions(); 594 dumpSessions();
595 } else if (msg.getEvent() == SessionEvent.CLOSED) { 595 } else if (msg.getEvent() == SessionEvent.CLOSED) {
596 log.debug("[{}] Canceling subscriptions for closed session [{}]", deviceId, sessionId); 596 log.debug("[{}] Canceling subscriptions for closed session [{}]", deviceId, sessionId);
@@ -620,7 +620,7 @@ class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcessor { @@ -620,7 +620,7 @@ class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcessor {
620 if (subscriptionInfo.getRpcSubscription()) { 620 if (subscriptionInfo.getRpcSubscription()) {
621 rpcSubscriptions.putIfAbsent(sessionId, sessionMD.getSessionInfo()); 621 rpcSubscriptions.putIfAbsent(sessionId, sessionMD.getSessionInfo());
622 } 622 }
623 - systemContext.getDeviceStateService().onDeviceActivity(deviceId, subscriptionInfo.getLastActivityTime()); 623 + systemContext.getDeviceStateService().onDeviceActivity(tenantId, deviceId, subscriptionInfo.getLastActivityTime());
624 dumpSessions(); 624 dumpSessions();
625 } 625 }
626 626
@@ -26,12 +26,12 @@ import org.springframework.web.bind.annotation.ResponseBody; @@ -26,12 +26,12 @@ import org.springframework.web.bind.annotation.ResponseBody;
26 import org.springframework.web.bind.annotation.RestController; 26 import org.springframework.web.bind.annotation.RestController;
27 import org.thingsboard.rule.engine.api.MailService; 27 import org.thingsboard.rule.engine.api.MailService;
28 import org.thingsboard.rule.engine.api.SmsService; 28 import org.thingsboard.rule.engine.api.SmsService;
29 -import org.thingsboard.server.common.data.sms.config.TestSmsRequest;  
30 import org.thingsboard.server.common.data.AdminSettings; 29 import org.thingsboard.server.common.data.AdminSettings;
31 import org.thingsboard.server.common.data.UpdateMessage; 30 import org.thingsboard.server.common.data.UpdateMessage;
32 import org.thingsboard.server.common.data.exception.ThingsboardException; 31 import org.thingsboard.server.common.data.exception.ThingsboardException;
33 import org.thingsboard.server.common.data.id.TenantId; 32 import org.thingsboard.server.common.data.id.TenantId;
34 import org.thingsboard.server.common.data.security.model.SecuritySettings; 33 import org.thingsboard.server.common.data.security.model.SecuritySettings;
  34 +import org.thingsboard.server.common.data.sms.config.TestSmsRequest;
35 import org.thingsboard.server.dao.settings.AdminSettingsService; 35 import org.thingsboard.server.dao.settings.AdminSettingsService;
36 import org.thingsboard.server.queue.util.TbCoreComponent; 36 import org.thingsboard.server.queue.util.TbCoreComponent;
37 import org.thingsboard.server.service.security.permission.Operation; 37 import org.thingsboard.server.service.security.permission.Operation;
@@ -67,7 +67,7 @@ public class AdminController extends BaseController { @@ -67,7 +67,7 @@ public class AdminController extends BaseController {
67 accessControlService.checkPermission(getCurrentUser(), Resource.ADMIN_SETTINGS, Operation.READ); 67 accessControlService.checkPermission(getCurrentUser(), Resource.ADMIN_SETTINGS, Operation.READ);
68 AdminSettings adminSettings = checkNotNull(adminSettingsService.findAdminSettingsByKey(TenantId.SYS_TENANT_ID, key)); 68 AdminSettings adminSettings = checkNotNull(adminSettingsService.findAdminSettingsByKey(TenantId.SYS_TENANT_ID, key));
69 if (adminSettings.getKey().equals("mail")) { 69 if (adminSettings.getKey().equals("mail")) {
70 - ((ObjectNode) adminSettings.getJsonValue()).put("password", ""); 70 + ((ObjectNode) adminSettings.getJsonValue()).remove("password");
71 } 71 }
72 return adminSettings; 72 return adminSettings;
73 } catch (Exception e) { 73 } catch (Exception e) {
@@ -84,7 +84,7 @@ public class AdminController extends BaseController { @@ -84,7 +84,7 @@ public class AdminController extends BaseController {
84 adminSettings = checkNotNull(adminSettingsService.saveAdminSettings(TenantId.SYS_TENANT_ID, adminSettings)); 84 adminSettings = checkNotNull(adminSettingsService.saveAdminSettings(TenantId.SYS_TENANT_ID, adminSettings));
85 if (adminSettings.getKey().equals("mail")) { 85 if (adminSettings.getKey().equals("mail")) {
86 mailService.updateMailConfiguration(); 86 mailService.updateMailConfiguration();
87 - ((ObjectNode) adminSettings.getJsonValue()).put("password", ""); 87 + ((ObjectNode) adminSettings.getJsonValue()).remove("password");
88 } else if (adminSettings.getKey().equals("sms")) { 88 } else if (adminSettings.getKey().equals("sms")) {
89 smsService.updateSmsConfiguration(); 89 smsService.updateSmsConfiguration();
90 } 90 }
@@ -126,6 +126,10 @@ public class AdminController extends BaseController { @@ -126,6 +126,10 @@ public class AdminController extends BaseController {
126 accessControlService.checkPermission(getCurrentUser(), Resource.ADMIN_SETTINGS, Operation.READ); 126 accessControlService.checkPermission(getCurrentUser(), Resource.ADMIN_SETTINGS, Operation.READ);
127 adminSettings = checkNotNull(adminSettings); 127 adminSettings = checkNotNull(adminSettings);
128 if (adminSettings.getKey().equals("mail")) { 128 if (adminSettings.getKey().equals("mail")) {
  129 + if(!adminSettings.getJsonValue().has("password")) {
  130 + AdminSettings mailSettings = checkNotNull(adminSettingsService.findAdminSettingsByKey(TenantId.SYS_TENANT_ID, "mail"));
  131 + ((ObjectNode) adminSettings.getJsonValue()).put("password", mailSettings.getJsonValue().get("password").asText());
  132 + }
129 String email = getCurrentUser().getEmail(); 133 String email = getCurrentUser().getEmail();
130 mailService.sendTestMail(adminSettings.getJsonValue(), email); 134 mailService.sendTestMail(adminSettings.getJsonValue(), email);
131 } 135 }
@@ -215,6 +215,7 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService { @@ -215,6 +215,7 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService {
215 node.put("password", ""); 215 node.put("password", "");
216 node.put("tlsVersion", "TLSv1.2");//NOSONAR, key used to identify password field (not password value itself) 216 node.put("tlsVersion", "TLSv1.2");//NOSONAR, key used to identify password field (not password value itself)
217 node.put("enableProxy", false); 217 node.put("enableProxy", false);
  218 + node.put("showChangePassword", false);
218 mailSettings.setJsonValue(node); 219 mailSettings.setJsonValue(node);
219 adminSettingsService.saveAdminSettings(TenantId.SYS_TENANT_ID, mailSettings); 220 adminSettingsService.saveAdminSettings(TenantId.SYS_TENANT_ID, mailSettings);
220 } 221 }
@@ -17,6 +17,7 @@ package org.thingsboard.server.service.security.auth.oauth2; @@ -17,6 +17,7 @@ package org.thingsboard.server.service.security.auth.oauth2;
17 17
18 import com.fasterxml.jackson.core.JsonProcessingException; 18 import com.fasterxml.jackson.core.JsonProcessingException;
19 import com.fasterxml.jackson.databind.ObjectMapper; 19 import com.fasterxml.jackson.databind.ObjectMapper;
  20 +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
20 import lombok.extern.slf4j.Slf4j; 21 import lombok.extern.slf4j.Slf4j;
21 import org.springframework.boot.web.client.RestTemplateBuilder; 22 import org.springframework.boot.web.client.RestTemplateBuilder;
22 import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken; 23 import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
@@ -29,6 +30,7 @@ import org.thingsboard.server.common.data.oauth2.OAuth2Registration; @@ -29,6 +30,7 @@ import org.thingsboard.server.common.data.oauth2.OAuth2Registration;
29 import org.thingsboard.server.dao.oauth2.OAuth2User; 30 import org.thingsboard.server.dao.oauth2.OAuth2User;
30 import org.thingsboard.server.service.security.model.SecurityUser; 31 import org.thingsboard.server.service.security.model.SecurityUser;
31 32
  33 +import javax.annotation.PostConstruct;
32 import javax.servlet.http.HttpServletRequest; 34 import javax.servlet.http.HttpServletRequest;
33 35
34 @Service(value = "customOAuth2ClientMapper") 36 @Service(value = "customOAuth2ClientMapper")
@@ -40,6 +42,15 @@ public class CustomOAuth2ClientMapper extends AbstractOAuth2ClientMapper impleme @@ -40,6 +42,15 @@ public class CustomOAuth2ClientMapper extends AbstractOAuth2ClientMapper impleme
40 42
41 private RestTemplateBuilder restTemplateBuilder = new RestTemplateBuilder(); 43 private RestTemplateBuilder restTemplateBuilder = new RestTemplateBuilder();
42 44
  45 + @PostConstruct
  46 + public void init() {
  47 + // Register time module to parse Instant objects.
  48 + // com.fasterxml.jackson.databind.exc.InvalidDefinitionException:
  49 + // Java 8 date/time type `java.time.Instant` not supported by default:
  50 + // add Module "com.fasterxml.jackson.datatype:jackson-datatype-jsr310" to enable handling
  51 + json.registerModule(new JavaTimeModule());
  52 + }
  53 +
43 @Override 54 @Override
44 public SecurityUser getOrCreateUserByClientPrincipal(HttpServletRequest request, OAuth2AuthenticationToken token, String providerAccessToken, OAuth2Registration registration) { 55 public SecurityUser getOrCreateUserByClientPrincipal(HttpServletRequest request, OAuth2AuthenticationToken token, String providerAccessToken, OAuth2Registration registration) {
45 OAuth2MapperConfig config = registration.getMapperConfig(); 56 OAuth2MapperConfig config = registration.getMapperConfig();
@@ -15,6 +15,7 @@ @@ -15,6 +15,7 @@
15 */ 15 */
16 package org.thingsboard.server.service.security.auth.oauth2; 16 package org.thingsboard.server.service.security.auth.oauth2;
17 17
  18 +import lombok.extern.slf4j.Slf4j;
18 import org.springframework.beans.factory.annotation.Autowired; 19 import org.springframework.beans.factory.annotation.Autowired;
19 import org.springframework.security.core.Authentication; 20 import org.springframework.security.core.Authentication;
20 import org.springframework.security.oauth2.client.OAuth2AuthorizedClient; 21 import org.springframework.security.oauth2.client.OAuth2AuthorizedClient;
@@ -42,6 +43,7 @@ import java.net.URLEncoder; @@ -42,6 +43,7 @@ import java.net.URLEncoder;
42 import java.nio.charset.StandardCharsets; 43 import java.nio.charset.StandardCharsets;
43 import java.util.UUID; 44 import java.util.UUID;
44 45
  46 +@Slf4j
45 @Component(value = "oauth2AuthenticationSuccessHandler") 47 @Component(value = "oauth2AuthenticationSuccessHandler")
46 public class Oauth2AuthenticationSuccessHandler extends SimpleUrlAuthenticationSuccessHandler { 48 public class Oauth2AuthenticationSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {
47 49
@@ -99,6 +101,8 @@ public class Oauth2AuthenticationSuccessHandler extends SimpleUrlAuthenticationS @@ -99,6 +101,8 @@ public class Oauth2AuthenticationSuccessHandler extends SimpleUrlAuthenticationS
99 clearAuthenticationAttributes(request, response); 101 clearAuthenticationAttributes(request, response);
100 getRedirectStrategy().sendRedirect(request, response, baseUrl + "/?accessToken=" + accessToken.getToken() + "&refreshToken=" + refreshToken.getToken()); 102 getRedirectStrategy().sendRedirect(request, response, baseUrl + "/?accessToken=" + accessToken.getToken() + "&refreshToken=" + refreshToken.getToken());
101 } catch (Exception e) { 103 } catch (Exception e) {
  104 + log.debug("Error occurred during processing authentication success result. " +
  105 + "request [{}], response [{}], authentication [{}]", request, response, authentication, e);
102 clearAuthenticationAttributes(request, response); 106 clearAuthenticationAttributes(request, response);
103 String errorPrefix; 107 String errorPrefix;
104 if (!StringUtils.isEmpty(callbackUrlScheme)) { 108 if (!StringUtils.isEmpty(callbackUrlScheme)) {
@@ -137,7 +137,6 @@ public class DefaultDeviceStateService extends TbApplicationEventListener<Partit @@ -137,7 +137,6 @@ public class DefaultDeviceStateService extends TbApplicationEventListener<Partit
137 private ExecutorService deviceStateExecutor; 137 private ExecutorService deviceStateExecutor;
138 private final ConcurrentMap<TopicPartitionInfo, Set<DeviceId>> partitionedDevices = new ConcurrentHashMap<>(); 138 private final ConcurrentMap<TopicPartitionInfo, Set<DeviceId>> partitionedDevices = new ConcurrentHashMap<>();
139 final ConcurrentMap<DeviceId, DeviceStateData> deviceStates = new ConcurrentHashMap<>(); 139 final ConcurrentMap<DeviceId, DeviceStateData> deviceStates = new ConcurrentHashMap<>();
140 - private final ConcurrentMap<DeviceId, Long> deviceLastSavedActivity = new ConcurrentHashMap<>();  
141 140
142 final Queue<Set<TopicPartitionInfo>> subscribeQueue = new ConcurrentLinkedQueue<>(); 141 final Queue<Set<TopicPartitionInfo>> subscribeQueue = new ConcurrentLinkedQueue<>();
143 142
@@ -192,7 +191,7 @@ public class DefaultDeviceStateService extends TbApplicationEventListener<Partit @@ -192,7 +191,7 @@ public class DefaultDeviceStateService extends TbApplicationEventListener<Partit
192 } 191 }
193 192
194 @Override 193 @Override
195 - public void onDeviceConnect(DeviceId deviceId) { 194 + public void onDeviceConnect(TenantId tenantId, DeviceId deviceId) {
196 log.trace("on Device Connect [{}]", deviceId.getId()); 195 log.trace("on Device Connect [{}]", deviceId.getId());
197 DeviceStateData stateData = getOrFetchDeviceStateData(deviceId); 196 DeviceStateData stateData = getOrFetchDeviceStateData(deviceId);
198 long ts = System.currentTimeMillis(); 197 long ts = System.currentTimeMillis();
@@ -200,23 +199,23 @@ public class DefaultDeviceStateService extends TbApplicationEventListener<Partit @@ -200,23 +199,23 @@ public class DefaultDeviceStateService extends TbApplicationEventListener<Partit
200 save(deviceId, LAST_CONNECT_TIME, ts); 199 save(deviceId, LAST_CONNECT_TIME, ts);
201 pushRuleEngineMessage(stateData, CONNECT_EVENT); 200 pushRuleEngineMessage(stateData, CONNECT_EVENT);
202 checkAndUpdateState(deviceId, stateData); 201 checkAndUpdateState(deviceId, stateData);
  202 + cleanDeviceStateIfBelongsExternalPartition(tenantId, deviceId);
203 } 203 }
204 204
205 @Override 205 @Override
206 - public void onDeviceActivity(DeviceId deviceId, long lastReportedActivity) { 206 + public void onDeviceActivity(TenantId tenantId, DeviceId deviceId, long lastReportedActivity) {
207 log.trace("on Device Activity [{}], lastReportedActivity [{}]", deviceId.getId(), lastReportedActivity); 207 log.trace("on Device Activity [{}], lastReportedActivity [{}]", deviceId.getId(), lastReportedActivity);
208 - long lastSavedActivity = deviceLastSavedActivity.getOrDefault(deviceId, 0L);  
209 - if (lastReportedActivity > 0 && lastReportedActivity > lastSavedActivity) {  
210 - final DeviceStateData stateData = getOrFetchDeviceStateData(deviceId); 208 + final DeviceStateData stateData = getOrFetchDeviceStateData(deviceId);
  209 + if (lastReportedActivity > 0 && lastReportedActivity > stateData.getState().getLastActivityTime()) {
211 updateActivityState(deviceId, stateData, lastReportedActivity); 210 updateActivityState(deviceId, stateData, lastReportedActivity);
212 } 211 }
  212 + cleanDeviceStateIfBelongsExternalPartition(tenantId, deviceId);
213 } 213 }
214 214
215 void updateActivityState(DeviceId deviceId, DeviceStateData stateData, long lastReportedActivity) { 215 void updateActivityState(DeviceId deviceId, DeviceStateData stateData, long lastReportedActivity) {
216 log.trace("updateActivityState - fetched state {} for device {}, lastReportedActivity {}", stateData, deviceId, lastReportedActivity); 216 log.trace("updateActivityState - fetched state {} for device {}, lastReportedActivity {}", stateData, deviceId, lastReportedActivity);
217 if (stateData != null) { 217 if (stateData != null) {
218 save(deviceId, LAST_ACTIVITY_TIME, lastReportedActivity); 218 save(deviceId, LAST_ACTIVITY_TIME, lastReportedActivity);
219 - deviceLastSavedActivity.put(deviceId, lastReportedActivity);  
220 DeviceState state = stateData.getState(); 219 DeviceState state = stateData.getState();
221 state.setLastActivityTime(lastReportedActivity); 220 state.setLastActivityTime(lastReportedActivity);
222 if (!state.isActive()) { 221 if (!state.isActive()) {
@@ -225,21 +224,23 @@ public class DefaultDeviceStateService extends TbApplicationEventListener<Partit @@ -225,21 +224,23 @@ public class DefaultDeviceStateService extends TbApplicationEventListener<Partit
225 pushRuleEngineMessage(stateData, ACTIVITY_EVENT); 224 pushRuleEngineMessage(stateData, ACTIVITY_EVENT);
226 } 225 }
227 } else { 226 } else {
228 - log.warn("updateActivityState - fetched state IN NULL for device {}, lastReportedActivity {}", deviceId, lastReportedActivity); 227 + log.debug("updateActivityState - fetched state IN NULL for device {}, lastReportedActivity {}", deviceId, lastReportedActivity);
  228 + cleanUpDeviceStateMap(deviceId);
229 } 229 }
230 } 230 }
231 231
232 @Override 232 @Override
233 - public void onDeviceDisconnect(DeviceId deviceId) { 233 + public void onDeviceDisconnect(TenantId tenantId, DeviceId deviceId) {
234 DeviceStateData stateData = getOrFetchDeviceStateData(deviceId); 234 DeviceStateData stateData = getOrFetchDeviceStateData(deviceId);
235 long ts = System.currentTimeMillis(); 235 long ts = System.currentTimeMillis();
236 stateData.getState().setLastDisconnectTime(ts); 236 stateData.getState().setLastDisconnectTime(ts);
237 save(deviceId, LAST_DISCONNECT_TIME, ts); 237 save(deviceId, LAST_DISCONNECT_TIME, ts);
238 pushRuleEngineMessage(stateData, DISCONNECT_EVENT); 238 pushRuleEngineMessage(stateData, DISCONNECT_EVENT);
  239 + cleanDeviceStateIfBelongsExternalPartition(tenantId, deviceId);
239 } 240 }
240 241
241 @Override 242 @Override
242 - public void onDeviceInactivityTimeoutUpdate(DeviceId deviceId, long inactivityTimeout) { 243 + public void onDeviceInactivityTimeoutUpdate(TenantId tenantId, DeviceId deviceId, long inactivityTimeout) {
243 if (inactivityTimeout <= 0L) { 244 if (inactivityTimeout <= 0L) {
244 return; 245 return;
245 } 246 }
@@ -247,6 +248,7 @@ public class DefaultDeviceStateService extends TbApplicationEventListener<Partit @@ -247,6 +248,7 @@ public class DefaultDeviceStateService extends TbApplicationEventListener<Partit
247 DeviceStateData stateData = getOrFetchDeviceStateData(deviceId); 248 DeviceStateData stateData = getOrFetchDeviceStateData(deviceId);
248 stateData.getState().setInactivityTimeout(inactivityTimeout); 249 stateData.getState().setInactivityTimeout(inactivityTimeout);
249 checkAndUpdateState(deviceId, stateData); 250 checkAndUpdateState(deviceId, stateData);
  251 + cleanDeviceStateIfBelongsExternalPartition(tenantId, deviceId);
250 } 252 }
251 253
252 @Override 254 @Override
@@ -283,12 +285,10 @@ public class DefaultDeviceStateService extends TbApplicationEventListener<Partit @@ -283,12 +285,10 @@ public class DefaultDeviceStateService extends TbApplicationEventListener<Partit
283 }, deviceStateExecutor); 285 }, deviceStateExecutor);
284 } else if (proto.getUpdated()) { 286 } else if (proto.getUpdated()) {
285 DeviceStateData stateData = getOrFetchDeviceStateData(device.getId()); 287 DeviceStateData stateData = getOrFetchDeviceStateData(device.getId());
286 - if (stateData != null) {  
287 - TbMsgMetaData md = new TbMsgMetaData();  
288 - md.putValue("deviceName", device.getName());  
289 - md.putValue("deviceType", device.getType());  
290 - stateData.setMetaData(md);  
291 - } 288 + TbMsgMetaData md = new TbMsgMetaData();
  289 + md.putValue("deviceName", device.getName());
  290 + md.putValue("deviceType", device.getType());
  291 + stateData.setMetaData(md);
292 } 292 }
293 } else { 293 } else {
294 //Device was probably deleted while message was in queue; 294 //Device was probably deleted while message was in queue;
@@ -356,10 +356,7 @@ public class DefaultDeviceStateService extends TbApplicationEventListener<Partit @@ -356,10 +356,7 @@ public class DefaultDeviceStateService extends TbApplicationEventListener<Partit
356 // We no longer manage current partition of devices; 356 // We no longer manage current partition of devices;
357 removedPartitions.forEach(partition -> { 357 removedPartitions.forEach(partition -> {
358 Set<DeviceId> devices = partitionedDevices.remove(partition); 358 Set<DeviceId> devices = partitionedDevices.remove(partition);
359 - devices.forEach(deviceId -> {  
360 - deviceStates.remove(deviceId);  
361 - deviceLastSavedActivity.remove(deviceId);  
362 - }); 359 + devices.forEach(this::cleanUpDeviceStateMap);
363 }); 360 });
364 361
365 addedPartitions.forEach(tpi -> partitionedDevices.computeIfAbsent(tpi, key -> ConcurrentHashMap.newKeySet())); 362 addedPartitions.forEach(tpi -> partitionedDevices.computeIfAbsent(tpi, key -> ConcurrentHashMap.newKeySet()));
@@ -463,11 +460,12 @@ public class DefaultDeviceStateService extends TbApplicationEventListener<Partit @@ -463,11 +460,12 @@ public class DefaultDeviceStateService extends TbApplicationEventListener<Partit
463 460
464 void updateInactivityStateIfExpired() { 461 void updateInactivityStateIfExpired() {
465 final long ts = System.currentTimeMillis(); 462 final long ts = System.currentTimeMillis();
466 - log.debug("Calculating state updates for {} devices", deviceStates.size());  
467 - Set<DeviceId> deviceIds = new HashSet<>(deviceStates.keySet());  
468 - for (DeviceId deviceId : deviceIds) {  
469 - updateInactivityStateIfExpired(ts, deviceId);  
470 - } 463 + partitionedDevices.forEach((tpi, deviceIds) -> {
  464 + log.debug("Calculating state updates. tpi {} for {} devices", tpi.getFullTopicName(), deviceIds.size());
  465 + for (DeviceId deviceId : deviceIds) {
  466 + updateInactivityStateIfExpired(ts, deviceId);
  467 + }
  468 + });
471 } 469 }
472 470
473 void updateInactivityStateIfExpired(long ts, DeviceId deviceId) { 471 void updateInactivityStateIfExpired(long ts, DeviceId deviceId) {
@@ -488,8 +486,7 @@ public class DefaultDeviceStateService extends TbApplicationEventListener<Partit @@ -488,8 +486,7 @@ public class DefaultDeviceStateService extends TbApplicationEventListener<Partit
488 } 486 }
489 } else { 487 } else {
490 log.debug("[{}] Device that belongs to other server is detected and removed.", deviceId); 488 log.debug("[{}] Device that belongs to other server is detected and removed.", deviceId);
491 - deviceStates.remove(deviceId);  
492 - deviceLastSavedActivity.remove(deviceId); 489 + cleanUpDeviceStateMap(deviceId);
493 } 490 }
494 } 491 }
495 492
@@ -522,6 +519,15 @@ public class DefaultDeviceStateService extends TbApplicationEventListener<Partit @@ -522,6 +519,15 @@ public class DefaultDeviceStateService extends TbApplicationEventListener<Partit
522 } 519 }
523 } 520 }
524 521
  522 + private void cleanDeviceStateIfBelongsExternalPartition(TenantId tenantId, final DeviceId deviceId) {
  523 + TopicPartitionInfo tpi = partitionService.resolve(ServiceType.TB_CORE, tenantId, deviceId);
  524 + if (!partitionedDevices.containsKey(tpi)) {
  525 + cleanUpDeviceStateMap(deviceId);
  526 + log.debug("[{}][{}] device belongs to external partition. Probably rebalancing is in progress. Topic: {}"
  527 + , tenantId, deviceId, tpi.getFullTopicName());
  528 + }
  529 + }
  530 +
525 private void sendDeviceEvent(TenantId tenantId, DeviceId deviceId, boolean added, boolean updated, boolean deleted) { 531 private void sendDeviceEvent(TenantId tenantId, DeviceId deviceId, boolean added, boolean updated, boolean deleted) {
526 TransportProtos.DeviceStateServiceMsgProto.Builder builder = TransportProtos.DeviceStateServiceMsgProto.newBuilder(); 532 TransportProtos.DeviceStateServiceMsgProto.Builder builder = TransportProtos.DeviceStateServiceMsgProto.newBuilder();
527 builder.setTenantIdMSB(tenantId.getId().getMostSignificantBits()); 533 builder.setTenantIdMSB(tenantId.getId().getMostSignificantBits());
@@ -536,13 +542,16 @@ public class DefaultDeviceStateService extends TbApplicationEventListener<Partit @@ -536,13 +542,16 @@ public class DefaultDeviceStateService extends TbApplicationEventListener<Partit
536 } 542 }
537 543
538 private void onDeviceDeleted(TenantId tenantId, DeviceId deviceId) { 544 private void onDeviceDeleted(TenantId tenantId, DeviceId deviceId) {
539 - deviceStates.remove(deviceId);  
540 - deviceLastSavedActivity.remove(deviceId); 545 + cleanUpDeviceStateMap(deviceId);
541 TopicPartitionInfo tpi = partitionService.resolve(ServiceType.TB_CORE, tenantId, deviceId); 546 TopicPartitionInfo tpi = partitionService.resolve(ServiceType.TB_CORE, tenantId, deviceId);
542 Set<DeviceId> deviceIdSet = partitionedDevices.get(tpi); 547 Set<DeviceId> deviceIdSet = partitionedDevices.get(tpi);
543 deviceIdSet.remove(deviceId); 548 deviceIdSet.remove(deviceId);
544 } 549 }
545 550
  551 + private void cleanUpDeviceStateMap(DeviceId deviceId) {
  552 + deviceStates.remove(deviceId);
  553 + }
  554 +
546 private ListenableFuture<DeviceStateData> fetchDeviceState(Device device) { 555 private ListenableFuture<DeviceStateData> fetchDeviceState(Device device) {
547 ListenableFuture<DeviceStateData> future; 556 ListenableFuture<DeviceStateData> future;
548 if (persistToTelemetry) { 557 if (persistToTelemetry) {
@@ -18,6 +18,7 @@ package org.thingsboard.server.service.state; @@ -18,6 +18,7 @@ package org.thingsboard.server.service.state;
18 import org.springframework.context.ApplicationListener; 18 import org.springframework.context.ApplicationListener;
19 import org.thingsboard.server.common.data.Device; 19 import org.thingsboard.server.common.data.Device;
20 import org.thingsboard.server.common.data.id.DeviceId; 20 import org.thingsboard.server.common.data.id.DeviceId;
  21 +import org.thingsboard.server.common.data.id.TenantId;
21 import org.thingsboard.server.queue.discovery.event.PartitionChangeEvent; 22 import org.thingsboard.server.queue.discovery.event.PartitionChangeEvent;
22 import org.thingsboard.server.gen.transport.TransportProtos; 23 import org.thingsboard.server.gen.transport.TransportProtos;
23 import org.thingsboard.server.common.msg.queue.TbCallback; 24 import org.thingsboard.server.common.msg.queue.TbCallback;
@@ -33,13 +34,13 @@ public interface DeviceStateService extends ApplicationListener<PartitionChangeE @@ -33,13 +34,13 @@ public interface DeviceStateService extends ApplicationListener<PartitionChangeE
33 34
34 void onDeviceDeleted(Device device); 35 void onDeviceDeleted(Device device);
35 36
36 - void onDeviceConnect(DeviceId deviceId); 37 + void onDeviceConnect(TenantId tenantId, DeviceId deviceId);
37 38
38 - void onDeviceActivity(DeviceId deviceId, long lastReportedActivityTime); 39 + void onDeviceActivity(TenantId tenantId, DeviceId deviceId, long lastReportedActivityTime);
39 40
40 - void onDeviceDisconnect(DeviceId deviceId); 41 + void onDeviceDisconnect(TenantId tenantId, DeviceId deviceId);
41 42
42 - void onDeviceInactivityTimeoutUpdate(DeviceId deviceId, long inactivityTimeout); 43 + void onDeviceInactivityTimeoutUpdate(TenantId tenantId, DeviceId deviceId, long inactivityTimeout);
43 44
44 void onQueueMsg(TransportProtos.DeviceStateServiceMsgProto proto, TbCallback bytes); 45 void onQueueMsg(TransportProtos.DeviceStateServiceMsgProto proto, TbCallback bytes);
45 46
@@ -224,7 +224,7 @@ public class DefaultSubscriptionManagerService extends TbApplicationEventListene @@ -224,7 +224,7 @@ public class DefaultSubscriptionManagerService extends TbApplicationEventListene
224 return subscriptionUpdate; 224 return subscriptionUpdate;
225 }); 225 });
226 if (entityId.getEntityType() == EntityType.DEVICE) { 226 if (entityId.getEntityType() == EntityType.DEVICE) {
227 - updateDeviceInactivityTimeout(entityId, ts); 227 + updateDeviceInactivityTimeout(tenantId, entityId, ts);
228 } 228 }
229 callback.onSuccess(); 229 callback.onSuccess();
230 } 230 }
@@ -259,7 +259,7 @@ public class DefaultSubscriptionManagerService extends TbApplicationEventListene @@ -259,7 +259,7 @@ public class DefaultSubscriptionManagerService extends TbApplicationEventListene
259 }); 259 });
260 if (entityId.getEntityType() == EntityType.DEVICE) { 260 if (entityId.getEntityType() == EntityType.DEVICE) {
261 if (TbAttributeSubscriptionScope.SERVER_SCOPE.name().equalsIgnoreCase(scope)) { 261 if (TbAttributeSubscriptionScope.SERVER_SCOPE.name().equalsIgnoreCase(scope)) {
262 - updateDeviceInactivityTimeout(entityId, attributes); 262 + updateDeviceInactivityTimeout(tenantId, entityId, attributes);
263 } else if (TbAttributeSubscriptionScope.SHARED_SCOPE.name().equalsIgnoreCase(scope) && notifyDevice) { 263 } else if (TbAttributeSubscriptionScope.SHARED_SCOPE.name().equalsIgnoreCase(scope) && notifyDevice) {
264 clusterService.pushMsgToCore(DeviceAttributesEventNotificationMsg.onUpdate(tenantId, 264 clusterService.pushMsgToCore(DeviceAttributesEventNotificationMsg.onUpdate(tenantId,
265 new DeviceId(entityId.getId()), DataConstants.SHARED_SCOPE, new ArrayList<>(attributes)) 265 new DeviceId(entityId.getId()), DataConstants.SHARED_SCOPE, new ArrayList<>(attributes))
@@ -269,10 +269,10 @@ public class DefaultSubscriptionManagerService extends TbApplicationEventListene @@ -269,10 +269,10 @@ public class DefaultSubscriptionManagerService extends TbApplicationEventListene
269 callback.onSuccess(); 269 callback.onSuccess();
270 } 270 }
271 271
272 - private void updateDeviceInactivityTimeout(EntityId entityId, List<? extends KvEntry> kvEntries) { 272 + private void updateDeviceInactivityTimeout(TenantId tenantId, EntityId entityId, List<? extends KvEntry> kvEntries) {
273 for (KvEntry kvEntry : kvEntries) { 273 for (KvEntry kvEntry : kvEntries) {
274 if (kvEntry.getKey().equals(DefaultDeviceStateService.INACTIVITY_TIMEOUT)) { 274 if (kvEntry.getKey().equals(DefaultDeviceStateService.INACTIVITY_TIMEOUT)) {
275 - deviceStateService.onDeviceInactivityTimeoutUpdate(new DeviceId(entityId.getId()), kvEntry.getLongValue().orElse(0L)); 275 + deviceStateService.onDeviceInactivityTimeoutUpdate(tenantId, new DeviceId(entityId.getId()), kvEntry.getLongValue().orElse(0L));
276 } 276 }
277 } 277 }
278 } 278 }
@@ -588,6 +588,7 @@ transport: @@ -588,6 +588,7 @@ transport:
588 bind_address: "${MQTT_BIND_ADDRESS:0.0.0.0}" 588 bind_address: "${MQTT_BIND_ADDRESS:0.0.0.0}"
589 bind_port: "${MQTT_BIND_PORT:1883}" 589 bind_port: "${MQTT_BIND_PORT:1883}"
590 timeout: "${MQTT_TIMEOUT:10000}" 590 timeout: "${MQTT_TIMEOUT:10000}"
  591 + 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
591 netty: 592 netty:
592 leak_detector_level: "${NETTY_LEAK_DETECTOR_LVL:DISABLED}" 593 leak_detector_level: "${NETTY_LEAK_DETECTOR_LVL:DISABLED}"
593 boss_group_thread_count: "${NETTY_BOSS_GROUP_THREADS:1}" 594 boss_group_thread_count: "${NETTY_BOSS_GROUP_THREADS:1}"
@@ -27,6 +27,7 @@ import org.springframework.mock.web.MockMultipartFile; @@ -27,6 +27,7 @@ import org.springframework.mock.web.MockMultipartFile;
27 import org.springframework.test.web.servlet.request.MockMultipartHttpServletRequestBuilder; 27 import org.springframework.test.web.servlet.request.MockMultipartHttpServletRequestBuilder;
28 import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; 28 import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
29 import org.thingsboard.common.util.JacksonUtil; 29 import org.thingsboard.common.util.JacksonUtil;
  30 +import org.thingsboard.common.util.ThingsBoardThreadFactory;
30 import org.thingsboard.server.common.data.Device; 31 import org.thingsboard.server.common.data.Device;
31 import org.thingsboard.server.common.data.DeviceProfile; 32 import org.thingsboard.server.common.data.DeviceProfile;
32 import org.thingsboard.server.common.data.DeviceProfileProvisionType; 33 import org.thingsboard.server.common.data.DeviceProfileProvisionType;
@@ -261,7 +262,7 @@ public class AbstractLwM2MIntegrationTest extends AbstractWebsocketTest { @@ -261,7 +262,7 @@ public class AbstractLwM2MIntegrationTest extends AbstractWebsocketTest {
261 262
262 @Before 263 @Before
263 public void beforeTest() throws Exception { 264 public void beforeTest() throws Exception {
264 - executor = Executors.newScheduledThreadPool(10); 265 + executor = Executors.newScheduledThreadPool(10, ThingsBoardThreadFactory.forName("test-lwm2m-scheduled"));
265 loginTenantAdmin(); 266 loginTenantAdmin();
266 267
267 String[] resources = new String[]{"1.xml", "2.xml", "3.xml", "5.xml", "9.xml"}; 268 String[] resources = new String[]{"1.xml", "2.xml", "3.xml", "5.xml", "9.xml"};
@@ -16,6 +16,8 @@ @@ -16,6 +16,8 @@
16 package org.thingsboard.server.transport.lwm2m; 16 package org.thingsboard.server.transport.lwm2m;
17 17
18 import com.fasterxml.jackson.core.type.TypeReference; 18 import com.fasterxml.jackson.core.type.TypeReference;
  19 +import lombok.extern.slf4j.Slf4j;
  20 +import org.junit.After;
19 import org.junit.Assert; 21 import org.junit.Assert;
20 import org.junit.Test; 22 import org.junit.Test;
21 import org.thingsboard.server.common.data.Device; 23 import org.thingsboard.server.common.data.Device;
@@ -23,16 +25,18 @@ import org.thingsboard.server.common.data.device.credentials.lwm2m.NoSecClientCr @@ -23,16 +25,18 @@ import org.thingsboard.server.common.data.device.credentials.lwm2m.NoSecClientCr
23 import org.thingsboard.server.common.data.kv.KvEntry; 25 import org.thingsboard.server.common.data.kv.KvEntry;
24 import org.thingsboard.server.common.data.kv.TsKvEntry; 26 import org.thingsboard.server.common.data.kv.TsKvEntry;
25 import org.thingsboard.server.common.data.ota.OtaPackageUpdateStatus; 27 import org.thingsboard.server.common.data.ota.OtaPackageUpdateStatus;
26 -import org.thingsboard.server.common.data.query.EntityKey;  
27 -import org.thingsboard.server.common.data.query.EntityKeyType;  
28 import org.thingsboard.server.transport.lwm2m.client.LwM2MTestClient; 28 import org.thingsboard.server.transport.lwm2m.client.LwM2MTestClient;
29 29
30 import java.util.Arrays; 30 import java.util.Arrays;
31 import java.util.Collections; 31 import java.util.Collections;
32 import java.util.Comparator; 32 import java.util.Comparator;
33 import java.util.List; 33 import java.util.List;
  34 +import java.util.UUID;
  35 +import java.util.concurrent.TimeUnit;
34 import java.util.stream.Collectors; 36 import java.util.stream.Collectors;
35 37
  38 +import static org.awaitility.Awaitility.await;
  39 +import static org.hamcrest.Matchers.is;
36 import static org.thingsboard.rest.client.utils.RestJsonConverter.toTimeseries; 40 import static org.thingsboard.rest.client.utils.RestJsonConverter.toTimeseries;
37 import static org.thingsboard.server.common.data.ota.OtaPackageUpdateStatus.DOWNLOADED; 41 import static org.thingsboard.server.common.data.ota.OtaPackageUpdateStatus.DOWNLOADED;
38 import static org.thingsboard.server.common.data.ota.OtaPackageUpdateStatus.DOWNLOADING; 42 import static org.thingsboard.server.common.data.ota.OtaPackageUpdateStatus.DOWNLOADING;
@@ -43,8 +47,10 @@ import static org.thingsboard.server.common.data.ota.OtaPackageUpdateStatus.UPDA @@ -43,8 +47,10 @@ import static org.thingsboard.server.common.data.ota.OtaPackageUpdateStatus.UPDA
43 import static org.thingsboard.server.common.data.ota.OtaPackageUpdateStatus.UPDATING; 47 import static org.thingsboard.server.common.data.ota.OtaPackageUpdateStatus.UPDATING;
44 import static org.thingsboard.server.common.data.ota.OtaPackageUpdateStatus.VERIFIED; 48 import static org.thingsboard.server.common.data.ota.OtaPackageUpdateStatus.VERIFIED;
45 49
  50 +@Slf4j
46 public class NoSecLwM2MIntegrationTest extends AbstractLwM2MIntegrationTest { 51 public class NoSecLwM2MIntegrationTest extends AbstractLwM2MIntegrationTest {
47 52
  53 + public static final int TIMEOUT = 30;
48 private final String OTA_TRANSPORT_CONFIGURATION = "{\n" + 54 private final String OTA_TRANSPORT_CONFIGURATION = "{\n" +
49 " \"observeAttr\": {\n" + 55 " \"observeAttr\": {\n" +
50 " \"keyName\": {\n" + 56 " \"keyName\": {\n" +
@@ -122,6 +128,15 @@ public class NoSecLwM2MIntegrationTest extends AbstractLwM2MIntegrationTest { @@ -122,6 +128,15 @@ public class NoSecLwM2MIntegrationTest extends AbstractLwM2MIntegrationTest {
122 " \"type\": \"LWM2M\"\n" + 128 " \"type\": \"LWM2M\"\n" +
123 "}"; 129 "}";
124 130
  131 + LwM2MTestClient client = null;
  132 +
  133 + @After
  134 + public void tearDown() {
  135 + if (client != null) {
  136 + client.destroy();
  137 + }
  138 + }
  139 +
125 @Test 140 @Test
126 public void testConnectAndObserveTelemetry() throws Exception { 141 public void testConnectAndObserveTelemetry() throws Exception {
127 NoSecClientCredentials clientCredentials = new NoSecClientCredentials(); 142 NoSecClientCredentials clientCredentials = new NoSecClientCredentials();
@@ -196,37 +211,68 @@ public class NoSecLwM2MIntegrationTest extends AbstractLwM2MIntegrationTest { @@ -196,37 +211,68 @@ public class NoSecLwM2MIntegrationTest extends AbstractLwM2MIntegrationTest {
196 } 211 }
197 } 212 }
198 213
  214 + /**
  215 + * This is the example how to use the AWAITILITY instead Thread.sleep()
  216 + * Test will finish as fast as possible, but will await until TIMEOUT if a build machine is busy or slow
  217 + * Check the detailed log output to learn how Awaitility polling the API and when exactly expected result appears
  218 + * */
199 @Test 219 @Test
200 public void testSoftwareUpdateByObject9() throws Exception { 220 public void testSoftwareUpdateByObject9() throws Exception {
201 - LwM2MTestClient client = null;  
202 - try {  
203 - createDeviceProfile(OTA_TRANSPORT_CONFIGURATION);  
204 - NoSecClientCredentials clientCredentials = new NoSecClientCredentials();  
205 - clientCredentials.setEndpoint("OTA_" + ENDPOINT);  
206 - Device device = createDevice(clientCredentials);  
207 -  
208 - device.setSoftwareId(createSoftware().getId());  
209 - device = doPost("/api/device", device, Device.class); 221 + //given
  222 + final List<OtaPackageUpdateStatus> expectedStatuses = Collections.unmodifiableList(Arrays.asList(
  223 + QUEUED, INITIATED, DOWNLOADING, DOWNLOADING, DOWNLOADING, DOWNLOADED, VERIFIED, UPDATED));
210 224
211 - Thread.sleep(1000);  
212 -  
213 - client = new LwM2MTestClient(executor, "OTA_" + ENDPOINT);  
214 - client.init(SECURITY, COAP_CONFIG);  
215 -  
216 - Thread.sleep(3000);  
217 -  
218 - 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<>() {  
219 - }));  
220 -  
221 - List<OtaPackageUpdateStatus> statuses = ts.stream().sorted(Comparator.comparingLong(TsKvEntry::getTs)).map(KvEntry::getValueAsString).map(OtaPackageUpdateStatus::valueOf).collect(Collectors.toList()); 225 + createDeviceProfile(OTA_TRANSPORT_CONFIGURATION);
  226 + NoSecClientCredentials clientCredentials = new NoSecClientCredentials();
  227 + clientCredentials.setEndpoint("OTA_" + ENDPOINT);
  228 + final Device device = createDevice(clientCredentials);
  229 + device.setSoftwareId(createSoftware().getId());
  230 +
  231 + log.warn("Saving by API " + device);
  232 + final Device savedDevice = doPost("/api/device", device, Device.class);
  233 + Assert.assertNotNull(savedDevice);
  234 + log.warn("Device saved by API {}", savedDevice);
  235 +
  236 + log.warn("AWAIT atMost {} SECONDS on get device by API...", TIMEOUT);
  237 + await()
  238 + .atMost(TIMEOUT, TimeUnit.SECONDS)
  239 + .until(() -> getDeviceFromAPI(device.getId().getId()), is(savedDevice));
  240 + log.warn("Got device by API.");
  241 +
  242 + //when
  243 + log.warn("Init the client...");
  244 + client = new LwM2MTestClient(executor, "OTA_" + ENDPOINT);
  245 + client.init(SECURITY, COAP_CONFIG);
  246 + log.warn("Init done");
  247 +
  248 + log.warn("AWAIT atMost {} SECONDS on timeseries List<TsKvEntry> by API with list size {}...", TIMEOUT, expectedStatuses.size());
  249 + await()
  250 + .atMost(30, TimeUnit.SECONDS)
  251 + .until(() -> getSwStateTelemetryFromAPI(device.getId().getId())
  252 + .size(), is(expectedStatuses.size()));
  253 + log.warn("Got an expected await condition!");
  254 +
  255 + //then
  256 + log.warn("Fetching ts for the final asserts");
  257 + List<TsKvEntry> ts = getSwStateTelemetryFromAPI(device.getId().getId());
  258 + log.warn("Got an ts {}", ts);
  259 +
  260 + List<OtaPackageUpdateStatus> statuses = ts.stream().sorted(Comparator.comparingLong(TsKvEntry::getTs)).map(KvEntry::getValueAsString).map(OtaPackageUpdateStatus::valueOf).collect(Collectors.toList());
  261 + log.warn("Converted ts to statuses {}", statuses);
  262 +
  263 + Assert.assertEquals(expectedStatuses, statuses);
  264 + }
222 265
223 - List<OtaPackageUpdateStatus> expectedStatuses = Arrays.asList(QUEUED, INITIATED, DOWNLOADING, DOWNLOADING, DOWNLOADING, DOWNLOADED, VERIFIED, UPDATED); 266 + private Device getDeviceFromAPI(UUID deviceId) throws Exception {
  267 + final Device device = doGet("/api/device/" + deviceId, Device.class);
  268 + log.warn("Fetched device by API for deviceId {}, device is {}", deviceId, device);
  269 + return device;
  270 + }
224 271
225 - Assert.assertEquals(expectedStatuses, statuses);  
226 - } finally {  
227 - if (client != null) {  
228 - client.destroy();  
229 - }  
230 - } 272 + private List<TsKvEntry> getSwStateTelemetryFromAPI(UUID deviceId) throws Exception {
  273 + 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<>() {
  274 + }));
  275 + log.warn("Fetched telemetry by API for deviceId {}, list size {}, tsKvEntries {}", deviceId, tsKvEntries.size(), tsKvEntries);
  276 + return tsKvEntries;
231 } 277 }
232 } 278 }
@@ -77,25 +77,34 @@ public class TbKafkaProducerTemplate<T extends TbQueueMsg> implements TbQueuePro @@ -77,25 +77,34 @@ public class TbKafkaProducerTemplate<T extends TbQueueMsg> implements TbQueuePro
77 77
78 @Override 78 @Override
79 public void send(TopicPartitionInfo tpi, T msg, TbQueueCallback callback) { 79 public void send(TopicPartitionInfo tpi, T msg, TbQueueCallback callback) {
80 - createTopicIfNotExist(tpi);  
81 - String key = msg.getKey().toString();  
82 - byte[] data = msg.getData();  
83 - ProducerRecord<String, byte[]> record;  
84 - Iterable<Header> headers = msg.getHeaders().getData().entrySet().stream().map(e -> new RecordHeader(e.getKey(), e.getValue())).collect(Collectors.toList());  
85 - record = new ProducerRecord<>(tpi.getFullTopicName(), null, key, data, headers);  
86 - producer.send(record, (metadata, exception) -> {  
87 - if (exception == null) {  
88 - if (callback != null) {  
89 - callback.onSuccess(new KafkaTbQueueMsgMetadata(metadata));  
90 - }  
91 - } else {  
92 - if (callback != null) {  
93 - callback.onFailure(exception); 80 + try {
  81 + createTopicIfNotExist(tpi);
  82 + String key = msg.getKey().toString();
  83 + byte[] data = msg.getData();
  84 + ProducerRecord<String, byte[]> record;
  85 + Iterable<Header> headers = msg.getHeaders().getData().entrySet().stream().map(e -> new RecordHeader(e.getKey(), e.getValue())).collect(Collectors.toList());
  86 + record = new ProducerRecord<>(tpi.getFullTopicName(), null, key, data, headers);
  87 + producer.send(record, (metadata, exception) -> {
  88 + if (exception == null) {
  89 + if (callback != null) {
  90 + callback.onSuccess(new KafkaTbQueueMsgMetadata(metadata));
  91 + }
94 } else { 92 } else {
95 - log.warn("Producer template failure: {}", exception.getMessage(), exception); 93 + if (callback != null) {
  94 + callback.onFailure(exception);
  95 + } else {
  96 + log.warn("Producer template failure: {}", exception.getMessage(), exception);
  97 + }
96 } 98 }
  99 + });
  100 + } catch (Exception e) {
  101 + if (callback != null) {
  102 + callback.onFailure(e);
  103 + } else {
  104 + log.warn("Producer template failure (send method wrapper): {}", e.getMessage(), e);
97 } 105 }
98 - }); 106 + throw e;
  107 + }
99 } 108 }
100 109
101 private void createTopicIfNotExist(TopicPartitionInfo tpi) { 110 private void createTopicIfNotExist(TopicPartitionInfo tpi) {
@@ -19,6 +19,9 @@ import lombok.RequiredArgsConstructor; @@ -19,6 +19,9 @@ import lombok.RequiredArgsConstructor;
19 import lombok.extern.slf4j.Slf4j; 19 import lombok.extern.slf4j.Slf4j;
20 import org.eclipse.californium.elements.util.SslContextUtil; 20 import org.eclipse.californium.elements.util.SslContextUtil;
21 import org.eclipse.californium.scandium.config.DtlsConnectorConfig; 21 import org.eclipse.californium.scandium.config.DtlsConnectorConfig;
  22 +import org.eclipse.leshan.core.model.ObjectLoader;
  23 +import org.eclipse.leshan.core.model.ObjectModel;
  24 +import org.eclipse.leshan.core.model.StaticModel;
22 import org.eclipse.leshan.server.bootstrap.BootstrapSessionManager; 25 import org.eclipse.leshan.server.bootstrap.BootstrapSessionManager;
23 import org.eclipse.leshan.server.californium.bootstrap.LeshanBootstrapServer; 26 import org.eclipse.leshan.server.californium.bootstrap.LeshanBootstrapServer;
24 import org.eclipse.leshan.server.californium.bootstrap.LeshanBootstrapServerBuilder; 27 import org.eclipse.leshan.server.californium.bootstrap.LeshanBootstrapServerBuilder;
@@ -26,6 +29,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; @@ -26,6 +29,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
26 import org.springframework.stereotype.Component; 29 import org.springframework.stereotype.Component;
27 import org.thingsboard.server.transport.lwm2m.bootstrap.secure.LwM2MBootstrapSecurityStore; 30 import org.thingsboard.server.transport.lwm2m.bootstrap.secure.LwM2MBootstrapSecurityStore;
28 import org.thingsboard.server.transport.lwm2m.bootstrap.secure.LwM2MInMemoryBootstrapConfigStore; 31 import org.thingsboard.server.transport.lwm2m.bootstrap.secure.LwM2MInMemoryBootstrapConfigStore;
  32 +import org.thingsboard.server.transport.lwm2m.bootstrap.secure.LwM2MInMemoryBootstrapConfigurationAdapter;
29 import org.thingsboard.server.transport.lwm2m.bootstrap.secure.LwM2mDefaultBootstrapSessionManager; 33 import org.thingsboard.server.transport.lwm2m.bootstrap.secure.LwM2mDefaultBootstrapSessionManager;
30 import org.thingsboard.server.transport.lwm2m.config.LwM2MTransportBootstrapConfig; 34 import org.thingsboard.server.transport.lwm2m.config.LwM2MTransportBootstrapConfig;
31 import org.thingsboard.server.transport.lwm2m.config.LwM2MTransportServerConfig; 35 import org.thingsboard.server.transport.lwm2m.config.LwM2MTransportServerConfig;
@@ -38,6 +42,7 @@ import java.security.KeyStoreException; @@ -38,6 +42,7 @@ import java.security.KeyStoreException;
38 import java.security.PrivateKey; 42 import java.security.PrivateKey;
39 import java.security.PublicKey; 43 import java.security.PublicKey;
40 import java.security.cert.X509Certificate; 44 import java.security.cert.X509Certificate;
  45 +import java.util.List;
41 46
42 import static org.thingsboard.server.transport.lwm2m.server.LwM2mNetworkConfig.getCoapConfig; 47 import static org.thingsboard.server.transport.lwm2m.server.LwM2mNetworkConfig.getCoapConfig;
43 48
@@ -79,12 +84,14 @@ public class LwM2MTransportBootstrapService { @@ -79,12 +84,14 @@ public class LwM2MTransportBootstrapService {
79 builder.setCoapConfig(getCoapConfig(bootstrapConfig.getPort(), bootstrapConfig.getSecurePort(), serverConfig)); 84 builder.setCoapConfig(getCoapConfig(bootstrapConfig.getPort(), bootstrapConfig.getSecurePort(), serverConfig));
80 85
81 /* Define model provider (Create Models )*/ 86 /* Define model provider (Create Models )*/
  87 + List<ObjectModel> models = ObjectLoader.loadDefault();
  88 + builder.setModel(new StaticModel(models));
82 89
83 /* Create credentials */ 90 /* Create credentials */
84 this.setServerWithCredentials(builder); 91 this.setServerWithCredentials(builder);
85 92
86 -// /** Set securityStore with new ConfigStore */  
87 -// builder.setConfigStore(lwM2MInMemoryBootstrapConfigStore); 93 + /* Set securityStore with new ConfigStore */
  94 + builder.setConfigStore(new LwM2MInMemoryBootstrapConfigurationAdapter(lwM2MInMemoryBootstrapConfigStore));
88 95
89 /* SecurityStore */ 96 /* SecurityStore */
90 builder.setSecurityStore(lwM2MBootstrapSecurityStore); 97 builder.setSecurityStore(lwM2MBootstrapSecurityStore);
@@ -74,15 +74,19 @@ public class LwM2MBootstrapConfig implements Serializable { @@ -74,15 +74,19 @@ public class LwM2MBootstrapConfig implements Serializable {
74 configBs.servers.put(0, server0); 74 configBs.servers.put(0, server0);
75 /* Security Configuration (object 0) as defined in LWM2M 1.0.x TS. Bootstrap instance = 0 */ 75 /* Security Configuration (object 0) as defined in LWM2M 1.0.x TS. Bootstrap instance = 0 */
76 this.bootstrapServer.setBootstrapServerIs(true); 76 this.bootstrapServer.setBootstrapServerIs(true);
77 - 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())); 77 + 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()));
78 /* Security Configuration (object 0) as defined in LWM2M 1.0.x TS. Server instance = 1 */ 78 /* Security Configuration (object 0) as defined in LWM2M 1.0.x TS. Server instance = 1 */
79 - 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())); 79 + 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()));
80 return configBs; 80 return configBs;
81 } 81 }
82 82
83 - private BootstrapConfig.ServerSecurity setServerSecurity(String host, Integer port, boolean bootstrapServer, SecurityMode securityMode, String clientPublicKey, String serverPublicKey, String secretKey, int serverId) { 83 + private BootstrapConfig.ServerSecurity setServerSecurity(String host, Integer port, String securityHost, Integer securityPort, boolean bootstrapServer, SecurityMode securityMode, String clientPublicKey, String serverPublicKey, String secretKey, int serverId) {
84 BootstrapConfig.ServerSecurity serverSecurity = new BootstrapConfig.ServerSecurity(); 84 BootstrapConfig.ServerSecurity serverSecurity = new BootstrapConfig.ServerSecurity();
85 - serverSecurity.uri = "coaps://" + host + ":" + Integer.toString(port); 85 + if (securityMode.equals(SecurityMode.NO_SEC)) {
  86 + serverSecurity.uri = "coap://" + host + ":" + Integer.toString(port);
  87 + } else {
  88 + serverSecurity.uri = "coaps://" + securityHost + ":" + Integer.toString(securityPort);
  89 + }
86 serverSecurity.bootstrapServer = bootstrapServer; 90 serverSecurity.bootstrapServer = bootstrapServer;
87 serverSecurity.securityMode = securityMode; 91 serverSecurity.securityMode = securityMode;
88 serverSecurity.publicKeyOrId = setPublicKeyOrId(clientPublicKey, securityMode); 92 serverSecurity.publicKeyOrId = setPublicKeyOrId(clientPublicKey, securityMode);
  1 +/**
  2 + * Copyright © 2016-2021 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +package org.thingsboard.server.transport.lwm2m.bootstrap.secure;
  17 +
  18 +import org.eclipse.leshan.server.bootstrap.BootstrapConfigStore;
  19 +import org.eclipse.leshan.server.bootstrap.BootstrapConfigurationStoreAdapter;
  20 +
  21 +public class LwM2MInMemoryBootstrapConfigurationAdapter extends BootstrapConfigurationStoreAdapter {
  22 +
  23 + public LwM2MInMemoryBootstrapConfigurationAdapter(BootstrapConfigStore store) {
  24 + super(store);
  25 + }
  26 +
  27 +}
@@ -31,24 +31,31 @@ public class LwM2MServerBootstrap { @@ -31,24 +31,31 @@ public class LwM2MServerBootstrap {
31 31
32 String host = "0.0.0.0"; 32 String host = "0.0.0.0";
33 Integer port = 0; 33 Integer port = 0;
  34 + String securityHost = "0.0.0.0";
  35 + Integer securityPort = 0;
34 36
35 SecurityMode securityMode = SecurityMode.NO_SEC; 37 SecurityMode securityMode = SecurityMode.NO_SEC;
36 38
37 Integer serverId = 123; 39 Integer serverId = 123;
38 boolean bootstrapServerIs = false; 40 boolean bootstrapServerIs = false;
39 41
40 - public LwM2MServerBootstrap(){}; 42 + public LwM2MServerBootstrap() {
  43 + }
  44 +
  45 + ;
41 46
42 public LwM2MServerBootstrap(LwM2MServerBootstrap bootstrapFromCredential, LwM2MServerBootstrap profileServerBootstrap) { 47 public LwM2MServerBootstrap(LwM2MServerBootstrap bootstrapFromCredential, LwM2MServerBootstrap profileServerBootstrap) {
43 - this.clientPublicKeyOrId = bootstrapFromCredential.getClientPublicKeyOrId();  
44 - this.clientSecretKey = bootstrapFromCredential.getClientSecretKey();  
45 - this.serverPublicKey = profileServerBootstrap.getServerPublicKey();  
46 - this.clientHoldOffTime = profileServerBootstrap.getClientHoldOffTime();  
47 - this.bootstrapServerAccountTimeout = profileServerBootstrap.getBootstrapServerAccountTimeout();  
48 - this.host = (profileServerBootstrap.getHost().equals("0.0.0.0")) ? "localhost" : profileServerBootstrap.getHost();  
49 - this.port = profileServerBootstrap.getPort();  
50 - this.securityMode = profileServerBootstrap.getSecurityMode();  
51 - this.serverId = profileServerBootstrap.getServerId();  
52 - this.bootstrapServerIs = profileServerBootstrap.bootstrapServerIs; 48 + this.clientPublicKeyOrId = bootstrapFromCredential.getClientPublicKeyOrId();
  49 + this.clientSecretKey = bootstrapFromCredential.getClientSecretKey();
  50 + this.serverPublicKey = profileServerBootstrap.getServerPublicKey();
  51 + this.clientHoldOffTime = profileServerBootstrap.getClientHoldOffTime();
  52 + this.bootstrapServerAccountTimeout = profileServerBootstrap.getBootstrapServerAccountTimeout();
  53 + this.host = (profileServerBootstrap.getHost().equals("0.0.0.0")) ? "localhost" : profileServerBootstrap.getHost();
  54 + this.port = profileServerBootstrap.getPort();
  55 + this.securityHost = (profileServerBootstrap.getSecurityHost().equals("0.0.0.0")) ? "localhost" : profileServerBootstrap.getSecurityHost();
  56 + this.securityPort = profileServerBootstrap.getSecurityPort();
  57 + this.securityMode = profileServerBootstrap.getSecurityMode();
  58 + this.serverId = profileServerBootstrap.getServerId();
  59 + this.bootstrapServerIs = profileServerBootstrap.bootstrapServerIs;
53 } 60 }
54 } 61 }
@@ -93,7 +93,12 @@ public class LwM2mClientContextImpl implements LwM2mClientContext { @@ -93,7 +93,12 @@ public class LwM2mClientContextImpl implements LwM2mClientContext {
93 log.debug("Fetched clients from store: {}", fetchedClients); 93 log.debug("Fetched clients from store: {}", fetchedClients);
94 fetchedClients.forEach(client -> { 94 fetchedClients.forEach(client -> {
95 lwM2mClientsByEndpoint.put(client.getEndpoint(), client); 95 lwM2mClientsByEndpoint.put(client.getEndpoint(), client);
96 - updateFetchedClient(nodeId, client); 96 + try {
  97 + client.lock();
  98 + updateFetchedClient(nodeId, client);
  99 + } finally {
  100 + client.unlock();
  101 + }
97 }); 102 });
98 } 103 }
99 104
@@ -161,7 +166,7 @@ public class LwM2mClientContextImpl implements LwM2mClientContext { @@ -161,7 +166,7 @@ public class LwM2mClientContextImpl implements LwM2mClientContext {
161 this.lwM2mClientsByRegistrationId.put(registration.getId(), client); 166 this.lwM2mClientsByRegistrationId.put(registration.getId(), client);
162 client.setState(LwM2MClientState.REGISTERED); 167 client.setState(LwM2MClientState.REGISTERED);
163 onUplink(client); 168 onUplink(client);
164 - if(!compareAndSetSleepFlag(client, false)){ 169 + if (!compareAndSetSleepFlag(client, false)) {
165 clientStore.put(client); 170 clientStore.put(client);
166 } 171 }
167 } finally { 172 } finally {
@@ -311,7 +316,11 @@ public class LwM2mClientContextImpl implements LwM2mClientContext { @@ -311,7 +316,11 @@ public class LwM2mClientContextImpl implements LwM2mClientContext {
311 public void update(LwM2mClient client) { 316 public void update(LwM2mClient client) {
312 client.lock(); 317 client.lock();
313 try { 318 try {
314 - clientStore.put(client); 319 + if (client.getState().equals(LwM2MClientState.REGISTERED)) {
  320 + clientStore.put(client);
  321 + } else {
  322 + log.error("[{}] Client is in invalid state: {}!", client.getEndpoint(), client.getState());
  323 + }
315 } finally { 324 } finally {
316 client.unlock(); 325 client.unlock();
317 } 326 }
@@ -106,6 +106,7 @@ public abstract class LwM2MClientOtaInfo<Strategy, State, Result> { @@ -106,6 +106,7 @@ public abstract class LwM2MClientOtaInfo<Strategy, State, Result> {
106 106
107 public abstract OtaPackageType getType(); 107 public abstract OtaPackageType getType();
108 108
  109 + @JsonIgnore
109 public String getTargetPackageId() { 110 public String getTargetPackageId() {
110 return getPackageId(targetName, targetVersion); 111 return getPackageId(targetName, targetVersion);
111 } 112 }
@@ -15,11 +15,13 @@ @@ -15,11 +15,13 @@
15 */ 15 */
16 package org.thingsboard.server.transport.lwm2m.server.store; 16 package org.thingsboard.server.transport.lwm2m.server.store;
17 17
  18 +import lombok.extern.slf4j.Slf4j;
18 import org.nustaq.serialization.FSTConfiguration; 19 import org.nustaq.serialization.FSTConfiguration;
19 import org.springframework.data.redis.connection.RedisClusterConnection; 20 import org.springframework.data.redis.connection.RedisClusterConnection;
20 import org.springframework.data.redis.connection.RedisConnectionFactory; 21 import org.springframework.data.redis.connection.RedisConnectionFactory;
21 import org.springframework.data.redis.core.Cursor; 22 import org.springframework.data.redis.core.Cursor;
22 import org.springframework.data.redis.core.ScanOptions; 23 import org.springframework.data.redis.core.ScanOptions;
  24 +import org.thingsboard.server.transport.lwm2m.server.client.LwM2MClientState;
23 import org.thingsboard.server.transport.lwm2m.server.client.LwM2mClient; 25 import org.thingsboard.server.transport.lwm2m.server.client.LwM2mClient;
24 26
25 import java.util.ArrayList; 27 import java.util.ArrayList;
@@ -27,6 +29,7 @@ import java.util.HashSet; @@ -27,6 +29,7 @@ import java.util.HashSet;
27 import java.util.List; 29 import java.util.List;
28 import java.util.Set; 30 import java.util.Set;
29 31
  32 +@Slf4j
30 public class TbRedisLwM2MClientStore implements TbLwM2MClientStore { 33 public class TbRedisLwM2MClientStore implements TbLwM2MClientStore {
31 34
32 private static final String CLIENT_EP = "CLIENT#EP#"; 35 private static final String CLIENT_EP = "CLIENT#EP#";
@@ -76,9 +79,13 @@ public class TbRedisLwM2MClientStore implements TbLwM2MClientStore { @@ -76,9 +79,13 @@ public class TbRedisLwM2MClientStore implements TbLwM2MClientStore {
76 79
77 @Override 80 @Override
78 public void put(LwM2mClient client) { 81 public void put(LwM2mClient client) {
79 - byte[] clientSerialized = serializer.asByteArray(client);  
80 - try (var connection = connectionFactory.getConnection()) {  
81 - connection.getSet(getKey(client.getEndpoint()), clientSerialized); 82 + if (client.getState().equals(LwM2MClientState.UNREGISTERED)) {
  83 + log.error("[{}] Client is in invalid state: {}!", client.getEndpoint(), client.getState(), new Exception());
  84 + } else {
  85 + byte[] clientSerialized = serializer.asByteArray(client);
  86 + try (var connection = connectionFactory.getConnection()) {
  87 + connection.getSet(getKey(client.getEndpoint()), clientSerialized);
  88 + }
82 } 89 }
83 } 90 }
84 91
@@ -89,6 +89,11 @@ @@ -89,6 +89,11 @@
89 <scope>test</scope> 89 <scope>test</scope>
90 </dependency> 90 </dependency>
91 <dependency> 91 <dependency>
  92 + <groupId>org.awaitility</groupId>
  93 + <artifactId>awaitility</artifactId>
  94 + <scope>test</scope>
  95 + </dependency>
  96 + <dependency>
92 <groupId>org.mockito</groupId> 97 <groupId>org.mockito</groupId>
93 <artifactId>mockito-core</artifactId> 98 <artifactId>mockito-core</artifactId>
94 <scope>test</scope> 99 <scope>test</scope>
@@ -23,10 +23,15 @@ import org.springframework.beans.factory.annotation.Autowired; @@ -23,10 +23,15 @@ import org.springframework.beans.factory.annotation.Autowired;
23 import org.springframework.beans.factory.annotation.Value; 23 import org.springframework.beans.factory.annotation.Value;
24 import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; 24 import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
25 import org.springframework.stereotype.Component; 25 import org.springframework.stereotype.Component;
  26 +import org.thingsboard.common.util.ThingsBoardExecutors;
26 import org.thingsboard.server.common.transport.TransportContext; 27 import org.thingsboard.server.common.transport.TransportContext;
27 import org.thingsboard.server.transport.mqtt.adaptors.JsonMqttAdaptor; 28 import org.thingsboard.server.transport.mqtt.adaptors.JsonMqttAdaptor;
28 import org.thingsboard.server.transport.mqtt.adaptors.ProtoMqttAdaptor; 29 import org.thingsboard.server.transport.mqtt.adaptors.ProtoMqttAdaptor;
29 30
  31 +import javax.annotation.PostConstruct;
  32 +import javax.annotation.PreDestroy;
  33 +import java.util.concurrent.ExecutorService;
  34 +
30 /** 35 /**
31 * Created by ashvayka on 04.10.18. 36 * Created by ashvayka on 04.10.18.
32 */ 37 */
@@ -59,4 +64,8 @@ public class MqttTransportContext extends TransportContext { @@ -59,4 +64,8 @@ public class MqttTransportContext extends TransportContext {
59 @Setter 64 @Setter
60 private SslHandler sslHandler; 65 private SslHandler sslHandler;
61 66
  67 + @Getter
  68 + @Value("${transport.mqtt.msg_queue_size_per_device_limit:100}")
  69 + private int messageQueueSizePerDeviceLimit;
  70 +
62 } 71 }
@@ -123,9 +123,9 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement @@ -123,9 +123,9 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement
123 private final SslHandler sslHandler; 123 private final SslHandler sslHandler;
124 private final ConcurrentMap<MqttTopicMatcher, Integer> mqttQoSMap; 124 private final ConcurrentMap<MqttTopicMatcher, Integer> mqttQoSMap;
125 125
126 - private final DeviceSessionCtx deviceSessionCtx;  
127 - private volatile InetSocketAddress address;  
128 - private volatile GatewaySessionHandler gatewaySessionHandler; 126 + final DeviceSessionCtx deviceSessionCtx;
  127 + volatile InetSocketAddress address;
  128 + volatile GatewaySessionHandler gatewaySessionHandler;
129 129
130 private final ConcurrentHashMap<String, String> otaPackSessions; 130 private final ConcurrentHashMap<String, String> otaPackSessions;
131 private final ConcurrentHashMap<String, Integer> chunkSizes; 131 private final ConcurrentHashMap<String, Integer> chunkSizes;
@@ -164,8 +164,8 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement @@ -164,8 +164,8 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement
164 } 164 }
165 } 165 }
166 166
167 - private void processMqttMsg(ChannelHandlerContext ctx, MqttMessage msg) {  
168 - address = (InetSocketAddress) ctx.channel().remoteAddress(); 167 + void processMqttMsg(ChannelHandlerContext ctx, MqttMessage msg) {
  168 + address = getAddress(ctx);
169 if (msg.fixedHeader() == null) { 169 if (msg.fixedHeader() == null) {
170 log.info("[{}:{}] Invalid message received", address.getHostName(), address.getPort()); 170 log.info("[{}:{}] Invalid message received", address.getHostName(), address.getPort());
171 processDisconnect(ctx); 171 processDisconnect(ctx);
@@ -177,10 +177,14 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement @@ -177,10 +177,14 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement
177 } else if (deviceSessionCtx.isProvisionOnly()) { 177 } else if (deviceSessionCtx.isProvisionOnly()) {
178 processProvisionSessionMsg(ctx, msg); 178 processProvisionSessionMsg(ctx, msg);
179 } else { 179 } else {
180 - processRegularSessionMsg(ctx, msg); 180 + enqueueRegularSessionMsg(ctx, msg);
181 } 181 }
182 } 182 }
183 183
  184 + InetSocketAddress getAddress(ChannelHandlerContext ctx) {
  185 + return (InetSocketAddress) ctx.channel().remoteAddress();
  186 + }
  187 +
184 private void processProvisionSessionMsg(ChannelHandlerContext ctx, MqttMessage msg) { 188 private void processProvisionSessionMsg(ChannelHandlerContext ctx, MqttMessage msg) {
185 switch (msg.fixedHeader().messageType()) { 189 switch (msg.fixedHeader().messageType()) {
186 case PUBLISH: 190 case PUBLISH:
@@ -223,7 +227,42 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement @@ -223,7 +227,42 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement
223 } 227 }
224 } 228 }
225 229
226 - private void processRegularSessionMsg(ChannelHandlerContext ctx, MqttMessage msg) { 230 + void enqueueRegularSessionMsg(ChannelHandlerContext ctx, MqttMessage msg) {
  231 + final int queueSize = deviceSessionCtx.getMsgQueueSize().incrementAndGet();
  232 + if (queueSize > context.getMessageQueueSizePerDeviceLimit()) {
  233 + log.warn("Closing current session because msq queue size for device {} exceed limit {} with msgQueueSize counter {} and actual queue size {}",
  234 + deviceSessionCtx.getDeviceId(), context.getMessageQueueSizePerDeviceLimit(), queueSize, deviceSessionCtx.getMsgQueue().size());
  235 + ctx.close();
  236 + return;
  237 + }
  238 +
  239 + deviceSessionCtx.getMsgQueue().add(msg);
  240 + 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
  241 + }
  242 +
  243 + void processMsgQueue(ChannelHandlerContext ctx) {
  244 + if (!deviceSessionCtx.isConnected()) {
  245 + log.trace("[{}][{}] Postpone processing msg due to device is not connected. Msg queue size is {}", sessionId, deviceSessionCtx.getDeviceId(), deviceSessionCtx.getMsgQueue().size());
  246 + return;
  247 + }
  248 + while (!deviceSessionCtx.getMsgQueue().isEmpty()) {
  249 + if (deviceSessionCtx.getMsgQueueProcessorLock().tryLock()) {
  250 + try {
  251 + MqttMessage msg;
  252 + while ((msg = deviceSessionCtx.getMsgQueue().poll()) != null) {
  253 + deviceSessionCtx.getMsgQueueSize().decrementAndGet();
  254 + processRegularSessionMsg(ctx, msg);
  255 + }
  256 + } finally {
  257 + deviceSessionCtx.getMsgQueueProcessorLock().unlock();
  258 + }
  259 + } else {
  260 + return;
  261 + }
  262 + }
  263 + }
  264 +
  265 + void processRegularSessionMsg(ChannelHandlerContext ctx, MqttMessage msg) {
227 switch (msg.fixedHeader().messageType()) { 266 switch (msg.fixedHeader().messageType()) {
228 case PUBLISH: 267 case PUBLISH:
229 processPublish(ctx, (MqttPublishMessage) msg); 268 processPublish(ctx, (MqttPublishMessage) msg);
@@ -304,6 +343,7 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement @@ -304,6 +343,7 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement
304 } 343 }
305 } catch (RuntimeException | AdaptorException e) { 344 } catch (RuntimeException | AdaptorException e) {
306 log.warn("[{}] Failed to process publish msg [{}][{}]", sessionId, topicName, msgId, e); 345 log.warn("[{}] Failed to process publish msg [{}][{}]", sessionId, topicName, msgId, e);
  346 + ctx.close();
307 } 347 }
308 } 348 }
309 349
@@ -588,7 +628,7 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement @@ -588,7 +628,7 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement
588 return new MqttMessage(mqttFixedHeader, mqttMessageIdVariableHeader); 628 return new MqttMessage(mqttFixedHeader, mqttMessageIdVariableHeader);
589 } 629 }
590 630
591 - private void processConnect(ChannelHandlerContext ctx, MqttConnectMessage msg) { 631 + void processConnect(ChannelHandlerContext ctx, MqttConnectMessage msg) {
592 log.info("[{}] Processing connect msg for client: {}!", sessionId, msg.payload().clientIdentifier()); 632 log.info("[{}] Processing connect msg for client: {}!", sessionId, msg.payload().clientIdentifier());
593 String userName = msg.payload().userName(); 633 String userName = msg.payload().userName();
594 String clientId = msg.payload().clientIdentifier(); 634 String clientId = msg.payload().clientIdentifier();
@@ -674,7 +714,7 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement @@ -674,7 +714,7 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement
674 return null; 714 return null;
675 } 715 }
676 716
677 - private void processDisconnect(ChannelHandlerContext ctx) { 717 + void processDisconnect(ChannelHandlerContext ctx) {
678 ctx.close(); 718 ctx.close();
679 log.info("[{}] Client disconnected!", sessionId); 719 log.info("[{}] Client disconnected!", sessionId);
680 doDisconnect(); 720 doDisconnect();
@@ -761,6 +801,11 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement @@ -761,6 +801,11 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement
761 } 801 }
762 deviceSessionCtx.setDisconnected(); 802 deviceSessionCtx.setDisconnected();
763 } 803 }
  804 +
  805 + if (!deviceSessionCtx.getMsgQueue().isEmpty()) {
  806 + log.warn("doDisconnect for device {} but unprocessed messages {} left in the msg queue", deviceSessionCtx.getDeviceId(), deviceSessionCtx.getMsgQueue().size());
  807 + deviceSessionCtx.getMsgQueue().clear();
  808 + }
764 } 809 }
765 810
766 811
@@ -778,7 +823,9 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement @@ -778,7 +823,9 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement
778 SessionMetaData sessionMetaData = transportService.registerAsyncSession(deviceSessionCtx.getSessionInfo(), MqttTransportHandler.this); 823 SessionMetaData sessionMetaData = transportService.registerAsyncSession(deviceSessionCtx.getSessionInfo(), MqttTransportHandler.this);
779 checkGatewaySession(sessionMetaData); 824 checkGatewaySession(sessionMetaData);
780 ctx.writeAndFlush(createMqttConnAckMsg(CONNECTION_ACCEPTED, connectMessage)); 825 ctx.writeAndFlush(createMqttConnAckMsg(CONNECTION_ACCEPTED, connectMessage));
  826 + deviceSessionCtx.setConnected(true);
781 log.info("[{}] Client connected!", sessionId); 827 log.info("[{}] Client connected!", sessionId);
  828 + 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.
782 } 829 }
783 830
784 @Override 831 @Override
@@ -18,6 +18,7 @@ package org.thingsboard.server.transport.mqtt.session; @@ -18,6 +18,7 @@ package org.thingsboard.server.transport.mqtt.session;
18 import com.google.protobuf.Descriptors; 18 import com.google.protobuf.Descriptors;
19 import com.google.protobuf.DynamicMessage; 19 import com.google.protobuf.DynamicMessage;
20 import io.netty.channel.ChannelHandlerContext; 20 import io.netty.channel.ChannelHandlerContext;
  21 +import io.netty.handler.codec.mqtt.MqttMessage;
21 import lombok.Getter; 22 import lombok.Getter;
22 import lombok.Setter; 23 import lombok.Setter;
23 import lombok.extern.slf4j.Slf4j; 24 import lombok.extern.slf4j.Slf4j;
@@ -35,8 +36,11 @@ import org.thingsboard.server.transport.mqtt.util.MqttTopicFilter; @@ -35,8 +36,11 @@ import org.thingsboard.server.transport.mqtt.util.MqttTopicFilter;
35 import org.thingsboard.server.transport.mqtt.util.MqttTopicFilterFactory; 36 import org.thingsboard.server.transport.mqtt.util.MqttTopicFilterFactory;
36 37
37 import java.util.UUID; 38 import java.util.UUID;
  39 +import java.util.concurrent.ConcurrentLinkedQueue;
38 import java.util.concurrent.ConcurrentMap; 40 import java.util.concurrent.ConcurrentMap;
39 import java.util.concurrent.atomic.AtomicInteger; 41 import java.util.concurrent.atomic.AtomicInteger;
  42 +import java.util.concurrent.locks.Lock;
  43 +import java.util.concurrent.locks.ReentrantLock;
40 44
41 /** 45 /**
42 * @author Andrew Shvayka 46 * @author Andrew Shvayka
@@ -45,14 +49,24 @@ import java.util.concurrent.atomic.AtomicInteger; @@ -45,14 +49,24 @@ import java.util.concurrent.atomic.AtomicInteger;
45 public class DeviceSessionCtx extends MqttDeviceAwareSessionContext { 49 public class DeviceSessionCtx extends MqttDeviceAwareSessionContext {
46 50
47 @Getter 51 @Getter
  52 + @Setter
48 private ChannelHandlerContext channel; 53 private ChannelHandlerContext channel;
49 54
50 @Getter 55 @Getter
51 - private MqttTransportContext context; 56 + private final MqttTransportContext context;
52 57
53 private final AtomicInteger msgIdSeq = new AtomicInteger(0); 58 private final AtomicInteger msgIdSeq = new AtomicInteger(0);
54 59
55 @Getter 60 @Getter
  61 + private final ConcurrentLinkedQueue<MqttMessage> msgQueue = new ConcurrentLinkedQueue<>();
  62 +
  63 + @Getter
  64 + private final Lock msgQueueProcessorLock = new ReentrantLock();
  65 +
  66 + @Getter
  67 + private final AtomicInteger msgQueueSize = new AtomicInteger(0);
  68 +
  69 + @Getter
56 @Setter 70 @Setter
57 private boolean provisionOnly = false; 71 private boolean provisionOnly = false;
58 72
@@ -73,10 +87,6 @@ public class DeviceSessionCtx extends MqttDeviceAwareSessionContext { @@ -73,10 +87,6 @@ public class DeviceSessionCtx extends MqttDeviceAwareSessionContext {
73 this.context = context; 87 this.context = context;
74 } 88 }
75 89
76 - public void setChannel(ChannelHandlerContext channel) {  
77 - this.channel = channel;  
78 - }  
79 -  
80 public int nextMsgId() { 90 public int nextMsgId() {
81 return msgIdSeq.incrementAndGet(); 91 return msgIdSeq.incrementAndGet();
82 } 92 }
@@ -60,6 +60,7 @@ public class GatewayDeviceSessionCtx extends MqttDeviceAwareSessionContext imple @@ -60,6 +60,7 @@ public class GatewayDeviceSessionCtx extends MqttDeviceAwareSessionContext imple
60 .setDeviceProfileIdLSB(deviceInfo.getDeviceProfileId().getId().getLeastSignificantBits()) 60 .setDeviceProfileIdLSB(deviceInfo.getDeviceProfileId().getId().getLeastSignificantBits())
61 .build()); 61 .build());
62 setDeviceInfo(deviceInfo); 62 setDeviceInfo(deviceInfo);
  63 + setConnected(true);
63 setDeviceProfile(deviceProfile); 64 setDeviceProfile(deviceProfile);
64 this.transportService = transportService; 65 this.transportService = transportService;
65 } 66 }
@@ -34,6 +34,7 @@ import io.netty.handler.codec.mqtt.MqttMessage; @@ -34,6 +34,7 @@ import io.netty.handler.codec.mqtt.MqttMessage;
34 import io.netty.handler.codec.mqtt.MqttPublishMessage; 34 import io.netty.handler.codec.mqtt.MqttPublishMessage;
35 import lombok.extern.slf4j.Slf4j; 35 import lombok.extern.slf4j.Slf4j;
36 import org.springframework.util.CollectionUtils; 36 import org.springframework.util.CollectionUtils;
  37 +import org.springframework.util.ConcurrentReferenceHashMap;
37 import org.springframework.util.StringUtils; 38 import org.springframework.util.StringUtils;
38 import org.thingsboard.server.common.data.id.DeviceId; 39 import org.thingsboard.server.common.data.id.DeviceId;
39 import org.thingsboard.server.common.transport.TransportService; 40 import org.thingsboard.server.common.transport.TransportService;
@@ -66,6 +67,8 @@ import java.util.concurrent.ConcurrentMap; @@ -66,6 +67,8 @@ import java.util.concurrent.ConcurrentMap;
66 import java.util.concurrent.locks.Lock; 67 import java.util.concurrent.locks.Lock;
67 import java.util.concurrent.locks.ReentrantLock; 68 import java.util.concurrent.locks.ReentrantLock;
68 69
  70 +import static org.springframework.util.ConcurrentReferenceHashMap.ReferenceType;
  71 +
69 /** 72 /**
70 * Created by ashvayka on 19.01.17. 73 * Created by ashvayka on 19.01.17.
71 */ 74 */
@@ -82,7 +85,7 @@ public class GatewaySessionHandler { @@ -82,7 +85,7 @@ public class GatewaySessionHandler {
82 private final UUID sessionId; 85 private final UUID sessionId;
83 private final ConcurrentMap<String, Lock> deviceCreationLockMap; 86 private final ConcurrentMap<String, Lock> deviceCreationLockMap;
84 private final ConcurrentMap<String, GatewayDeviceSessionCtx> devices; 87 private final ConcurrentMap<String, GatewayDeviceSessionCtx> devices;
85 - private final ConcurrentMap<String, SettableFuture<GatewayDeviceSessionCtx>> deviceFutures; 88 + private final ConcurrentMap<String, ListenableFuture<GatewayDeviceSessionCtx>> deviceFutures;
86 private final ConcurrentMap<MqttTopicMatcher, Integer> mqttQoSMap; 89 private final ConcurrentMap<MqttTopicMatcher, Integer> mqttQoSMap;
87 private final ChannelHandlerContext channel; 90 private final ChannelHandlerContext channel;
88 private final DeviceSessionCtx deviceSessionCtx; 91 private final DeviceSessionCtx deviceSessionCtx;
@@ -95,11 +98,15 @@ public class GatewaySessionHandler { @@ -95,11 +98,15 @@ public class GatewaySessionHandler {
95 this.sessionId = sessionId; 98 this.sessionId = sessionId;
96 this.devices = new ConcurrentHashMap<>(); 99 this.devices = new ConcurrentHashMap<>();
97 this.deviceFutures = new ConcurrentHashMap<>(); 100 this.deviceFutures = new ConcurrentHashMap<>();
98 - this.deviceCreationLockMap = new ConcurrentHashMap<>(); 101 + this.deviceCreationLockMap = createWeakMap();
99 this.mqttQoSMap = deviceSessionCtx.getMqttQoSMap(); 102 this.mqttQoSMap = deviceSessionCtx.getMqttQoSMap();
100 this.channel = deviceSessionCtx.getChannel(); 103 this.channel = deviceSessionCtx.getChannel();
101 } 104 }
102 105
  106 + ConcurrentReferenceHashMap<String, Lock> createWeakMap() {
  107 + return new ConcurrentReferenceHashMap<>(16, ReferenceType.WEAK);
  108 + }
  109 +
103 public void onDeviceConnect(MqttPublishMessage mqttMsg) throws AdaptorException { 110 public void onDeviceConnect(MqttPublishMessage mqttMsg) throws AdaptorException {
104 if (isJsonPayloadType()) { 111 if (isJsonPayloadType()) {
105 onDeviceConnectJson(mqttMsg); 112 onDeviceConnectJson(mqttMsg);
@@ -228,21 +235,22 @@ public class GatewaySessionHandler { @@ -228,21 +235,22 @@ public class GatewaySessionHandler {
228 if (result == null) { 235 if (result == null) {
229 return getDeviceCreationFuture(deviceName, deviceType); 236 return getDeviceCreationFuture(deviceName, deviceType);
230 } else { 237 } else {
231 - return toCompletedFuture(result); 238 + return Futures.immediateFuture(result);
232 } 239 }
233 } finally { 240 } finally {
234 deviceCreationLock.unlock(); 241 deviceCreationLock.unlock();
235 } 242 }
236 } else { 243 } else {
237 - return toCompletedFuture(result); 244 + return Futures.immediateFuture(result);
238 } 245 }
239 } 246 }
240 247
241 private ListenableFuture<GatewayDeviceSessionCtx> getDeviceCreationFuture(String deviceName, String deviceType) { 248 private ListenableFuture<GatewayDeviceSessionCtx> getDeviceCreationFuture(String deviceName, String deviceType) {
242 - SettableFuture<GatewayDeviceSessionCtx> future = deviceFutures.get(deviceName);  
243 - if (future == null) {  
244 - final SettableFuture<GatewayDeviceSessionCtx> futureToSet = SettableFuture.create();  
245 - deviceFutures.put(deviceName, futureToSet); 249 + final SettableFuture<GatewayDeviceSessionCtx> futureToSet = SettableFuture.create();
  250 + ListenableFuture<GatewayDeviceSessionCtx> future = deviceFutures.putIfAbsent(deviceName, futureToSet);
  251 + if (future != null) {
  252 + return future;
  253 + }
246 try { 254 try {
247 transportService.process(GetOrCreateDeviceFromGatewayRequestMsg.newBuilder() 255 transportService.process(GetOrCreateDeviceFromGatewayRequestMsg.newBuilder()
248 .setDeviceName(deviceName) 256 .setDeviceName(deviceName)
@@ -282,15 +290,6 @@ public class GatewaySessionHandler { @@ -282,15 +290,6 @@ public class GatewaySessionHandler {
282 deviceFutures.remove(deviceName); 290 deviceFutures.remove(deviceName);
283 throw e; 291 throw e;
284 } 292 }
285 - } else {  
286 - return future;  
287 - }  
288 - }  
289 -  
290 - private ListenableFuture<GatewayDeviceSessionCtx> toCompletedFuture(GatewayDeviceSessionCtx result) {  
291 - SettableFuture<GatewayDeviceSessionCtx> future = SettableFuture.create();  
292 - future.set(result);  
293 - return future;  
294 } 293 }
295 294
296 private int getMsgId(MqttPublishMessage mqttMsg) { 295 private int getMsgId(MqttPublishMessage mqttMsg) {
@@ -353,6 +352,7 @@ public class GatewaySessionHandler { @@ -353,6 +352,7 @@ public class GatewaySessionHandler {
353 processPostTelemetryMsg(deviceCtx, postTelemetryMsg, deviceName, msgId); 352 processPostTelemetryMsg(deviceCtx, postTelemetryMsg, deviceName, msgId);
354 } catch (Throwable e) { 353 } catch (Throwable e) {
355 log.warn("[{}][{}] Failed to convert telemetry: {}", gateway.getDeviceId(), deviceName, deviceEntry.getValue(), e); 354 log.warn("[{}][{}] Failed to convert telemetry: {}", gateway.getDeviceId(), deviceName, deviceEntry.getValue(), e);
  355 + channel.close();
356 } 356 }
357 } 357 }
358 358
@@ -384,6 +384,7 @@ public class GatewaySessionHandler { @@ -384,6 +384,7 @@ public class GatewaySessionHandler {
384 processPostTelemetryMsg(deviceCtx, postTelemetryMsg, deviceName, msgId); 384 processPostTelemetryMsg(deviceCtx, postTelemetryMsg, deviceName, msgId);
385 } catch (Throwable e) { 385 } catch (Throwable e) {
386 log.warn("[{}][{}] Failed to convert telemetry: {}", gateway.getDeviceId(), deviceName, msg, e); 386 log.warn("[{}][{}] Failed to convert telemetry: {}", gateway.getDeviceId(), deviceName, msg, e);
  387 + channel.close();
387 } 388 }
388 } 389 }
389 390
  1 +/**
  2 + * Copyright © 2016-2021 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +package org.thingsboard.server.transport.mqtt;
  17 +
  18 +import io.netty.buffer.ByteBuf;
  19 +import io.netty.buffer.EmptyByteBuf;
  20 +import io.netty.buffer.PooledByteBufAllocator;
  21 +import io.netty.channel.ChannelHandlerContext;
  22 +import io.netty.handler.codec.mqtt.MqttConnectMessage;
  23 +import io.netty.handler.codec.mqtt.MqttConnectPayload;
  24 +import io.netty.handler.codec.mqtt.MqttConnectVariableHeader;
  25 +import io.netty.handler.codec.mqtt.MqttFixedHeader;
  26 +import io.netty.handler.codec.mqtt.MqttMessage;
  27 +import io.netty.handler.codec.mqtt.MqttMessageType;
  28 +import io.netty.handler.codec.mqtt.MqttPublishMessage;
  29 +import io.netty.handler.codec.mqtt.MqttPublishVariableHeader;
  30 +import io.netty.handler.codec.mqtt.MqttQoS;
  31 +import io.netty.handler.ssl.SslHandler;
  32 +import lombok.extern.slf4j.Slf4j;
  33 +import org.junit.After;
  34 +import org.junit.Before;
  35 +import org.junit.Test;
  36 +import org.junit.runner.RunWith;
  37 +import org.mockito.Mock;
  38 +import org.mockito.junit.MockitoJUnitRunner;
  39 +import org.thingsboard.common.util.ThingsBoardThreadFactory;
  40 +
  41 +import java.net.InetSocketAddress;
  42 +import java.nio.charset.StandardCharsets;
  43 +import java.util.List;
  44 +import java.util.concurrent.CountDownLatch;
  45 +import java.util.concurrent.ExecutorService;
  46 +import java.util.concurrent.Executors;
  47 +import java.util.concurrent.TimeUnit;
  48 +import java.util.concurrent.atomic.AtomicInteger;
  49 +import java.util.stream.Collectors;
  50 +import java.util.stream.Stream;
  51 +
  52 +import static org.hamcrest.MatcherAssert.assertThat;
  53 +import static org.hamcrest.Matchers.contains;
  54 +import static org.hamcrest.Matchers.empty;
  55 +import static org.hamcrest.Matchers.greaterThan;
  56 +import static org.hamcrest.Matchers.is;
  57 +import static org.junit.Assert.fail;
  58 +import static org.mockito.ArgumentMatchers.any;
  59 +import static org.mockito.BDDMockito.willDoNothing;
  60 +import static org.mockito.BDDMockito.willReturn;
  61 +import static org.mockito.Mockito.never;
  62 +import static org.mockito.Mockito.spy;
  63 +import static org.mockito.Mockito.times;
  64 +import static org.mockito.Mockito.verify;
  65 +
  66 +@Slf4j
  67 +@RunWith(MockitoJUnitRunner.class)
  68 +public class MqttTransportHandlerTest {
  69 +
  70 + public static final int MSG_QUEUE_LIMIT = 10;
  71 + public static final InetSocketAddress IP_ADDR = new InetSocketAddress("127.0.0.1", 9876);
  72 + public static final int TIMEOUT = 30;
  73 +
  74 + @Mock
  75 + MqttTransportContext context;
  76 + @Mock
  77 + SslHandler sslHandler;
  78 + @Mock
  79 + ChannelHandlerContext ctx;
  80 +
  81 + AtomicInteger packedId = new AtomicInteger();
  82 + ExecutorService executor;
  83 + MqttTransportHandler handler;
  84 +
  85 + @Before
  86 + public void setUp() throws Exception {
  87 + willReturn(MSG_QUEUE_LIMIT).given(context).getMessageQueueSizePerDeviceLimit();
  88 +
  89 + handler = spy(new MqttTransportHandler(context, sslHandler));
  90 + willReturn(IP_ADDR).given(handler).getAddress(any());
  91 + }
  92 +
  93 + @After
  94 + public void tearDown() {
  95 + if (executor != null) {
  96 + executor.shutdownNow();
  97 + }
  98 + }
  99 +
  100 + MqttConnectMessage getMqttConnectMessage() {
  101 + MqttFixedHeader mqttFixedHeader = new MqttFixedHeader(MqttMessageType.CONNECT, true, MqttQoS.AT_LEAST_ONCE, false, 123);
  102 + MqttConnectVariableHeader variableHeader = new MqttConnectVariableHeader("device", packedId.incrementAndGet(), true, true, true, 1, true, false, 60);
  103 + MqttConnectPayload payload = new MqttConnectPayload("clientId", "topic", "message".getBytes(StandardCharsets.UTF_8), "username", "password".getBytes(StandardCharsets.UTF_8));
  104 + return new MqttConnectMessage(mqttFixedHeader, variableHeader, payload);
  105 + }
  106 +
  107 + MqttPublishMessage getMqttPublishMessage() {
  108 + MqttFixedHeader mqttFixedHeader = new MqttFixedHeader(MqttMessageType.PUBLISH, true, MqttQoS.AT_LEAST_ONCE, false, 123);
  109 + MqttPublishVariableHeader variableHeader = new MqttPublishVariableHeader("v1/gateway/telemetry", packedId.incrementAndGet());
  110 + ByteBuf payload = new EmptyByteBuf(new PooledByteBufAllocator());
  111 + return new MqttPublishMessage(mqttFixedHeader, variableHeader, payload);
  112 + }
  113 +
  114 + @Test
  115 + public void givenMessageWithoutFixedHeader_whenProcessMqttMsg_thenProcessDisconnect() {
  116 + MqttFixedHeader mqttFixedHeader = null;
  117 + MqttMessage msg = new MqttMessage(mqttFixedHeader);
  118 + willDoNothing().given(handler).processDisconnect(ctx);
  119 +
  120 + handler.processMqttMsg(ctx, msg);
  121 +
  122 + assertThat(handler.address, is(IP_ADDR));
  123 + verify(handler, times(1)).processDisconnect(ctx);
  124 + }
  125 +
  126 + @Test
  127 + public void givenMqttConnectMessage_whenProcessMqttMsg_thenProcessConnect() {
  128 + MqttConnectMessage msg = getMqttConnectMessage();
  129 + willDoNothing().given(handler).processConnect(ctx, msg);
  130 +
  131 + handler.processMqttMsg(ctx, msg);
  132 +
  133 + assertThat(handler.address, is(IP_ADDR));
  134 + assertThat(handler.deviceSessionCtx.getChannel(), is(ctx));
  135 + verify(handler, never()).processDisconnect(any());
  136 + verify(handler, times(1)).processConnect(ctx, msg);
  137 + }
  138 +
  139 + @Test
  140 + public void givenQueueLimit_whenEnqueueRegularSessionMsgOverLimit_thenOK() {
  141 + List<MqttPublishMessage> messages = Stream.generate(this::getMqttPublishMessage).limit(MSG_QUEUE_LIMIT).collect(Collectors.toList());
  142 + messages.forEach(msg -> handler.enqueueRegularSessionMsg(ctx, msg));
  143 + assertThat(handler.deviceSessionCtx.getMsgQueueSize().get(), is(MSG_QUEUE_LIMIT));
  144 + assertThat(handler.deviceSessionCtx.getMsgQueue(), contains(messages.toArray()));
  145 + }
  146 +
  147 + @Test
  148 + public void givenQueueLimit_whenEnqueueRegularSessionMsgOverLimit_thenCtxClose() {
  149 + final int limit = MSG_QUEUE_LIMIT + 1;
  150 + willDoNothing().given(handler).processMsgQueue(ctx);
  151 + List<MqttPublishMessage> messages = Stream.generate(this::getMqttPublishMessage).limit(limit).collect(Collectors.toList());
  152 +
  153 + messages.forEach((msg) -> handler.enqueueRegularSessionMsg(ctx, msg));
  154 +
  155 + assertThat(handler.deviceSessionCtx.getMsgQueueSize().get(), is(limit));
  156 + verify(handler, times(limit)).enqueueRegularSessionMsg(any(), any());
  157 + verify(handler, times(MSG_QUEUE_LIMIT)).processMsgQueue(any());
  158 + verify(ctx, times(1)).close();
  159 + }
  160 +
  161 + @Test
  162 + public void givenMqttConnectMessageAndPublishImmediately_whenProcessMqttMsg_thenEnqueueRegularSessionMsg() {
  163 + givenMqttConnectMessage_whenProcessMqttMsg_thenProcessConnect();
  164 +
  165 + List<MqttPublishMessage> messages = Stream.generate(this::getMqttPublishMessage).limit(MSG_QUEUE_LIMIT).collect(Collectors.toList());
  166 +
  167 + messages.forEach((msg) -> handler.processMqttMsg(ctx, msg));
  168 +
  169 + assertThat(handler.address, is(IP_ADDR));
  170 + assertThat(handler.deviceSessionCtx.getChannel(), is(ctx));
  171 + assertThat(handler.deviceSessionCtx.isConnected(), is(false));
  172 + assertThat(handler.deviceSessionCtx.getMsgQueueSize().get(), is(MSG_QUEUE_LIMIT));
  173 + assertThat(handler.deviceSessionCtx.getMsgQueue(), contains(messages.toArray()));
  174 + verify(handler, never()).processDisconnect(any());
  175 + verify(handler, times(1)).processConnect(any(), any());
  176 + verify(handler, times(MSG_QUEUE_LIMIT)).enqueueRegularSessionMsg(any(), any());
  177 + verify(handler, never()).processRegularSessionMsg(any(), any());
  178 + messages.forEach((msg) -> verify(handler, times(1)).enqueueRegularSessionMsg(ctx, msg));
  179 + }
  180 +
  181 + @Test
  182 + public void givenMessageQueue_whenProcessMqttMsgConcurrently_thenEnqueueRegularSessionMsg() throws InterruptedException {
  183 + //given
  184 + assertThat(handler.deviceSessionCtx.isConnected(), is(false));
  185 + assertThat(MSG_QUEUE_LIMIT, greaterThan(2));
  186 + List<MqttPublishMessage> messages = Stream.generate(this::getMqttPublishMessage).limit(MSG_QUEUE_LIMIT).collect(Collectors.toList());
  187 + messages.forEach((msg) -> handler.enqueueRegularSessionMsg(ctx, msg));
  188 + willDoNothing().given(handler).processRegularSessionMsg(any(), any());
  189 + executor = Executors.newCachedThreadPool(ThingsBoardThreadFactory.forName(getClass().getName()));
  190 +
  191 + CountDownLatch readyLatch = new CountDownLatch(MSG_QUEUE_LIMIT);
  192 + CountDownLatch startLatch = new CountDownLatch(1);
  193 + CountDownLatch finishLatch = new CountDownLatch(MSG_QUEUE_LIMIT);
  194 +
  195 + Stream.iterate(0, i -> i + 1).limit(MSG_QUEUE_LIMIT).forEach(x ->
  196 + executor.submit(() -> {
  197 + try {
  198 + readyLatch.countDown();
  199 + assertThat(startLatch.await(TIMEOUT, TimeUnit.SECONDS), is(true));
  200 + handler.processMsgQueue(ctx);
  201 + finishLatch.countDown();
  202 + } catch (Exception e) {
  203 + log.error("Failed to run processMsgQueue", e);
  204 + fail("Failed to run processMsgQueue");
  205 + }
  206 + }));
  207 +
  208 + //when
  209 + assertThat(readyLatch.await(TIMEOUT, TimeUnit.SECONDS), is(true));
  210 + handler.deviceSessionCtx.setConnected(true);
  211 + startLatch.countDown();
  212 + assertThat(finishLatch.await(TIMEOUT, TimeUnit.SECONDS), is(true));
  213 +
  214 + //then
  215 + assertThat(handler.deviceSessionCtx.getMsgQueueSize().get(), is(0));
  216 + assertThat(handler.deviceSessionCtx.getMsgQueue(), empty());
  217 + verify(handler, times(MSG_QUEUE_LIMIT)).processRegularSessionMsg(any(), any());
  218 + messages.forEach((msg) -> verify(handler, times(1)).processRegularSessionMsg(ctx, msg));
  219 + }
  220 +
  221 +}
  1 +/**
  2 + * Copyright © 2016-2021 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +package org.thingsboard.server.transport.mqtt.session;
  17 +
  18 +import org.junit.Test;
  19 +
  20 +import java.util.WeakHashMap;
  21 +import java.util.concurrent.ConcurrentMap;
  22 +import java.util.concurrent.TimeUnit;
  23 +import java.util.concurrent.locks.Lock;
  24 +import java.util.concurrent.locks.ReentrantLock;
  25 +
  26 +import static org.awaitility.Awaitility.await;
  27 +import static org.junit.Assert.assertTrue;
  28 +import static org.mockito.BDDMockito.willCallRealMethod;
  29 +import static org.mockito.Mockito.mock;
  30 +
  31 +public class GatewaySessionHandlerTest {
  32 +
  33 + @Test
  34 + public void givenWeakHashMap_WhenGC_thenMapIsEmpty() {
  35 + WeakHashMap<String, Lock> map = new WeakHashMap<>();
  36 +
  37 + String deviceName = new String("device"); //constants are static and doesn't affected by GC, so use new instead
  38 + map.put(deviceName, new ReentrantLock());
  39 + assertTrue(map.containsKey(deviceName));
  40 +
  41 + deviceName = null;
  42 + System.gc();
  43 +
  44 + await().atMost(10, TimeUnit.SECONDS).until(() -> !map.containsKey("device"));
  45 + }
  46 +
  47 + @Test
  48 + public void givenConcurrentReferenceHashMap_WhenGC_thenMapIsEmpty() {
  49 + GatewaySessionHandler gsh = mock(GatewaySessionHandler.class);
  50 + willCallRealMethod().given(gsh).createWeakMap();
  51 +
  52 + ConcurrentMap<String, Lock> map = gsh.createWeakMap();
  53 + map.put("device", new ReentrantLock());
  54 + assertTrue(map.containsKey("device"));
  55 +
  56 + System.gc();
  57 +
  58 + await().atMost(10, TimeUnit.SECONDS).until(() -> !map.containsKey("device"));
  59 + }
  60 +
  61 +}
@@ -188,6 +188,7 @@ public class SnmpTransportContext extends TransportContext { @@ -188,6 +188,7 @@ public class SnmpTransportContext extends TransportContext {
188 188
189 deviceSessionContext.setSessionInfo(sessionInfo); 189 deviceSessionContext.setSessionInfo(sessionInfo);
190 deviceSessionContext.setDeviceInfo(msg.getDeviceInfo()); 190 deviceSessionContext.setDeviceInfo(msg.getDeviceInfo());
  191 + deviceSessionContext.setConnected(true);
191 } else { 192 } else {
192 log.warn("[{}] Failed to process device auth", deviceSessionContext.getDeviceId()); 193 log.warn("[{}] Failed to process device auth", deviceSessionContext.getDeviceId());
193 } 194 }
@@ -56,6 +56,8 @@ import org.thingsboard.server.gen.transport.TransportProtos.ValidateDeviceLwM2MC @@ -56,6 +56,8 @@ import org.thingsboard.server.gen.transport.TransportProtos.ValidateDeviceLwM2MC
56 import org.thingsboard.server.gen.transport.TransportProtos.ValidateDeviceTokenRequestMsg; 56 import org.thingsboard.server.gen.transport.TransportProtos.ValidateDeviceTokenRequestMsg;
57 import org.thingsboard.server.gen.transport.TransportProtos.ValidateDeviceX509CertRequestMsg; 57 import org.thingsboard.server.gen.transport.TransportProtos.ValidateDeviceX509CertRequestMsg;
58 58
  59 +import java.util.concurrent.ExecutorService;
  60 +
59 /** 61 /**
60 * Created by ashvayka on 04.10.18. 62 * Created by ashvayka on 04.10.18.
61 */ 63 */
@@ -131,4 +133,6 @@ public interface TransportService { @@ -131,4 +133,6 @@ public interface TransportService {
131 void log(SessionInfoProto sessionInfo, String msg); 133 void log(SessionInfoProto sessionInfo, String msg);
132 134
133 void notifyAboutUplink(SessionInfoProto sessionInfo, TransportProtos.UplinkNotificationMsg build, TransportServiceCallback<Void> empty); 135 void notifyAboutUplink(SessionInfoProto sessionInfo, TransportProtos.UplinkNotificationMsg build, TransportServiceCallback<Void> empty);
  136 +
  137 + ExecutorService getCallbackExecutor();
134 } 138 }
@@ -1141,4 +1141,9 @@ public class DefaultTransportService implements TransportService { @@ -1141,4 +1141,9 @@ public class DefaultTransportService implements TransportService {
1141 callback.onError(e); 1141 callback.onError(e);
1142 } 1142 }
1143 } 1143 }
  1144 +
  1145 + @Override
  1146 + public ExecutorService getCallbackExecutor() {
  1147 + return transportCallbackExecutor;
  1148 + }
1144 } 1149 }
@@ -46,6 +46,7 @@ public abstract class DeviceAwareSessionContext implements SessionContext { @@ -46,6 +46,7 @@ public abstract class DeviceAwareSessionContext implements SessionContext {
46 @Setter 46 @Setter
47 private volatile TransportProtos.SessionInfoProto sessionInfo; 47 private volatile TransportProtos.SessionInfoProto sessionInfo;
48 48
  49 + @Setter
49 private volatile boolean connected; 50 private volatile boolean connected;
50 51
51 public DeviceId getDeviceId() { 52 public DeviceId getDeviceId() {
@@ -54,7 +55,6 @@ public abstract class DeviceAwareSessionContext implements SessionContext { @@ -54,7 +55,6 @@ public abstract class DeviceAwareSessionContext implements SessionContext {
54 55
55 public void setDeviceInfo(TransportDeviceInfo deviceInfo) { 56 public void setDeviceInfo(TransportDeviceInfo deviceInfo) {
56 this.deviceInfo = deviceInfo; 57 this.deviceInfo = deviceInfo;
57 - this.connected = true;  
58 this.deviceId = deviceInfo.getDeviceId(); 58 this.deviceId = deviceInfo.getDeviceId();
59 } 59 }
60 60
@@ -52,7 +52,7 @@ public class AdminSettingsServiceImpl implements AdminSettingsService { @@ -52,7 +52,7 @@ public class AdminSettingsServiceImpl implements AdminSettingsService {
52 public AdminSettings saveAdminSettings(TenantId tenantId, AdminSettings adminSettings) { 52 public AdminSettings saveAdminSettings(TenantId tenantId, AdminSettings adminSettings) {
53 log.trace("Executing saveAdminSettings [{}]", adminSettings); 53 log.trace("Executing saveAdminSettings [{}]", adminSettings);
54 adminSettingsValidator.validate(adminSettings, data -> tenantId); 54 adminSettingsValidator.validate(adminSettings, data -> tenantId);
55 - if (adminSettings.getKey().equals("mail") && "".equals(adminSettings.getJsonValue().get("password").asText())) { 55 + if(adminSettings.getKey().equals("mail") && !adminSettings.getJsonValue().has("password")) {
56 AdminSettings mailSettings = findAdminSettingsByKey(tenantId, "mail"); 56 AdminSettings mailSettings = findAdminSettingsByKey(tenantId, "mail");
57 if (mailSettings != null) { 57 if (mailSettings != null) {
58 ((ObjectNode) adminSettings.getJsonValue()).put("password", mailSettings.getJsonValue().get("password").asText()); 58 ((ObjectNode) adminSettings.getJsonValue()).put("password", mailSettings.getJsonValue().get("password").asText());
@@ -61,7 +61,7 @@ public class AdminSettingsServiceImpl implements AdminSettingsService { @@ -61,7 +61,7 @@ public class AdminSettingsServiceImpl implements AdminSettingsService {
61 61
62 return adminSettingsDao.save(tenantId, adminSettings); 62 return adminSettingsDao.save(tenantId, adminSettings);
63 } 63 }
64 - 64 +
65 private DataValidator<AdminSettings> adminSettingsValidator = 65 private DataValidator<AdminSettings> adminSettingsValidator =
66 new DataValidator<AdminSettings>() { 66 new DataValidator<AdminSettings>() {
67 67
@@ -100,7 +100,7 @@ public abstract class AbstractChunkedAggregationTimeseriesDao extends AbstractSq @@ -100,7 +100,7 @@ public abstract class AbstractChunkedAggregationTimeseriesDao extends AbstractSq
100 } 100 }
101 101
102 @Override 102 @Override
103 - public ListenableFuture<Integer> savePartition(TenantId tenantId, EntityId entityId, long tsKvEntryTs, String key, long ttl) { 103 + public ListenableFuture<Integer> savePartition(TenantId tenantId, EntityId entityId, long tsKvEntryTs, String key) {
104 return Futures.immediateFuture(null); 104 return Futures.immediateFuture(null);
105 } 105 }
106 106
@@ -124,7 +124,7 @@ public class TimescaleTimeseriesDao extends AbstractSqlTimeseriesDao implements @@ -124,7 +124,7 @@ public class TimescaleTimeseriesDao extends AbstractSqlTimeseriesDao implements
124 } 124 }
125 125
126 @Override 126 @Override
127 - public ListenableFuture<Integer> savePartition(TenantId tenantId, EntityId entityId, long tsKvEntryTs, String key, long ttl) { 127 + public ListenableFuture<Integer> savePartition(TenantId tenantId, EntityId entityId, long tsKvEntryTs, String key) {
128 return Futures.immediateFuture(0); 128 return Futures.immediateFuture(0);
129 } 129 }
130 130
@@ -170,7 +170,7 @@ public class BaseTimeseriesService implements TimeseriesService { @@ -170,7 +170,7 @@ public class BaseTimeseriesService implements TimeseriesService {
170 if (entityId.getEntityType().equals(EntityType.ENTITY_VIEW)) { 170 if (entityId.getEntityType().equals(EntityType.ENTITY_VIEW)) {
171 throw new IncorrectParameterException("Telemetry data can't be stored for entity view. Read only"); 171 throw new IncorrectParameterException("Telemetry data can't be stored for entity view. Read only");
172 } 172 }
173 - futures.add(timeseriesDao.savePartition(tenantId, entityId, tsKvEntry.getTs(), tsKvEntry.getKey(), ttl)); 173 + futures.add(timeseriesDao.savePartition(tenantId, entityId, tsKvEntry.getTs(), tsKvEntry.getKey()));
174 futures.add(Futures.transform(timeseriesLatestDao.saveLatest(tenantId, entityId, tsKvEntry), v -> 0, MoreExecutors.directExecutor())); 174 futures.add(Futures.transform(timeseriesLatestDao.saveLatest(tenantId, entityId, tsKvEntry), v -> 0, MoreExecutors.directExecutor()));
175 futures.add(timeseriesDao.save(tenantId, entityId, tsKvEntry, ttl)); 175 futures.add(timeseriesDao.save(tenantId, entityId, tsKvEntry, ttl));
176 } 176 }
@@ -181,11 +181,14 @@ public class CassandraBaseTimeseriesDao extends AbstractCassandraBaseTimeseriesD @@ -181,11 +181,14 @@ public class CassandraBaseTimeseriesDao extends AbstractCassandraBaseTimeseriesD
181 } 181 }
182 182
183 @Override 183 @Override
184 - public ListenableFuture<Integer> savePartition(TenantId tenantId, EntityId entityId, long tsKvEntryTs, String key, long ttl) { 184 + public ListenableFuture<Integer> savePartition(TenantId tenantId, EntityId entityId, long tsKvEntryTs, String key) {
185 if (isFixedPartitioning()) { 185 if (isFixedPartitioning()) {
186 return Futures.immediateFuture(null); 186 return Futures.immediateFuture(null);
187 } 187 }
188 - ttl = computeTtl(ttl); 188 + // DO NOT apply custom TTL to partition, otherwise, short TTL will remove partition too early
  189 + // partitions must remain in the DB forever or be removed only by systemTtl
  190 + // removal of empty partition is too expensive (we need to scan all data keys for these partitions with ALLOW FILTERING)
  191 + long ttl = computeTtl(0);
189 long partition = toPartitionTs(tsKvEntryTs); 192 long partition = toPartitionTs(tsKvEntryTs);
190 if (cassandraTsPartitionsCache == null) { 193 if (cassandraTsPartitionsCache == null) {
191 return doSavePartition(tenantId, entityId, key, ttl, partition); 194 return doSavePartition(tenantId, entityId, key, ttl, partition);
@@ -33,7 +33,7 @@ public interface TimeseriesDao { @@ -33,7 +33,7 @@ public interface TimeseriesDao {
33 33
34 ListenableFuture<Integer> save(TenantId tenantId, EntityId entityId, TsKvEntry tsKvEntry, long ttl); 34 ListenableFuture<Integer> save(TenantId tenantId, EntityId entityId, TsKvEntry tsKvEntry, long ttl);
35 35
36 - ListenableFuture<Integer> savePartition(TenantId tenantId, EntityId entityId, long tsKvEntryTs, String key, long ttl); 36 + ListenableFuture<Integer> savePartition(TenantId tenantId, EntityId entityId, long tsKvEntryTs, String key);
37 37
38 ListenableFuture<Void> remove(TenantId tenantId, EntityId entityId, DeleteTsKvQuery query); 38 ListenableFuture<Void> remove(TenantId tenantId, EntityId entityId, DeleteTsKvQuery query);
39 39
@@ -100,10 +100,10 @@ public class CassandraPartitionsCacheTest { @@ -100,10 +100,10 @@ public class CassandraPartitionsCacheTest {
100 long tsKvEntryTs = System.currentTimeMillis(); 100 long tsKvEntryTs = System.currentTimeMillis();
101 101
102 for (int i = 0; i < 50000; i++) { 102 for (int i = 0; i < 50000; i++) {
103 - cassandraBaseTimeseriesDao.savePartition(tenantId, tenantId, tsKvEntryTs, "test" + i, 0); 103 + cassandraBaseTimeseriesDao.savePartition(tenantId, tenantId, tsKvEntryTs, "test" + i);
104 } 104 }
105 for (int i = 0; i < 60000; i++) { 105 for (int i = 0; i < 60000; i++) {
106 - cassandraBaseTimeseriesDao.savePartition(tenantId, tenantId, tsKvEntryTs, "test" + i, 0); 106 + cassandraBaseTimeseriesDao.savePartition(tenantId, tenantId, tsKvEntryTs, "test" + i);
107 } 107 }
108 verify(cassandraBaseTimeseriesDao, times(60000)).executeAsyncWrite(any(TenantId.class), any(Statement.class)); 108 verify(cassandraBaseTimeseriesDao, times(60000)).executeAsyncWrite(any(TenantId.class), any(Statement.class));
109 } 109 }
@@ -42,13 +42,14 @@ @@ -42,13 +42,14 @@
42 <spring-boot.version>2.3.12.RELEASE</spring-boot.version> 42 <spring-boot.version>2.3.12.RELEASE</spring-boot.version>
43 <spring.version>5.2.16.RELEASE</spring.version> 43 <spring.version>5.2.16.RELEASE</spring.version>
44 <spring-redis.version>5.2.11.RELEASE</spring-redis.version> 44 <spring-redis.version>5.2.11.RELEASE</spring-redis.version>
45 - <spring-security.version>5.4.1</spring-security.version> 45 + <spring-security.version>5.4.4</spring-security.version>
46 <spring-data-redis.version>2.4.3</spring-data-redis.version> 46 <spring-data-redis.version>2.4.3</spring-data-redis.version>
47 <jedis.version>3.3.0</jedis.version> 47 <jedis.version>3.3.0</jedis.version>
48 <jjwt.version>0.7.0</jjwt.version> 48 <jjwt.version>0.7.0</jjwt.version>
49 <json-path.version>2.2.0</json-path.version> 49 <json-path.version>2.2.0</json-path.version>
50 <junit.version>4.12</junit.version> 50 <junit.version>4.12</junit.version>
51 <jupiter.version>5.7.1</jupiter.version> 51 <jupiter.version>5.7.1</jupiter.version>
  52 + <awaitility.version>4.1.0</awaitility.version>
52 <hamcrest.version>2.2</hamcrest.version> 53 <hamcrest.version>2.2</hamcrest.version>
53 <slf4j.version>1.7.7</slf4j.version> 54 <slf4j.version>1.7.7</slf4j.version>
54 <logback.version>1.2.3</logback.version> 55 <logback.version>1.2.3</logback.version>
@@ -1382,6 +1383,12 @@ @@ -1382,6 +1383,12 @@
1382 <groupId>io.grpc</groupId> 1383 <groupId>io.grpc</groupId>
1383 <artifactId>grpc-netty</artifactId> 1384 <artifactId>grpc-netty</artifactId>
1384 <version>${grpc.version}</version> 1385 <version>${grpc.version}</version>
  1386 + <exclusions>
  1387 + <exclusion>
  1388 + <groupId>io.netty</groupId>
  1389 + <artifactId>*</artifactId>
  1390 + </exclusion>
  1391 + </exclusions>
1385 </dependency> 1392 </dependency>
1386 <dependency> 1393 <dependency>
1387 <groupId>io.grpc</groupId> 1394 <groupId>io.grpc</groupId>
@@ -1438,6 +1445,12 @@ @@ -1438,6 +1445,12 @@
1438 <scope>test</scope> 1445 <scope>test</scope>
1439 </dependency> 1446 </dependency>
1440 <dependency> 1447 <dependency>
  1448 + <groupId>org.awaitility</groupId>
  1449 + <artifactId>awaitility</artifactId>
  1450 + <version>${awaitility.version}</version>
  1451 + <scope>test</scope>
  1452 + </dependency>
  1453 + <dependency>
1441 <groupId>org.hamcrest</groupId> 1454 <groupId>org.hamcrest</groupId>
1442 <artifactId>hamcrest</artifactId> 1455 <artifactId>hamcrest</artifactId>
1443 <version>${hamcrest.version}</version> 1456 <version>${hamcrest.version}</version>
@@ -1580,6 +1593,12 @@ @@ -1580,6 +1593,12 @@
1580 <groupId>com.microsoft.azure</groupId> 1593 <groupId>com.microsoft.azure</groupId>
1581 <artifactId>azure-servicebus</artifactId> 1594 <artifactId>azure-servicebus</artifactId>
1582 <version>${azure-servicebus.version}</version> 1595 <version>${azure-servicebus.version}</version>
  1596 + <exclusions>
  1597 + <exclusion>
  1598 + <groupId>io.netty</groupId>
  1599 + <artifactId>*</artifactId>
  1600 + </exclusion>
  1601 + </exclusions>
1583 </dependency> 1602 </dependency>
1584 <dependency> 1603 <dependency>
1585 <groupId>org.passay</groupId> 1604 <groupId>org.passay</groupId>
@@ -188,7 +188,7 @@ class AlarmState { @@ -188,7 +188,7 @@ class AlarmState {
188 setAlarmConditionMetadata(ruleState, metaData); 188 setAlarmConditionMetadata(ruleState, metaData);
189 TbMsg newMsg = ctx.newMsg(lastMsgQueueName != null ? lastMsgQueueName : ServiceQueue.MAIN, "ALARM", 189 TbMsg newMsg = ctx.newMsg(lastMsgQueueName != null ? lastMsgQueueName : ServiceQueue.MAIN, "ALARM",
190 originator, msg != null ? msg.getCustomerId() : null, metaData, data); 190 originator, msg != null ? msg.getCustomerId() : null, metaData, data);
191 - ctx.tellNext(newMsg, relationType); 191 + ctx.enqueueForTellNext(newMsg, relationType);
192 } 192 }
193 193
194 protected void setAlarmConditionMetadata(AlarmRuleState ruleState, TbMsgMetaData metaData) { 194 protected void setAlarmConditionMetadata(AlarmRuleState ruleState, TbMsgMetaData metaData) {
@@ -120,7 +120,7 @@ public class TbSendRPCRequestNode implements TbNode { @@ -120,7 +120,7 @@ public class TbSendRPCRequestNode implements TbNode {
120 ctx.enqueueForTellNext(next, TbRelationTypes.SUCCESS); 120 ctx.enqueueForTellNext(next, TbRelationTypes.SUCCESS);
121 } else { 121 } else {
122 TbMsg next = ctx.newMsg(msg.getQueueName(), msg.getType(), msg.getOriginator(), msg.getCustomerId(), msg.getMetaData(), wrap("error", ruleEngineDeviceRpcResponse.getError().get().name())); 122 TbMsg next = ctx.newMsg(msg.getQueueName(), msg.getType(), msg.getOriginator(), msg.getCustomerId(), msg.getMetaData(), wrap("error", ruleEngineDeviceRpcResponse.getError().get().name()));
123 - ctx.tellFailure(next, new RuntimeException(ruleEngineDeviceRpcResponse.getError().get().name())); 123 + ctx.enqueueForTellFailure(next, ruleEngineDeviceRpcResponse.getError().get().name());
124 } 124 }
125 }); 125 });
126 ctx.ack(msg); 126 ctx.ack(msg);
@@ -196,7 +196,7 @@ public class TbDeviceProfileNodeTest { @@ -196,7 +196,7 @@ public class TbDeviceProfileNodeTest {
196 TbMsgDataType.JSON, mapper.writeValueAsString(data), null, null); 196 TbMsgDataType.JSON, mapper.writeValueAsString(data), null, null);
197 node.onMsg(ctx, msg); 197 node.onMsg(ctx, msg);
198 verify(ctx).tellSuccess(msg); 198 verify(ctx).tellSuccess(msg);
199 - verify(ctx).tellNext(theMsg, "Alarm Created"); 199 + verify(ctx).enqueueForTellNext(theMsg, "Alarm Created");
200 verify(ctx, Mockito.never()).tellFailure(Mockito.any(), Mockito.any()); 200 verify(ctx, Mockito.never()).tellFailure(Mockito.any(), Mockito.any());
201 201
202 TbMsg theMsg2 = TbMsg.newMsg("ALARM", deviceId, new TbMsgMetaData(), "2"); 202 TbMsg theMsg2 = TbMsg.newMsg("ALARM", deviceId, new TbMsgMetaData(), "2");
@@ -207,7 +207,7 @@ public class TbDeviceProfileNodeTest { @@ -207,7 +207,7 @@ public class TbDeviceProfileNodeTest {
207 TbMsgDataType.JSON, mapper.writeValueAsString(data), null, null); 207 TbMsgDataType.JSON, mapper.writeValueAsString(data), null, null);
208 node.onMsg(ctx, msg2); 208 node.onMsg(ctx, msg2);
209 verify(ctx).tellSuccess(msg2); 209 verify(ctx).tellSuccess(msg2);
210 - verify(ctx).tellNext(theMsg2, "Alarm Updated"); 210 + verify(ctx).enqueueForTellNext(theMsg2, "Alarm Updated");
211 211
212 } 212 }
213 213
@@ -289,7 +289,7 @@ public class TbDeviceProfileNodeTest { @@ -289,7 +289,7 @@ public class TbDeviceProfileNodeTest {
289 289
290 node.onMsg(ctx, msg); 290 node.onMsg(ctx, msg);
291 verify(ctx).tellSuccess(msg); 291 verify(ctx).tellSuccess(msg);
292 - verify(ctx).tellNext(theMsg, "Alarm Created"); 292 + verify(ctx).enqueueForTellNext(theMsg, "Alarm Created");
293 verify(ctx, Mockito.never()).tellFailure(Mockito.any(), Mockito.any()); 293 verify(ctx, Mockito.never()).tellFailure(Mockito.any(), Mockito.any());
294 } 294 }
295 295
@@ -376,7 +376,7 @@ public class TbDeviceProfileNodeTest { @@ -376,7 +376,7 @@ public class TbDeviceProfileNodeTest {
376 376
377 node.onMsg(ctx, msg); 377 node.onMsg(ctx, msg);
378 verify(ctx).tellSuccess(msg); 378 verify(ctx).tellSuccess(msg);
379 - verify(ctx).tellNext(theMsg, "Alarm Created"); 379 + verify(ctx).enqueueForTellNext(theMsg, "Alarm Created");
380 verify(ctx, Mockito.never()).tellFailure(Mockito.any(), Mockito.any()); 380 verify(ctx, Mockito.never()).tellFailure(Mockito.any(), Mockito.any());
381 } 381 }
382 382
@@ -445,7 +445,7 @@ public class TbDeviceProfileNodeTest { @@ -445,7 +445,7 @@ public class TbDeviceProfileNodeTest {
445 445
446 node.onMsg(ctx, msg); 446 node.onMsg(ctx, msg);
447 verify(ctx).tellSuccess(msg); 447 verify(ctx).tellSuccess(msg);
448 - verify(ctx).tellNext(theMsg, "Alarm Created"); 448 + verify(ctx).enqueueForTellNext(theMsg, "Alarm Created");
449 verify(ctx, Mockito.never()).tellFailure(Mockito.any(), Mockito.any()); 449 verify(ctx, Mockito.never()).tellFailure(Mockito.any(), Mockito.any());
450 } 450 }
451 451
@@ -554,7 +554,7 @@ public class TbDeviceProfileNodeTest { @@ -554,7 +554,7 @@ public class TbDeviceProfileNodeTest {
554 554
555 node.onMsg(ctx, msg2); 555 node.onMsg(ctx, msg2);
556 verify(ctx).tellSuccess(msg2); 556 verify(ctx).tellSuccess(msg2);
557 - verify(ctx).tellNext(theMsg, "Alarm Created"); 557 + verify(ctx).enqueueForTellNext(theMsg, "Alarm Created");
558 verify(ctx, Mockito.never()).tellFailure(Mockito.any(), Mockito.any()); 558 verify(ctx, Mockito.never()).tellFailure(Mockito.any(), Mockito.any());
559 } 559 }
560 560
@@ -678,7 +678,7 @@ public class TbDeviceProfileNodeTest { @@ -678,7 +678,7 @@ public class TbDeviceProfileNodeTest {
678 678
679 node.onMsg(ctx, msg2); 679 node.onMsg(ctx, msg2);
680 verify(ctx).tellSuccess(msg2); 680 verify(ctx).tellSuccess(msg2);
681 - verify(ctx).tellNext(theMsg, "Alarm Created"); 681 + verify(ctx).enqueueForTellNext(theMsg, "Alarm Created");
682 verify(ctx, Mockito.never()).tellFailure(Mockito.any(), Mockito.any()); 682 verify(ctx, Mockito.never()).tellFailure(Mockito.any(), Mockito.any());
683 } 683 }
684 684
@@ -781,7 +781,7 @@ public class TbDeviceProfileNodeTest { @@ -781,7 +781,7 @@ public class TbDeviceProfileNodeTest {
781 781
782 node.onMsg(ctx, msg2); 782 node.onMsg(ctx, msg2);
783 verify(ctx).tellSuccess(msg2); 783 verify(ctx).tellSuccess(msg2);
784 - verify(ctx).tellNext(theMsg, "Alarm Created"); 784 + verify(ctx).enqueueForTellNext(theMsg, "Alarm Created");
785 verify(ctx, Mockito.never()).tellFailure(Mockito.any(), Mockito.any()); 785 verify(ctx, Mockito.never()).tellFailure(Mockito.any(), Mockito.any());
786 } 786 }
787 787
@@ -897,7 +897,7 @@ public class TbDeviceProfileNodeTest { @@ -897,7 +897,7 @@ public class TbDeviceProfileNodeTest {
897 897
898 node.onMsg(ctx, msg2); 898 node.onMsg(ctx, msg2);
899 verify(ctx).tellSuccess(msg2); 899 verify(ctx).tellSuccess(msg2);
900 - verify(ctx).tellNext(theMsg, "Alarm Created"); 900 + verify(ctx).enqueueForTellNext(theMsg, "Alarm Created");
901 verify(ctx, Mockito.never()).tellFailure(Mockito.any(), Mockito.any()); 901 verify(ctx, Mockito.never()).tellFailure(Mockito.any(), Mockito.any());
902 } 902 }
903 903
@@ -999,7 +999,7 @@ public class TbDeviceProfileNodeTest { @@ -999,7 +999,7 @@ public class TbDeviceProfileNodeTest {
999 999
1000 node.onMsg(ctx, msg2); 1000 node.onMsg(ctx, msg2);
1001 verify(ctx).tellSuccess(msg2); 1001 verify(ctx).tellSuccess(msg2);
1002 - verify(ctx).tellNext(theMsg, "Alarm Created"); 1002 + verify(ctx).enqueueForTellNext(theMsg, "Alarm Created");
1003 verify(ctx, Mockito.never()).tellFailure(Mockito.any(), Mockito.any()); 1003 verify(ctx, Mockito.never()).tellFailure(Mockito.any(), Mockito.any());
1004 } 1004 }
1005 1005
@@ -1082,7 +1082,7 @@ public class TbDeviceProfileNodeTest { @@ -1082,7 +1082,7 @@ public class TbDeviceProfileNodeTest {
1082 1082
1083 node.onMsg(ctx, msg); 1083 node.onMsg(ctx, msg);
1084 verify(ctx).tellSuccess(msg); 1084 verify(ctx).tellSuccess(msg);
1085 - verify(ctx).tellNext(theMsg, "Alarm Created"); 1085 + verify(ctx).enqueueForTellNext(theMsg, "Alarm Created");
1086 verify(ctx, Mockito.never()).tellFailure(Mockito.any(), Mockito.any()); 1086 verify(ctx, Mockito.never()).tellFailure(Mockito.any(), Mockito.any());
1087 } 1087 }
1088 1088
@@ -1163,7 +1163,7 @@ public class TbDeviceProfileNodeTest { @@ -1163,7 +1163,7 @@ public class TbDeviceProfileNodeTest {
1163 1163
1164 node.onMsg(ctx, msg); 1164 node.onMsg(ctx, msg);
1165 verify(ctx).tellSuccess(msg); 1165 verify(ctx).tellSuccess(msg);
1166 - verify(ctx).tellNext(theMsg, "Alarm Created"); 1166 + verify(ctx).enqueueForTellNext(theMsg, "Alarm Created");
1167 verify(ctx, Mockito.never()).tellFailure(Mockito.any(), Mockito.any()); 1167 verify(ctx, Mockito.never()).tellFailure(Mockito.any(), Mockito.any());
1168 } 1168 }
1169 1169
@@ -1237,7 +1237,7 @@ public class TbDeviceProfileNodeTest { @@ -1237,7 +1237,7 @@ public class TbDeviceProfileNodeTest {
1237 1237
1238 node.onMsg(ctx, msg); 1238 node.onMsg(ctx, msg);
1239 verify(ctx).tellSuccess(msg); 1239 verify(ctx).tellSuccess(msg);
1240 - verify(ctx).tellNext(theMsg, "Alarm Created"); 1240 + verify(ctx).enqueueForTellNext(theMsg, "Alarm Created");
1241 verify(ctx, Mockito.never()).tellFailure(Mockito.any(), Mockito.any()); 1241 verify(ctx, Mockito.never()).tellFailure(Mockito.any(), Mockito.any());
1242 } 1242 }
1243 1243
@@ -1321,7 +1321,7 @@ public class TbDeviceProfileNodeTest { @@ -1321,7 +1321,7 @@ public class TbDeviceProfileNodeTest {
1321 1321
1322 node.onMsg(ctx, msg); 1322 node.onMsg(ctx, msg);
1323 verify(ctx).tellSuccess(msg); 1323 verify(ctx).tellSuccess(msg);
1324 - verify(ctx).tellNext(theMsg, "Alarm Created"); 1324 + verify(ctx).enqueueForTellNext(theMsg, "Alarm Created");
1325 verify(ctx, Mockito.never()).tellFailure(Mockito.any(), Mockito.any()); 1325 verify(ctx, Mockito.never()).tellFailure(Mockito.any(), Mockito.any());
1326 1326
1327 } 1327 }
@@ -1407,7 +1407,7 @@ public class TbDeviceProfileNodeTest { @@ -1407,7 +1407,7 @@ public class TbDeviceProfileNodeTest {
1407 1407
1408 node.onMsg(ctx, msg); 1408 node.onMsg(ctx, msg);
1409 verify(ctx).tellSuccess(msg); 1409 verify(ctx).tellSuccess(msg);
1410 - verify(ctx).tellNext(theMsg, "Alarm Created"); 1410 + verify(ctx).enqueueForTellNext(theMsg, "Alarm Created");
1411 verify(ctx, Mockito.never()).tellFailure(Mockito.any(), Mockito.any()); 1411 verify(ctx, Mockito.never()).tellFailure(Mockito.any(), Mockito.any());
1412 } 1412 }
1413 1413
@@ -89,6 +89,7 @@ transport: @@ -89,6 +89,7 @@ transport:
89 bind_address: "${MQTT_BIND_ADDRESS:0.0.0.0}" 89 bind_address: "${MQTT_BIND_ADDRESS:0.0.0.0}"
90 bind_port: "${MQTT_BIND_PORT:1883}" 90 bind_port: "${MQTT_BIND_PORT:1883}"
91 timeout: "${MQTT_TIMEOUT:10000}" 91 timeout: "${MQTT_TIMEOUT:10000}"
  92 + 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
92 netty: 93 netty:
93 leak_detector_level: "${NETTY_LEAK_DETECTOR_LVL:DISABLED}" 94 leak_detector_level: "${NETTY_LEAK_DETECTOR_LVL:DISABLED}"
94 boss_group_thread_count: "${NETTY_BOSS_GROUP_THREADS:1}" 95 boss_group_thread_count: "${NETTY_BOSS_GROUP_THREADS:1}"
@@ -84,6 +84,8 @@ export interface WidgetActionsApi { @@ -84,6 +84,8 @@ export interface WidgetActionsApi {
84 entityId?: EntityId, entityName?: string, additionalParams?: any, entityLabel?: string) => void; 84 entityId?: EntityId, entityName?: string, additionalParams?: any, entityLabel?: string) => void;
85 elementClick: ($event: Event) => void; 85 elementClick: ($event: Event) => void;
86 getActiveEntityInfo: () => SubscriptionEntityInfo; 86 getActiveEntityInfo: () => SubscriptionEntityInfo;
  87 + openDashboardStateInSeparateDialog: (targetDashboardStateId: string, params?: StateParams, dialogTitle?: string,
  88 + hideDashboardToolbar?: boolean, dialogWidth?: number, dialogHeight?: number) => void;
87 } 89 }
88 90
89 export interface AliasInfo { 91 export interface AliasInfo {
@@ -50,11 +50,17 @@ export class AttributeService { @@ -50,11 +50,17 @@ export class AttributeService {
50 } 50 }
51 51
52 public deleteEntityTimeseries(entityId: EntityId, timeseries: Array<AttributeData>, deleteAllDataForKeys = false, 52 public deleteEntityTimeseries(entityId: EntityId, timeseries: Array<AttributeData>, deleteAllDataForKeys = false,
53 - config?: RequestConfig): Observable<any> { 53 + startTs?: number, endTs?: number, config?: RequestConfig): Observable<any> {
54 const keys = timeseries.map(attribute => encodeURI(attribute.key)).join(','); 54 const keys = timeseries.map(attribute => encodeURI(attribute.key)).join(',');
55 - return this.http.delete(`/api/plugins/telemetry/${entityId.entityType}/${entityId.id}/timeseries/delete` +  
56 - `?keys=${keys}&deleteAllDataForKeys=${deleteAllDataForKeys}`,  
57 - defaultHttpOptionsFromConfig(config)); 55 + let url = `/api/plugins/telemetry/${entityId.entityType}/${entityId.id}/timeseries/delete` +
  56 + `?keys=${keys}&deleteAllDataForKeys=${deleteAllDataForKeys}`;
  57 + if (isDefinedAndNotNull(startTs)) {
  58 + url += `&startTs=${startTs}`;
  59 + }
  60 + if (isDefinedAndNotNull(endTs)) {
  61 + url += `&endTs=${endTs}`;
  62 + }
  63 + return this.http.delete(url, defaultHttpOptionsFromConfig(config));
58 } 64 }
59 65
60 public saveEntityAttributes(entityId: EntityId, attributeScope: AttributeScope, attributes: Array<AttributeData>, 66 public saveEntityAttributes(entityId: EntityId, attributeScope: AttributeScope, attributes: Array<AttributeData>,
@@ -97,7 +103,7 @@ export class AttributeService { @@ -97,7 +103,7 @@ export class AttributeService {
97 }); 103 });
98 let deleteEntityTimeseriesObservable: Observable<any>; 104 let deleteEntityTimeseriesObservable: Observable<any>;
99 if (deleteTimeseries.length) { 105 if (deleteTimeseries.length) {
100 - deleteEntityTimeseriesObservable = this.deleteEntityTimeseries(entityId, deleteTimeseries, true, config); 106 + deleteEntityTimeseriesObservable = this.deleteEntityTimeseries(entityId, deleteTimeseries, true, null, null, config);
101 } else { 107 } else {
102 deleteEntityTimeseriesObservable = of(null); 108 deleteEntityTimeseriesObservable = of(null);
103 } 109 }
@@ -18,7 +18,7 @@ import { Store } from '@ngrx/store'; @@ -18,7 +18,7 @@ import { Store } from '@ngrx/store';
18 import { AppState } from '@core/core.state'; 18 import { AppState } from '@core/core.state';
19 import { FormBuilder, FormGroup, ValidatorFn, Validators } from '@angular/forms'; 19 import { FormBuilder, FormGroup, ValidatorFn, Validators } from '@angular/forms';
20 import { ContactBased } from '@shared/models/contact-based.model'; 20 import { ContactBased } from '@shared/models/contact-based.model';
21 -import { AfterViewInit, Directive } from '@angular/core'; 21 +import { AfterViewInit, ChangeDetectorRef, Directive } from '@angular/core';
22 import { POSTAL_CODE_PATTERNS } from '@home/models/contact.models'; 22 import { POSTAL_CODE_PATTERNS } from '@home/models/contact.models';
23 import { HasId } from '@shared/models/base-data'; 23 import { HasId } from '@shared/models/base-data';
24 import { EntityComponent } from './entity.component'; 24 import { EntityComponent } from './entity.component';
@@ -30,8 +30,9 @@ export abstract class ContactBasedComponent<T extends ContactBased<HasId>> exten @@ -30,8 +30,9 @@ export abstract class ContactBasedComponent<T extends ContactBased<HasId>> exten
30 protected constructor(protected store: Store<AppState>, 30 protected constructor(protected store: Store<AppState>,
31 protected fb: FormBuilder, 31 protected fb: FormBuilder,
32 protected entityValue: T, 32 protected entityValue: T,
33 - protected entitiesTableConfigValue: EntityTableConfig<T>) {  
34 - super(store, fb, entityValue, entitiesTableConfigValue); 33 + protected entitiesTableConfigValue: EntityTableConfig<T>,
  34 + protected cd: ChangeDetectorRef) {
  35 + super(store, fb, entityValue, entitiesTableConfigValue, cd);
35 } 36 }
36 37
37 buildForm(entity: T): FormGroup { 38 buildForm(entity: T): FormGroup {
@@ -17,7 +17,7 @@ @@ -17,7 +17,7 @@
17 import { BaseData, HasId } from '@shared/models/base-data'; 17 import { BaseData, HasId } from '@shared/models/base-data';
18 import { FormBuilder, FormGroup } from '@angular/forms'; 18 import { FormBuilder, FormGroup } from '@angular/forms';
19 import { PageComponent } from '@shared/components/page.component'; 19 import { PageComponent } from '@shared/components/page.component';
20 -import { Directive, EventEmitter, Input, OnInit, Output } from '@angular/core'; 20 +import { ChangeDetectorRef, Directive, EventEmitter, Input, OnInit, Output } from '@angular/core';
21 import { Store } from '@ngrx/store'; 21 import { Store } from '@ngrx/store';
22 import { AppState } from '@core/core.state'; 22 import { AppState } from '@core/core.state';
23 import { EntityAction } from '@home/models/entity/entity-component.models'; 23 import { EntityAction } from '@home/models/entity/entity-component.models';
@@ -50,6 +50,7 @@ export abstract class EntityComponent<T extends BaseData<HasId>, @@ -50,6 +50,7 @@ export abstract class EntityComponent<T extends BaseData<HasId>,
50 @Input() 50 @Input()
51 set isEdit(isEdit: boolean) { 51 set isEdit(isEdit: boolean) {
52 this.isEditValue = isEdit; 52 this.isEditValue = isEdit;
  53 + this.cd.markForCheck();
53 this.updateFormState(); 54 this.updateFormState();
54 } 55 }
55 56
@@ -80,7 +81,8 @@ export abstract class EntityComponent<T extends BaseData<HasId>, @@ -80,7 +81,8 @@ export abstract class EntityComponent<T extends BaseData<HasId>,
80 protected constructor(protected store: Store<AppState>, 81 protected constructor(protected store: Store<AppState>,
81 protected fb: FormBuilder, 82 protected fb: FormBuilder,
82 protected entityValue: T, 83 protected entityValue: T,
83 - protected entitiesTableConfigValue: C) { 84 + protected entitiesTableConfigValue: C,
  85 + protected cd: ChangeDetectorRef) {
84 super(store); 86 super(store);
85 this.entityForm = this.buildForm(this.entityValue); 87 this.entityForm = this.buildForm(this.entityValue);
86 } 88 }
@@ -14,7 +14,7 @@ @@ -14,7 +14,7 @@
14 /// limitations under the License. 14 /// limitations under the License.
15 /// 15 ///
16 16
17 -import { Component, Inject, Input, Optional } from '@angular/core'; 17 +import { ChangeDetectorRef, Component, Inject, Input, Optional } from '@angular/core';
18 import { Store } from '@ngrx/store'; 18 import { Store } from '@ngrx/store';
19 import { AppState } from '@core/core.state'; 19 import { AppState } from '@core/core.state';
20 import { FormBuilder, FormGroup, Validators } from '@angular/forms'; 20 import { FormBuilder, FormGroup, Validators } from '@angular/forms';
@@ -77,8 +77,9 @@ export class DeviceProfileComponent extends EntityComponent<DeviceProfile> { @@ -77,8 +77,9 @@ export class DeviceProfileComponent extends EntityComponent<DeviceProfile> {
77 protected translate: TranslateService, 77 protected translate: TranslateService,
78 @Optional() @Inject('entity') protected entityValue: DeviceProfile, 78 @Optional() @Inject('entity') protected entityValue: DeviceProfile,
79 @Optional() @Inject('entitiesTableConfig') protected entitiesTableConfigValue: EntityTableConfig<DeviceProfile>, 79 @Optional() @Inject('entitiesTableConfig') protected entitiesTableConfigValue: EntityTableConfig<DeviceProfile>,
80 - protected fb: FormBuilder) {  
81 - super(store, fb, entityValue, entitiesTableConfigValue); 80 + protected fb: FormBuilder,
  81 + protected cd: ChangeDetectorRef) {
  82 + super(store, fb, entityValue, entitiesTableConfigValue, cd);
82 } 83 }
83 84
84 hideDelete() { 85 hideDelete() {
@@ -14,7 +14,7 @@ @@ -14,7 +14,7 @@
14 /// limitations under the License. 14 /// limitations under the License.
15 /// 15 ///
16 16
17 -import { Component, Inject, Input, Optional } from '@angular/core'; 17 +import { ChangeDetectorRef, Component, Inject, Input, Optional } from '@angular/core';
18 import { Store } from '@ngrx/store'; 18 import { Store } from '@ngrx/store';
19 import { AppState } from '@core/core.state'; 19 import { AppState } from '@core/core.state';
20 import { FormBuilder, FormGroup, Validators } from '@angular/forms'; 20 import { FormBuilder, FormGroup, Validators } from '@angular/forms';
@@ -43,8 +43,9 @@ export class TenantProfileComponent extends EntityComponent<TenantProfile> { @@ -43,8 +43,9 @@ export class TenantProfileComponent extends EntityComponent<TenantProfile> {
43 protected translate: TranslateService, 43 protected translate: TranslateService,
44 @Optional() @Inject('entity') protected entityValue: TenantProfile, 44 @Optional() @Inject('entity') protected entityValue: TenantProfile,
45 @Optional() @Inject('entitiesTableConfig') protected entitiesTableConfigValue: EntityTableConfig<TenantProfile>, 45 @Optional() @Inject('entitiesTableConfig') protected entitiesTableConfigValue: EntityTableConfig<TenantProfile>,
46 - protected fb: FormBuilder) {  
47 - super(store, fb, entityValue, entitiesTableConfigValue); 46 + protected fb: FormBuilder,
  47 + protected cd: ChangeDetectorRef) {
  48 + super(store, fb, entityValue, entitiesTableConfigValue, cd);
48 } 49 }
49 50
50 hideDelete() { 51 hideDelete() {
@@ -284,7 +284,8 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI @@ -284,7 +284,8 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
284 getActionDescriptors: this.getActionDescriptors.bind(this), 284 getActionDescriptors: this.getActionDescriptors.bind(this),
285 handleWidgetAction: this.handleWidgetAction.bind(this), 285 handleWidgetAction: this.handleWidgetAction.bind(this),
286 elementClick: this.elementClick.bind(this), 286 elementClick: this.elementClick.bind(this),
287 - getActiveEntityInfo: this.getActiveEntityInfo.bind(this) 287 + getActiveEntityInfo: this.getActiveEntityInfo.bind(this),
  288 + openDashboardStateInSeparateDialog: this.openDashboardStateInSeparateDialog.bind(this)
288 }; 289 };
289 290
290 this.widgetContext.customHeaderActions = []; 291 this.widgetContext.customHeaderActions = [];
@@ -1025,7 +1026,8 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI @@ -1025,7 +1026,8 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
1025 this.updateEntityParams(params, targetEntityParamName, targetEntityId, entityName, entityLabel); 1026 this.updateEntityParams(params, targetEntityParamName, targetEntityId, entityName, entityLabel);
1026 if (type === WidgetActionType.openDashboardState) { 1027 if (type === WidgetActionType.openDashboardState) {
1027 if (descriptor.openInSeparateDialog) { 1028 if (descriptor.openInSeparateDialog) {
1028 - this.openDashboardStateInDialog(descriptor, entityId, entityName, additionalParams, entityLabel); 1029 + this.openDashboardStateInSeparateDialog(descriptor.targetDashboardStateId, params, descriptor.dialogTitle,
  1030 + descriptor.dialogHideDashboardToolbar, descriptor.dialogWidth, descriptor.dialogHeight);
1029 } else { 1031 } else {
1030 this.widgetContext.stateController.openState(targetDashboardStateId, params, descriptor.openRightLayout); 1032 this.widgetContext.stateController.openState(targetDashboardStateId, params, descriptor.openRightLayout);
1031 } 1033 }
@@ -1276,22 +1278,15 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI @@ -1276,22 +1278,15 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
1276 } 1278 }
1277 } 1279 }
1278 1280
1279 - private openDashboardStateInDialog(descriptor: WidgetActionDescriptor,  
1280 - entityId?: EntityId, entityName?: string, additionalParams?: any, entityLabel?: string) { 1281 + private openDashboardStateInSeparateDialog(targetDashboardStateId: string, params?: StateParams, dialogTitle?: string,
  1282 + hideDashboardToolbar = true, dialogWidth?: number, dialogHeight?: number) {
1281 const dashboard = deepClone(this.widgetContext.stateController.dashboardCtrl.dashboardCtx.getDashboard()); 1283 const dashboard = deepClone(this.widgetContext.stateController.dashboardCtrl.dashboardCtx.getDashboard());
1282 const stateObject: StateObject = {}; 1284 const stateObject: StateObject = {};
1283 - stateObject.params = {};  
1284 - const targetEntityParamName = descriptor.stateEntityParamName;  
1285 - const targetDashboardStateId = descriptor.targetDashboardStateId;  
1286 - let targetEntityId: EntityId;  
1287 - if (descriptor.setEntityId) {  
1288 - targetEntityId = entityId;  
1289 - }  
1290 - this.updateEntityParams(stateObject.params, targetEntityParamName, targetEntityId, entityName, entityLabel); 1285 + stateObject.params = params;
1291 if (targetDashboardStateId) { 1286 if (targetDashboardStateId) {
1292 stateObject.id = targetDashboardStateId; 1287 stateObject.id = targetDashboardStateId;
1293 } 1288 }
1294 - let title = descriptor.dialogTitle; 1289 + let title = dialogTitle;
1295 if (!title) { 1290 if (!title) {
1296 if (targetDashboardStateId && dashboard.configuration.states) { 1291 if (targetDashboardStateId && dashboard.configuration.states) {
1297 const dashboardState = dashboard.configuration.states[targetDashboardStateId]; 1292 const dashboardState = dashboard.configuration.states[targetDashboardStateId];
@@ -1304,7 +1299,6 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI @@ -1304,7 +1299,6 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
1304 title = dashboard.title; 1299 title = dashboard.title;
1305 } 1300 }
1306 title = this.utils.customTranslation(title, title); 1301 title = this.utils.customTranslation(title, title);
1307 - const params = stateObject.params;  
1308 const paramsEntityName = params && params.entityName ? params.entityName : ''; 1302 const paramsEntityName = params && params.entityName ? params.entityName : '';
1309 const paramsEntityLabel = params && params.entityLabel ? params.entityLabel : ''; 1303 const paramsEntityLabel = params && params.entityLabel ? params.entityLabel : '';
1310 title = insertVariable(title, 'entityName', paramsEntityName); 1304 title = insertVariable(title, 'entityName', paramsEntityName);
@@ -1324,28 +1318,27 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI @@ -1324,28 +1318,27 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
1324 dashboard, 1318 dashboard,
1325 state: objToBase64([ stateObject ]), 1319 state: objToBase64([ stateObject ]),
1326 title, 1320 title,
1327 - hideToolbar: descriptor.dialogHideDashboardToolbar,  
1328 - width: descriptor.dialogWidth,  
1329 - height: descriptor.dialogHeight 1321 + hideToolbar: hideDashboardToolbar,
  1322 + width: dialogWidth,
  1323 + height: dialogHeight
1330 } 1324 }
1331 }); 1325 });
1332 } 1326 }
1333 1327
1334 private elementClick($event: Event) { 1328 private elementClick($event: Event) {
1335 - const e = ($event.target || $event.srcElement) as Element;  
1336 - if (e.id) {  
1337 - const descriptors = this.getActionDescriptors('elementClick');  
1338 - if (descriptors.length) {  
1339 - descriptors.forEach((descriptor) => {  
1340 - if (descriptor.name === e.id) {  
1341 - $event.stopPropagation();  
1342 - const entityInfo = this.getActiveEntityInfo();  
1343 - const entityId = entityInfo ? entityInfo.entityId : null;  
1344 - const entityName = entityInfo ? entityInfo.entityName : null;  
1345 - const entityLabel = entityInfo && entityInfo.entityLabel ? entityInfo.entityLabel : null;  
1346 - this.handleWidgetAction($event, descriptor, entityId, entityName, null, entityLabel);  
1347 - }  
1348 - }); 1329 + const elementClicked = ($event.target || $event.srcElement) as Element;
  1330 + const descriptors = this.getActionDescriptors('elementClick');
  1331 + if (descriptors.length) {
  1332 + const idsList = descriptors.map(descriptor => `#${descriptor.name}`).join(',');
  1333 + const targetElement = $(elementClicked).closest(idsList, this.widgetContext.$container[0]);
  1334 + if (targetElement.length && targetElement[0].id) {
  1335 + $event.stopPropagation();
  1336 + const descriptor = descriptors.find(descriptorInfo => descriptorInfo.name === targetElement[0].id);
  1337 + const entityInfo = this.getActiveEntityInfo();
  1338 + const entityId = entityInfo ? entityInfo.entityId : null;
  1339 + const entityName = entityInfo ? entityInfo.entityName : null;
  1340 + const entityLabel = entityInfo && entityInfo.entityLabel ? entityInfo.entityLabel : null;
  1341 + this.handleWidgetAction($event, descriptor, entityId, entityName, null, entityLabel);
1349 } 1342 }
1350 } 1343 }
1351 } 1344 }
@@ -39,7 +39,7 @@ @@ -39,7 +39,7 @@
39 </mat-form-field> 39 </mat-form-field>
40 <mat-form-field class="mat-block"> 40 <mat-form-field class="mat-block">
41 <mat-label translate>admin.smtp-protocol</mat-label> 41 <mat-label translate>admin.smtp-protocol</mat-label>
42 - <mat-select matInput formControlName="smtpProtocol"> 42 + <mat-select formControlName="smtpProtocol">
43 <mat-option *ngFor="let protocol of smtpProtocols" [value]="protocol"> 43 <mat-option *ngFor="let protocol of smtpProtocols" [value]="protocol">
44 {{protocol.toUpperCase()}} 44 {{protocol.toUpperCase()}}
45 </mat-option> 45 </mat-option>
@@ -127,7 +127,10 @@ @@ -127,7 +127,10 @@
127 <input matInput formControlName="username" placeholder="{{ 'common.enter-username' | translate }}" 127 <input matInput formControlName="username" placeholder="{{ 'common.enter-username' | translate }}"
128 autocomplete="new-username"/> 128 autocomplete="new-username"/>
129 </mat-form-field> 129 </mat-form-field>
130 - <mat-form-field class="mat-block"> 130 + <mat-checkbox *ngIf="showChangePassword" formControlName="changePassword" style="padding-bottom: 16px;">
  131 + {{ 'admin.change-password' | translate }}
  132 + </mat-checkbox>
  133 + <mat-form-field class="mat-block" *ngIf="mailSettings.get('changePassword').value || !showChangePassword">
131 <mat-label translate>common.password</mat-label> 134 <mat-label translate>common.password</mat-label>
132 <input matInput formControlName="password" type="password" 135 <input matInput formControlName="password" type="password"
133 placeholder="{{ 'common.enter-password' | translate }}" autocomplete="new-password"/> 136 placeholder="{{ 'common.enter-password' | translate }}" autocomplete="new-password"/>
@@ -14,7 +14,7 @@ @@ -14,7 +14,7 @@
14 /// limitations under the License. 14 /// limitations under the License.
15 /// 15 ///
16 16
17 -import { Component, OnInit } from '@angular/core'; 17 +import { Component, OnDestroy, OnInit } from '@angular/core';
18 import { Store } from '@ngrx/store'; 18 import { Store } from '@ngrx/store';
19 import { AppState } from '@core/core.state'; 19 import { AppState } from '@core/core.state';
20 import { PageComponent } from '@shared/components/page.component'; 20 import { PageComponent } from '@shared/components/page.component';
@@ -25,21 +25,26 @@ import { AdminService } from '@core/http/admin.service'; @@ -25,21 +25,26 @@ import { AdminService } from '@core/http/admin.service';
25 import { ActionNotificationShow } from '@core/notification/notification.actions'; 25 import { ActionNotificationShow } from '@core/notification/notification.actions';
26 import { TranslateService } from '@ngx-translate/core'; 26 import { TranslateService } from '@ngx-translate/core';
27 import { HasConfirmForm } from '@core/guards/confirm-on-exit.guard'; 27 import { HasConfirmForm } from '@core/guards/confirm-on-exit.guard';
28 -import { isString } from '@core/utils'; 28 +import { isDefinedAndNotNull, isString } from '@core/utils';
  29 +import { Subject } from 'rxjs';
  30 +import { takeUntil } from 'rxjs/operators';
29 31
30 @Component({ 32 @Component({
31 selector: 'tb-mail-server', 33 selector: 'tb-mail-server',
32 templateUrl: './mail-server.component.html', 34 templateUrl: './mail-server.component.html',
33 styleUrls: ['./mail-server.component.scss', './settings-card.scss'] 35 styleUrls: ['./mail-server.component.scss', './settings-card.scss']
34 }) 36 })
35 -export class MailServerComponent extends PageComponent implements OnInit, HasConfirmForm { 37 +export class MailServerComponent extends PageComponent implements OnInit, OnDestroy, HasConfirmForm {
36 38
37 mailSettings: FormGroup; 39 mailSettings: FormGroup;
38 adminSettings: AdminSettings<MailServerSettings>; 40 adminSettings: AdminSettings<MailServerSettings>;
39 smtpProtocols = ['smtp', 'smtps']; 41 smtpProtocols = ['smtp', 'smtps'];
  42 + showChangePassword = false;
40 43
41 tlsVersions = ['TLSv1', 'TLSv1.1', 'TLSv1.2', 'TLSv1.3']; 44 tlsVersions = ['TLSv1', 'TLSv1.1', 'TLSv1.2', 'TLSv1.3'];
42 45
  46 + private destroy$ = new Subject();
  47 +
43 constructor(protected store: Store<AppState>, 48 constructor(protected store: Store<AppState>,
44 private router: Router, 49 private router: Router,
45 private adminService: AdminService, 50 private adminService: AdminService,
@@ -56,12 +61,22 @@ export class MailServerComponent extends PageComponent implements OnInit, HasCon @@ -56,12 +61,22 @@ export class MailServerComponent extends PageComponent implements OnInit, HasCon
56 if (this.adminSettings.jsonValue && isString(this.adminSettings.jsonValue.enableTls)) { 61 if (this.adminSettings.jsonValue && isString(this.adminSettings.jsonValue.enableTls)) {
57 this.adminSettings.jsonValue.enableTls = (this.adminSettings.jsonValue.enableTls as any) === 'true'; 62 this.adminSettings.jsonValue.enableTls = (this.adminSettings.jsonValue.enableTls as any) === 'true';
58 } 63 }
  64 + this.showChangePassword =
  65 + isDefinedAndNotNull(this.adminSettings.jsonValue.showChangePassword) ? this.adminSettings.jsonValue.showChangePassword : true ;
  66 + delete this.adminSettings.jsonValue.showChangePassword;
59 this.mailSettings.reset(this.adminSettings.jsonValue); 67 this.mailSettings.reset(this.adminSettings.jsonValue);
  68 + this.enableMailPassword(!this.showChangePassword);
60 this.enableProxyChanged(); 69 this.enableProxyChanged();
61 } 70 }
62 ); 71 );
63 } 72 }
64 73
  74 + ngOnDestroy() {
  75 + this.destroy$.next();
  76 + this.destroy$.complete();
  77 + super.ngOnDestroy();
  78 + }
  79 +
65 buildMailServerSettingsForm() { 80 buildMailServerSettingsForm() {
66 this.mailSettings = this.fb.group({ 81 this.mailSettings = this.fb.group({
67 mailFrom: ['', [Validators.required]], 82 mailFrom: ['', [Validators.required]],
@@ -81,14 +96,23 @@ export class MailServerComponent extends PageComponent implements OnInit, HasCon @@ -81,14 +96,23 @@ export class MailServerComponent extends PageComponent implements OnInit, HasCon
81 proxyUser: [''], 96 proxyUser: [''],
82 proxyPassword: [''], 97 proxyPassword: [''],
83 username: [''], 98 username: [''],
  99 + changePassword: [false],
84 password: [''] 100 password: ['']
85 }); 101 });
86 this.registerDisableOnLoadFormControl(this.mailSettings.get('smtpProtocol')); 102 this.registerDisableOnLoadFormControl(this.mailSettings.get('smtpProtocol'));
87 this.registerDisableOnLoadFormControl(this.mailSettings.get('enableTls')); 103 this.registerDisableOnLoadFormControl(this.mailSettings.get('enableTls'));
88 this.registerDisableOnLoadFormControl(this.mailSettings.get('enableProxy')); 104 this.registerDisableOnLoadFormControl(this.mailSettings.get('enableProxy'));
89 - this.mailSettings.get('enableProxy').valueChanges.subscribe(() => { 105 + this.registerDisableOnLoadFormControl(this.mailSettings.get('changePassword'));
  106 + this.mailSettings.get('enableProxy').valueChanges.pipe(
  107 + takeUntil(this.destroy$)
  108 + ).subscribe(() => {
90 this.enableProxyChanged(); 109 this.enableProxyChanged();
91 }); 110 });
  111 + this.mailSettings.get('changePassword').valueChanges.pipe(
  112 + takeUntil(this.destroy$)
  113 + ).subscribe((value) => {
  114 + this.enableMailPassword(value);
  115 + });
92 } 116 }
93 117
94 enableProxyChanged(): void { 118 enableProxyChanged(): void {
@@ -102,8 +126,16 @@ export class MailServerComponent extends PageComponent implements OnInit, HasCon @@ -102,8 +126,16 @@ export class MailServerComponent extends PageComponent implements OnInit, HasCon
102 } 126 }
103 } 127 }
104 128
  129 + enableMailPassword(enable: boolean) {
  130 + if (enable) {
  131 + this.mailSettings.get('password').enable({emitEvent: false});
  132 + } else {
  133 + this.mailSettings.get('password').disable({emitEvent: false});
  134 + }
  135 + }
  136 +
105 sendTestMail(): void { 137 sendTestMail(): void {
106 - this.adminSettings.jsonValue = {...this.adminSettings.jsonValue, ...this.mailSettings.value}; 138 + this.adminSettings.jsonValue = {...this.adminSettings.jsonValue, ...this.mailSettingsFormValue};
107 this.adminService.sendTestMail(this.adminSettings).subscribe( 139 this.adminService.sendTestMail(this.adminSettings).subscribe(
108 () => { 140 () => {
109 this.store.dispatch(new ActionNotificationShow({ message: this.translate.instant('admin.test-mail-sent'), 141 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 @@ -113,13 +145,11 @@ export class MailServerComponent extends PageComponent implements OnInit, HasCon
113 } 145 }
114 146
115 save(): void { 147 save(): void {
116 - this.adminSettings.jsonValue = {...this.adminSettings.jsonValue, ...this.mailSettings.value}; 148 + this.adminSettings.jsonValue = {...this.adminSettings.jsonValue, ...this.mailSettingsFormValue};
117 this.adminService.saveAdminSettings(this.adminSettings).subscribe( 149 this.adminService.saveAdminSettings(this.adminSettings).subscribe(
118 (adminSettings) => { 150 (adminSettings) => {
119 - if (!adminSettings.jsonValue.password) {  
120 - adminSettings.jsonValue.password = this.mailSettings.value.password;  
121 - }  
122 this.adminSettings = adminSettings; 151 this.adminSettings = adminSettings;
  152 + this.showChangePassword = true;
123 this.mailSettings.reset(this.adminSettings.jsonValue); 153 this.mailSettings.reset(this.adminSettings.jsonValue);
124 } 154 }
125 ); 155 );
@@ -129,4 +159,9 @@ export class MailServerComponent extends PageComponent implements OnInit, HasCon @@ -129,4 +159,9 @@ export class MailServerComponent extends PageComponent implements OnInit, HasCon
129 return this.mailSettings; 159 return this.mailSettings;
130 } 160 }
131 161
  162 + private get mailSettingsFormValue(): MailServerSettings {
  163 + const formValue = this.mailSettings.value;
  164 + delete formValue.changePassword;
  165 + return formValue;
  166 + }
132 } 167 }
@@ -14,7 +14,7 @@ @@ -14,7 +14,7 @@
14 /// limitations under the License. 14 /// limitations under the License.
15 /// 15 ///
16 16
17 -import { Component, Inject, OnDestroy, OnInit } from '@angular/core'; 17 +import { ChangeDetectorRef, Component, Inject, OnDestroy, OnInit } from '@angular/core';
18 import { Subject } from 'rxjs'; 18 import { Subject } from 'rxjs';
19 import { Store } from '@ngrx/store'; 19 import { Store } from '@ngrx/store';
20 import { AppState } from '@core/core.state'; 20 import { AppState } from '@core/core.state';
@@ -30,7 +30,7 @@ import { @@ -30,7 +30,7 @@ import {
30 ResourceTypeTranslationMap 30 ResourceTypeTranslationMap
31 } from '@shared/models/resource.models'; 31 } from '@shared/models/resource.models';
32 import { pairwise, startWith, takeUntil } from 'rxjs/operators'; 32 import { pairwise, startWith, takeUntil } from 'rxjs/operators';
33 -import { ActionNotificationShow } from "@core/notification/notification.actions"; 33 +import { ActionNotificationShow } from '@core/notification/notification.actions';
34 34
35 @Component({ 35 @Component({
36 selector: 'tb-resources-library', 36 selector: 'tb-resources-library',
@@ -48,8 +48,9 @@ export class ResourcesLibraryComponent extends EntityComponent<Resource> impleme @@ -48,8 +48,9 @@ export class ResourcesLibraryComponent extends EntityComponent<Resource> impleme
48 protected translate: TranslateService, 48 protected translate: TranslateService,
49 @Inject('entity') protected entityValue: Resource, 49 @Inject('entity') protected entityValue: Resource,
50 @Inject('entitiesTableConfig') protected entitiesTableConfigValue: EntityTableConfig<Resource>, 50 @Inject('entitiesTableConfig') protected entitiesTableConfigValue: EntityTableConfig<Resource>,
51 - public fb: FormBuilder) {  
52 - super(store, fb, entityValue, entitiesTableConfigValue); 51 + public fb: FormBuilder,
  52 + protected cd: ChangeDetectorRef) {
  53 + super(store, fb, entityValue, entitiesTableConfigValue, cd);
53 } 54 }
54 55
55 ngOnInit() { 56 ngOnInit() {
@@ -102,7 +103,7 @@ export class ResourcesLibraryComponent extends EntityComponent<Resource> impleme @@ -102,7 +103,7 @@ export class ResourcesLibraryComponent extends EntityComponent<Resource> impleme
102 if (this.isAdd) { 103 if (this.isAdd) {
103 form.addControl('data', this.fb.control(null, Validators.required)); 104 form.addControl('data', this.fb.control(null, Validators.required));
104 } 105 }
105 - return form 106 + return form;
106 } 107 }
107 108
108 updateForm(entity: Resource) { 109 updateForm(entity: Resource) {
@@ -14,7 +14,7 @@ @@ -14,7 +14,7 @@
14 /// limitations under the License. 14 /// limitations under the License.
15 /// 15 ///
16 16
17 -import { Component, Inject } from '@angular/core'; 17 +import { ChangeDetectorRef, Component, Inject } from '@angular/core';
18 import { Store } from '@ngrx/store'; 18 import { Store } from '@ngrx/store';
19 import { AppState } from '@core/core.state'; 19 import { AppState } from '@core/core.state';
20 import { EntityComponent } from '../../components/entity/entity.component'; 20 import { EntityComponent } from '../../components/entity/entity.component';
@@ -41,8 +41,9 @@ export class AssetComponent extends EntityComponent<AssetInfo> { @@ -41,8 +41,9 @@ export class AssetComponent extends EntityComponent<AssetInfo> {
41 protected translate: TranslateService, 41 protected translate: TranslateService,
42 @Inject('entity') protected entityValue: AssetInfo, 42 @Inject('entity') protected entityValue: AssetInfo,
43 @Inject('entitiesTableConfig') protected entitiesTableConfigValue: EntityTableConfig<AssetInfo>, 43 @Inject('entitiesTableConfig') protected entitiesTableConfigValue: EntityTableConfig<AssetInfo>,
44 - public fb: FormBuilder) {  
45 - super(store, fb, entityValue, entitiesTableConfigValue); 44 + public fb: FormBuilder,
  45 + protected cd: ChangeDetectorRef) {
  46 + super(store, fb, entityValue, entitiesTableConfigValue, cd);
46 } 47 }
47 48
48 ngOnInit() { 49 ngOnInit() {
@@ -14,7 +14,7 @@ @@ -14,7 +14,7 @@
14 /// limitations under the License. 14 /// limitations under the License.
15 /// 15 ///
16 16
17 -import { Component, Inject } from '@angular/core'; 17 +import { ChangeDetectorRef, Component, Inject } from '@angular/core';
18 import { Store } from '@ngrx/store'; 18 import { Store } from '@ngrx/store';
19 import { AppState } from '@core/core.state'; 19 import { AppState } from '@core/core.state';
20 import { FormBuilder, FormGroup, Validators } from '@angular/forms'; 20 import { FormBuilder, FormGroup, Validators } from '@angular/forms';
@@ -42,8 +42,9 @@ export class CustomerComponent extends ContactBasedComponent<Customer> { @@ -42,8 +42,9 @@ export class CustomerComponent extends ContactBasedComponent<Customer> {
42 protected translate: TranslateService, 42 protected translate: TranslateService,
43 @Inject('entity') protected entityValue: Customer, 43 @Inject('entity') protected entityValue: Customer,
44 @Inject('entitiesTableConfig') protected entitiesTableConfigValue: EntityTableConfig<Customer>, 44 @Inject('entitiesTableConfig') protected entitiesTableConfigValue: EntityTableConfig<Customer>,
45 - protected fb: FormBuilder) {  
46 - super(store, fb, entityValue, entitiesTableConfigValue); 45 + protected fb: FormBuilder,
  46 + protected cd: ChangeDetectorRef) {
  47 + super(store, fb, entityValue, entitiesTableConfigValue, cd);
47 } 48 }
48 49
49 hideDelete() { 50 hideDelete() {
@@ -14,7 +14,7 @@ @@ -14,7 +14,7 @@
14 /// limitations under the License. 14 /// limitations under the License.
15 /// 15 ///
16 16
17 -import { Component, Inject } from '@angular/core'; 17 +import { ChangeDetectorRef, Component, Inject } from '@angular/core';
18 import { Store } from '@ngrx/store'; 18 import { Store } from '@ngrx/store';
19 import { AppState } from '@core/core.state'; 19 import { AppState } from '@core/core.state';
20 import { EntityComponent } from '../../components/entity/entity.component'; 20 import { EntityComponent } from '../../components/entity/entity.component';
@@ -49,8 +49,9 @@ export class DashboardFormComponent extends EntityComponent<Dashboard> { @@ -49,8 +49,9 @@ export class DashboardFormComponent extends EntityComponent<Dashboard> {
49 private dashboardService: DashboardService, 49 private dashboardService: DashboardService,
50 @Inject('entity') protected entityValue: Dashboard, 50 @Inject('entity') protected entityValue: Dashboard,
51 @Inject('entitiesTableConfig') protected entitiesTableConfigValue: EntityTableConfig<Dashboard>, 51 @Inject('entitiesTableConfig') protected entitiesTableConfigValue: EntityTableConfig<Dashboard>,
52 - public fb: FormBuilder) {  
53 - super(store, fb, entityValue, entitiesTableConfigValue); 52 + public fb: FormBuilder,
  53 + protected cd: ChangeDetectorRef) {
  54 + super(store, fb, entityValue, entitiesTableConfigValue, cd);
54 } 55 }
55 56
56 ngOnInit() { 57 ngOnInit() {
@@ -14,7 +14,7 @@ @@ -14,7 +14,7 @@
14 /// limitations under the License. 14 /// limitations under the License.
15 /// 15 ///
16 16
17 -import { Component, Inject } from '@angular/core'; 17 +import { ChangeDetectorRef, Component, Inject } from '@angular/core';
18 import { Store } from '@ngrx/store'; 18 import { Store } from '@ngrx/store';
19 import { AppState } from '@core/core.state'; 19 import { AppState } from '@core/core.state';
20 import { EntityComponent } from '../../components/entity/entity.component'; 20 import { EntityComponent } from '../../components/entity/entity.component';
@@ -56,8 +56,9 @@ export class DeviceComponent extends EntityComponent<DeviceInfo> { @@ -56,8 +56,9 @@ export class DeviceComponent extends EntityComponent<DeviceInfo> {
56 protected translate: TranslateService, 56 protected translate: TranslateService,
57 @Inject('entity') protected entityValue: DeviceInfo, 57 @Inject('entity') protected entityValue: DeviceInfo,
58 @Inject('entitiesTableConfig') protected entitiesTableConfigValue: EntityTableConfig<DeviceInfo>, 58 @Inject('entitiesTableConfig') protected entitiesTableConfigValue: EntityTableConfig<DeviceInfo>,
59 - public fb: FormBuilder) {  
60 - super(store, fb, entityValue, entitiesTableConfigValue); 59 + public fb: FormBuilder,
  60 + protected cd: ChangeDetectorRef) {
  61 + super(store, fb, entityValue, entitiesTableConfigValue, cd);
61 } 62 }
62 63
63 ngOnInit() { 64 ngOnInit() {
@@ -14,7 +14,7 @@ @@ -14,7 +14,7 @@
14 /// limitations under the License. 14 /// limitations under the License.
15 /// 15 ///
16 16
17 -import { Component, Inject } from '@angular/core'; 17 +import { ChangeDetectorRef, Component, Inject } from '@angular/core';
18 import { Store } from '@ngrx/store'; 18 import { Store } from '@ngrx/store';
19 import { AppState } from '@core/core.state'; 19 import { AppState } from '@core/core.state';
20 import { EntityComponent } from '@home/components/entity/entity.component'; 20 import { EntityComponent } from '@home/components/entity/entity.component';
@@ -42,8 +42,9 @@ export class EdgeComponent extends EntityComponent<EdgeInfo> { @@ -42,8 +42,9 @@ export class EdgeComponent extends EntityComponent<EdgeInfo> {
42 protected translate: TranslateService, 42 protected translate: TranslateService,
43 @Inject('entity') protected entityValue: EdgeInfo, 43 @Inject('entity') protected entityValue: EdgeInfo,
44 @Inject('entitiesTableConfig') protected entitiesTableConfigValue: EntityTableConfig<EdgeInfo>, 44 @Inject('entitiesTableConfig') protected entitiesTableConfigValue: EntityTableConfig<EdgeInfo>,
45 - public fb: FormBuilder) {  
46 - super(store, fb, entityValue, entitiesTableConfigValue); 45 + public fb: FormBuilder,
  46 + protected cd: ChangeDetectorRef) {
  47 + super(store, fb, entityValue, entitiesTableConfigValue, cd);
47 } 48 }
48 49
49 ngOnInit() { 50 ngOnInit() {
@@ -14,7 +14,7 @@ @@ -14,7 +14,7 @@
14 /// limitations under the License. 14 /// limitations under the License.
15 /// 15 ///
16 16
17 -import { Component, Inject } from '@angular/core'; 17 +import { ChangeDetectorRef, Component, Inject } from '@angular/core';
18 import { Store } from '@ngrx/store'; 18 import { Store } from '@ngrx/store';
19 import { AppState } from '@core/core.state'; 19 import { AppState } from '@core/core.state';
20 import { EntityComponent } from '../../components/entity/entity.component'; 20 import { EntityComponent } from '../../components/entity/entity.component';
@@ -53,8 +53,9 @@ export class EntityViewComponent extends EntityComponent<EntityViewInfo> { @@ -53,8 +53,9 @@ export class EntityViewComponent extends EntityComponent<EntityViewInfo> {
53 protected translate: TranslateService, 53 protected translate: TranslateService,
54 @Inject('entity') protected entityValue: EntityViewInfo, 54 @Inject('entity') protected entityValue: EntityViewInfo,
55 @Inject('entitiesTableConfig') protected entitiesTableConfigValue: EntityTableConfig<EntityViewInfo>, 55 @Inject('entitiesTableConfig') protected entitiesTableConfigValue: EntityTableConfig<EntityViewInfo>,
56 - public fb: FormBuilder) {  
57 - super(store, fb, entityValue, entitiesTableConfigValue); 56 + public fb: FormBuilder,
  57 + protected cd: ChangeDetectorRef) {
  58 + super(store, fb, entityValue, entitiesTableConfigValue, cd);
58 } 59 }
59 60
60 ngOnInit() { 61 ngOnInit() {
@@ -14,7 +14,7 @@ @@ -14,7 +14,7 @@
14 /// limitations under the License. 14 /// limitations under the License.
15 /// 15 ///
16 16
17 -import { Component, Inject, OnDestroy, OnInit } from '@angular/core'; 17 +import { ChangeDetectorRef, Component, Inject, OnDestroy, OnInit } from '@angular/core';
18 import { combineLatest, Subject } from 'rxjs'; 18 import { combineLatest, Subject } from 'rxjs';
19 import { Store } from '@ngrx/store'; 19 import { Store } from '@ngrx/store';
20 import { AppState } from '@core/core.state'; 20 import { AppState } from '@core/core.state';
@@ -50,8 +50,9 @@ export class OtaUpdateComponent extends EntityComponent<OtaPackage> implements O @@ -50,8 +50,9 @@ export class OtaUpdateComponent extends EntityComponent<OtaPackage> implements O
50 protected translate: TranslateService, 50 protected translate: TranslateService,
51 @Inject('entity') protected entityValue: OtaPackage, 51 @Inject('entity') protected entityValue: OtaPackage,
52 @Inject('entitiesTableConfig') protected entitiesTableConfigValue: EntityTableConfig<OtaPackage>, 52 @Inject('entitiesTableConfig') protected entitiesTableConfigValue: EntityTableConfig<OtaPackage>,
53 - public fb: FormBuilder) {  
54 - super(store, fb, entityValue, entitiesTableConfigValue); 53 + public fb: FormBuilder,
  54 + protected cd: ChangeDetectorRef) {
  55 + super(store, fb, entityValue, entitiesTableConfigValue, cd);
55 } 56 }
56 57
57 ngOnInit() { 58 ngOnInit() {
@@ -14,7 +14,7 @@ @@ -14,7 +14,7 @@
14 /// limitations under the License. 14 /// limitations under the License.
15 /// 15 ///
16 16
17 -import { Component, Inject } from '@angular/core'; 17 +import { ChangeDetectorRef, Component, Inject } from '@angular/core';
18 import { Store } from '@ngrx/store'; 18 import { Store } from '@ngrx/store';
19 import { AppState } from '@core/core.state'; 19 import { AppState } from '@core/core.state';
20 import { EntityComponent } from '../../components/entity/entity.component'; 20 import { EntityComponent } from '../../components/entity/entity.component';
@@ -37,8 +37,9 @@ export class RuleChainComponent extends EntityComponent<RuleChain> { @@ -37,8 +37,9 @@ export class RuleChainComponent extends EntityComponent<RuleChain> {
37 protected translate: TranslateService, 37 protected translate: TranslateService,
38 @Inject('entity') protected entityValue: RuleChain, 38 @Inject('entity') protected entityValue: RuleChain,
39 @Inject('entitiesTableConfig') protected entitiesTableConfigValue: EntityTableConfig<RuleChain>, 39 @Inject('entitiesTableConfig') protected entitiesTableConfigValue: EntityTableConfig<RuleChain>,
40 - public fb: FormBuilder) {  
41 - super(store, fb, entityValue, entitiesTableConfigValue); 40 + public fb: FormBuilder,
  41 + protected cd: ChangeDetectorRef) {
  42 + super(store, fb, entityValue, entitiesTableConfigValue, cd);
42 } 43 }
43 44
44 ngOnInit() { 45 ngOnInit() {
@@ -14,7 +14,7 @@ @@ -14,7 +14,7 @@
14 /// limitations under the License. 14 /// limitations under the License.
15 /// 15 ///
16 16
17 -import { Component, Inject } from '@angular/core'; 17 +import { ChangeDetectorRef, Component, Inject } from '@angular/core';
18 import { Store } from '@ngrx/store'; 18 import { Store } from '@ngrx/store';
19 import { AppState } from '@core/core.state'; 19 import { AppState } from '@core/core.state';
20 import { FormBuilder, FormGroup, Validators } from '@angular/forms'; 20 import { FormBuilder, FormGroup, Validators } from '@angular/forms';
@@ -36,8 +36,9 @@ export class TenantComponent extends ContactBasedComponent<TenantInfo> { @@ -36,8 +36,9 @@ export class TenantComponent extends ContactBasedComponent<TenantInfo> {
36 protected translate: TranslateService, 36 protected translate: TranslateService,
37 @Inject('entity') protected entityValue: TenantInfo, 37 @Inject('entity') protected entityValue: TenantInfo,
38 @Inject('entitiesTableConfig') protected entitiesTableConfigValue: EntityTableConfig<TenantInfo>, 38 @Inject('entitiesTableConfig') protected entitiesTableConfigValue: EntityTableConfig<TenantInfo>,
39 - protected fb: FormBuilder) {  
40 - super(store, fb, entityValue, entitiesTableConfigValue); 39 + protected fb: FormBuilder,
  40 + protected cd: ChangeDetectorRef) {
  41 + super(store, fb, entityValue, entitiesTableConfigValue, cd);
41 } 42 }
42 43
43 hideDelete() { 44 hideDelete() {
@@ -14,7 +14,7 @@ @@ -14,7 +14,7 @@
14 /// limitations under the License. 14 /// limitations under the License.
15 /// 15 ///
16 16
17 -import { Component, Inject, Optional } from '@angular/core'; 17 +import { ChangeDetectorRef, Component, Inject, Optional } from '@angular/core';
18 import { select, Store } from '@ngrx/store'; 18 import { select, Store } from '@ngrx/store';
19 import { AppState } from '@core/core.state'; 19 import { AppState } from '@core/core.state';
20 import { EntityComponent } from '../../components/entity/entity.component'; 20 import { EntityComponent } from '../../components/entity/entity.component';
@@ -43,8 +43,9 @@ export class UserComponent extends EntityComponent<User> { @@ -43,8 +43,9 @@ export class UserComponent extends EntityComponent<User> {
43 constructor(protected store: Store<AppState>, 43 constructor(protected store: Store<AppState>,
44 @Optional() @Inject('entity') protected entityValue: User, 44 @Optional() @Inject('entity') protected entityValue: User,
45 @Optional() @Inject('entitiesTableConfig') protected entitiesTableConfigValue: EntityTableConfig<User>, 45 @Optional() @Inject('entitiesTableConfig') protected entitiesTableConfigValue: EntityTableConfig<User>,
46 - public fb: FormBuilder) {  
47 - super(store, fb, entityValue, entitiesTableConfigValue); 46 + public fb: FormBuilder,
  47 + protected cd: ChangeDetectorRef) {
  48 + super(store, fb, entityValue, entitiesTableConfigValue, cd);
48 } 49 }
49 50
50 hideDelete() { 51 hideDelete() {
@@ -14,7 +14,7 @@ @@ -14,7 +14,7 @@
14 /// limitations under the License. 14 /// limitations under the License.
15 /// 15 ///
16 16
17 -import { Component, Inject } from '@angular/core'; 17 +import { ChangeDetectorRef, Component, Inject } from '@angular/core';
18 import { Store } from '@ngrx/store'; 18 import { Store } from '@ngrx/store';
19 import { AppState } from '@core/core.state'; 19 import { AppState } from '@core/core.state';
20 import { EntityComponent } from '../../components/entity/entity.component'; 20 import { EntityComponent } from '../../components/entity/entity.component';
@@ -32,8 +32,9 @@ export class WidgetsBundleComponent extends EntityComponent<WidgetsBundle> { @@ -32,8 +32,9 @@ export class WidgetsBundleComponent extends EntityComponent<WidgetsBundle> {
32 constructor(protected store: Store<AppState>, 32 constructor(protected store: Store<AppState>,
33 @Inject('entity') protected entityValue: WidgetsBundle, 33 @Inject('entity') protected entityValue: WidgetsBundle,
34 @Inject('entitiesTableConfig') protected entitiesTableConfigValue: EntityTableConfig<WidgetsBundle>, 34 @Inject('entitiesTableConfig') protected entitiesTableConfigValue: EntityTableConfig<WidgetsBundle>,
35 - public fb: FormBuilder) {  
36 - super(store, fb, entityValue, entitiesTableConfigValue); 35 + public fb: FormBuilder,
  36 + protected cd: ChangeDetectorRef) {
  37 + super(store, fb, entityValue, entitiesTableConfigValue, cd);
37 } 38 }
38 39
39 hideDelete() { 40 hideDelete() {
@@ -27,6 +27,7 @@ export interface AdminSettings<T> { @@ -27,6 +27,7 @@ export interface AdminSettings<T> {
27 export declare type SmtpProtocol = 'smtp' | 'smtps'; 27 export declare type SmtpProtocol = 'smtp' | 'smtps';
28 28
29 export interface MailServerSettings { 29 export interface MailServerSettings {
  30 + showChangePassword: boolean;
30 mailFrom: string; 31 mailFrom: string;
31 smtpProtocol: SmtpProtocol; 32 smtpProtocol: SmtpProtocol;
32 smtpHost: string; 33 smtpHost: string;
@@ -34,7 +35,8 @@ export interface MailServerSettings { @@ -34,7 +35,8 @@ export interface MailServerSettings {
34 timeout: number; 35 timeout: number;
35 enableTls: boolean; 36 enableTls: boolean;
36 username: string; 37 username: string;
37 - password: string; 38 + changePassword?: boolean;
  39 + password?: string;
38 enableProxy: boolean; 40 enableProxy: boolean;
39 proxyHost: string; 41 proxyHost: string;
40 proxyPort: number; 42 proxyPort: number;
@@ -803,7 +803,7 @@ @@ -803,7 +803,7 @@
803 "rulechain-templates": "Regelkettenvorlagen", 803 "rulechain-templates": "Regelkettenvorlagen",
804 "rulechains": "Rand Regelketten", 804 "rulechains": "Rand Regelketten",
805 "search": "Kanten durchsuchen", 805 "search": "Kanten durchsuchen",
806 - "selected-edges": "{Anzahl, Plural, 1 {1 Kante} andere {# Kanten}} ausgewählt", 806 + "selected-edges": "{count, plural, 1 {1 Rand} other {# Rand} } ausgewählt",
807 "any-edge": "Beliebige Kante", 807 "any-edge": "Beliebige Kante",
808 "no-edge-types-matching": "Es wurden keine Kantentypen gefunden, die mit '{{entitySubtype}}' übereinstimmen.", 808 "no-edge-types-matching": "Es wurden keine Kantentypen gefunden, die mit '{{entitySubtype}}' übereinstimmen.",
809 "edge-type-list-empty": "Keine Kantentypen ausgewählt.", 809 "edge-type-list-empty": "Keine Kantentypen ausgewählt.",
@@ -1452,7 +1452,7 @@ @@ -1452,7 +1452,7 @@
1452 "unset-auto-assign-to-edge-text": "Nach der Bestätigung wird die Kantenregelkette bei der Erstellung nicht mehr automatisch den Kanten zugewiesen.", 1452 "unset-auto-assign-to-edge-text": "Nach der Bestätigung wird die Kantenregelkette bei der Erstellung nicht mehr automatisch den Kanten zugewiesen.",
1453 "edge-template-root": "Vorlagenstamm", 1453 "edge-template-root": "Vorlagenstamm",
1454 "search": "Suchen Sie nach Regelketten", 1454 "search": "Suchen Sie nach Regelketten",
1455 - "selected-rulechains": "{count, plural, 1 {1 Regelkette} andere {# Regelketten}} ausgewählt", 1455 + "selected-rulechains": "{count, plural, 1 {1 Regelkette} other {# Regelketten} } ausgewählt",
1456 "open-rulechain": "Regelkette öffnen", 1456 "open-rulechain": "Regelkette öffnen",
1457 "assign-to-edge": "Rand zuweisen", 1457 "assign-to-edge": "Rand zuweisen",
1458 "edge-rulechain": "Kantenregelkette" 1458 "edge-rulechain": "Kantenregelkette"
@@ -104,6 +104,7 @@ @@ -104,6 +104,7 @@
104 "proxy-port-range": "Proxy port should be in a range from 1 to 65535.", 104 "proxy-port-range": "Proxy port should be in a range from 1 to 65535.",
105 "proxy-user": "Proxy user", 105 "proxy-user": "Proxy user",
106 "proxy-password": "Proxy password", 106 "proxy-password": "Proxy password",
  107 + "change-password": "Change password",
107 "send-test-mail": "Send test mail", 108 "send-test-mail": "Send test mail",
108 "sms-provider": "SMS provider", 109 "sms-provider": "SMS provider",
109 "sms-provider-settings": "SMS provider settings", 110 "sms-provider-settings": "SMS provider settings",
@@ -413,8 +413,8 @@ @@ -413,8 +413,8 @@
413 "unassign-asset-from-edge": "Anular activo de bodre", 413 "unassign-asset-from-edge": "Anular activo de bodre",
414 "unassign-asset-from-edge-title": "¿Está seguro de que desea desasignar el activo '{{assetName}}'?", 414 "unassign-asset-from-edge-title": "¿Está seguro de que desea desasignar el activo '{{assetName}}'?",
415 "unassign-asset-from-edge-text": "Después de la confirmación, el activo no será asignado y el borde no podrá acceder a él", 415 "unassign-asset-from-edge-text": "Después de la confirmación, el activo no será asignado y el borde no podrá acceder a él",
416 - "unassign-assets-from-edge-action-title": "Anular asignación {count, plural, 1 {1 activo} other {# activos}} desde el borde",  
417 - "unassign-assets-from-edge-title": "¿Está seguro de que desea desasignar {count, plural, 1 {1 activo} other {# activos}}?", 416 + "unassign-assets-from-edge-action-title": "Anular asignación {count, plural, 1 {1 activo} other {# activos} } desde el borde",
  417 + "unassign-assets-from-edge-title": "¿Está seguro de que desea desasignar {count, plural, 1 {1 activo} other {# activos} }?",
418 "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." 418 "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."
419 }, 419 },
420 "attribute": { 420 "attribute": {
@@ -950,7 +950,7 @@ @@ -950,7 +950,7 @@
950 "assign-device-to-edge-text": "Seleccione los dispositivos para asignar al borde", 950 "assign-device-to-edge-text": "Seleccione los dispositivos para asignar al borde",
951 "unassign-device-from-edge-title": "¿Está seguro de que desea desasignar el dispositivo '{{deviceName}}'?", 951 "unassign-device-from-edge-title": "¿Está seguro de que desea desasignar el dispositivo '{{deviceName}}'?",
952 "unassign-device-from-edge-text": "Después de la confirmación, el dispositivo no será asignado y el borde no podrá acceder a él", 952 "unassign-device-from-edge-text": "Después de la confirmación, el dispositivo no será asignado y el borde no podrá acceder a él",
953 - "unassign-devices-from-edge-title": "¿Está seguro de que desea desasignar {count, plural, 1 {1 dispositivo} other {# dispositivos}}?", 953 + "unassign-devices-from-edge-title": "¿Está seguro de que desea desasignar {count, plural, 1 {1 dispositivo} other {# dispositivos} }?",
954 "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." 954 "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."
955 }, 955 },
956 "device-profile": { 956 "device-profile": {
@@ -1123,7 +1123,7 @@ @@ -1123,7 +1123,7 @@
1123 "delete": "Eliminar borde", 1123 "delete": "Eliminar borde",
1124 "delete-edge-title": "¿Está seguro de que desea eliminar el borde '{{edgeName}}'?", 1124 "delete-edge-title": "¿Está seguro de que desea eliminar el borde '{{edgeName}}'?",
1125 "delete-edge-text": "Tenga cuidado, después de la confirmación, el borde y todos los datos relacionados serán irrecuperables", 1125 "delete-edge-text": "Tenga cuidado, después de la confirmación, el borde y todos los datos relacionados serán irrecuperables",
1126 - "delete-edges-title": "¿Está seguro de que desea edge {count, plural, 1 {1 borde} other {# bordes}}?", 1126 + "delete-edges-title": "¿Está seguro de que desea edge {count, plural, 1 {1 borde} other {# bordes} }?",
1127 "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", 1127 "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",
1128 "name": "Nombre", 1128 "name": "Nombre",
1129 "name-starts-with": "Edge name starts with", 1129 "name-starts-with": "Edge name starts with",
@@ -1156,7 +1156,7 @@ @@ -1156,7 +1156,7 @@
1156 "unassign-from-customer": "Anular asignación del cliente", 1156 "unassign-from-customer": "Anular asignación del cliente",
1157 "unassign-edge-title": "¿Está seguro de que desea desasignar el borde '{{edgeName}}'?", 1157 "unassign-edge-title": "¿Está seguro de que desea desasignar el borde '{{edgeName}}'?",
1158 "unassign-edge-text": "Después de la confirmación, el borde quedará sin asignar y el cliente no podrá acceder a él", 1158 "unassign-edge-text": "Después de la confirmación, el borde quedará sin asignar y el cliente no podrá acceder a él",
1159 - "unassign-edges-title": "¿Está seguro de que desea anular la asignación de {count, plural, 1 {1 borde} other {# bordes}}?", 1159 + "unassign-edges-title": "¿Está seguro de que desea anular la asignación de {count, plural, 1 {1 borde} other {# bordes} }?",
1160 "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.", 1160 "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.",
1161 "make-public": "Hacer público el borde", 1161 "make-public": "Hacer público el borde",
1162 "make-public-edge-title": "¿Estás seguro de que quieres hacer público el edge '{{edgeName}}'?", 1162 "make-public-edge-title": "¿Estás seguro de que quieres hacer público el edge '{{edgeName}}'?",
@@ -1189,14 +1189,14 @@ @@ -1189,14 +1189,14 @@
1189 "rulechain-templates": "Plantillas, de cadena de reglas", 1189 "rulechain-templates": "Plantillas, de cadena de reglas",
1190 "rulechains": "Cadenas de regla de borde", 1190 "rulechains": "Cadenas de regla de borde",
1191 "search": "Bordes de búsqueda", 1191 "search": "Bordes de búsqueda",
1192 - "selected-edges": "{count, plural, 1 {1 borde} other {# bordes}} seleccionados", 1192 + "selected-edges": "{count, plural, 1 {1 borde} other {# bordes} } seleccionadas",
1193 "any-edge": "Cualquier bordee", 1193 "any-edge": "Cualquier bordee",
1194 "no-edge-types-matching": "No se encontraron tipos de aristas que coincidan con '{{entitySubtype}}'.", 1194 "no-edge-types-matching": "No se encontraron tipos de aristas que coincidan con '{{entitySubtype}}'.",
1195 "edge-type-list-empty": "No se seleccionó ningún tipo de borde.", 1195 "edge-type-list-empty": "No se seleccionó ningún tipo de borde.",
1196 "edge-types": "Tipos de bordes", 1196 "edge-types": "Tipos de bordes",
1197 "enter-edge-type": "Ingrese el tipo de borde", 1197 "enter-edge-type": "Ingrese el tipo de borde",
1198 "deployed": "Desplegada", 1198 "deployed": "Desplegada",
1199 - "pending": "Pending", 1199 + "pending": "Pendiente",
1200 "downlinks": "Enlaces descendentes", 1200 "downlinks": "Enlaces descendentes",
1201 "no-downlinks-prompt": "No se encontraron enlaces descendentes", 1201 "no-downlinks-prompt": "No se encontraron enlaces descendentes",
1202 "sync-process-started-successfully": "¡El proceso de sincronización se inició correctamente!", 1202 "sync-process-started-successfully": "¡El proceso de sincronización se inició correctamente!",
@@ -1356,7 +1356,7 @@ @@ -1356,7 +1356,7 @@
1356 "type-api-usage-state": "Estado de uso de la API", 1356 "type-api-usage-state": "Estado de uso de la API",
1357 "type-edge": "Borde", 1357 "type-edge": "Borde",
1358 "type-edges": "Bordes", 1358 "type-edges": "Bordes",
1359 - "list-of-edges": "{cuenta, plural, 1 {Un borde} other {Lista de # bordes}}", 1359 + "list-of-edges": "{count, plural, 1 {Un borde} other {Lista de # bordes} }",
1360 "edge-name-starts-with": "Bordes cuyos nombres comienzan con '{{prefijo}}'" 1360 "edge-name-starts-with": "Bordes cuyos nombres comienzan con '{{prefijo}}'"
1361 }, 1361 },
1362 "entity-field": { 1362 "entity-field": {
@@ -1481,9 +1481,9 @@ @@ -1481,9 +1481,9 @@
1481 "assign-entity-view-to-edge-text": "Seleccione las vistas de entidad para asignar al borde", 1481 "assign-entity-view-to-edge-text": "Seleccione las vistas de entidad para asignar al borde",
1482 "unassign-entity-view-from-edge-title": "¿Está seguro de que desea anular la asignación de la vista de entidad '{{entityViewName}}'?", 1482 "unassign-entity-view-from-edge-title": "¿Está seguro de que desea anular la asignación de la vista de entidad '{{entityViewName}}'?",
1483 "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", 1483 "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",
1484 - "unassign-entity-views-from-edge-action-title": "Anular asignación {recuento, plural, 1 {1 vista de entidad} otras {# vistas de entidad}} del borde", 1484 + "unassign-entity-views-from-edge-action-title": "Anular asignación {count, plural, 1 {1 vista de entidad} other {# vistas de entidad} } del borde",
1485 "unassign-entity-view-from-edge": "Anular asignación de vista de entidad", 1485 "unassign-entity-view-from-edge": "Anular asignación de vista de entidad",
1486 - "unassign-entity-views-from-edge-title": "¿Está seguro de que desea desasignar {count, plural, 1 {1 vista de entidad} other {# vistas de entidad}}?", 1486 + "unassign-entity-views-from-edge-title": "¿Está seguro de que desea desasignar {count, plural, 1 {1 vista de entidad} other {# vistas de entidad} }?",
1487 "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" 1487 "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"
1488 }, 1488 },
1489 "event": { 1489 "event": {
@@ -2074,9 +2074,9 @@ @@ -2074,9 +2074,9 @@
2074 "delete-rulechains": "Eliminar cadenas de reglas", 2074 "delete-rulechains": "Eliminar cadenas de reglas",
2075 "unassign-rulechain": "Anular asignación de cadena de reglas", 2075 "unassign-rulechain": "Anular asignación de cadena de reglas",
2076 "unassign-rulechains": "Anular asignación de cadenas de reglas", 2076 "unassign-rulechains": "Anular asignación de cadenas de reglas",
2077 - "unassign-rulechain-title": "¿Está seguro de que desea desasignar la cadena de reglas '{{ruleChainTitle}}'?", 2077 + "unassign-rulechain-title": "¿Está seguro de que desea desasignar la cadena de reglas '{{ruleChainName}}'?",
2078 "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", 2078 "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",
2079 - "unassign-rulechains-from-edge-action-title": "Anular asignación {count, plural, 1 {1 cadena de reglas} other {# cadenas de reglas}} des bordes", 2079 + "unassign-rulechains-from-edge-action-title": "Anular asignación {count, plural, 1 {1 cadena de reglas} other {# cadenas de reglas} } des bordes",
2080 "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", 2080 "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",
2081 "assign-rulechain-to-edge-title": "Asignar cadena (s) de reglas a borde", 2081 "assign-rulechain-to-edge-title": "Asignar cadena (s) de reglas a borde",
2082 "assign-rulechain-to-edge-text": "Seleccione las cadenas de reglas para asignar al borde", 2082 "assign-rulechain-to-edge-text": "Seleccione las cadenas de reglas para asignar al borde",
@@ -736,7 +736,7 @@ @@ -736,7 +736,7 @@
736 "assign-device-to-edge-text":"Veuillez sélectionner la bordure pour attribuer le ou les dispositifs", 736 "assign-device-to-edge-text":"Veuillez sélectionner la bordure pour attribuer le ou les dispositifs",
737 "unassign-device-from-edge-title": "Êtes-vous sûr de vouloir annuler l'affection du dispositif {{deviceName}} '?", 737 "unassign-device-from-edge-title": "Êtes-vous sûr de vouloir annuler l'affection du dispositif {{deviceName}} '?",
738 "unassign-device-from-edge-text": "Après la confirmation, dispositif sera non attribué et ne sera pas accessible a la bordure.", 738 "unassign-device-from-edge-text": "Après la confirmation, dispositif sera non attribué et ne sera pas accessible a la bordure.",
739 - "unassign-devices-from-edge-title": "Voulez-vous vraiment annuler l'affectation de {count, plural, 1 {1 device} other {# devices}}?", 739 + "unassign-devices-from-edge-title": "Voulez-vous vraiment annuler l'affectation de {count, plural, 1 {1 device} other {# devices} }?",
740 "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." 740 "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."
741 }, 741 },
742 "dialog": { 742 "dialog": {
@@ -755,7 +755,7 @@ @@ -755,7 +755,7 @@
755 "delete": "Supprimer la bordure", 755 "delete": "Supprimer la bordure",
756 "delete-edge-title": "Êtes-vous sûr de vouloir supprimer la bordure '{{edgeName}}'?", 756 "delete-edge-title": "Êtes-vous sûr de vouloir supprimer la bordure '{{edgeName}}'?",
757 "delete-edge-text": "Faites attention, après la confirmation, la bordure et toutes les données associées deviendront irrécupérables", 757 "delete-edge-text": "Faites attention, après la confirmation, la bordure et toutes les données associées deviendront irrécupérables",
758 - "delete-edges-title": "Êtes-vous sûr de vouloir supprimer {count, plural, 1 {1 bordure} other {# bordure}}?", 758 + "delete-edges-title": "Êtes-vous sûr de vouloir supprimer {count, plural, 1 {1 bordure} other {# bordure} }?",
759 "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.", 759 "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.",
760 "name": "Nom", 760 "name": "Nom",
761 "name-starts-with": "Le nom du bord commence par", 761 "name-starts-with": "Le nom du bord commence par",
@@ -788,7 +788,7 @@ @@ -788,7 +788,7 @@
788 "unassign-from-customer": "Retirer du client", 788 "unassign-from-customer": "Retirer du client",
789 "unassign-edge-title": "Êtes-vous sûr de vouloir annuler l'affection du dispositif {{edgeName}}", 789 "unassign-edge-title": "Êtes-vous sûr de vouloir annuler l'affection du dispositif {{edgeName}}",
790 "unassign-edge-text": "Après la confirmation, le dispositif ne sera pas attribué et ne sera pas accessible au client", 790 "unassign-edge-text": "Après la confirmation, le dispositif ne sera pas attribué et ne sera pas accessible au client",
791 - "unassign-edges-title": "Voulez-vous vraiment annuler l'attribution de {count, plural, 1 {1 bordure} other {# bordures}}?", 791 + "unassign-edges-title": "Voulez-vous vraiment annuler l'attribution de {count, plural, 1 {1 bordure} other {# bordures} }?",
792 "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.", 792 "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.",
793 "make-public": "Make edge public", 793 "make-public": "Make edge public",
794 "make-public-edge-title": "Are you sure you want to make the edge '{{edgeName}}' public?", 794 "make-public-edge-title": "Are you sure you want to make the edge '{{edgeName}}' public?",
@@ -821,7 +821,7 @@ @@ -821,7 +821,7 @@
821 "rulechain-templates": "Modèles de chaîne de règles", 821 "rulechain-templates": "Modèles de chaîne de règles",
822 "rulechains": "Chaînes de règles de la bordure", 822 "rulechains": "Chaînes de règles de la bordure",
823 "search": "Rechercher les bords", 823 "search": "Rechercher les bords",
824 - "selected-edges": "{count, plural, 1 {1 edge} other {# bords}} sélectionné", 824 + "selected-edges": "{count, plural, 1 {1 bordure} other {# bords} } sélectionné",
825 "any-edge": "Tout bord", 825 "any-edge": "Tout bord",
826 "no-edge-types-matching": "Aucun type d'arête correspondant à \"{{entitySubtype}}\" n'a été trouvé.", 826 "no-edge-types-matching": "Aucun type d'arête correspondant à \"{{entitySubtype}}\" n'a été trouvé.",
827 "edge-type-list-empty": "Aucun type d'arête sélectionné.", 827 "edge-type-list-empty": "Aucun type d'arête sélectionné.",
@@ -1479,9 +1479,9 @@ @@ -1479,9 +1479,9 @@
1479 "delete-rulechains": "Supprimer une chaînes de règles", 1479 "delete-rulechains": "Supprimer une chaînes de règles",
1480 "unassign-rulechain": "Retirer chaîne de règles", 1480 "unassign-rulechain": "Retirer chaîne de règles",
1481 "unassign-rulechains": "Retirer chaînes de règles", 1481 "unassign-rulechains": "Retirer chaînes de règles",
1482 - "unassign-rulechain-title": "AÊtes-vous sûr de vouloir retirer l'attribution de chaînes de règles '{{ruleChainTitle}}'?", 1482 + "unassign-rulechain-title": "AÊtes-vous sûr de vouloir retirer l'attribution de chaînes de règles '{{ruleChainName}}'?",
1483 "unassign-rulechain-from-edge-text": "Après la confirmation, l'actif sera non attribué et ne sera pas accessible a la bordure.", 1483 "unassign-rulechain-from-edge-text": "Après la confirmation, l'actif sera non attribué et ne sera pas accessible a la bordure.",
1484 - "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", 1484 + "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",
1485 "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.", 1485 "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.",
1486 "assign-rulechain-to-edge-title": "Attribuer les chaînes de règles a la bordure", 1486 "assign-rulechain-to-edge-title": "Attribuer les chaînes de règles a la bordure",
1487 "assign-rulechain-to-edge-text": "Veuillez sélectionner la bordure pour attribuer le ou les chaînes de règles", 1487 "assign-rulechain-to-edge-text": "Veuillez sélectionner la bordure pour attribuer le ou les chaînes de règles",
@@ -1497,11 +1497,11 @@ @@ -1497,11 +1497,11 @@
1497 "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.", 1497 "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.",
1498 "edge-template-root": "Racine du modèle", 1498 "edge-template-root": "Racine du modèle",
1499 "search": "Rechercher des chaînes de règles", 1499 "search": "Rechercher des chaînes de règles",
1500 - "selected-rulechains": "{count, plural, 1 {1 rule chain} other {# rule chains}} sélectionné", 1500 + "selected-rulechains": "{count, plural, 1 {1 rule chain} other {# rule chains} } sélectionné",
1501 "open-rulechain": "Chaîne de règles ouverte", 1501 "open-rulechain": "Chaîne de règles ouverte",
1502 - "assign-to-edge": "Attribuer à Edge",  
1503 - "edge-rulechain": "Chaîne de règles Edge",  
1504 - "unassign-rulechains-from-edge-title": "Voulez-vous vraiment annuler l'attribution de {count, plural, 1 {1 rulechain} other {# rulechains}}?" 1502 + "assign-to-edge": "Attribuer à Bordure",
  1503 + "edge-rulechain": "Chaîne de règles Bordure",
  1504 + "unassign-rulechains-from-edge-title": "Voulez-vous vraiment annuler l'attribution de {count, plural, 1 {1 rulechain} other {# rulechains} }?"
1505 }, 1505 },
1506 "rulenode": { 1506 "rulenode": {
1507 "add": "Ajouter un noeud de règle", 1507 "add": "Ajouter un noeud de règle",