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 290 <scope>test</scope>
291 291 </dependency>
292 292 <dependency>
  293 + <groupId>org.awaitility</groupId>
  294 + <artifactId>awaitility</artifactId>
  295 + <scope>test</scope>
  296 + </dependency>
  297 + <dependency>
293 298 <groupId>org.mockito</groupId>
294 299 <artifactId>mockito-core</artifactId>
295 300 <scope>test</scope>
... ...
... ... @@ -381,11 +381,11 @@ class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcessor {
381 381 }
382 382
383 383 private void reportSessionOpen() {
384   - systemContext.getDeviceStateService().onDeviceConnect(deviceId);
  384 + systemContext.getDeviceStateService().onDeviceConnect(tenantId, deviceId);
385 385 }
386 386
387 387 private void reportSessionClose() {
388   - systemContext.getDeviceStateService().onDeviceDisconnect(deviceId);
  388 + systemContext.getDeviceStateService().onDeviceDisconnect(tenantId, deviceId);
389 389 }
390 390
391 391 private void handleGetAttributesRequest(TbActorCtx context, SessionInfoProto sessionInfo, GetAttributeRequestMsg request) {
... ... @@ -590,7 +590,7 @@ class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcessor {
590 590 if (sessions.size() == 1) {
591 591 reportSessionOpen();
592 592 }
593   - systemContext.getDeviceStateService().onDeviceActivity(deviceId, System.currentTimeMillis());
  593 + systemContext.getDeviceStateService().onDeviceActivity(tenantId, deviceId, System.currentTimeMillis());
594 594 dumpSessions();
595 595 } else if (msg.getEvent() == SessionEvent.CLOSED) {
596 596 log.debug("[{}] Canceling subscriptions for closed session [{}]", deviceId, sessionId);
... ... @@ -620,7 +620,7 @@ class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcessor {
620 620 if (subscriptionInfo.getRpcSubscription()) {
621 621 rpcSubscriptions.putIfAbsent(sessionId, sessionMD.getSessionInfo());
622 622 }
623   - systemContext.getDeviceStateService().onDeviceActivity(deviceId, subscriptionInfo.getLastActivityTime());
  623 + systemContext.getDeviceStateService().onDeviceActivity(tenantId, deviceId, subscriptionInfo.getLastActivityTime());
624 624 dumpSessions();
625 625 }
626 626
... ...
... ... @@ -26,12 +26,12 @@ import org.springframework.web.bind.annotation.ResponseBody;
26 26 import org.springframework.web.bind.annotation.RestController;
27 27 import org.thingsboard.rule.engine.api.MailService;
28 28 import org.thingsboard.rule.engine.api.SmsService;
29   -import org.thingsboard.server.common.data.sms.config.TestSmsRequest;
30 29 import org.thingsboard.server.common.data.AdminSettings;
31 30 import org.thingsboard.server.common.data.UpdateMessage;
32 31 import org.thingsboard.server.common.data.exception.ThingsboardException;
33 32 import org.thingsboard.server.common.data.id.TenantId;
34 33 import org.thingsboard.server.common.data.security.model.SecuritySettings;
  34 +import org.thingsboard.server.common.data.sms.config.TestSmsRequest;
35 35 import org.thingsboard.server.dao.settings.AdminSettingsService;
36 36 import org.thingsboard.server.queue.util.TbCoreComponent;
37 37 import org.thingsboard.server.service.security.permission.Operation;
... ... @@ -67,7 +67,7 @@ public class AdminController extends BaseController {
67 67 accessControlService.checkPermission(getCurrentUser(), Resource.ADMIN_SETTINGS, Operation.READ);
68 68 AdminSettings adminSettings = checkNotNull(adminSettingsService.findAdminSettingsByKey(TenantId.SYS_TENANT_ID, key));
69 69 if (adminSettings.getKey().equals("mail")) {
70   - ((ObjectNode) adminSettings.getJsonValue()).put("password", "");
  70 + ((ObjectNode) adminSettings.getJsonValue()).remove("password");
71 71 }
72 72 return adminSettings;
73 73 } catch (Exception e) {
... ... @@ -84,7 +84,7 @@ public class AdminController extends BaseController {
84 84 adminSettings = checkNotNull(adminSettingsService.saveAdminSettings(TenantId.SYS_TENANT_ID, adminSettings));
85 85 if (adminSettings.getKey().equals("mail")) {
86 86 mailService.updateMailConfiguration();
87   - ((ObjectNode) adminSettings.getJsonValue()).put("password", "");
  87 + ((ObjectNode) adminSettings.getJsonValue()).remove("password");
88 88 } else if (adminSettings.getKey().equals("sms")) {
89 89 smsService.updateSmsConfiguration();
90 90 }
... ... @@ -126,6 +126,10 @@ public class AdminController extends BaseController {
126 126 accessControlService.checkPermission(getCurrentUser(), Resource.ADMIN_SETTINGS, Operation.READ);
127 127 adminSettings = checkNotNull(adminSettings);
128 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 133 String email = getCurrentUser().getEmail();
130 134 mailService.sendTestMail(adminSettings.getJsonValue(), email);
131 135 }
... ...
... ... @@ -215,6 +215,7 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService {
215 215 node.put("password", "");
216 216 node.put("tlsVersion", "TLSv1.2");//NOSONAR, key used to identify password field (not password value itself)
217 217 node.put("enableProxy", false);
  218 + node.put("showChangePassword", false);
218 219 mailSettings.setJsonValue(node);
219 220 adminSettingsService.saveAdminSettings(TenantId.SYS_TENANT_ID, mailSettings);
220 221 }
... ...
... ... @@ -17,6 +17,7 @@ package org.thingsboard.server.service.security.auth.oauth2;
17 17
18 18 import com.fasterxml.jackson.core.JsonProcessingException;
19 19 import com.fasterxml.jackson.databind.ObjectMapper;
  20 +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
20 21 import lombok.extern.slf4j.Slf4j;
21 22 import org.springframework.boot.web.client.RestTemplateBuilder;
22 23 import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
... ... @@ -29,6 +30,7 @@ import org.thingsboard.server.common.data.oauth2.OAuth2Registration;
29 30 import org.thingsboard.server.dao.oauth2.OAuth2User;
30 31 import org.thingsboard.server.service.security.model.SecurityUser;
31 32
  33 +import javax.annotation.PostConstruct;
32 34 import javax.servlet.http.HttpServletRequest;
33 35
34 36 @Service(value = "customOAuth2ClientMapper")
... ... @@ -40,6 +42,15 @@ public class CustomOAuth2ClientMapper extends AbstractOAuth2ClientMapper impleme
40 42
41 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 54 @Override
44 55 public SecurityUser getOrCreateUserByClientPrincipal(HttpServletRequest request, OAuth2AuthenticationToken token, String providerAccessToken, OAuth2Registration registration) {
45 56 OAuth2MapperConfig config = registration.getMapperConfig();
... ...
... ... @@ -15,6 +15,7 @@
15 15 */
16 16 package org.thingsboard.server.service.security.auth.oauth2;
17 17
  18 +import lombok.extern.slf4j.Slf4j;
18 19 import org.springframework.beans.factory.annotation.Autowired;
19 20 import org.springframework.security.core.Authentication;
20 21 import org.springframework.security.oauth2.client.OAuth2AuthorizedClient;
... ... @@ -42,6 +43,7 @@ import java.net.URLEncoder;
42 43 import java.nio.charset.StandardCharsets;
43 44 import java.util.UUID;
44 45
  46 +@Slf4j
45 47 @Component(value = "oauth2AuthenticationSuccessHandler")
46 48 public class Oauth2AuthenticationSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {
47 49
... ... @@ -99,6 +101,8 @@ public class Oauth2AuthenticationSuccessHandler extends SimpleUrlAuthenticationS
99 101 clearAuthenticationAttributes(request, response);
100 102 getRedirectStrategy().sendRedirect(request, response, baseUrl + "/?accessToken=" + accessToken.getToken() + "&refreshToken=" + refreshToken.getToken());
101 103 } catch (Exception e) {
  104 + log.debug("Error occurred during processing authentication success result. " +
  105 + "request [{}], response [{}], authentication [{}]", request, response, authentication, e);
102 106 clearAuthenticationAttributes(request, response);
103 107 String errorPrefix;
104 108 if (!StringUtils.isEmpty(callbackUrlScheme)) {
... ...
... ... @@ -137,7 +137,6 @@ public class DefaultDeviceStateService extends TbApplicationEventListener<Partit
137 137 private ExecutorService deviceStateExecutor;
138 138 private final ConcurrentMap<TopicPartitionInfo, Set<DeviceId>> partitionedDevices = new ConcurrentHashMap<>();
139 139 final ConcurrentMap<DeviceId, DeviceStateData> deviceStates = new ConcurrentHashMap<>();
140   - private final ConcurrentMap<DeviceId, Long> deviceLastSavedActivity = new ConcurrentHashMap<>();
141 140
142 141 final Queue<Set<TopicPartitionInfo>> subscribeQueue = new ConcurrentLinkedQueue<>();
143 142
... ... @@ -192,7 +191,7 @@ public class DefaultDeviceStateService extends TbApplicationEventListener<Partit
192 191 }
193 192
194 193 @Override
195   - public void onDeviceConnect(DeviceId deviceId) {
  194 + public void onDeviceConnect(TenantId tenantId, DeviceId deviceId) {
196 195 log.trace("on Device Connect [{}]", deviceId.getId());
197 196 DeviceStateData stateData = getOrFetchDeviceStateData(deviceId);
198 197 long ts = System.currentTimeMillis();
... ... @@ -200,23 +199,23 @@ public class DefaultDeviceStateService extends TbApplicationEventListener<Partit
200 199 save(deviceId, LAST_CONNECT_TIME, ts);
201 200 pushRuleEngineMessage(stateData, CONNECT_EVENT);
202 201 checkAndUpdateState(deviceId, stateData);
  202 + cleanDeviceStateIfBelongsExternalPartition(tenantId, deviceId);
203 203 }
204 204
205 205 @Override
206   - public void onDeviceActivity(DeviceId deviceId, long lastReportedActivity) {
  206 + public void onDeviceActivity(TenantId tenantId, DeviceId deviceId, long lastReportedActivity) {
207 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 210 updateActivityState(deviceId, stateData, lastReportedActivity);
212 211 }
  212 + cleanDeviceStateIfBelongsExternalPartition(tenantId, deviceId);
213 213 }
214 214
215 215 void updateActivityState(DeviceId deviceId, DeviceStateData stateData, long lastReportedActivity) {
216 216 log.trace("updateActivityState - fetched state {} for device {}, lastReportedActivity {}", stateData, deviceId, lastReportedActivity);
217 217 if (stateData != null) {
218 218 save(deviceId, LAST_ACTIVITY_TIME, lastReportedActivity);
219   - deviceLastSavedActivity.put(deviceId, lastReportedActivity);
220 219 DeviceState state = stateData.getState();
221 220 state.setLastActivityTime(lastReportedActivity);
222 221 if (!state.isActive()) {
... ... @@ -225,21 +224,23 @@ public class DefaultDeviceStateService extends TbApplicationEventListener<Partit
225 224 pushRuleEngineMessage(stateData, ACTIVITY_EVENT);
226 225 }
227 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 232 @Override
233   - public void onDeviceDisconnect(DeviceId deviceId) {
  233 + public void onDeviceDisconnect(TenantId tenantId, DeviceId deviceId) {
234 234 DeviceStateData stateData = getOrFetchDeviceStateData(deviceId);
235 235 long ts = System.currentTimeMillis();
236 236 stateData.getState().setLastDisconnectTime(ts);
237 237 save(deviceId, LAST_DISCONNECT_TIME, ts);
238 238 pushRuleEngineMessage(stateData, DISCONNECT_EVENT);
  239 + cleanDeviceStateIfBelongsExternalPartition(tenantId, deviceId);
239 240 }
240 241
241 242 @Override
242   - public void onDeviceInactivityTimeoutUpdate(DeviceId deviceId, long inactivityTimeout) {
  243 + public void onDeviceInactivityTimeoutUpdate(TenantId tenantId, DeviceId deviceId, long inactivityTimeout) {
243 244 if (inactivityTimeout <= 0L) {
244 245 return;
245 246 }
... ... @@ -247,6 +248,7 @@ public class DefaultDeviceStateService extends TbApplicationEventListener<Partit
247 248 DeviceStateData stateData = getOrFetchDeviceStateData(deviceId);
248 249 stateData.getState().setInactivityTimeout(inactivityTimeout);
249 250 checkAndUpdateState(deviceId, stateData);
  251 + cleanDeviceStateIfBelongsExternalPartition(tenantId, deviceId);
250 252 }
251 253
252 254 @Override
... ... @@ -283,12 +285,10 @@ public class DefaultDeviceStateService extends TbApplicationEventListener<Partit
283 285 }, deviceStateExecutor);
284 286 } else if (proto.getUpdated()) {
285 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 293 } else {
294 294 //Device was probably deleted while message was in queue;
... ... @@ -356,10 +356,7 @@ public class DefaultDeviceStateService extends TbApplicationEventListener<Partit
356 356 // We no longer manage current partition of devices;
357 357 removedPartitions.forEach(partition -> {
358 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 362 addedPartitions.forEach(tpi -> partitionedDevices.computeIfAbsent(tpi, key -> ConcurrentHashMap.newKeySet()));
... ... @@ -463,11 +460,12 @@ public class DefaultDeviceStateService extends TbApplicationEventListener<Partit
463 460
464 461 void updateInactivityStateIfExpired() {
465 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 471 void updateInactivityStateIfExpired(long ts, DeviceId deviceId) {
... ... @@ -488,8 +486,7 @@ public class DefaultDeviceStateService extends TbApplicationEventListener<Partit
488 486 }
489 487 } else {
490 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 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 531 private void sendDeviceEvent(TenantId tenantId, DeviceId deviceId, boolean added, boolean updated, boolean deleted) {
526 532 TransportProtos.DeviceStateServiceMsgProto.Builder builder = TransportProtos.DeviceStateServiceMsgProto.newBuilder();
527 533 builder.setTenantIdMSB(tenantId.getId().getMostSignificantBits());
... ... @@ -536,13 +542,16 @@ public class DefaultDeviceStateService extends TbApplicationEventListener<Partit
536 542 }
537 543
538 544 private void onDeviceDeleted(TenantId tenantId, DeviceId deviceId) {
539   - deviceStates.remove(deviceId);
540   - deviceLastSavedActivity.remove(deviceId);
  545 + cleanUpDeviceStateMap(deviceId);
541 546 TopicPartitionInfo tpi = partitionService.resolve(ServiceType.TB_CORE, tenantId, deviceId);
542 547 Set<DeviceId> deviceIdSet = partitionedDevices.get(tpi);
543 548 deviceIdSet.remove(deviceId);
544 549 }
545 550
  551 + private void cleanUpDeviceStateMap(DeviceId deviceId) {
  552 + deviceStates.remove(deviceId);
  553 + }
  554 +
546 555 private ListenableFuture<DeviceStateData> fetchDeviceState(Device device) {
547 556 ListenableFuture<DeviceStateData> future;
548 557 if (persistToTelemetry) {
... ...
... ... @@ -18,6 +18,7 @@ package org.thingsboard.server.service.state;
18 18 import org.springframework.context.ApplicationListener;
19 19 import org.thingsboard.server.common.data.Device;
20 20 import org.thingsboard.server.common.data.id.DeviceId;
  21 +import org.thingsboard.server.common.data.id.TenantId;
21 22 import org.thingsboard.server.queue.discovery.event.PartitionChangeEvent;
22 23 import org.thingsboard.server.gen.transport.TransportProtos;
23 24 import org.thingsboard.server.common.msg.queue.TbCallback;
... ... @@ -33,13 +34,13 @@ public interface DeviceStateService extends ApplicationListener<PartitionChangeE
33 34
34 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 45 void onQueueMsg(TransportProtos.DeviceStateServiceMsgProto proto, TbCallback bytes);
45 46
... ...
... ... @@ -224,7 +224,7 @@ public class DefaultSubscriptionManagerService extends TbApplicationEventListene
224 224 return subscriptionUpdate;
225 225 });
226 226 if (entityId.getEntityType() == EntityType.DEVICE) {
227   - updateDeviceInactivityTimeout(entityId, ts);
  227 + updateDeviceInactivityTimeout(tenantId, entityId, ts);
228 228 }
229 229 callback.onSuccess();
230 230 }
... ... @@ -259,7 +259,7 @@ public class DefaultSubscriptionManagerService extends TbApplicationEventListene
259 259 });
260 260 if (entityId.getEntityType() == EntityType.DEVICE) {
261 261 if (TbAttributeSubscriptionScope.SERVER_SCOPE.name().equalsIgnoreCase(scope)) {
262   - updateDeviceInactivityTimeout(entityId, attributes);
  262 + updateDeviceInactivityTimeout(tenantId, entityId, attributes);
263 263 } else if (TbAttributeSubscriptionScope.SHARED_SCOPE.name().equalsIgnoreCase(scope) && notifyDevice) {
264 264 clusterService.pushMsgToCore(DeviceAttributesEventNotificationMsg.onUpdate(tenantId,
265 265 new DeviceId(entityId.getId()), DataConstants.SHARED_SCOPE, new ArrayList<>(attributes))
... ... @@ -269,10 +269,10 @@ public class DefaultSubscriptionManagerService extends TbApplicationEventListene
269 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 273 for (KvEntry kvEntry : kvEntries) {
274 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 588 bind_address: "${MQTT_BIND_ADDRESS:0.0.0.0}"
589 589 bind_port: "${MQTT_BIND_PORT:1883}"
590 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 592 netty:
592 593 leak_detector_level: "${NETTY_LEAK_DETECTOR_LVL:DISABLED}"
593 594 boss_group_thread_count: "${NETTY_BOSS_GROUP_THREADS:1}"
... ...
... ... @@ -27,6 +27,7 @@ import org.springframework.mock.web.MockMultipartFile;
27 27 import org.springframework.test.web.servlet.request.MockMultipartHttpServletRequestBuilder;
28 28 import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
29 29 import org.thingsboard.common.util.JacksonUtil;
  30 +import org.thingsboard.common.util.ThingsBoardThreadFactory;
30 31 import org.thingsboard.server.common.data.Device;
31 32 import org.thingsboard.server.common.data.DeviceProfile;
32 33 import org.thingsboard.server.common.data.DeviceProfileProvisionType;
... ... @@ -261,7 +262,7 @@ public class AbstractLwM2MIntegrationTest extends AbstractWebsocketTest {
261 262
262 263 @Before
263 264 public void beforeTest() throws Exception {
264   - executor = Executors.newScheduledThreadPool(10);
  265 + executor = Executors.newScheduledThreadPool(10, ThingsBoardThreadFactory.forName("test-lwm2m-scheduled"));
265 266 loginTenantAdmin();
266 267
267 268 String[] resources = new String[]{"1.xml", "2.xml", "3.xml", "5.xml", "9.xml"};
... ...
... ... @@ -16,6 +16,8 @@
16 16 package org.thingsboard.server.transport.lwm2m;
17 17
18 18 import com.fasterxml.jackson.core.type.TypeReference;
  19 +import lombok.extern.slf4j.Slf4j;
  20 +import org.junit.After;
19 21 import org.junit.Assert;
20 22 import org.junit.Test;
21 23 import org.thingsboard.server.common.data.Device;
... ... @@ -23,16 +25,18 @@ import org.thingsboard.server.common.data.device.credentials.lwm2m.NoSecClientCr
23 25 import org.thingsboard.server.common.data.kv.KvEntry;
24 26 import org.thingsboard.server.common.data.kv.TsKvEntry;
25 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 28 import org.thingsboard.server.transport.lwm2m.client.LwM2MTestClient;
29 29
30 30 import java.util.Arrays;
31 31 import java.util.Collections;
32 32 import java.util.Comparator;
33 33 import java.util.List;
  34 +import java.util.UUID;
  35 +import java.util.concurrent.TimeUnit;
34 36 import java.util.stream.Collectors;
35 37
  38 +import static org.awaitility.Awaitility.await;
  39 +import static org.hamcrest.Matchers.is;
36 40 import static org.thingsboard.rest.client.utils.RestJsonConverter.toTimeseries;
37 41 import static org.thingsboard.server.common.data.ota.OtaPackageUpdateStatus.DOWNLOADED;
38 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 47 import static org.thingsboard.server.common.data.ota.OtaPackageUpdateStatus.UPDATING;
44 48 import static org.thingsboard.server.common.data.ota.OtaPackageUpdateStatus.VERIFIED;
45 49
  50 +@Slf4j
46 51 public class NoSecLwM2MIntegrationTest extends AbstractLwM2MIntegrationTest {
47 52
  53 + public static final int TIMEOUT = 30;
48 54 private final String OTA_TRANSPORT_CONFIGURATION = "{\n" +
49 55 " \"observeAttr\": {\n" +
50 56 " \"keyName\": {\n" +
... ... @@ -122,6 +128,15 @@ public class NoSecLwM2MIntegrationTest extends AbstractLwM2MIntegrationTest {
122 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 140 @Test
126 141 public void testConnectAndObserveTelemetry() throws Exception {
127 142 NoSecClientCredentials clientCredentials = new NoSecClientCredentials();
... ... @@ -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 219 @Test
200 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 77
78 78 @Override
79 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 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 110 private void createTopicIfNotExist(TopicPartitionInfo tpi) {
... ...
... ... @@ -19,6 +19,9 @@ import lombok.RequiredArgsConstructor;
19 19 import lombok.extern.slf4j.Slf4j;
20 20 import org.eclipse.californium.elements.util.SslContextUtil;
21 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 25 import org.eclipse.leshan.server.bootstrap.BootstrapSessionManager;
23 26 import org.eclipse.leshan.server.californium.bootstrap.LeshanBootstrapServer;
24 27 import org.eclipse.leshan.server.californium.bootstrap.LeshanBootstrapServerBuilder;
... ... @@ -26,6 +29,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
26 29 import org.springframework.stereotype.Component;
27 30 import org.thingsboard.server.transport.lwm2m.bootstrap.secure.LwM2MBootstrapSecurityStore;
28 31 import org.thingsboard.server.transport.lwm2m.bootstrap.secure.LwM2MInMemoryBootstrapConfigStore;
  32 +import org.thingsboard.server.transport.lwm2m.bootstrap.secure.LwM2MInMemoryBootstrapConfigurationAdapter;
29 33 import org.thingsboard.server.transport.lwm2m.bootstrap.secure.LwM2mDefaultBootstrapSessionManager;
30 34 import org.thingsboard.server.transport.lwm2m.config.LwM2MTransportBootstrapConfig;
31 35 import org.thingsboard.server.transport.lwm2m.config.LwM2MTransportServerConfig;
... ... @@ -38,6 +42,7 @@ import java.security.KeyStoreException;
38 42 import java.security.PrivateKey;
39 43 import java.security.PublicKey;
40 44 import java.security.cert.X509Certificate;
  45 +import java.util.List;
41 46
42 47 import static org.thingsboard.server.transport.lwm2m.server.LwM2mNetworkConfig.getCoapConfig;
43 48
... ... @@ -79,12 +84,14 @@ public class LwM2MTransportBootstrapService {
79 84 builder.setCoapConfig(getCoapConfig(bootstrapConfig.getPort(), bootstrapConfig.getSecurePort(), serverConfig));
80 85
81 86 /* Define model provider (Create Models )*/
  87 + List<ObjectModel> models = ObjectLoader.loadDefault();
  88 + builder.setModel(new StaticModel(models));
82 89
83 90 /* Create credentials */
84 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 96 /* SecurityStore */
90 97 builder.setSecurityStore(lwM2MBootstrapSecurityStore);
... ...
... ... @@ -74,15 +74,19 @@ public class LwM2MBootstrapConfig implements Serializable {
74 74 configBs.servers.put(0, server0);
75 75 /* Security Configuration (object 0) as defined in LWM2M 1.0.x TS. Bootstrap instance = 0 */
76 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 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 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 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 90 serverSecurity.bootstrapServer = bootstrapServer;
87 91 serverSecurity.securityMode = securityMode;
88 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 31
32 32 String host = "0.0.0.0";
33 33 Integer port = 0;
  34 + String securityHost = "0.0.0.0";
  35 + Integer securityPort = 0;
34 36
35 37 SecurityMode securityMode = SecurityMode.NO_SEC;
36 38
37 39 Integer serverId = 123;
38 40 boolean bootstrapServerIs = false;
39 41
40   - public LwM2MServerBootstrap(){};
  42 + public LwM2MServerBootstrap() {
  43 + }
  44 +
  45 + ;
41 46
42 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 93 log.debug("Fetched clients from store: {}", fetchedClients);
94 94 fetchedClients.forEach(client -> {
95 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 166 this.lwM2mClientsByRegistrationId.put(registration.getId(), client);
162 167 client.setState(LwM2MClientState.REGISTERED);
163 168 onUplink(client);
164   - if(!compareAndSetSleepFlag(client, false)){
  169 + if (!compareAndSetSleepFlag(client, false)) {
165 170 clientStore.put(client);
166 171 }
167 172 } finally {
... ... @@ -311,7 +316,11 @@ public class LwM2mClientContextImpl implements LwM2mClientContext {
311 316 public void update(LwM2mClient client) {
312 317 client.lock();
313 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 324 } finally {
316 325 client.unlock();
317 326 }
... ...
... ... @@ -106,6 +106,7 @@ public abstract class LwM2MClientOtaInfo<Strategy, State, Result> {
106 106
107 107 public abstract OtaPackageType getType();
108 108
  109 + @JsonIgnore
109 110 public String getTargetPackageId() {
110 111 return getPackageId(targetName, targetVersion);
111 112 }
... ...
... ... @@ -15,11 +15,13 @@
15 15 */
16 16 package org.thingsboard.server.transport.lwm2m.server.store;
17 17
  18 +import lombok.extern.slf4j.Slf4j;
18 19 import org.nustaq.serialization.FSTConfiguration;
19 20 import org.springframework.data.redis.connection.RedisClusterConnection;
20 21 import org.springframework.data.redis.connection.RedisConnectionFactory;
21 22 import org.springframework.data.redis.core.Cursor;
22 23 import org.springframework.data.redis.core.ScanOptions;
  24 +import org.thingsboard.server.transport.lwm2m.server.client.LwM2MClientState;
23 25 import org.thingsboard.server.transport.lwm2m.server.client.LwM2mClient;
24 26
25 27 import java.util.ArrayList;
... ... @@ -27,6 +29,7 @@ import java.util.HashSet;
27 29 import java.util.List;
28 30 import java.util.Set;
29 31
  32 +@Slf4j
30 33 public class TbRedisLwM2MClientStore implements TbLwM2MClientStore {
31 34
32 35 private static final String CLIENT_EP = "CLIENT#EP#";
... ... @@ -76,9 +79,13 @@ public class TbRedisLwM2MClientStore implements TbLwM2MClientStore {
76 79
77 80 @Override
78 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 89 <scope>test</scope>
90 90 </dependency>
91 91 <dependency>
  92 + <groupId>org.awaitility</groupId>
  93 + <artifactId>awaitility</artifactId>
  94 + <scope>test</scope>
  95 + </dependency>
  96 + <dependency>
92 97 <groupId>org.mockito</groupId>
93 98 <artifactId>mockito-core</artifactId>
94 99 <scope>test</scope>
... ...
... ... @@ -23,10 +23,15 @@ import org.springframework.beans.factory.annotation.Autowired;
23 23 import org.springframework.beans.factory.annotation.Value;
24 24 import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
25 25 import org.springframework.stereotype.Component;
  26 +import org.thingsboard.common.util.ThingsBoardExecutors;
26 27 import org.thingsboard.server.common.transport.TransportContext;
27 28 import org.thingsboard.server.transport.mqtt.adaptors.JsonMqttAdaptor;
28 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 36 * Created by ashvayka on 04.10.18.
32 37 */
... ... @@ -59,4 +64,8 @@ public class MqttTransportContext extends TransportContext {
59 64 @Setter
60 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 123 private final SslHandler sslHandler;
124 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 130 private final ConcurrentHashMap<String, String> otaPackSessions;
131 131 private final ConcurrentHashMap<String, Integer> chunkSizes;
... ... @@ -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 169 if (msg.fixedHeader() == null) {
170 170 log.info("[{}:{}] Invalid message received", address.getHostName(), address.getPort());
171 171 processDisconnect(ctx);
... ... @@ -177,10 +177,14 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement
177 177 } else if (deviceSessionCtx.isProvisionOnly()) {
178 178 processProvisionSessionMsg(ctx, msg);
179 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 188 private void processProvisionSessionMsg(ChannelHandlerContext ctx, MqttMessage msg) {
185 189 switch (msg.fixedHeader().messageType()) {
186 190 case PUBLISH:
... ... @@ -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 266 switch (msg.fixedHeader().messageType()) {
228 267 case PUBLISH:
229 268 processPublish(ctx, (MqttPublishMessage) msg);
... ... @@ -304,6 +343,7 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement
304 343 }
305 344 } catch (RuntimeException | AdaptorException e) {
306 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 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 632 log.info("[{}] Processing connect msg for client: {}!", sessionId, msg.payload().clientIdentifier());
593 633 String userName = msg.payload().userName();
594 634 String clientId = msg.payload().clientIdentifier();
... ... @@ -674,7 +714,7 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement
674 714 return null;
675 715 }
676 716
677   - private void processDisconnect(ChannelHandlerContext ctx) {
  717 + void processDisconnect(ChannelHandlerContext ctx) {
678 718 ctx.close();
679 719 log.info("[{}] Client disconnected!", sessionId);
680 720 doDisconnect();
... ... @@ -761,6 +801,11 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement
761 801 }
762 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 823 SessionMetaData sessionMetaData = transportService.registerAsyncSession(deviceSessionCtx.getSessionInfo(), MqttTransportHandler.this);
779 824 checkGatewaySession(sessionMetaData);
780 825 ctx.writeAndFlush(createMqttConnAckMsg(CONNECTION_ACCEPTED, connectMessage));
  826 + deviceSessionCtx.setConnected(true);
781 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 831 @Override
... ...
... ... @@ -18,6 +18,7 @@ package org.thingsboard.server.transport.mqtt.session;
18 18 import com.google.protobuf.Descriptors;
19 19 import com.google.protobuf.DynamicMessage;
20 20 import io.netty.channel.ChannelHandlerContext;
  21 +import io.netty.handler.codec.mqtt.MqttMessage;
21 22 import lombok.Getter;
22 23 import lombok.Setter;
23 24 import lombok.extern.slf4j.Slf4j;
... ... @@ -35,8 +36,11 @@ import org.thingsboard.server.transport.mqtt.util.MqttTopicFilter;
35 36 import org.thingsboard.server.transport.mqtt.util.MqttTopicFilterFactory;
36 37
37 38 import java.util.UUID;
  39 +import java.util.concurrent.ConcurrentLinkedQueue;
38 40 import java.util.concurrent.ConcurrentMap;
39 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 46 * @author Andrew Shvayka
... ... @@ -45,14 +49,24 @@ import java.util.concurrent.atomic.AtomicInteger;
45 49 public class DeviceSessionCtx extends MqttDeviceAwareSessionContext {
46 50
47 51 @Getter
  52 + @Setter
48 53 private ChannelHandlerContext channel;
49 54
50 55 @Getter
51   - private MqttTransportContext context;
  56 + private final MqttTransportContext context;
52 57
53 58 private final AtomicInteger msgIdSeq = new AtomicInteger(0);
54 59
55 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 70 @Setter
57 71 private boolean provisionOnly = false;
58 72
... ... @@ -73,10 +87,6 @@ public class DeviceSessionCtx extends MqttDeviceAwareSessionContext {
73 87 this.context = context;
74 88 }
75 89
76   - public void setChannel(ChannelHandlerContext channel) {
77   - this.channel = channel;
78   - }
79   -
80 90 public int nextMsgId() {
81 91 return msgIdSeq.incrementAndGet();
82 92 }
... ...
... ... @@ -60,6 +60,7 @@ public class GatewayDeviceSessionCtx extends MqttDeviceAwareSessionContext imple
60 60 .setDeviceProfileIdLSB(deviceInfo.getDeviceProfileId().getId().getLeastSignificantBits())
61 61 .build());
62 62 setDeviceInfo(deviceInfo);
  63 + setConnected(true);
63 64 setDeviceProfile(deviceProfile);
64 65 this.transportService = transportService;
65 66 }
... ...
... ... @@ -34,6 +34,7 @@ import io.netty.handler.codec.mqtt.MqttMessage;
34 34 import io.netty.handler.codec.mqtt.MqttPublishMessage;
35 35 import lombok.extern.slf4j.Slf4j;
36 36 import org.springframework.util.CollectionUtils;
  37 +import org.springframework.util.ConcurrentReferenceHashMap;
37 38 import org.springframework.util.StringUtils;
38 39 import org.thingsboard.server.common.data.id.DeviceId;
39 40 import org.thingsboard.server.common.transport.TransportService;
... ... @@ -66,6 +67,8 @@ import java.util.concurrent.ConcurrentMap;
66 67 import java.util.concurrent.locks.Lock;
67 68 import java.util.concurrent.locks.ReentrantLock;
68 69
  70 +import static org.springframework.util.ConcurrentReferenceHashMap.ReferenceType;
  71 +
69 72 /**
70 73 * Created by ashvayka on 19.01.17.
71 74 */
... ... @@ -82,7 +85,7 @@ public class GatewaySessionHandler {
82 85 private final UUID sessionId;
83 86 private final ConcurrentMap<String, Lock> deviceCreationLockMap;
84 87 private final ConcurrentMap<String, GatewayDeviceSessionCtx> devices;
85   - private final ConcurrentMap<String, SettableFuture<GatewayDeviceSessionCtx>> deviceFutures;
  88 + private final ConcurrentMap<String, ListenableFuture<GatewayDeviceSessionCtx>> deviceFutures;
86 89 private final ConcurrentMap<MqttTopicMatcher, Integer> mqttQoSMap;
87 90 private final ChannelHandlerContext channel;
88 91 private final DeviceSessionCtx deviceSessionCtx;
... ... @@ -95,11 +98,15 @@ public class GatewaySessionHandler {
95 98 this.sessionId = sessionId;
96 99 this.devices = new ConcurrentHashMap<>();
97 100 this.deviceFutures = new ConcurrentHashMap<>();
98   - this.deviceCreationLockMap = new ConcurrentHashMap<>();
  101 + this.deviceCreationLockMap = createWeakMap();
99 102 this.mqttQoSMap = deviceSessionCtx.getMqttQoSMap();
100 103 this.channel = deviceSessionCtx.getChannel();
101 104 }
102 105
  106 + ConcurrentReferenceHashMap<String, Lock> createWeakMap() {
  107 + return new ConcurrentReferenceHashMap<>(16, ReferenceType.WEAK);
  108 + }
  109 +
103 110 public void onDeviceConnect(MqttPublishMessage mqttMsg) throws AdaptorException {
104 111 if (isJsonPayloadType()) {
105 112 onDeviceConnectJson(mqttMsg);
... ... @@ -228,21 +235,22 @@ public class GatewaySessionHandler {
228 235 if (result == null) {
229 236 return getDeviceCreationFuture(deviceName, deviceType);
230 237 } else {
231   - return toCompletedFuture(result);
  238 + return Futures.immediateFuture(result);
232 239 }
233 240 } finally {
234 241 deviceCreationLock.unlock();
235 242 }
236 243 } else {
237   - return toCompletedFuture(result);
  244 + return Futures.immediateFuture(result);
238 245 }
239 246 }
240 247
241 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 254 try {
247 255 transportService.process(GetOrCreateDeviceFromGatewayRequestMsg.newBuilder()
248 256 .setDeviceName(deviceName)
... ... @@ -282,15 +290,6 @@ public class GatewaySessionHandler {
282 290 deviceFutures.remove(deviceName);
283 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 295 private int getMsgId(MqttPublishMessage mqttMsg) {
... ... @@ -353,6 +352,7 @@ public class GatewaySessionHandler {
353 352 processPostTelemetryMsg(deviceCtx, postTelemetryMsg, deviceName, msgId);
354 353 } catch (Throwable e) {
355 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 384 processPostTelemetryMsg(deviceCtx, postTelemetryMsg, deviceName, msgId);
385 385 } catch (Throwable e) {
386 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 +}
\ No newline at end of file
... ...
  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 +}
\ No newline at end of file
... ...
... ... @@ -188,6 +188,7 @@ public class SnmpTransportContext extends TransportContext {
188 188
189 189 deviceSessionContext.setSessionInfo(sessionInfo);
190 190 deviceSessionContext.setDeviceInfo(msg.getDeviceInfo());
  191 + deviceSessionContext.setConnected(true);
191 192 } else {
192 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 56 import org.thingsboard.server.gen.transport.TransportProtos.ValidateDeviceTokenRequestMsg;
57 57 import org.thingsboard.server.gen.transport.TransportProtos.ValidateDeviceX509CertRequestMsg;
58 58
  59 +import java.util.concurrent.ExecutorService;
  60 +
59 61 /**
60 62 * Created by ashvayka on 04.10.18.
61 63 */
... ... @@ -131,4 +133,6 @@ public interface TransportService {
131 133 void log(SessionInfoProto sessionInfo, String msg);
132 134
133 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 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 46 @Setter
47 47 private volatile TransportProtos.SessionInfoProto sessionInfo;
48 48
  49 + @Setter
49 50 private volatile boolean connected;
50 51
51 52 public DeviceId getDeviceId() {
... ... @@ -54,7 +55,6 @@ public abstract class DeviceAwareSessionContext implements SessionContext {
54 55
55 56 public void setDeviceInfo(TransportDeviceInfo deviceInfo) {
56 57 this.deviceInfo = deviceInfo;
57   - this.connected = true;
58 58 this.deviceId = deviceInfo.getDeviceId();
59 59 }
60 60
... ...
... ... @@ -52,7 +52,7 @@ public class AdminSettingsServiceImpl implements AdminSettingsService {
52 52 public AdminSettings saveAdminSettings(TenantId tenantId, AdminSettings adminSettings) {
53 53 log.trace("Executing saveAdminSettings [{}]", adminSettings);
54 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 56 AdminSettings mailSettings = findAdminSettingsByKey(tenantId, "mail");
57 57 if (mailSettings != null) {
58 58 ((ObjectNode) adminSettings.getJsonValue()).put("password", mailSettings.getJsonValue().get("password").asText());
... ... @@ -61,7 +61,7 @@ public class AdminSettingsServiceImpl implements AdminSettingsService {
61 61
62 62 return adminSettingsDao.save(tenantId, adminSettings);
63 63 }
64   -
  64 +
65 65 private DataValidator<AdminSettings> adminSettingsValidator =
66 66 new DataValidator<AdminSettings>() {
67 67
... ...
... ... @@ -100,7 +100,7 @@ public abstract class AbstractChunkedAggregationTimeseriesDao extends AbstractSq
100 100 }
101 101
102 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 104 return Futures.immediateFuture(null);
105 105 }
106 106
... ...
... ... @@ -124,7 +124,7 @@ public class TimescaleTimeseriesDao extends AbstractSqlTimeseriesDao implements
124 124 }
125 125
126 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 128 return Futures.immediateFuture(0);
129 129 }
130 130
... ...
... ... @@ -170,7 +170,7 @@ public class BaseTimeseriesService implements TimeseriesService {
170 170 if (entityId.getEntityType().equals(EntityType.ENTITY_VIEW)) {
171 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 174 futures.add(Futures.transform(timeseriesLatestDao.saveLatest(tenantId, entityId, tsKvEntry), v -> 0, MoreExecutors.directExecutor()));
175 175 futures.add(timeseriesDao.save(tenantId, entityId, tsKvEntry, ttl));
176 176 }
... ...
... ... @@ -181,11 +181,14 @@ public class CassandraBaseTimeseriesDao extends AbstractCassandraBaseTimeseriesD
181 181 }
182 182
183 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 185 if (isFixedPartitioning()) {
186 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 192 long partition = toPartitionTs(tsKvEntryTs);
190 193 if (cassandraTsPartitionsCache == null) {
191 194 return doSavePartition(tenantId, entityId, key, ttl, partition);
... ...
... ... @@ -33,7 +33,7 @@ public interface TimeseriesDao {
33 33
34 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 38 ListenableFuture<Void> remove(TenantId tenantId, EntityId entityId, DeleteTsKvQuery query);
39 39
... ...
... ... @@ -100,10 +100,10 @@ public class CassandraPartitionsCacheTest {
100 100 long tsKvEntryTs = System.currentTimeMillis();
101 101
102 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 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 108 verify(cassandraBaseTimeseriesDao, times(60000)).executeAsyncWrite(any(TenantId.class), any(Statement.class));
109 109 }
... ...
... ... @@ -42,13 +42,14 @@
42 42 <spring-boot.version>2.3.12.RELEASE</spring-boot.version>
43 43 <spring.version>5.2.16.RELEASE</spring.version>
44 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 46 <spring-data-redis.version>2.4.3</spring-data-redis.version>
47 47 <jedis.version>3.3.0</jedis.version>
48 48 <jjwt.version>0.7.0</jjwt.version>
49 49 <json-path.version>2.2.0</json-path.version>
50 50 <junit.version>4.12</junit.version>
51 51 <jupiter.version>5.7.1</jupiter.version>
  52 + <awaitility.version>4.1.0</awaitility.version>
52 53 <hamcrest.version>2.2</hamcrest.version>
53 54 <slf4j.version>1.7.7</slf4j.version>
54 55 <logback.version>1.2.3</logback.version>
... ... @@ -1382,6 +1383,12 @@
1382 1383 <groupId>io.grpc</groupId>
1383 1384 <artifactId>grpc-netty</artifactId>
1384 1385 <version>${grpc.version}</version>
  1386 + <exclusions>
  1387 + <exclusion>
  1388 + <groupId>io.netty</groupId>
  1389 + <artifactId>*</artifactId>
  1390 + </exclusion>
  1391 + </exclusions>
1385 1392 </dependency>
1386 1393 <dependency>
1387 1394 <groupId>io.grpc</groupId>
... ... @@ -1438,6 +1445,12 @@
1438 1445 <scope>test</scope>
1439 1446 </dependency>
1440 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 1454 <groupId>org.hamcrest</groupId>
1442 1455 <artifactId>hamcrest</artifactId>
1443 1456 <version>${hamcrest.version}</version>
... ... @@ -1580,6 +1593,12 @@
1580 1593 <groupId>com.microsoft.azure</groupId>
1581 1594 <artifactId>azure-servicebus</artifactId>
1582 1595 <version>${azure-servicebus.version}</version>
  1596 + <exclusions>
  1597 + <exclusion>
  1598 + <groupId>io.netty</groupId>
  1599 + <artifactId>*</artifactId>
  1600 + </exclusion>
  1601 + </exclusions>
1583 1602 </dependency>
1584 1603 <dependency>
1585 1604 <groupId>org.passay</groupId>
... ...
... ... @@ -188,7 +188,7 @@ class AlarmState {
188 188 setAlarmConditionMetadata(ruleState, metaData);
189 189 TbMsg newMsg = ctx.newMsg(lastMsgQueueName != null ? lastMsgQueueName : ServiceQueue.MAIN, "ALARM",
190 190 originator, msg != null ? msg.getCustomerId() : null, metaData, data);
191   - ctx.tellNext(newMsg, relationType);
  191 + ctx.enqueueForTellNext(newMsg, relationType);
192 192 }
193 193
194 194 protected void setAlarmConditionMetadata(AlarmRuleState ruleState, TbMsgMetaData metaData) {
... ...
... ... @@ -120,7 +120,7 @@ public class TbSendRPCRequestNode implements TbNode {
120 120 ctx.enqueueForTellNext(next, TbRelationTypes.SUCCESS);
121 121 } else {
122 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 126 ctx.ack(msg);
... ...
... ... @@ -196,7 +196,7 @@ public class TbDeviceProfileNodeTest {
196 196 TbMsgDataType.JSON, mapper.writeValueAsString(data), null, null);
197 197 node.onMsg(ctx, msg);
198 198 verify(ctx).tellSuccess(msg);
199   - verify(ctx).tellNext(theMsg, "Alarm Created");
  199 + verify(ctx).enqueueForTellNext(theMsg, "Alarm Created");
200 200 verify(ctx, Mockito.never()).tellFailure(Mockito.any(), Mockito.any());
201 201
202 202 TbMsg theMsg2 = TbMsg.newMsg("ALARM", deviceId, new TbMsgMetaData(), "2");
... ... @@ -207,7 +207,7 @@ public class TbDeviceProfileNodeTest {
207 207 TbMsgDataType.JSON, mapper.writeValueAsString(data), null, null);
208 208 node.onMsg(ctx, msg2);
209 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 289
290 290 node.onMsg(ctx, msg);
291 291 verify(ctx).tellSuccess(msg);
292   - verify(ctx).tellNext(theMsg, "Alarm Created");
  292 + verify(ctx).enqueueForTellNext(theMsg, "Alarm Created");
293 293 verify(ctx, Mockito.never()).tellFailure(Mockito.any(), Mockito.any());
294 294 }
295 295
... ... @@ -376,7 +376,7 @@ public class TbDeviceProfileNodeTest {
376 376
377 377 node.onMsg(ctx, msg);
378 378 verify(ctx).tellSuccess(msg);
379   - verify(ctx).tellNext(theMsg, "Alarm Created");
  379 + verify(ctx).enqueueForTellNext(theMsg, "Alarm Created");
380 380 verify(ctx, Mockito.never()).tellFailure(Mockito.any(), Mockito.any());
381 381 }
382 382
... ... @@ -445,7 +445,7 @@ public class TbDeviceProfileNodeTest {
445 445
446 446 node.onMsg(ctx, msg);
447 447 verify(ctx).tellSuccess(msg);
448   - verify(ctx).tellNext(theMsg, "Alarm Created");
  448 + verify(ctx).enqueueForTellNext(theMsg, "Alarm Created");
449 449 verify(ctx, Mockito.never()).tellFailure(Mockito.any(), Mockito.any());
450 450 }
451 451
... ... @@ -554,7 +554,7 @@ public class TbDeviceProfileNodeTest {
554 554
555 555 node.onMsg(ctx, msg2);
556 556 verify(ctx).tellSuccess(msg2);
557   - verify(ctx).tellNext(theMsg, "Alarm Created");
  557 + verify(ctx).enqueueForTellNext(theMsg, "Alarm Created");
558 558 verify(ctx, Mockito.never()).tellFailure(Mockito.any(), Mockito.any());
559 559 }
560 560
... ... @@ -678,7 +678,7 @@ public class TbDeviceProfileNodeTest {
678 678
679 679 node.onMsg(ctx, msg2);
680 680 verify(ctx).tellSuccess(msg2);
681   - verify(ctx).tellNext(theMsg, "Alarm Created");
  681 + verify(ctx).enqueueForTellNext(theMsg, "Alarm Created");
682 682 verify(ctx, Mockito.never()).tellFailure(Mockito.any(), Mockito.any());
683 683 }
684 684
... ... @@ -781,7 +781,7 @@ public class TbDeviceProfileNodeTest {
781 781
782 782 node.onMsg(ctx, msg2);
783 783 verify(ctx).tellSuccess(msg2);
784   - verify(ctx).tellNext(theMsg, "Alarm Created");
  784 + verify(ctx).enqueueForTellNext(theMsg, "Alarm Created");
785 785 verify(ctx, Mockito.never()).tellFailure(Mockito.any(), Mockito.any());
786 786 }
787 787
... ... @@ -897,7 +897,7 @@ public class TbDeviceProfileNodeTest {
897 897
898 898 node.onMsg(ctx, msg2);
899 899 verify(ctx).tellSuccess(msg2);
900   - verify(ctx).tellNext(theMsg, "Alarm Created");
  900 + verify(ctx).enqueueForTellNext(theMsg, "Alarm Created");
901 901 verify(ctx, Mockito.never()).tellFailure(Mockito.any(), Mockito.any());
902 902 }
903 903
... ... @@ -999,7 +999,7 @@ public class TbDeviceProfileNodeTest {
999 999
1000 1000 node.onMsg(ctx, msg2);
1001 1001 verify(ctx).tellSuccess(msg2);
1002   - verify(ctx).tellNext(theMsg, "Alarm Created");
  1002 + verify(ctx).enqueueForTellNext(theMsg, "Alarm Created");
1003 1003 verify(ctx, Mockito.never()).tellFailure(Mockito.any(), Mockito.any());
1004 1004 }
1005 1005
... ... @@ -1082,7 +1082,7 @@ public class TbDeviceProfileNodeTest {
1082 1082
1083 1083 node.onMsg(ctx, msg);
1084 1084 verify(ctx).tellSuccess(msg);
1085   - verify(ctx).tellNext(theMsg, "Alarm Created");
  1085 + verify(ctx).enqueueForTellNext(theMsg, "Alarm Created");
1086 1086 verify(ctx, Mockito.never()).tellFailure(Mockito.any(), Mockito.any());
1087 1087 }
1088 1088
... ... @@ -1163,7 +1163,7 @@ public class TbDeviceProfileNodeTest {
1163 1163
1164 1164 node.onMsg(ctx, msg);
1165 1165 verify(ctx).tellSuccess(msg);
1166   - verify(ctx).tellNext(theMsg, "Alarm Created");
  1166 + verify(ctx).enqueueForTellNext(theMsg, "Alarm Created");
1167 1167 verify(ctx, Mockito.never()).tellFailure(Mockito.any(), Mockito.any());
1168 1168 }
1169 1169
... ... @@ -1237,7 +1237,7 @@ public class TbDeviceProfileNodeTest {
1237 1237
1238 1238 node.onMsg(ctx, msg);
1239 1239 verify(ctx).tellSuccess(msg);
1240   - verify(ctx).tellNext(theMsg, "Alarm Created");
  1240 + verify(ctx).enqueueForTellNext(theMsg, "Alarm Created");
1241 1241 verify(ctx, Mockito.never()).tellFailure(Mockito.any(), Mockito.any());
1242 1242 }
1243 1243
... ... @@ -1321,7 +1321,7 @@ public class TbDeviceProfileNodeTest {
1321 1321
1322 1322 node.onMsg(ctx, msg);
1323 1323 verify(ctx).tellSuccess(msg);
1324   - verify(ctx).tellNext(theMsg, "Alarm Created");
  1324 + verify(ctx).enqueueForTellNext(theMsg, "Alarm Created");
1325 1325 verify(ctx, Mockito.never()).tellFailure(Mockito.any(), Mockito.any());
1326 1326
1327 1327 }
... ... @@ -1407,7 +1407,7 @@ public class TbDeviceProfileNodeTest {
1407 1407
1408 1408 node.onMsg(ctx, msg);
1409 1409 verify(ctx).tellSuccess(msg);
1410   - verify(ctx).tellNext(theMsg, "Alarm Created");
  1410 + verify(ctx).enqueueForTellNext(theMsg, "Alarm Created");
1411 1411 verify(ctx, Mockito.never()).tellFailure(Mockito.any(), Mockito.any());
1412 1412 }
1413 1413
... ...
... ... @@ -89,6 +89,7 @@ transport:
89 89 bind_address: "${MQTT_BIND_ADDRESS:0.0.0.0}"
90 90 bind_port: "${MQTT_BIND_PORT:1883}"
91 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 93 netty:
93 94 leak_detector_level: "${NETTY_LEAK_DETECTOR_LVL:DISABLED}"
94 95 boss_group_thread_count: "${NETTY_BOSS_GROUP_THREADS:1}"
... ...
... ... @@ -84,6 +84,8 @@ export interface WidgetActionsApi {
84 84 entityId?: EntityId, entityName?: string, additionalParams?: any, entityLabel?: string) => void;
85 85 elementClick: ($event: Event) => void;
86 86 getActiveEntityInfo: () => SubscriptionEntityInfo;
  87 + openDashboardStateInSeparateDialog: (targetDashboardStateId: string, params?: StateParams, dialogTitle?: string,
  88 + hideDashboardToolbar?: boolean, dialogWidth?: number, dialogHeight?: number) => void;
87 89 }
88 90
89 91 export interface AliasInfo {
... ...
... ... @@ -50,11 +50,17 @@ export class AttributeService {
50 50 }
51 51
52 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 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 66 public saveEntityAttributes(entityId: EntityId, attributeScope: AttributeScope, attributes: Array<AttributeData>,
... ... @@ -97,7 +103,7 @@ export class AttributeService {
97 103 });
98 104 let deleteEntityTimeseriesObservable: Observable<any>;
99 105 if (deleteTimeseries.length) {
100   - deleteEntityTimeseriesObservable = this.deleteEntityTimeseries(entityId, deleteTimeseries, true, config);
  106 + deleteEntityTimeseriesObservable = this.deleteEntityTimeseries(entityId, deleteTimeseries, true, null, null, config);
101 107 } else {
102 108 deleteEntityTimeseriesObservable = of(null);
103 109 }
... ...
... ... @@ -18,7 +18,7 @@ import { Store } from '@ngrx/store';
18 18 import { AppState } from '@core/core.state';
19 19 import { FormBuilder, FormGroup, ValidatorFn, Validators } from '@angular/forms';
20 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 22 import { POSTAL_CODE_PATTERNS } from '@home/models/contact.models';
23 23 import { HasId } from '@shared/models/base-data';
24 24 import { EntityComponent } from './entity.component';
... ... @@ -30,8 +30,9 @@ export abstract class ContactBasedComponent<T extends ContactBased<HasId>> exten
30 30 protected constructor(protected store: Store<AppState>,
31 31 protected fb: FormBuilder,
32 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 38 buildForm(entity: T): FormGroup {
... ...
... ... @@ -17,7 +17,7 @@
17 17 import { BaseData, HasId } from '@shared/models/base-data';
18 18 import { FormBuilder, FormGroup } from '@angular/forms';
19 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 21 import { Store } from '@ngrx/store';
22 22 import { AppState } from '@core/core.state';
23 23 import { EntityAction } from '@home/models/entity/entity-component.models';
... ... @@ -50,6 +50,7 @@ export abstract class EntityComponent<T extends BaseData<HasId>,
50 50 @Input()
51 51 set isEdit(isEdit: boolean) {
52 52 this.isEditValue = isEdit;
  53 + this.cd.markForCheck();
53 54 this.updateFormState();
54 55 }
55 56
... ... @@ -80,7 +81,8 @@ export abstract class EntityComponent<T extends BaseData<HasId>,
80 81 protected constructor(protected store: Store<AppState>,
81 82 protected fb: FormBuilder,
82 83 protected entityValue: T,
83   - protected entitiesTableConfigValue: C) {
  84 + protected entitiesTableConfigValue: C,
  85 + protected cd: ChangeDetectorRef) {
84 86 super(store);
85 87 this.entityForm = this.buildForm(this.entityValue);
86 88 }
... ...
... ... @@ -14,7 +14,7 @@
14 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 18 import { Store } from '@ngrx/store';
19 19 import { AppState } from '@core/core.state';
20 20 import { FormBuilder, FormGroup, Validators } from '@angular/forms';
... ... @@ -77,8 +77,9 @@ export class DeviceProfileComponent extends EntityComponent<DeviceProfile> {
77 77 protected translate: TranslateService,
78 78 @Optional() @Inject('entity') protected entityValue: DeviceProfile,
79 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 85 hideDelete() {
... ...
... ... @@ -14,7 +14,7 @@
14 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 18 import { Store } from '@ngrx/store';
19 19 import { AppState } from '@core/core.state';
20 20 import { FormBuilder, FormGroup, Validators } from '@angular/forms';
... ... @@ -43,8 +43,9 @@ export class TenantProfileComponent extends EntityComponent<TenantProfile> {
43 43 protected translate: TranslateService,
44 44 @Optional() @Inject('entity') protected entityValue: TenantProfile,
45 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 51 hideDelete() {
... ...
... ... @@ -284,7 +284,8 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
284 284 getActionDescriptors: this.getActionDescriptors.bind(this),
285 285 handleWidgetAction: this.handleWidgetAction.bind(this),
286 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 291 this.widgetContext.customHeaderActions = [];
... ... @@ -1025,7 +1026,8 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
1025 1026 this.updateEntityParams(params, targetEntityParamName, targetEntityId, entityName, entityLabel);
1026 1027 if (type === WidgetActionType.openDashboardState) {
1027 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 1031 } else {
1030 1032 this.widgetContext.stateController.openState(targetDashboardStateId, params, descriptor.openRightLayout);
1031 1033 }
... ... @@ -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 1283 const dashboard = deepClone(this.widgetContext.stateController.dashboardCtrl.dashboardCtx.getDashboard());
1282 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 1286 if (targetDashboardStateId) {
1292 1287 stateObject.id = targetDashboardStateId;
1293 1288 }
1294   - let title = descriptor.dialogTitle;
  1289 + let title = dialogTitle;
1295 1290 if (!title) {
1296 1291 if (targetDashboardStateId && dashboard.configuration.states) {
1297 1292 const dashboardState = dashboard.configuration.states[targetDashboardStateId];
... ... @@ -1304,7 +1299,6 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
1304 1299 title = dashboard.title;
1305 1300 }
1306 1301 title = this.utils.customTranslation(title, title);
1307   - const params = stateObject.params;
1308 1302 const paramsEntityName = params && params.entityName ? params.entityName : '';
1309 1303 const paramsEntityLabel = params && params.entityLabel ? params.entityLabel : '';
1310 1304 title = insertVariable(title, 'entityName', paramsEntityName);
... ... @@ -1324,28 +1318,27 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
1324 1318 dashboard,
1325 1319 state: objToBase64([ stateObject ]),
1326 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 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 39 </mat-form-field>
40 40 <mat-form-field class="mat-block">
41 41 <mat-label translate>admin.smtp-protocol</mat-label>
42   - <mat-select matInput formControlName="smtpProtocol">
  42 + <mat-select formControlName="smtpProtocol">
43 43 <mat-option *ngFor="let protocol of smtpProtocols" [value]="protocol">
44 44 {{protocol.toUpperCase()}}
45 45 </mat-option>
... ... @@ -127,7 +127,10 @@
127 127 <input matInput formControlName="username" placeholder="{{ 'common.enter-username' | translate }}"
128 128 autocomplete="new-username"/>
129 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 134 <mat-label translate>common.password</mat-label>
132 135 <input matInput formControlName="password" type="password"
133 136 placeholder="{{ 'common.enter-password' | translate }}" autocomplete="new-password"/>
... ...
... ... @@ -14,7 +14,7 @@
14 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 18 import { Store } from '@ngrx/store';
19 19 import { AppState } from '@core/core.state';
20 20 import { PageComponent } from '@shared/components/page.component';
... ... @@ -25,21 +25,26 @@ import { AdminService } from '@core/http/admin.service';
25 25 import { ActionNotificationShow } from '@core/notification/notification.actions';
26 26 import { TranslateService } from '@ngx-translate/core';
27 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 32 @Component({
31 33 selector: 'tb-mail-server',
32 34 templateUrl: './mail-server.component.html',
33 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 39 mailSettings: FormGroup;
38 40 adminSettings: AdminSettings<MailServerSettings>;
39 41 smtpProtocols = ['smtp', 'smtps'];
  42 + showChangePassword = false;
40 43
41 44 tlsVersions = ['TLSv1', 'TLSv1.1', 'TLSv1.2', 'TLSv1.3'];
42 45
  46 + private destroy$ = new Subject();
  47 +
43 48 constructor(protected store: Store<AppState>,
44 49 private router: Router,
45 50 private adminService: AdminService,
... ... @@ -56,12 +61,22 @@ export class MailServerComponent extends PageComponent implements OnInit, HasCon
56 61 if (this.adminSettings.jsonValue && isString(this.adminSettings.jsonValue.enableTls)) {
57 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 67 this.mailSettings.reset(this.adminSettings.jsonValue);
  68 + this.enableMailPassword(!this.showChangePassword);
60 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 80 buildMailServerSettingsForm() {
66 81 this.mailSettings = this.fb.group({
67 82 mailFrom: ['', [Validators.required]],
... ... @@ -81,14 +96,23 @@ export class MailServerComponent extends PageComponent implements OnInit, HasCon
81 96 proxyUser: [''],
82 97 proxyPassword: [''],
83 98 username: [''],
  99 + changePassword: [false],
84 100 password: ['']
85 101 });
86 102 this.registerDisableOnLoadFormControl(this.mailSettings.get('smtpProtocol'));
87 103 this.registerDisableOnLoadFormControl(this.mailSettings.get('enableTls'));
88 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 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 118 enableProxyChanged(): void {
... ... @@ -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 137 sendTestMail(): void {
106   - this.adminSettings.jsonValue = {...this.adminSettings.jsonValue, ...this.mailSettings.value};
  138 + this.adminSettings.jsonValue = {...this.adminSettings.jsonValue, ...this.mailSettingsFormValue};
107 139 this.adminService.sendTestMail(this.adminSettings).subscribe(
108 140 () => {
109 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 145 }
114 146
115 147 save(): void {
116   - this.adminSettings.jsonValue = {...this.adminSettings.jsonValue, ...this.mailSettings.value};
  148 + this.adminSettings.jsonValue = {...this.adminSettings.jsonValue, ...this.mailSettingsFormValue};
117 149 this.adminService.saveAdminSettings(this.adminSettings).subscribe(
118 150 (adminSettings) => {
119   - if (!adminSettings.jsonValue.password) {
120   - adminSettings.jsonValue.password = this.mailSettings.value.password;
121   - }
122 151 this.adminSettings = adminSettings;
  152 + this.showChangePassword = true;
123 153 this.mailSettings.reset(this.adminSettings.jsonValue);
124 154 }
125 155 );
... ... @@ -129,4 +159,9 @@ export class MailServerComponent extends PageComponent implements OnInit, HasCon
129 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 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 18 import { Subject } from 'rxjs';
19 19 import { Store } from '@ngrx/store';
20 20 import { AppState } from '@core/core.state';
... ... @@ -30,7 +30,7 @@ import {
30 30 ResourceTypeTranslationMap
31 31 } from '@shared/models/resource.models';
32 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 35 @Component({
36 36 selector: 'tb-resources-library',
... ... @@ -48,8 +48,9 @@ export class ResourcesLibraryComponent extends EntityComponent<Resource> impleme
48 48 protected translate: TranslateService,
49 49 @Inject('entity') protected entityValue: Resource,
50 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 56 ngOnInit() {
... ... @@ -102,7 +103,7 @@ export class ResourcesLibraryComponent extends EntityComponent<Resource> impleme
102 103 if (this.isAdd) {
103 104 form.addControl('data', this.fb.control(null, Validators.required));
104 105 }
105   - return form
  106 + return form;
106 107 }
107 108
108 109 updateForm(entity: Resource) {
... ...
... ... @@ -14,7 +14,7 @@
14 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 18 import { Store } from '@ngrx/store';
19 19 import { AppState } from '@core/core.state';
20 20 import { EntityComponent } from '../../components/entity/entity.component';
... ... @@ -41,8 +41,9 @@ export class AssetComponent extends EntityComponent<AssetInfo> {
41 41 protected translate: TranslateService,
42 42 @Inject('entity') protected entityValue: AssetInfo,
43 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 49 ngOnInit() {
... ...
... ... @@ -14,7 +14,7 @@
14 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 18 import { Store } from '@ngrx/store';
19 19 import { AppState } from '@core/core.state';
20 20 import { FormBuilder, FormGroup, Validators } from '@angular/forms';
... ... @@ -42,8 +42,9 @@ export class CustomerComponent extends ContactBasedComponent<Customer> {
42 42 protected translate: TranslateService,
43 43 @Inject('entity') protected entityValue: Customer,
44 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 50 hideDelete() {
... ...
... ... @@ -14,7 +14,7 @@
14 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 18 import { Store } from '@ngrx/store';
19 19 import { AppState } from '@core/core.state';
20 20 import { EntityComponent } from '../../components/entity/entity.component';
... ... @@ -49,8 +49,9 @@ export class DashboardFormComponent extends EntityComponent<Dashboard> {
49 49 private dashboardService: DashboardService,
50 50 @Inject('entity') protected entityValue: Dashboard,
51 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 57 ngOnInit() {
... ...
... ... @@ -14,7 +14,7 @@
14 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 18 import { Store } from '@ngrx/store';
19 19 import { AppState } from '@core/core.state';
20 20 import { EntityComponent } from '../../components/entity/entity.component';
... ... @@ -56,8 +56,9 @@ export class DeviceComponent extends EntityComponent<DeviceInfo> {
56 56 protected translate: TranslateService,
57 57 @Inject('entity') protected entityValue: DeviceInfo,
58 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 64 ngOnInit() {
... ...
... ... @@ -14,7 +14,7 @@
14 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 18 import { Store } from '@ngrx/store';
19 19 import { AppState } from '@core/core.state';
20 20 import { EntityComponent } from '@home/components/entity/entity.component';
... ... @@ -42,8 +42,9 @@ export class EdgeComponent extends EntityComponent<EdgeInfo> {
42 42 protected translate: TranslateService,
43 43 @Inject('entity') protected entityValue: EdgeInfo,
44 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 50 ngOnInit() {
... ...
... ... @@ -14,7 +14,7 @@
14 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 18 import { Store } from '@ngrx/store';
19 19 import { AppState } from '@core/core.state';
20 20 import { EntityComponent } from '../../components/entity/entity.component';
... ... @@ -53,8 +53,9 @@ export class EntityViewComponent extends EntityComponent<EntityViewInfo> {
53 53 protected translate: TranslateService,
54 54 @Inject('entity') protected entityValue: EntityViewInfo,
55 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 61 ngOnInit() {
... ...
... ... @@ -14,7 +14,7 @@
14 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 18 import { combineLatest, Subject } from 'rxjs';
19 19 import { Store } from '@ngrx/store';
20 20 import { AppState } from '@core/core.state';
... ... @@ -50,8 +50,9 @@ export class OtaUpdateComponent extends EntityComponent<OtaPackage> implements O
50 50 protected translate: TranslateService,
51 51 @Inject('entity') protected entityValue: OtaPackage,
52 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 58 ngOnInit() {
... ...
... ... @@ -14,7 +14,7 @@
14 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 18 import { Store } from '@ngrx/store';
19 19 import { AppState } from '@core/core.state';
20 20 import { EntityComponent } from '../../components/entity/entity.component';
... ... @@ -37,8 +37,9 @@ export class RuleChainComponent extends EntityComponent<RuleChain> {
37 37 protected translate: TranslateService,
38 38 @Inject('entity') protected entityValue: RuleChain,
39 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 45 ngOnInit() {
... ...
... ... @@ -14,7 +14,7 @@
14 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 18 import { Store } from '@ngrx/store';
19 19 import { AppState } from '@core/core.state';
20 20 import { FormBuilder, FormGroup, Validators } from '@angular/forms';
... ... @@ -36,8 +36,9 @@ export class TenantComponent extends ContactBasedComponent<TenantInfo> {
36 36 protected translate: TranslateService,
37 37 @Inject('entity') protected entityValue: TenantInfo,
38 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 44 hideDelete() {
... ...
... ... @@ -14,7 +14,7 @@
14 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 18 import { select, Store } from '@ngrx/store';
19 19 import { AppState } from '@core/core.state';
20 20 import { EntityComponent } from '../../components/entity/entity.component';
... ... @@ -43,8 +43,9 @@ export class UserComponent extends EntityComponent<User> {
43 43 constructor(protected store: Store<AppState>,
44 44 @Optional() @Inject('entity') protected entityValue: User,
45 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 51 hideDelete() {
... ...
... ... @@ -14,7 +14,7 @@
14 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 18 import { Store } from '@ngrx/store';
19 19 import { AppState } from '@core/core.state';
20 20 import { EntityComponent } from '../../components/entity/entity.component';
... ... @@ -32,8 +32,9 @@ export class WidgetsBundleComponent extends EntityComponent<WidgetsBundle> {
32 32 constructor(protected store: Store<AppState>,
33 33 @Inject('entity') protected entityValue: WidgetsBundle,
34 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 40 hideDelete() {
... ...
... ... @@ -27,6 +27,7 @@ export interface AdminSettings<T> {
27 27 export declare type SmtpProtocol = 'smtp' | 'smtps';
28 28
29 29 export interface MailServerSettings {
  30 + showChangePassword: boolean;
30 31 mailFrom: string;
31 32 smtpProtocol: SmtpProtocol;
32 33 smtpHost: string;
... ... @@ -34,7 +35,8 @@ export interface MailServerSettings {
34 35 timeout: number;
35 36 enableTls: boolean;
36 37 username: string;
37   - password: string;
  38 + changePassword?: boolean;
  39 + password?: string;
38 40 enableProxy: boolean;
39 41 proxyHost: string;
40 42 proxyPort: number;
... ...
... ... @@ -803,7 +803,7 @@
803 803 "rulechain-templates": "Regelkettenvorlagen",
804 804 "rulechains": "Rand Regelketten",
805 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 807 "any-edge": "Beliebige Kante",
808 808 "no-edge-types-matching": "Es wurden keine Kantentypen gefunden, die mit '{{entitySubtype}}' übereinstimmen.",
809 809 "edge-type-list-empty": "Keine Kantentypen ausgewählt.",
... ... @@ -1452,7 +1452,7 @@
1452 1452 "unset-auto-assign-to-edge-text": "Nach der Bestätigung wird die Kantenregelkette bei der Erstellung nicht mehr automatisch den Kanten zugewiesen.",
1453 1453 "edge-template-root": "Vorlagenstamm",
1454 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 1456 "open-rulechain": "Regelkette öffnen",
1457 1457 "assign-to-edge": "Rand zuweisen",
1458 1458 "edge-rulechain": "Kantenregelkette"
... ...
... ... @@ -104,6 +104,7 @@
104 104 "proxy-port-range": "Proxy port should be in a range from 1 to 65535.",
105 105 "proxy-user": "Proxy user",
106 106 "proxy-password": "Proxy password",
  107 + "change-password": "Change password",
107 108 "send-test-mail": "Send test mail",
108 109 "sms-provider": "SMS provider",
109 110 "sms-provider-settings": "SMS provider settings",
... ...
... ... @@ -413,8 +413,8 @@
413 413 "unassign-asset-from-edge": "Anular activo de bodre",
414 414 "unassign-asset-from-edge-title": "¿Está seguro de que desea desasignar el activo '{{assetName}}'?",
415 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 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 420 "attribute": {
... ... @@ -950,7 +950,7 @@
950 950 "assign-device-to-edge-text": "Seleccione los dispositivos para asignar al borde",
951 951 "unassign-device-from-edge-title": "¿Está seguro de que desea desasignar el dispositivo '{{deviceName}}'?",
952 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 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 956 "device-profile": {
... ... @@ -1123,7 +1123,7 @@
1123 1123 "delete": "Eliminar borde",
1124 1124 "delete-edge-title": "¿Está seguro de que desea eliminar el borde '{{edgeName}}'?",
1125 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 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 1128 "name": "Nombre",
1129 1129 "name-starts-with": "Edge name starts with",
... ... @@ -1156,7 +1156,7 @@
1156 1156 "unassign-from-customer": "Anular asignación del cliente",
1157 1157 "unassign-edge-title": "¿Está seguro de que desea desasignar el borde '{{edgeName}}'?",
1158 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 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 1161 "make-public": "Hacer público el borde",
1162 1162 "make-public-edge-title": "¿Estás seguro de que quieres hacer público el edge '{{edgeName}}'?",
... ... @@ -1189,14 +1189,14 @@
1189 1189 "rulechain-templates": "Plantillas, de cadena de reglas",
1190 1190 "rulechains": "Cadenas de regla de borde",
1191 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 1193 "any-edge": "Cualquier bordee",
1194 1194 "no-edge-types-matching": "No se encontraron tipos de aristas que coincidan con '{{entitySubtype}}'.",
1195 1195 "edge-type-list-empty": "No se seleccionó ningún tipo de borde.",
1196 1196 "edge-types": "Tipos de bordes",
1197 1197 "enter-edge-type": "Ingrese el tipo de borde",
1198 1198 "deployed": "Desplegada",
1199   - "pending": "Pending",
  1199 + "pending": "Pendiente",
1200 1200 "downlinks": "Enlaces descendentes",
1201 1201 "no-downlinks-prompt": "No se encontraron enlaces descendentes",
1202 1202 "sync-process-started-successfully": "¡El proceso de sincronización se inició correctamente!",
... ... @@ -1356,7 +1356,7 @@
1356 1356 "type-api-usage-state": "Estado de uso de la API",
1357 1357 "type-edge": "Borde",
1358 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 1360 "edge-name-starts-with": "Bordes cuyos nombres comienzan con '{{prefijo}}'"
1361 1361 },
1362 1362 "entity-field": {
... ... @@ -1481,9 +1481,9 @@
1481 1481 "assign-entity-view-to-edge-text": "Seleccione las vistas de entidad para asignar al borde",
1482 1482 "unassign-entity-view-from-edge-title": "¿Está seguro de que desea anular la asignación de la vista de entidad '{{entityViewName}}'?",
1483 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 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 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 1489 "event": {
... ... @@ -2074,9 +2074,9 @@
2074 2074 "delete-rulechains": "Eliminar cadenas de reglas",
2075 2075 "unassign-rulechain": "Anular asignación de cadena de reglas",
2076 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 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 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 2081 "assign-rulechain-to-edge-title": "Asignar cadena (s) de reglas a borde",
2082 2082 "assign-rulechain-to-edge-text": "Seleccione las cadenas de reglas para asignar al borde",
... ...
... ... @@ -736,7 +736,7 @@
736 736 "assign-device-to-edge-text":"Veuillez sélectionner la bordure pour attribuer le ou les dispositifs",
737 737 "unassign-device-from-edge-title": "Êtes-vous sûr de vouloir annuler l'affection du dispositif {{deviceName}} '?",
738 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 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 742 "dialog": {
... ... @@ -755,7 +755,7 @@
755 755 "delete": "Supprimer la bordure",
756 756 "delete-edge-title": "Êtes-vous sûr de vouloir supprimer la bordure '{{edgeName}}'?",
757 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 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 760 "name": "Nom",
761 761 "name-starts-with": "Le nom du bord commence par",
... ... @@ -788,7 +788,7 @@
788 788 "unassign-from-customer": "Retirer du client",
789 789 "unassign-edge-title": "Êtes-vous sûr de vouloir annuler l'affection du dispositif {{edgeName}}",
790 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 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 793 "make-public": "Make edge public",
794 794 "make-public-edge-title": "Are you sure you want to make the edge '{{edgeName}}' public?",
... ... @@ -821,7 +821,7 @@
821 821 "rulechain-templates": "Modèles de chaîne de règles",
822 822 "rulechains": "Chaînes de règles de la bordure",
823 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 825 "any-edge": "Tout bord",
826 826 "no-edge-types-matching": "Aucun type d'arête correspondant à \"{{entitySubtype}}\" n'a été trouvé.",
827 827 "edge-type-list-empty": "Aucun type d'arête sélectionné.",
... ... @@ -1479,9 +1479,9 @@
1479 1479 "delete-rulechains": "Supprimer une chaînes de règles",
1480 1480 "unassign-rulechain": "Retirer chaîne de règles",
1481 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 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 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 1486 "assign-rulechain-to-edge-title": "Attribuer les chaînes de règles a la bordure",
1487 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 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 1498 "edge-template-root": "Racine du modèle",
1499 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 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 1506 "rulenode": {
1507 1507 "add": "Ajouter un noeud de règle",
... ...