Browse Source

Merge remote-tracking branch 'upstream/master' into handle-null-values-in-proto

pull/5038/head
Volodymyr Babak 5 years ago
parent
commit
eced5a49fa
  1. 3
      application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java
  2. 26
      application/src/main/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessor.java
  3. 99
      application/src/main/java/org/thingsboard/server/controller/AbstractRpcController.java
  4. 55
      application/src/main/java/org/thingsboard/server/controller/RpcV1Controller.java
  5. 108
      application/src/main/java/org/thingsboard/server/controller/RpcV2Controller.java
  6. 3
      application/src/main/java/org/thingsboard/server/controller/TbUrlConstants.java
  7. 48
      application/src/main/java/org/thingsboard/server/service/device/ClaimDevicesServiceImpl.java
  8. 11
      application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/CustomOAuth2ClientMapper.java
  9. 4
      application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/Oauth2AuthenticationSuccessHandler.java
  10. 67
      application/src/main/java/org/thingsboard/server/service/state/DefaultDeviceStateService.java
  11. 9
      application/src/main/java/org/thingsboard/server/service/state/DeviceStateService.java
  12. 8
      application/src/main/java/org/thingsboard/server/service/subscription/DefaultSubscriptionManagerService.java
  13. 3
      application/src/main/resources/logback.xml
  14. 8
      application/src/test/java/org/thingsboard/server/transport/coap/rpc/AbstractCoapServerSideRpcDefaultIntegrationTest.java
  15. 6
      application/src/test/java/org/thingsboard/server/transport/coap/rpc/AbstractCoapServerSideRpcIntegrationTest.java
  16. 3
      application/src/test/java/org/thingsboard/server/transport/lwm2m/AbstractLwM2MIntegrationTest.java
  17. 104
      application/src/test/java/org/thingsboard/server/transport/lwm2m/NoSecLwM2MIntegrationTest.java
  18. 8
      application/src/test/java/org/thingsboard/server/transport/mqtt/rpc/AbstractMqttServerSideRpcDefaultIntegrationTest.java
  19. 8
      application/src/test/java/org/thingsboard/server/transport/mqtt/rpc/AbstractMqttServerSideRpcIntegrationTest.java
  20. 2
      application/src/test/java/org/thingsboard/server/transport/mqtt/rpc/AbstractMqttServerSideRpcProtoIntegrationTest.java
  21. 41
      common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaProducerTemplate.java
  22. 1
      common/queue/src/main/proto/queue.proto
  23. 20
      common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/client/DefaultCoapClientContext.java
  24. 2
      common/transport/http/src/main/java/org/thingsboard/server/transport/http/DeviceApiController.java
  25. 11
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/LwM2MTransportBootstrapService.java
  26. 12
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/secure/LwM2MBootstrapConfig.java
  27. 27
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/secure/LwM2MInMemoryBootstrapConfigurationAdapter.java
  28. 29
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/secure/LwM2MServerBootstrap.java
  29. 1
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mSessionMsgListener.java
  30. 5
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClient.java
  31. 7
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClientContextImpl.java
  32. 38
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/ota/DefaultLwM2MOtaUpdateService.java
  33. 10
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/ota/LwM2MClientOtaInfo.java
  34. 164
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/rpc/DefaultLwM2MRpcRequestHandler.java
  35. 1
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/rpc/LwM2MRpcResponseBody.java
  36. 23
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/rpc/RpcDownlinkRequestCallbackProxy.java
  37. 9
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/uplink/DefaultLwM2MUplinkMsgHandler.java
  38. 5
      common/transport/mqtt/pom.xml
  39. 17
      common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java
  40. 11
      common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/GatewayDeviceSessionCtx.java
  41. 35
      common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/GatewaySessionHandler.java
  42. 61
      common/transport/mqtt/src/test/java/org/thingsboard/server/transport/mqtt/session/GatewaySessionHandlerTest.java
  43. 2
      common/transport/snmp/src/main/java/org/thingsboard/server/transport/snmp/session/DeviceSessionContext.java
  44. 2
      common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/TransportService.java
  45. 12
      common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportService.java
  46. 2
      dao/src/main/java/org/thingsboard/server/dao/sqlts/AbstractChunkedAggregationTimeseriesDao.java
  47. 2
      dao/src/main/java/org/thingsboard/server/dao/sqlts/timescale/TimescaleTimeseriesDao.java
  48. 2
      dao/src/main/java/org/thingsboard/server/dao/timeseries/BaseTimeseriesService.java
  49. 7
      dao/src/main/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesDao.java
  50. 2
      dao/src/main/java/org/thingsboard/server/dao/timeseries/TimeseriesDao.java
  51. 4
      dao/src/test/java/org/thingsboard/server/dao/nosql/CassandraPartitionsCacheTest.java
  52. 2
      dao/src/test/java/org/thingsboard/server/dao/service/BaseDashboardServiceTest.java
  53. 2
      msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/MqttClientTest.java
  54. 2
      msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/MqttGatewayClientTest.java
  55. 6
      rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java
  56. 34
      rest-client/src/main/resources/logback.xml
  57. 2
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/AlarmState.java
  58. 2
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rpc/TbSendRPCRequestNode.java
  59. 8
      ui-ngx/src/app/core/api/widget-subscription.ts
  60. 4
      ui-ngx/src/app/core/http/device.service.ts
  61. 4
      ui-ngx/src/app/core/interceptors/global-http-interceptor.ts
  62. 3
      ui-ngx/src/app/modules/home/components/dashboard-page/states/state-controller.component.ts
  63. 5
      ui-ngx/src/app/modules/home/components/device/device-credentials-lwm2m.component.html
  64. 19
      ui-ngx/src/app/modules/home/components/device/device-credentials-lwm2m.component.ts
  65. 2
      ui-ngx/src/app/modules/home/components/device/device-credentials.component.ts
  66. 6
      ui-ngx/src/app/modules/home/components/profile/device/coap-device-profile-transport-configuration.component.ts
  67. 6
      ui-ngx/src/app/modules/home/components/profile/device/common/device-profile-common.module.ts
  68. 55
      ui-ngx/src/app/modules/home/components/profile/device/common/power-mode-setting.component.html
  69. 13
      ui-ngx/src/app/modules/home/components/profile/device/common/power-mode-setting.component.ts
  70. 40
      ui-ngx/src/app/modules/home/components/profile/device/common/time-unit-select.component.html
  71. 194
      ui-ngx/src/app/modules/home/components/profile/device/common/time-unit-select.component.ts
  72. 2
      ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-profile-transport-configuration.component.html
  73. 28
      ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-profile-transport-configuration.component.ts
  74. 11
      ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-object-list.component.html
  75. 8
      ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-object-list.component.ts
  76. 20
      ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-profile-config.models.ts
  77. 4
      ui-ngx/src/app/modules/home/components/widget/lib/multiple-input-widget.component.ts
  78. 5
      ui-ngx/src/app/modules/home/components/widget/lib/qrcode-widget.component.html
  79. 20
      ui-ngx/src/app/modules/home/components/widget/lib/qrcode-widget.component.ts
  80. 6
      ui-ngx/src/app/modules/home/pages/device/data/coap-device-transport-configuration.component.ts
  81. 6
      ui-ngx/src/app/modules/home/pages/device/data/lwm2m-device-transport-configuration.component.ts
  82. 2
      ui-ngx/src/app/shared/models/lwm2m-security-config.models.ts
  83. 9
      ui-ngx/src/app/shared/models/time/time.models.ts
  84. 6
      ui-ngx/src/assets/locale/locale.constant-cs_CZ.json
  85. 2
      ui-ngx/src/assets/locale/locale.constant-de_DE.json
  86. 2
      ui-ngx/src/assets/locale/locale.constant-el_GR.json
  87. 16
      ui-ngx/src/assets/locale/locale.constant-en_US.json
  88. 2
      ui-ngx/src/assets/locale/locale.constant-es_ES.json
  89. 2
      ui-ngx/src/assets/locale/locale.constant-fa_IR.json
  90. 2
      ui-ngx/src/assets/locale/locale.constant-fr_FR.json
  91. 2
      ui-ngx/src/assets/locale/locale.constant-it_IT.json
  92. 2
      ui-ngx/src/assets/locale/locale.constant-ja_JA.json
  93. 2
      ui-ngx/src/assets/locale/locale.constant-ka_GE.json
  94. 2
      ui-ngx/src/assets/locale/locale.constant-ko_KR.json
  95. 2
      ui-ngx/src/assets/locale/locale.constant-lv_LV.json
  96. 2
      ui-ngx/src/assets/locale/locale.constant-pt_BR.json
  97. 4
      ui-ngx/src/assets/locale/locale.constant-ro_RO.json
  98. 2
      ui-ngx/src/assets/locale/locale.constant-ru_RU.json
  99. 2
      ui-ngx/src/assets/locale/locale.constant-sl_SI.json
  100. 2
      ui-ngx/src/assets/locale/locale.constant-tr_TR.json

3
application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java

@ -265,7 +265,8 @@ public class ActorSystemContext {
@Getter
private SmsSenderFactory smsSenderFactory;
@Autowired
@Lazy
@Autowired(required = false)
@Getter
private ClaimDevicesService claimDevicesService;

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

@ -15,6 +15,7 @@
*/
package org.thingsboard.server.actors.device;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
@ -35,6 +36,7 @@ import org.thingsboard.server.actors.TbActorCtx;
import org.thingsboard.server.actors.shared.AbstractContextAwareMsgProcessor;
import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.edge.EdgeEvent;
import org.thingsboard.server.common.data.edge.EdgeEventActionType;
import org.thingsboard.server.common.data.edge.EdgeEventType;
@ -379,11 +381,11 @@ class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcessor {
}
private void reportSessionOpen() {
systemContext.getDeviceStateService().onDeviceConnect(deviceId);
systemContext.getDeviceStateService().onDeviceConnect(tenantId, deviceId);
}
private void reportSessionClose() {
systemContext.getDeviceStateService().onDeviceDisconnect(deviceId);
systemContext.getDeviceStateService().onDeviceDisconnect(tenantId, deviceId);
}
private void handleGetAttributesRequest(TbActorCtx context, SessionInfoProto sessionInfo, GetAttributeRequestMsg request) {
@ -510,10 +512,20 @@ class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcessor {
ToDeviceRpcRequestMetadata requestMd = toDeviceRpcPendingMap.remove(responseMsg.getRequestId());
boolean success = requestMd != null;
if (success) {
systemContext.getTbCoreDeviceRpcService().processRpcResponseFromDeviceActor(new FromDeviceRpcResponse(requestMd.getMsg().getMsg().getId(),
responseMsg.getPayload(), null));
boolean hasError = StringUtils.isNotEmpty(responseMsg.getError());
String payload = hasError ? responseMsg.getError() : responseMsg.getPayload();
systemContext.getTbCoreDeviceRpcService().processRpcResponseFromDeviceActor(
new FromDeviceRpcResponse(requestMd.getMsg().getMsg().getId(),
payload, null));
if (requestMd.getMsg().getMsg().isPersisted()) {
systemContext.getTbRpcService().save(tenantId, new RpcId(requestMd.getMsg().getMsg().getId()), RpcStatus.SUCCESSFUL, JacksonUtil.toJsonNode(responseMsg.getPayload()));
RpcStatus status = hasError ? RpcStatus.FAILED : RpcStatus.SUCCESSFUL;
JsonNode response;
try {
response = JacksonUtil.toJsonNode(payload);
} catch (IllegalArgumentException e) {
response = JacksonUtil.newObjectNode().put("error", payload);
}
systemContext.getTbRpcService().save(tenantId, new RpcId(requestMd.getMsg().getMsg().getId()), status, response);
}
} else {
log.debug("[{}] Rpc command response [{}] is stale!", deviceId, responseMsg.getRequestId());
@ -578,7 +590,7 @@ class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcessor {
if (sessions.size() == 1) {
reportSessionOpen();
}
systemContext.getDeviceStateService().onDeviceActivity(deviceId, System.currentTimeMillis());
systemContext.getDeviceStateService().onDeviceActivity(tenantId, deviceId, System.currentTimeMillis());
dumpSessions();
} else if (msg.getEvent() == SessionEvent.CLOSED) {
log.debug("[{}] Canceling subscriptions for closed session [{}]", deviceId, sessionId);
@ -608,7 +620,7 @@ class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcessor {
if (subscriptionInfo.getRpcSubscription()) {
rpcSubscriptions.putIfAbsent(sessionId, sessionMD.getSessionInfo());
}
systemContext.getDeviceStateService().onDeviceActivity(deviceId, subscriptionInfo.getLastActivityTime());
systemContext.getDeviceStateService().onDeviceActivity(tenantId, deviceId, subscriptionInfo.getLastActivityTime());
dumpSessions();
}

99
application/src/main/java/org/thingsboard/server/controller/RpcController.java → application/src/main/java/org/thingsboard/server/controller/AbstractRpcController.java

@ -16,7 +16,6 @@
package org.thingsboard.server.controller;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.util.concurrent.FutureCallback;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
@ -26,13 +25,12 @@ import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.context.request.async.DeferredResult;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.rule.engine.api.RpcError;
import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.audit.ActionType;
@ -59,20 +57,15 @@ import org.thingsboard.server.service.security.permission.Operation;
import org.thingsboard.server.service.telemetry.exception.ToErrorResponseEntity;
import javax.annotation.Nullable;
import java.io.IOException;
import java.util.Optional;
import java.util.UUID;
/**
* Created by ashvayka on 22.03.18.
*/
@RestController
@TbCoreComponent
@RequestMapping(TbUrlConstants.RPC_URL_PREFIX)
@Slf4j
public class RpcController extends BaseController {
protected final ObjectMapper jsonMapper = new ObjectMapper();
public abstract class AbstractRpcController extends BaseController {
@Autowired
private TbCoreDeviceRpcService deviceRpcService;
@ -81,75 +74,15 @@ public class RpcController extends BaseController {
private AccessValidator accessValidator;
@Value("${server.rest.server_side_rpc.min_timeout:5000}")
private long minTimeout;
protected long minTimeout;
@Value("${server.rest.server_side_rpc.default_timeout:10000}")
private long defaultTimeout;
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/oneway/{deviceId}", method = RequestMethod.POST)
@ResponseBody
public DeferredResult<ResponseEntity> handleOneWayDeviceRPCRequest(@PathVariable("deviceId") String deviceIdStr, @RequestBody String requestBody) throws ThingsboardException {
return handleDeviceRPCRequest(true, new DeviceId(UUID.fromString(deviceIdStr)), requestBody);
}
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/twoway/{deviceId}", method = RequestMethod.POST)
@ResponseBody
public DeferredResult<ResponseEntity> handleTwoWayDeviceRPCRequest(@PathVariable("deviceId") String deviceIdStr, @RequestBody String requestBody) throws ThingsboardException {
return handleDeviceRPCRequest(false, new DeviceId(UUID.fromString(deviceIdStr)), requestBody);
}
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/persistent/{rpcId}", method = RequestMethod.GET)
@ResponseBody
public Rpc getPersistedRpc(@PathVariable("rpcId") String strRpc) throws ThingsboardException {
checkParameter("RpcId", strRpc);
try {
RpcId rpcId = new RpcId(UUID.fromString(strRpc));
return checkRpcId(rpcId, Operation.READ);
} catch (Exception e) {
throw handleException(e);
}
}
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/persistent/device/{deviceId}", method = RequestMethod.GET)
@ResponseBody
public PageData<Rpc> getPersistedRpcByDevice(@PathVariable("deviceId") String strDeviceId,
@RequestParam int pageSize,
@RequestParam int page,
@RequestParam RpcStatus rpcStatus,
@RequestParam(required = false) String textSearch,
@RequestParam(required = false) String sortProperty,
@RequestParam(required = false) String sortOrder) throws ThingsboardException {
checkParameter("DeviceId", strDeviceId);
try {
TenantId tenantId = getCurrentUser().getTenantId();
PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder);
DeviceId deviceId = new DeviceId(UUID.fromString(strDeviceId));
return checkNotNull(rpcService.findAllByDeviceIdAndStatus(tenantId, deviceId, rpcStatus, pageLink));
} catch (Exception e) {
throw handleException(e);
}
}
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/persistent/{rpcId}", method = RequestMethod.DELETE)
@ResponseBody
public void deleteResource(@PathVariable("rpcId") String strRpc) throws ThingsboardException {
checkParameter("RpcId", strRpc);
try {
rpcService.deleteRpc(getTenantId(), new RpcId(UUID.fromString(strRpc)));
} catch (Exception e) {
throw handleException(e);
}
}
protected long defaultTimeout;
private DeferredResult<ResponseEntity> handleDeviceRPCRequest(boolean oneWay, DeviceId deviceId, String requestBody) throws ThingsboardException {
protected DeferredResult<ResponseEntity> handleDeviceRPCRequest(boolean oneWay, DeviceId deviceId, String requestBody, HttpStatus timeoutStatus, HttpStatus noActiveConnectionStatus) throws ThingsboardException {
try {
JsonNode rpcRequestBody = jsonMapper.readTree(requestBody);
ToDeviceRpcRequestBody body = new ToDeviceRpcRequestBody(rpcRequestBody.get("method").asText(), jsonMapper.writeValueAsString(rpcRequestBody.get("params")));
JsonNode rpcRequestBody = JacksonUtil.toJsonNode(requestBody);
ToDeviceRpcRequestBody body = new ToDeviceRpcRequestBody(rpcRequestBody.get("method").asText(), JacksonUtil.toString(rpcRequestBody.get("params")));
SecurityUser currentUser = getCurrentUser();
TenantId tenantId = currentUser.getTenantId();
final DeferredResult<ResponseEntity> response = new DeferredResult<>();
@ -157,7 +90,7 @@ public class RpcController extends BaseController {
long expTime = System.currentTimeMillis() + Math.max(minTimeout, timeout);
UUID rpcRequestUUID = rpcRequestBody.has("requestUUID") ? UUID.fromString(rpcRequestBody.get("requestUUID").asText()) : UUID.randomUUID();
boolean persisted = rpcRequestBody.has(DataConstants.PERSISTENT) && rpcRequestBody.get(DataConstants.PERSISTENT).asBoolean();
accessValidator.validate(currentUser, Operation.RPC_CALL, deviceId, new HttpValidationCallback(response, new FutureCallback<DeferredResult<ResponseEntity>>() {
accessValidator.validate(currentUser, Operation.RPC_CALL, deviceId, new HttpValidationCallback(response, new FutureCallback<>() {
@Override
public void onSuccess(@Nullable DeferredResult<ResponseEntity> result) {
ToDeviceRpcRequest rpcRequest = new ToDeviceRpcRequest(rpcRequestUUID,
@ -168,7 +101,7 @@ public class RpcController extends BaseController {
body,
persisted
);
deviceRpcService.processRestApiRpcRequest(rpcRequest, fromDeviceRpcResponse -> reply(new LocalRequestMetaData(rpcRequest, currentUser, result), fromDeviceRpcResponse), currentUser);
deviceRpcService.processRestApiRpcRequest(rpcRequest, fromDeviceRpcResponse -> reply(new LocalRequestMetaData(rpcRequest, currentUser, result), fromDeviceRpcResponse, timeoutStatus, noActiveConnectionStatus), currentUser);
}
@Override
@ -184,12 +117,12 @@ public class RpcController extends BaseController {
}
}));
return response;
} catch (IOException ioe) {
} catch (IllegalArgumentException ioe) {
throw new ThingsboardException("Invalid request body", ioe, ThingsboardErrorCode.BAD_REQUEST_PARAMS);
}
}
public void reply(LocalRequestMetaData rpcRequest, FromDeviceRpcResponse response) {
public void reply(LocalRequestMetaData rpcRequest, FromDeviceRpcResponse response, HttpStatus timeoutStatus, HttpStatus noActiveConnectionStatus) {
Optional<RpcError> rpcError = response.getError();
DeferredResult<ResponseEntity> responseWriter = rpcRequest.getResponseWriter();
if (rpcError.isPresent()) {
@ -197,13 +130,13 @@ public class RpcController extends BaseController {
RpcError error = rpcError.get();
switch (error) {
case TIMEOUT:
responseWriter.setResult(new ResponseEntity<>(HttpStatus.REQUEST_TIMEOUT));
responseWriter.setResult(new ResponseEntity<>(timeoutStatus));
break;
case NO_ACTIVE_CONNECTION:
responseWriter.setResult(new ResponseEntity<>(HttpStatus.CONFLICT));
responseWriter.setResult(new ResponseEntity<>(noActiveConnectionStatus));
break;
default:
responseWriter.setResult(new ResponseEntity<>(HttpStatus.REQUEST_TIMEOUT));
responseWriter.setResult(new ResponseEntity<>(timeoutStatus));
break;
}
} else {
@ -212,8 +145,8 @@ public class RpcController extends BaseController {
String data = responseData.get();
try {
logRpcCall(rpcRequest, rpcError, null);
responseWriter.setResult(new ResponseEntity<>(jsonMapper.readTree(data), HttpStatus.OK));
} catch (IOException e) {
responseWriter.setResult(new ResponseEntity<>(JacksonUtil.toJsonNode(data), HttpStatus.OK));
} catch (IllegalArgumentException e) {
log.debug("Failed to decode device response: {}", data, e);
logRpcCall(rpcRequest, rpcError, e);
responseWriter.setResult(new ResponseEntity<>(HttpStatus.NOT_ACCEPTABLE));

55
application/src/main/java/org/thingsboard/server/controller/RpcV1Controller.java

@ -0,0 +1,55 @@
/**
* Copyright © 2016-2021 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.controller;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.context.request.async.DeferredResult;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.queue.util.TbCoreComponent;
import java.util.UUID;
@RestController
@TbCoreComponent
@RequestMapping(TbUrlConstants.RPC_V1_URL_PREFIX)
@Slf4j
public class RpcV1Controller extends AbstractRpcController {
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/oneway/{deviceId}", method = RequestMethod.POST)
@ResponseBody
public DeferredResult<ResponseEntity> handleOneWayDeviceRPCRequest(@PathVariable("deviceId") String deviceIdStr, @RequestBody String requestBody) throws ThingsboardException {
return handleDeviceRPCRequest(true, new DeviceId(UUID.fromString(deviceIdStr)), requestBody, HttpStatus.REQUEST_TIMEOUT, HttpStatus.CONFLICT);
}
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/twoway/{deviceId}", method = RequestMethod.POST)
@ResponseBody
public DeferredResult<ResponseEntity> handleTwoWayDeviceRPCRequest(@PathVariable("deviceId") String deviceIdStr, @RequestBody String requestBody) throws ThingsboardException {
return handleDeviceRPCRequest(false, new DeviceId(UUID.fromString(deviceIdStr)), requestBody, HttpStatus.REQUEST_TIMEOUT, HttpStatus.CONFLICT);
}
}

108
application/src/main/java/org/thingsboard/server/controller/RpcV2Controller.java

@ -0,0 +1,108 @@
/**
* Copyright © 2016-2021 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.controller;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.context.request.async.DeferredResult;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.RpcId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.rpc.Rpc;
import org.thingsboard.server.common.data.rpc.RpcStatus;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.security.permission.Operation;
import java.util.UUID;
@RestController
@TbCoreComponent
@RequestMapping(TbUrlConstants.RPC_V2_URL_PREFIX)
@Slf4j
public class RpcV2Controller extends AbstractRpcController {
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/oneway/{deviceId}", method = RequestMethod.POST)
@ResponseBody
public DeferredResult<ResponseEntity> handleOneWayDeviceRPCRequest(@PathVariable("deviceId") String deviceIdStr, @RequestBody String requestBody) throws ThingsboardException {
return handleDeviceRPCRequest(true, new DeviceId(UUID.fromString(deviceIdStr)), requestBody, HttpStatus.GATEWAY_TIMEOUT, HttpStatus.GATEWAY_TIMEOUT);
}
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/twoway/{deviceId}", method = RequestMethod.POST)
@ResponseBody
public DeferredResult<ResponseEntity> handleTwoWayDeviceRPCRequest(@PathVariable("deviceId") String deviceIdStr, @RequestBody String requestBody) throws ThingsboardException {
return handleDeviceRPCRequest(false, new DeviceId(UUID.fromString(deviceIdStr)), requestBody, HttpStatus.GATEWAY_TIMEOUT, HttpStatus.GATEWAY_TIMEOUT);
}
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/persistent/{rpcId}", method = RequestMethod.GET)
@ResponseBody
public Rpc getPersistedRpc(@PathVariable("rpcId") String strRpc) throws ThingsboardException {
checkParameter("RpcId", strRpc);
try {
RpcId rpcId = new RpcId(UUID.fromString(strRpc));
return checkRpcId(rpcId, Operation.READ);
} catch (Exception e) {
throw handleException(e);
}
}
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/persistent/device/{deviceId}", method = RequestMethod.GET)
@ResponseBody
public PageData<Rpc> getPersistedRpcByDevice(@PathVariable("deviceId") String strDeviceId,
@RequestParam int pageSize,
@RequestParam int page,
@RequestParam RpcStatus rpcStatus,
@RequestParam(required = false) String textSearch,
@RequestParam(required = false) String sortProperty,
@RequestParam(required = false) String sortOrder) throws ThingsboardException {
checkParameter("DeviceId", strDeviceId);
try {
TenantId tenantId = getCurrentUser().getTenantId();
PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder);
DeviceId deviceId = new DeviceId(UUID.fromString(strDeviceId));
return checkNotNull(rpcService.findAllByDeviceIdAndStatus(tenantId, deviceId, rpcStatus, pageLink));
} catch (Exception e) {
throw handleException(e);
}
}
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/persistent/{rpcId}", method = RequestMethod.DELETE)
@ResponseBody
public void deleteResource(@PathVariable("rpcId") String strRpc) throws ThingsboardException {
checkParameter("RpcId", strRpc);
try {
rpcService.deleteRpc(getTenantId(), new RpcId(UUID.fromString(strRpc)));
} catch (Exception e) {
throw handleException(e);
}
}
}

3
application/src/main/java/org/thingsboard/server/controller/TbUrlConstants.java

@ -20,5 +20,6 @@ package org.thingsboard.server.controller;
*/
public class TbUrlConstants {
public static final String TELEMETRY_URL_PREFIX = "/api/plugins/telemetry";
public static final String RPC_URL_PREFIX = "/api/plugins/rpc";
public static final String RPC_V1_URL_PREFIX = "/api/plugins/rpc";
public static final String RPC_V2_URL_PREFIX = "/api/rpc";
}

48
dao/src/main/java/org/thingsboard/server/dao/device/ClaimDevicesServiceImpl.java → application/src/main/java/org/thingsboard/server/service/device/ClaimDevicesServiceImpl.java

@ -13,12 +13,14 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.dao.device;
package org.thingsboard.server.service.device;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors;
import com.google.common.util.concurrent.SettableFuture;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
@ -26,6 +28,7 @@ import org.springframework.cache.Cache;
import org.springframework.cache.CacheManager;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import org.thingsboard.rule.engine.api.RuleEngineTelemetryService;
import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.Device;
@ -37,12 +40,17 @@ import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry;
import org.thingsboard.server.common.data.kv.BooleanDataEntry;
import org.thingsboard.server.dao.attributes.AttributesService;
import org.thingsboard.server.dao.customer.CustomerService;
import org.thingsboard.server.dao.device.ClaimDataInfo;
import org.thingsboard.server.dao.device.ClaimDevicesService;
import org.thingsboard.server.dao.device.DeviceService;
import org.thingsboard.server.dao.device.claim.ClaimData;
import org.thingsboard.server.dao.device.claim.ClaimResponse;
import org.thingsboard.server.dao.device.claim.ClaimResult;
import org.thingsboard.server.dao.device.claim.ReclaimResult;
import org.thingsboard.server.dao.model.ModelConstants;
import org.thingsboard.server.queue.util.TbCoreComponent;
import javax.annotation.Nullable;
import java.io.IOException;
import java.util.Arrays;
import java.util.Collections;
@ -54,6 +62,7 @@ import static org.thingsboard.server.common.data.CacheConstants.CLAIM_DEVICES_CA
@Service
@Slf4j
@TbCoreComponent
public class ClaimDevicesServiceImpl implements ClaimDevicesService {
private static final String CLAIM_ATTRIBUTE_NAME = "claimingAllowed";
@ -65,6 +74,8 @@ public class ClaimDevicesServiceImpl implements ClaimDevicesService {
@Autowired
private AttributesService attributesService;
@Autowired
private RuleEngineTelemetryService telemetryService;
@Autowired
private CustomerService customerService;
@Autowired
private CacheManager cacheManager;
@ -172,10 +183,23 @@ public class ClaimDevicesServiceImpl implements ClaimDevicesService {
if (isAllowedClaimingByDefault) {
return Futures.immediateFuture(new ReclaimResult(unassignedCustomer));
}
return Futures.transform(attributesService.save(
SettableFuture<ReclaimResult> result = SettableFuture.create();
telemetryService.saveAndNotify(
tenantId, device.getId(), DataConstants.SERVER_SCOPE, Collections.singletonList(
new BaseAttributeKvEntry(new BooleanDataEntry(CLAIM_ATTRIBUTE_NAME, true), System.currentTimeMillis())
)), result -> new ReclaimResult(unassignedCustomer), MoreExecutors.directExecutor());
),
new FutureCallback<>() {
@Override
public void onSuccess(@Nullable Void tmp) {
result.set(new ReclaimResult(unassignedCustomer));
}
@Override
public void onFailure(Throwable t) {
result.setException(t);
}
});
return result;
}
cacheEviction(device.getId());
return Futures.immediateFuture(new ReclaimResult(null));
@ -198,12 +222,24 @@ public class ClaimDevicesServiceImpl implements ClaimDevicesService {
return systemDurationMs;
}
private ListenableFuture<List<Void>> removeClaimingSavedData(Cache cache, ClaimDataInfo data, Device device) {
private ListenableFuture<Void> removeClaimingSavedData(Cache cache, ClaimDataInfo data, Device device) {
if (data.isFromCache()) {
cache.evict(data.getKey());
}
return attributesService.removeAll(device.getTenantId(),
device.getId(), DataConstants.SERVER_SCOPE, Arrays.asList(CLAIM_ATTRIBUTE_NAME, CLAIM_DATA_ATTRIBUTE_NAME));
SettableFuture<Void> result = SettableFuture.create();
telemetryService.deleteAndNotify(device.getTenantId(),
device.getId(), DataConstants.SERVER_SCOPE, Arrays.asList(CLAIM_ATTRIBUTE_NAME, CLAIM_DATA_ATTRIBUTE_NAME), new FutureCallback<>() {
@Override
public void onSuccess(@Nullable Void tmp) {
result.set(tmp);
}
@Override
public void onFailure(Throwable t) {
result.setException(t);
}
});
return result;
}
private void cacheEviction(DeviceId deviceId) {

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

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

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

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

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

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

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

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

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

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

3
application/src/main/resources/logback.xml

@ -41,9 +41,6 @@
<!-- <logger name="org.thingsboard.server.service.queue.TbMsgPackProcessingContext" level="DEBUG" /> -->
<logger name="com.microsoft.azure.servicebus.primitives.CoreMessageReceiver" level="OFF" />
<logger name="org.thingsboard.server.transport" level="TRACE" />
<logger name="org.thingsboard.server.coap.transport" level="TRACE" />
<logger name="org.eclipse" level="TRACE" />
<root level="INFO">
<appender-ref ref="STDOUT"/>

8
application/src/test/java/org/thingsboard/server/transport/coap/rpc/AbstractCoapServerSideRpcDefaultIntegrationTest.java

@ -43,7 +43,7 @@ public abstract class AbstractCoapServerSideRpcDefaultIntegrationTest extends Ab
String setGpioRequest = "{\"method\":\"setGpio\",\"params\":{\"pin\": \"24\",\"value\": 1},\"timeout\": 6000}";
String deviceId = savedDevice.getId().getId().toString();
doPostAsync("/api/plugins/rpc/oneway/" + deviceId, setGpioRequest, String.class, status().is(409),
doPostAsync("/api/rpc/oneway/" + deviceId, setGpioRequest, String.class, status().is(504),
asyncContextTimeoutToUseRpcPlugin);
}
@ -52,7 +52,7 @@ public abstract class AbstractCoapServerSideRpcDefaultIntegrationTest extends Ab
String setGpioRequest = "{\"method\":\"setGpio\",\"params\":{\"pin\": \"25\",\"value\": 1}}";
String nonExistentDeviceId = Uuids.timeBased().toString();
String result = doPostAsync("/api/plugins/rpc/oneway/" + nonExistentDeviceId, setGpioRequest, String.class,
String result = doPostAsync("/api/rpc/oneway/" + nonExistentDeviceId, setGpioRequest, String.class,
status().isNotFound());
Assert.assertEquals(AccessValidator.DEVICE_WITH_REQUESTED_ID_NOT_FOUND, result);
}
@ -62,7 +62,7 @@ public abstract class AbstractCoapServerSideRpcDefaultIntegrationTest extends Ab
String setGpioRequest = "{\"method\":\"setGpio\",\"params\":{\"pin\": \"27\",\"value\": 1},\"timeout\": 6000}";
String deviceId = savedDevice.getId().getId().toString();
doPostAsync("/api/plugins/rpc/twoway/" + deviceId, setGpioRequest, String.class, status().is(409),
doPostAsync("/api/rpc/twoway/" + deviceId, setGpioRequest, String.class, status().is(504),
asyncContextTimeoutToUseRpcPlugin);
}
@ -71,7 +71,7 @@ public abstract class AbstractCoapServerSideRpcDefaultIntegrationTest extends Ab
String setGpioRequest = "{\"method\":\"setGpio\",\"params\":{\"pin\": \"28\",\"value\": 1}}";
String nonExistentDeviceId = Uuids.timeBased().toString();
String result = doPostAsync("/api/plugins/rpc/twoway/" + nonExistentDeviceId, setGpioRequest, String.class,
String result = doPostAsync("/api/rpc/twoway/" + nonExistentDeviceId, setGpioRequest, String.class,
status().isNotFound());
Assert.assertEquals(AccessValidator.DEVICE_WITH_REQUESTED_ID_NOT_FOUND, result);
}

6
application/src/test/java/org/thingsboard/server/transport/coap/rpc/AbstractCoapServerSideRpcIntegrationTest.java

@ -71,7 +71,7 @@ public abstract class AbstractCoapServerSideRpcIntegrationTest extends AbstractC
String setGpioRequest = "{\"method\":\"setGpio\",\"params\":{\"pin\": \"23\",\"value\": 1}}";
String deviceId = savedDevice.getId().getId().toString();
String result = doPostAsync("/api/plugins/rpc/oneway/" + deviceId, setGpioRequest, String.class, status().isOk());
String result = doPostAsync("/api/rpc/oneway/" + deviceId, setGpioRequest, String.class, status().isOk());
latch.await(3, TimeUnit.SECONDS);
@ -99,14 +99,14 @@ public abstract class AbstractCoapServerSideRpcIntegrationTest extends AbstractC
String setGpioRequest = "{\"method\":\"setGpio\",\"params\":{\"pin\": \"26\",\"value\": 1}}";
String deviceId = savedDevice.getId().getId().toString();
String actualResult = doPostAsync("/api/plugins/rpc/twoway/" + deviceId, setGpioRequest, String.class, status().isOk());
String actualResult = doPostAsync("/api/rpc/twoway/" + deviceId, setGpioRequest, String.class, status().isOk());
latch.await(3, TimeUnit.SECONDS);
validateTwoWayStateChangedNotification(callback, 1, expectedResponseResult, actualResult);
latch = new CountDownLatch(1);
actualResult = doPostAsync("/api/plugins/rpc/twoway/" + deviceId, setGpioRequest, String.class, status().isOk());
actualResult = doPostAsync("/api/rpc/twoway/" + deviceId, setGpioRequest, String.class, status().isOk());
latch.await(3, TimeUnit.SECONDS);
validateTwoWayStateChangedNotification(callback, 2, expectedResponseResult, actualResult);

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

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

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

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

8
application/src/test/java/org/thingsboard/server/transport/mqtt/rpc/AbstractMqttServerSideRpcDefaultIntegrationTest.java

@ -46,7 +46,7 @@ public abstract class AbstractMqttServerSideRpcDefaultIntegrationTest extends Ab
String setGpioRequest = "{\"method\":\"setGpio\",\"params\":{\"pin\": \"24\",\"value\": 1},\"timeout\": 6000}";
String deviceId = savedDevice.getId().getId().toString();
doPostAsync("/api/plugins/rpc/oneway/" + deviceId, setGpioRequest, String.class, status().is(409),
doPostAsync("/api/rpc/oneway/" + deviceId, setGpioRequest, String.class, status().is(504),
asyncContextTimeoutToUseRpcPlugin);
}
@ -55,7 +55,7 @@ public abstract class AbstractMqttServerSideRpcDefaultIntegrationTest extends Ab
String setGpioRequest = "{\"method\":\"setGpio\",\"params\":{\"pin\": \"25\",\"value\": 1}}";
String nonExistentDeviceId = Uuids.timeBased().toString();
String result = doPostAsync("/api/plugins/rpc/oneway/" + nonExistentDeviceId, setGpioRequest, String.class,
String result = doPostAsync("/api/rpc/oneway/" + nonExistentDeviceId, setGpioRequest, String.class,
status().isNotFound());
Assert.assertEquals(AccessValidator.DEVICE_WITH_REQUESTED_ID_NOT_FOUND, result);
}
@ -65,7 +65,7 @@ public abstract class AbstractMqttServerSideRpcDefaultIntegrationTest extends Ab
String setGpioRequest = "{\"method\":\"setGpio\",\"params\":{\"pin\": \"27\",\"value\": 1},\"timeout\": 6000}";
String deviceId = savedDevice.getId().getId().toString();
doPostAsync("/api/plugins/rpc/twoway/" + deviceId, setGpioRequest, String.class, status().is(409),
doPostAsync("/api/rpc/twoway/" + deviceId, setGpioRequest, String.class, status().is(504),
asyncContextTimeoutToUseRpcPlugin);
}
@ -74,7 +74,7 @@ public abstract class AbstractMqttServerSideRpcDefaultIntegrationTest extends Ab
String setGpioRequest = "{\"method\":\"setGpio\",\"params\":{\"pin\": \"28\",\"value\": 1}}";
String nonExistentDeviceId = Uuids.timeBased().toString();
String result = doPostAsync("/api/plugins/rpc/twoway/" + nonExistentDeviceId, setGpioRequest, String.class,
String result = doPostAsync("/api/rpc/twoway/" + nonExistentDeviceId, setGpioRequest, String.class,
status().isNotFound());
Assert.assertEquals(AccessValidator.DEVICE_WITH_REQUESTED_ID_NOT_FOUND, result);
}

8
application/src/test/java/org/thingsboard/server/transport/mqtt/rpc/AbstractMqttServerSideRpcIntegrationTest.java

@ -69,7 +69,7 @@ public abstract class AbstractMqttServerSideRpcIntegrationTest extends AbstractM
String setGpioRequest = "{\"method\":\"setGpio\",\"params\":{\"pin\": \"23\",\"value\": 1}}";
String deviceId = savedDevice.getId().getId().toString();
String result = doPostAsync("/api/plugins/rpc/oneway/" + deviceId, setGpioRequest, String.class, status().isOk());
String result = doPostAsync("/api/rpc/oneway/" + deviceId, setGpioRequest, String.class, status().isOk());
Assert.assertTrue(StringUtils.isEmpty(result));
latch.await(3, TimeUnit.SECONDS);
assertEquals(MqttQoS.AT_MOST_ONCE.value(), callback.getQoS());
@ -95,7 +95,7 @@ public abstract class AbstractMqttServerSideRpcIntegrationTest extends AbstractM
String setGpioRequest = "{\"method\":\"setGpio\",\"params\":{\"pin\": \"26\",\"value\": 1}}";
String deviceId = savedDevice.getId().getId().toString();
String result = doPostAsync("/api/plugins/rpc/twoway/" + deviceId, setGpioRequest, String.class, status().isOk());
String result = doPostAsync("/api/rpc/twoway/" + deviceId, setGpioRequest, String.class, status().isOk());
String expected = "{\"value1\":\"A\",\"value2\":\"B\"}";
latch.await(3, TimeUnit.SECONDS);
Assert.assertEquals(expected, result);
@ -130,7 +130,7 @@ public abstract class AbstractMqttServerSideRpcIntegrationTest extends AbstractM
String setGpioRequest = "{\"method\": \"toggle_gpio\", \"params\": {\"pin\":1}}";
String deviceId = savedDevice.getId().getId().toString();
String result = doPostAsync("/api/plugins/rpc/oneway/" + deviceId, setGpioRequest, String.class, status().isOk());
String result = doPostAsync("/api/rpc/oneway/" + deviceId, setGpioRequest, String.class, status().isOk());
Assert.assertTrue(StringUtils.isEmpty(result));
latch.await(3, TimeUnit.SECONDS);
assertEquals(MqttQoS.AT_MOST_ONCE.value(), callback.getQoS());
@ -156,7 +156,7 @@ public abstract class AbstractMqttServerSideRpcIntegrationTest extends AbstractM
String setGpioRequest = "{\"method\": \"toggle_gpio\", \"params\": {\"pin\":1}}";
String deviceId = savedDevice.getId().getId().toString();
String result = doPostAsync("/api/plugins/rpc/twoway/" + deviceId, setGpioRequest, String.class, status().isOk());
String result = doPostAsync("/api/rpc/twoway/" + deviceId, setGpioRequest, String.class, status().isOk());
latch.await(3, TimeUnit.SECONDS);
String expected = "{\"success\":true}";
assertEquals(expected, result);

2
application/src/test/java/org/thingsboard/server/transport/mqtt/rpc/AbstractMqttServerSideRpcProtoIntegrationTest.java

@ -131,7 +131,7 @@ public abstract class AbstractMqttServerSideRpcProtoIntegrationTest extends Abst
String setGpioRequest = "{\"method\":\"setGpio\",\"params\":{\"pin\": \"26\",\"value\": 1}}";
String deviceId = savedDevice.getId().getId().toString();
String result = doPostAsync("/api/plugins/rpc/twoway/" + deviceId, setGpioRequest, String.class, status().isOk());
String result = doPostAsync("/api/rpc/twoway/" + deviceId, setGpioRequest, String.class, status().isOk());
String expected = "{\"payload\":\"{\\\"value1\\\":\\\"A\\\",\\\"value2\\\":\\\"B\\\"}\"}";
latch.await(3, TimeUnit.SECONDS);
Assert.assertEquals(expected, result);

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

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

1
common/queue/src/main/proto/queue.proto

@ -339,6 +339,7 @@ message ToDeviceRpcRequestMsg {
message ToDeviceRpcResponseMsg {
int32 requestId = 1;
string payload = 2;
string error = 3;
}
message UplinkNotificationMsg {

20
common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/client/DefaultCoapClientContext.java

@ -28,6 +28,7 @@ import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.DeviceProfile;
import org.thingsboard.server.common.data.DeviceTransportType;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.device.data.PowerMode;
import org.thingsboard.server.common.data.device.data.PowerSavingConfiguration;
import org.thingsboard.server.common.data.device.profile.CoapDeviceProfileTransportConfiguration;
@ -507,6 +508,7 @@ public class DefaultCoapClientContext implements CoapClientContext {
return;
}
boolean sent = false;
String error = null;
boolean conRequest = AbstractSyncSessionCallback.isConRequest(state.getRpc());
try {
Response response = state.getAdaptor().convertToPublish(conRequest, msg, state.getConfiguration().getRpcRequestDynamicMessageBuilder());
@ -515,15 +517,12 @@ public class DefaultCoapClientContext implements CoapClientContext {
if (msg.getPersisted() && conRequest) {
transportContext.getRpcAwaitingAck().put(requestId, msg);
transportContext.getScheduler().schedule(() -> {
TransportProtos.ToDeviceRpcRequestMsg awaitingAckMsg = transportContext.getRpcAwaitingAck().remove(requestId);
if (awaitingAckMsg != null) {
transportService.process(state.getSession(), msg, true, TransportServiceCallback.EMPTY);
}
transportContext.getRpcAwaitingAck().remove(requestId);
}, Math.max(0, msg.getExpirationTime() - System.currentTimeMillis()), TimeUnit.MILLISECONDS);
response.addMessageObserver(new TbCoapMessageObserver(requestId, id -> {
TransportProtos.ToDeviceRpcRequestMsg rpcRequestMsg = transportContext.getRpcAwaitingAck().remove(id);
if (rpcRequestMsg != null) {
transportService.process(state.getSession(), rpcRequestMsg, false, TransportServiceCallback.EMPTY);
transportService.process(state.getSession(), rpcRequestMsg, TransportServiceCallback.EMPTY);
}
}, null));
}
@ -536,9 +535,16 @@ public class DefaultCoapClientContext implements CoapClientContext {
log.trace("Failed to reply due to error", e);
cancelObserveRelation(state.getRpc());
cancelRpcSubscription(state);
error = "Failed to convert device RPC command to CoAP msg";
} catch (Exception e) {
error = "Internal error: " + e.getMessage();
} finally {
if (msg.getPersisted() && !conRequest) {
transportService.process(state.getSession(), msg, sent, TransportServiceCallback.EMPTY);
if (StringUtils.isNotEmpty(error)) {
transportService.process(state.getSession(),
TransportProtos.ToDeviceRpcResponseMsg.newBuilder()
.setRequestId(msg.getRequestId()).setError(error).build(), TransportServiceCallback.EMPTY);
} else if (msg.getPersisted() && !conRequest && sent) {
transportService.process(state.getSession(), msg, TransportServiceCallback.EMPTY);
}
}
}

2
common/transport/http/src/main/java/org/thingsboard/server/transport/http/DeviceApiController.java

@ -409,7 +409,7 @@ public class DeviceApiController implements TbTransportService {
public void onToDeviceRpcRequest(UUID sessionId, ToDeviceRpcRequestMsg msg) {
log.trace("[{}] Received RPC command to device", sessionId);
responseWriter.setResult(new ResponseEntity<>(JsonConverter.toJson(msg, true).toString(), HttpStatus.OK));
transportService.process(sessionInfo, msg, false, TransportServiceCallback.EMPTY);
transportService.process(sessionInfo, msg, TransportServiceCallback.EMPTY);
}
@Override

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

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

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

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

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

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

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

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

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

@ -84,7 +84,6 @@ public class LwM2mSessionMsgListener implements GenericFutureListener<Future<? s
public void onToDeviceRpcRequest(UUID sessionId, ToDeviceRpcRequestMsg toDeviceRequest) {
log.trace("[{}] Received RPC command to device", sessionId);
this.rpcHandler.onToDeviceRpcRequest(toDeviceRequest, this.sessionInfo);
transportService.process(sessionInfo, toDeviceRequest, false, TransportServiceCallback.EMPTY);
}
@Override

5
common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClient.java

@ -51,6 +51,7 @@ import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Future;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.stream.Collectors;
@ -113,6 +114,9 @@ public class LwM2mClient implements Serializable {
private boolean firstEdrxDownlink = true;
@Getter
private final AtomicInteger retryAttempts;
public Object clone() throws CloneNotSupportedException {
return super.clone();
}
@ -124,6 +128,7 @@ public class LwM2mClient implements Serializable {
this.resources = new ConcurrentHashMap<>();
this.state = LwM2MClientState.CREATED;
this.lock = new ReentrantLock();
this.retryAttempts = new AtomicInteger(0);
}
public void init(ValidateDeviceCredentialsResponse credentials, UUID sessionId) {

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

@ -93,7 +93,12 @@ public class LwM2mClientContextImpl implements LwM2mClientContext {
log.debug("Fetched clients from store: {}", fetchedClients);
fetchedClients.forEach(client -> {
lwM2mClientsByEndpoint.put(client.getEndpoint(), client);
updateFetchedClient(nodeId, client);
try {
client.lock();
updateFetchedClient(nodeId, client);
} finally {
client.unlock();
}
});
}

38
common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/ota/DefaultLwM2MOtaUpdateService.java

@ -45,11 +45,11 @@ import org.thingsboard.server.transport.lwm2m.server.downlink.TbLwM2MExecuteRequ
import org.thingsboard.server.transport.lwm2m.server.downlink.TbLwM2MWriteReplaceRequest;
import org.thingsboard.server.transport.lwm2m.server.downlink.TbLwM2MWriteResponseCallback;
import org.thingsboard.server.transport.lwm2m.server.log.LwM2MTelemetryLogService;
import org.thingsboard.server.transport.lwm2m.server.ota.firmware.LwM2MClientFwOtaInfo;
import org.thingsboard.server.transport.lwm2m.server.ota.firmware.LwM2MFirmwareUpdateStrategy;
import org.thingsboard.server.transport.lwm2m.server.ota.firmware.FirmwareDeliveryMethod;
import org.thingsboard.server.transport.lwm2m.server.ota.firmware.FirmwareUpdateResult;
import org.thingsboard.server.transport.lwm2m.server.ota.firmware.FirmwareUpdateState;
import org.thingsboard.server.transport.lwm2m.server.ota.firmware.LwM2MClientFwOtaInfo;
import org.thingsboard.server.transport.lwm2m.server.ota.firmware.LwM2MFirmwareUpdateStrategy;
import org.thingsboard.server.transport.lwm2m.server.ota.software.LwM2MClientSwOtaInfo;
import org.thingsboard.server.transport.lwm2m.server.ota.software.LwM2MSoftwareUpdateStrategy;
import org.thingsboard.server.transport.lwm2m.server.ota.software.SoftwareUpdateResult;
@ -178,7 +178,7 @@ public class DefaultLwM2MOtaUpdateService extends LwM2MExecutorAwareService impl
attributesToFetch.add(SOFTWARE_TAG);
attributesToFetch.add(SOFTWARE_URL);
}
var clientSettings = clientContext.getProfile(client.getProfileId()).getClientLwM2mSettings();
onFirmwareStrategyUpdate(client, clientSettings);
@ -283,8 +283,17 @@ public class DefaultLwM2MOtaUpdateService extends LwM2MExecutorAwareService impl
}
fwInfo.setUpdateState(state);
Optional<OtaPackageUpdateStatus> status = toOtaPackageUpdateStatus(state);
status.ifPresent(otaStatus -> sendStateUpdateToTelemetry(client, fwInfo,
otaStatus, "Firmware Update State: " + state.name()));
if (FirmwareUpdateState.IDLE.equals(state) && DOWNLOADING.equals(fwInfo.getStatus())) {
fwInfo.setFailedPackageId(fwInfo.getTargetPackageId());
status = Optional.of(FAILED);
}
status.ifPresent(otaStatus -> {
fwInfo.setStatus(otaStatus);
sendStateUpdateToTelemetry(client, fwInfo,
otaStatus, "Firmware Update State: " + state.name());
});
update(fwInfo);
}
@ -294,8 +303,19 @@ public class DefaultLwM2MOtaUpdateService extends LwM2MExecutorAwareService impl
LwM2MClientFwOtaInfo fwInfo = getOrInitFwInfo(client);
FirmwareUpdateResult result = FirmwareUpdateResult.fromUpdateResultFwByCode(code.intValue());
Optional<OtaPackageUpdateStatus> status = toOtaPackageUpdateStatus(result);
status.ifPresent(otaStatus -> sendStateUpdateToTelemetry(client, fwInfo,
otaStatus, "Firmware Update Result: " + result.name()));
if (FirmwareUpdateResult.INITIAL.equals(result) && OtaPackageUpdateStatus.UPDATING.equals(fwInfo.getStatus())) {
status = Optional.of(UPDATED);
fwInfo.setRetryAttempts(0);
}
status.ifPresent(otaStatus -> {
fwInfo.setStatus(otaStatus);
sendStateUpdateToTelemetry(client, fwInfo,
otaStatus, "Firmware Update Result: " + result.name());
}
);
if (result.isAgain() && fwInfo.getRetryAttempts() <= 2) {
fwInfo.setRetryAttempts(fwInfo.getRetryAttempts() + 1);
startFirmwareUpdateIfNeeded(client, fwInfo);
@ -369,7 +389,7 @@ public class DefaultLwM2MOtaUpdateService extends LwM2MExecutorAwareService impl
private void startFirmwareUpdateIfNeeded(LwM2mClient client, LwM2MClientFwOtaInfo fwInfo) {
try {
if (!fwInfo.isSupported()) {
if (!fwInfo.isSupported() && fwInfo.isAssigned()) {
log.debug("[{}] Fw update is not supported: {}", client.getEndpoint(), fwInfo);
sendStateUpdateToTelemetry(client, fwInfo, OtaPackageUpdateStatus.FAILED, "Client does not support firmware update or profile misconfiguration!");
} else if (fwInfo.isUpdateRequired()) {
@ -389,7 +409,7 @@ public class DefaultLwM2MOtaUpdateService extends LwM2MExecutorAwareService impl
private void startSoftwareUpdateIfNeeded(LwM2mClient client, LwM2MClientSwOtaInfo swInfo) {
try {
if (!swInfo.isSupported()) {
if (!swInfo.isSupported() && swInfo.isAssigned()) {
log.debug("[{}] Sw update is not supported: {}", client.getEndpoint(), swInfo);
sendStateUpdateToTelemetry(client, swInfo, OtaPackageUpdateStatus.FAILED, "Client does not support software update or profile misconfiguration!");
} else if (swInfo.isUpdateRequired()) {

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

@ -20,6 +20,7 @@ import lombok.Data;
import lombok.NoArgsConstructor;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.ota.OtaPackageType;
import org.thingsboard.server.common.data.ota.OtaPackageUpdateStatus;
import java.util.Optional;
@ -39,6 +40,7 @@ public abstract class LwM2MClientOtaInfo<Strategy, State, Result> {
protected Strategy strategy;
protected State updateState;
protected Result result;
protected OtaPackageUpdateStatus status;
protected String failedPackageId;
protected int retryAttempts;
@ -91,6 +93,11 @@ public abstract class LwM2MClientOtaInfo<Strategy, State, Result> {
return StringUtils.isNotEmpty(currentName) || StringUtils.isNotEmpty(currentVersion) || StringUtils.isNotEmpty(currentVersion3);
}
@JsonIgnore
public boolean isAssigned() {
return StringUtils.isNotEmpty(targetName) && StringUtils.isNotEmpty(targetVersion);
}
public abstract void update(Result result);
protected static String getPackageId(String name, String version) {
@ -99,4 +106,7 @@ public abstract class LwM2MClientOtaInfo<Strategy, State, Result> {
public abstract OtaPackageType getType();
public String getTargetPackageId() {
return getPackageId(targetName, targetVersion);
}
}

164
common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/rpc/DefaultLwM2MRpcRequestHandler.java

@ -77,96 +77,89 @@ public class DefaultLwM2MRpcRequestHandler implements LwM2MRpcRequestHandler {
private final LwM2mUplinkMsgHandler uplinkHandler;
private final LwM2mDownlinkMsgHandler downlinkHandler;
private final LwM2MTelemetryLogService logService;
private final Map<UUID, Long> rpcSubscriptions = new ConcurrentHashMap<>();
@Override
public void onToDeviceRpcRequest(TransportProtos.ToDeviceRpcRequestMsg rpcRequest, TransportProtos.SessionInfoProto sessionInfo) {
this.cleanupOldSessions();
UUID requestUUID = new UUID(rpcRequest.getRequestIdMSB(), rpcRequest.getRequestIdLSB());
log.debug("Received params: {}", rpcRequest.getParams());
// We use this map to protect from browser issue that the same command is sent twice.
// TODO: This is probably not the best place and should be moved to DeviceActor
if (!this.rpcSubscriptions.containsKey(requestUUID)) {
LwM2mOperationType operationType = LwM2mOperationType.fromType(rpcRequest.getMethodName());
if (operationType == null) {
this.sendErrorRpcResponse(sessionInfo, rpcRequest.getRequestId(), ResponseCode.METHOD_NOT_ALLOWED.getName(), "Unsupported operation type: " + rpcRequest.getMethodName());
return;
}
LwM2mClient client = clientContext.getClientBySessionInfo(sessionInfo);
if (client.getRegistration() == null) {
this.sendErrorRpcResponse(sessionInfo, rpcRequest.getRequestId(), ResponseCode.INTERNAL_SERVER_ERROR.getName(), "Registration is empty");
return;
}
try {
if (operationType.isHasObjectId()) {
String objectId = getIdFromParameters(client, rpcRequest);
LwM2mOperationType operationType = LwM2mOperationType.fromType(rpcRequest.getMethodName());
if (operationType == null) {
this.sendErrorRpcResponse(sessionInfo, rpcRequest.getRequestId(), ResponseCode.METHOD_NOT_ALLOWED, "Unsupported operation type: " + rpcRequest.getMethodName());
return;
}
LwM2mClient client = clientContext.getClientBySessionInfo(sessionInfo);
if (client.getRegistration() == null) {
this.sendErrorRpcResponse(sessionInfo, rpcRequest.getRequestId(), ResponseCode.INTERNAL_SERVER_ERROR, "Registration is empty");
return;
}
try {
if (operationType.isHasObjectId()) {
String objectId = getIdFromParameters(client, rpcRequest);
switch (operationType) {
case READ:
sendReadRequest(client, rpcRequest, objectId);
break;
case OBSERVE:
sendObserveRequest(client, rpcRequest, objectId);
break;
case DISCOVER:
sendDiscoverRequest(client, rpcRequest, objectId);
break;
case EXECUTE:
sendExecuteRequest(client, rpcRequest, objectId);
break;
case WRITE_ATTRIBUTES:
sendWriteAttributesRequest(client, rpcRequest, objectId);
break;
case OBSERVE_CANCEL:
sendCancelObserveRequest(client, rpcRequest, objectId);
break;
case DELETE:
sendDeleteRequest(client, rpcRequest, objectId);
break;
case WRITE_UPDATE:
sendWriteUpdateRequest(client, rpcRequest, objectId);
break;
case WRITE_REPLACE:
sendWriteReplaceRequest(client, rpcRequest, objectId);
break;
default:
throw new IllegalArgumentException("Unsupported operation: " + operationType.name());
}
} else if (operationType.isComposite()) {
if (clientContext.isComposite(client)) {
switch (operationType) {
case READ:
sendReadRequest(client, rpcRequest, objectId);
break;
case OBSERVE:
sendObserveRequest(client, rpcRequest, objectId);
break;
case DISCOVER:
sendDiscoverRequest(client, rpcRequest, objectId);
break;
case EXECUTE:
sendExecuteRequest(client, rpcRequest, objectId);
break;
case WRITE_ATTRIBUTES:
sendWriteAttributesRequest(client, rpcRequest, objectId);
break;
case OBSERVE_CANCEL:
sendCancelObserveRequest(client, rpcRequest, objectId);
break;
case DELETE:
sendDeleteRequest(client, rpcRequest, objectId);
break;
case WRITE_UPDATE:
sendWriteUpdateRequest(client, rpcRequest, objectId);
case READ_COMPOSITE:
sendReadCompositeRequest(client, rpcRequest);
break;
case WRITE_REPLACE:
sendWriteReplaceRequest(client, rpcRequest, objectId);
case WRITE_COMPOSITE:
sendWriteCompositeRequest(client, rpcRequest);
break;
default:
throw new IllegalArgumentException("Unsupported operation: " + operationType.name());
}
} else if (operationType.isComposite()) {
if (clientContext.isComposite(client)) {
switch (operationType) {
case READ_COMPOSITE:
sendReadCompositeRequest(client, rpcRequest);
break;
case WRITE_COMPOSITE:
sendWriteCompositeRequest(client, rpcRequest);
break;
default:
throw new IllegalArgumentException("Unsupported operation: " + operationType.name());
}
} else {
this.sendErrorRpcResponse(sessionInfo, rpcRequest.getRequestId(),
ResponseCode.INTERNAL_SERVER_ERROR.getName(), "This device does not support Composite Operation");
}
} else {
switch (operationType) {
case OBSERVE_CANCEL_ALL:
sendCancelAllObserveRequest(client, rpcRequest);
break;
case OBSERVE_READ_ALL:
sendObserveAllRequest(client, rpcRequest);
break;
case DISCOVER_ALL:
sendDiscoverAllRequest(client, rpcRequest);
break;
case FW_UPDATE:
//TODO: implement and add break statement
default:
throw new IllegalArgumentException("Unsupported operation: " + operationType.name());
}
this.sendErrorRpcResponse(sessionInfo, rpcRequest.getRequestId(),
ResponseCode.INTERNAL_SERVER_ERROR, "This device does not support Composite Operation");
}
} else {
switch (operationType) {
case OBSERVE_CANCEL_ALL:
sendCancelAllObserveRequest(client, rpcRequest);
break;
case OBSERVE_READ_ALL:
sendObserveAllRequest(client, rpcRequest);
break;
case DISCOVER_ALL:
sendDiscoverAllRequest(client, rpcRequest);
break;
case FW_UPDATE:
//TODO: implement and add break statement
default:
throw new IllegalArgumentException("Unsupported operation: " + operationType.name());
}
} catch (IllegalArgumentException e) {
this.sendErrorRpcResponse(sessionInfo, rpcRequest.getRequestId(), ResponseCode.BAD_REQUEST.getName(), e.getMessage());
}
} catch (IllegalArgumentException e) {
this.sendErrorRpcResponse(sessionInfo, rpcRequest.getRequestId(), ResponseCode.BAD_REQUEST, e.getMessage());
}
}
@ -312,23 +305,12 @@ public class DefaultLwM2MRpcRequestHandler implements LwM2MRpcRequestHandler {
}
}
private void sendErrorRpcResponse(TransportProtos.SessionInfoProto sessionInfo, int requestId, String result, String error) {
String payload = JacksonUtil.toString(JacksonUtil.newObjectNode().put("result", result).put("error", error));
TransportProtos.ToDeviceRpcResponseMsg msg = TransportProtos.ToDeviceRpcResponseMsg.newBuilder().setRequestId(requestId).setPayload(payload).build();
private void sendErrorRpcResponse(TransportProtos.SessionInfoProto sessionInfo, int requestId, ResponseCode result, String error) {
String payload = JacksonUtil.toString(LwM2MRpcResponseBody.builder().result(result.getName()).error(error).build());
TransportProtos.ToDeviceRpcResponseMsg msg = TransportProtos.ToDeviceRpcResponseMsg.newBuilder().setRequestId(requestId).setError(payload).build();
transportService.process(sessionInfo, msg, null);
}
private void cleanupOldSessions() {
log.debug("Before rpcSubscriptions.size(): [{}]", rpcSubscriptions.size());
if (rpcSubscriptions.size() > 0) {
long currentTime = System.currentTimeMillis();
Set<UUID> rpcSubscriptionsToRemove = rpcSubscriptions.entrySet().stream().filter(kv -> currentTime > kv.getValue()).map(Map.Entry::getKey).collect(Collectors.toSet());
log.debug("RpcSubscriptionsToRemove: [{}]", rpcSubscriptionsToRemove);
rpcSubscriptionsToRemove.forEach(rpcSubscriptions::remove);
}
log.debug("After rpcSubscriptions.size(): [{}]", rpcSubscriptions.size());
}
@Override
public void onToDeviceRpcResponse(TransportProtos.ToDeviceRpcResponseMsg toDeviceResponse, TransportProtos.SessionInfoProto sessionInfo) {
log.debug("OnToDeviceRpcResponse: [{}], sessionUUID: [{}]", toDeviceResponse, new UUID(sessionInfo.getSessionIdMSB(), sessionInfo.getSessionIdLSB()));

1
common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/rpc/LwM2MRpcResponseBody.java

@ -27,6 +27,5 @@ public class LwM2MRpcResponseBody {
private String result;
private String value;
private String error;
private String info;
}

23
common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/rpc/RpcDownlinkRequestCallbackProxy.java

@ -16,12 +16,17 @@
package org.thingsboard.server.transport.lwm2m.server.rpc;
import org.eclipse.leshan.core.ResponseCode;
import org.eclipse.leshan.core.request.exception.ClientSleepingException;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.transport.TransportService;
import org.thingsboard.server.common.transport.TransportServiceCallback;
import org.thingsboard.server.gen.transport.TransportProtos;
import org.thingsboard.server.transport.lwm2m.server.client.LwM2mClient;
import org.thingsboard.server.transport.lwm2m.server.downlink.DownlinkRequestCallback;
import java.util.concurrent.TimeoutException;
public abstract class RpcDownlinkRequestCallbackProxy<R, T> implements DownlinkRequestCallback<R, T> {
private final TransportService transportService;
@ -39,6 +44,7 @@ public abstract class RpcDownlinkRequestCallbackProxy<R, T> implements DownlinkR
@Override
public void onSuccess(R request, T response) {
transportService.process(client.getSession(), this.request, TransportServiceCallback.EMPTY);
sendRpcReplyOnSuccess(response);
if (callback != null) {
callback.onSuccess(request, response);
@ -55,18 +61,23 @@ public abstract class RpcDownlinkRequestCallbackProxy<R, T> implements DownlinkR
@Override
public void onError(String params, Exception e) {
sendRpcReplyOnError(e);
if (!(e instanceof TimeoutException || e instanceof ClientSleepingException)) {
sendRpcReplyOnError(e);
}
if (callback != null) {
callback.onError(params, e);
}
}
protected void reply(LwM2MRpcResponseBody response) {
TransportProtos.ToDeviceRpcResponseMsg msg = TransportProtos.ToDeviceRpcResponseMsg.newBuilder()
.setPayload(JacksonUtil.toString(response))
.setRequestId(request.getRequestId())
.build();
transportService.process(client.getSession(), msg, null);
TransportProtos.ToDeviceRpcResponseMsg.Builder msg = TransportProtos.ToDeviceRpcResponseMsg.newBuilder().setRequestId(request.getRequestId());
String responseAsString = JacksonUtil.toString(response);
if (StringUtils.isEmpty(response.getError())) {
msg.setPayload(responseAsString);
} else {
msg.setError(responseAsString);
}
transportService.process(client.getSession(), msg.build(), null);
}
abstract protected void sendRpcReplyOnSuccess(T response);

9
common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/uplink/DefaultLwM2MUplinkMsgHandler.java

@ -110,11 +110,11 @@ import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.c
import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.convertOtaUpdateValueToString;
import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.fromVersionedIdToObjectId;
import static org.thingsboard.server.transport.lwm2m.server.ota.DefaultLwM2MOtaUpdateService.FW_3_VER_ID;
import static org.thingsboard.server.transport.lwm2m.server.ota.DefaultLwM2MOtaUpdateService.FW_VER_ID;
import static org.thingsboard.server.transport.lwm2m.server.ota.DefaultLwM2MOtaUpdateService.FW_DELIVERY_METHOD;
import static org.thingsboard.server.transport.lwm2m.server.ota.DefaultLwM2MOtaUpdateService.FW_NAME_ID;
import static org.thingsboard.server.transport.lwm2m.server.ota.DefaultLwM2MOtaUpdateService.FW_RESULT_ID;
import static org.thingsboard.server.transport.lwm2m.server.ota.DefaultLwM2MOtaUpdateService.FW_STATE_ID;
import static org.thingsboard.server.transport.lwm2m.server.ota.DefaultLwM2MOtaUpdateService.FW_VER_ID;
import static org.thingsboard.server.transport.lwm2m.server.ota.DefaultLwM2MOtaUpdateService.SW_3_VER_ID;
import static org.thingsboard.server.transport.lwm2m.server.ota.DefaultLwM2MOtaUpdateService.SW_NAME_ID;
import static org.thingsboard.server.transport.lwm2m.server.ota.DefaultLwM2MOtaUpdateService.SW_RESULT_ID;
@ -219,11 +219,16 @@ public class DefaultLwM2MUplinkMsgHandler extends LwM2MExecutorAwareService impl
this.initClientTelemetry(lwM2MClient);
this.initAttributes(lwM2MClient);
otaService.init(lwM2MClient);
lwM2MClient.getRetryAttempts().set(0);
} catch (LwM2MClientStateException stateException) {
if (LwM2MClientState.UNREGISTERED.equals(stateException.getState())) {
log.info("[{}] retry registration due to race condition: [{}].", registration.getEndpoint(), stateException.getState());
// Race condition detected and the client was in progress of unregistration while new registration arrived. Let's try again.
onRegistered(registration, previousObservations);
if (lwM2MClient.getRetryAttempts().incrementAndGet() <= 5) {
context.getScheduler().schedule(() -> onRegistered(registration, previousObservations), 1, TimeUnit.SECONDS);
} else {
logService.log(lwM2MClient, LOG_LWM2M_WARN + ": Client registration failed due to retry attempts: " + lwM2MClient.getRetryAttempts().get());
}
} else {
logService.log(lwM2MClient, LOG_LWM2M_WARN + ": Client registration failed due to invalid state: " + stateException.getState());
}

5
common/transport/mqtt/pom.xml

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

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

@ -249,7 +249,7 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement
int msgId = ((MqttPubAckMessage) msg).variableHeader().messageId();
TransportProtos.ToDeviceRpcRequestMsg rpcRequest = rpcAwaitingAck.remove(msgId);
if (rpcRequest != null) {
transportService.process(deviceSessionCtx.getSessionInfo(), rpcRequest, false, TransportServiceCallback.EMPTY);
transportService.process(deviceSessionCtx.getSessionInfo(), rpcRequest, TransportServiceCallback.EMPTY);
}
break;
default:
@ -304,6 +304,7 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement
}
} catch (RuntimeException | AdaptorException e) {
log.warn("[{}] Failed to process publish msg [{}][{}]", sessionId, topicName, msgId, e);
ctx.close();
}
}
@ -829,18 +830,22 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement
if (rpcRequest.getPersisted() && isAckExpected(payload)) {
rpcAwaitingAck.put(msgId, rpcRequest);
context.getScheduler().schedule(() -> {
TransportProtos.ToDeviceRpcRequestMsg awaitingAckMsg = rpcAwaitingAck.remove(msgId);
if (awaitingAckMsg != null) {
transportService.process(deviceSessionCtx.getSessionInfo(), rpcRequest, true, TransportServiceCallback.EMPTY);
}
rpcAwaitingAck.remove(msgId);
}, Math.max(0, rpcRequest.getExpirationTime() - System.currentTimeMillis()), TimeUnit.MILLISECONDS);
}
var cf = publish(payload, deviceSessionCtx);
if (rpcRequest.getPersisted() && !isAckExpected(payload)) {
cf.addListener(result -> transportService.process(deviceSessionCtx.getSessionInfo(), rpcRequest, result.cause() != null, TransportServiceCallback.EMPTY));
cf.addListener(result -> {
if (result.cause() == null) {
transportService.process(deviceSessionCtx.getSessionInfo(), rpcRequest, TransportServiceCallback.EMPTY);
}
});
}
});
} catch (Exception e) {
transportService.process(deviceSessionCtx.getSessionInfo(),
TransportProtos.ToDeviceRpcResponseMsg.newBuilder()
.setRequestId(rpcRequest.getRequestId()).setError("Failed to convert device RPC command to MQTT msg").build(), TransportServiceCallback.EMPTY);
log.trace("[{}] Failed to convert device RPC command to MQTT msg", sessionId, e);
}
}

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

@ -101,13 +101,18 @@ public class GatewayDeviceSessionCtx extends MqttDeviceAwareSessionContext imple
payload -> {
ChannelFuture channelFuture = parent.writeAndFlush(payload);
if (request.getPersisted()) {
channelFuture.addListener(future ->
transportService.process(getSessionInfo(), request, future.cause() != null, TransportServiceCallback.EMPTY)
);
channelFuture.addListener(future -> {
if (future.cause() == null) {
transportService.process(getSessionInfo(), request, TransportServiceCallback.EMPTY);
}
});
}
}
);
} catch (Exception e) {
transportService.process(getSessionInfo(),
TransportProtos.ToDeviceRpcResponseMsg.newBuilder()
.setRequestId(request.getRequestId()).setError("Failed to convert device RPC command to MQTT msg").build(), TransportServiceCallback.EMPTY);
log.trace("[{}] Failed to convert device attributes response to MQTT msg", sessionId, e);
}
}

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

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

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

@ -0,0 +1,61 @@
/**
* Copyright © 2016-2021 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.transport.mqtt.session;
import org.junit.Test;
import java.util.WeakHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import static org.awaitility.Awaitility.await;
import static org.junit.Assert.assertTrue;
import static org.mockito.BDDMockito.willCallRealMethod;
import static org.mockito.Mockito.mock;
public class GatewaySessionHandlerTest {
@Test
public void givenWeakHashMap_WhenGC_thenMapIsEmpty() {
WeakHashMap<String, Lock> map = new WeakHashMap<>();
String deviceName = new String("device"); //constants are static and doesn't affected by GC, so use new instead
map.put(deviceName, new ReentrantLock());
assertTrue(map.containsKey(deviceName));
deviceName = null;
System.gc();
await().atMost(10, TimeUnit.SECONDS).until(() -> !map.containsKey("device"));
}
@Test
public void givenConcurrentReferenceHashMap_WhenGC_thenMapIsEmpty() {
GatewaySessionHandler gsh = mock(GatewaySessionHandler.class);
willCallRealMethod().given(gsh).createWeakMap();
ConcurrentMap<String, Lock> map = gsh.createWeakMap();
map.put("device", new ReentrantLock());
assertTrue(map.containsKey("device"));
System.gc();
await().atMost(10, TimeUnit.SECONDS).until(() -> !map.containsKey("device"));
}
}

2
common/transport/snmp/src/main/java/org/thingsboard/server/transport/snmp/session/DeviceSessionContext.java

@ -142,7 +142,7 @@ public class DeviceSessionContext extends DeviceAwareSessionContext implements S
public void onToDeviceRpcRequest(UUID sessionId, ToDeviceRpcRequestMsg toDeviceRequest) {
log.trace("[{}] Received RPC command to device", sessionId);
snmpTransportContext.getSnmpTransportService().onToDeviceRpcRequest(this, toDeviceRequest);
snmpTransportContext.getTransportService().process(getSessionInfo(), toDeviceRequest, false, TransportServiceCallback.EMPTY);
snmpTransportContext.getTransportService().process(getSessionInfo(), toDeviceRequest, TransportServiceCallback.EMPTY);
}
@Override

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

@ -110,7 +110,7 @@ public interface TransportService {
void process(SessionInfoProto sessionInfo, ToServerRpcRequestMsg msg, TransportServiceCallback<Void> callback);
void process(SessionInfoProto sessionInfo, ToDeviceRpcRequestMsg msg, boolean isFailedRpc, TransportServiceCallback<Void> callback);
void process(SessionInfoProto sessionInfo, ToDeviceRpcRequestMsg msg, TransportServiceCallback<Void> callback);
void process(SessionInfoProto sessionInfo, SubscriptionInfoProto msg, TransportServiceCallback<Void> callback);

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

@ -580,17 +580,9 @@ public class DefaultTransportService implements TransportService {
}
@Override
public void process(TransportProtos.SessionInfoProto sessionInfo, TransportProtos.ToDeviceRpcRequestMsg msg, boolean isFailedRpc, TransportServiceCallback<Void> callback) {
public void process(TransportProtos.SessionInfoProto sessionInfo, TransportProtos.ToDeviceRpcRequestMsg msg, TransportServiceCallback<Void> callback) {
if (msg.getPersisted()) {
RpcStatus status;
if (isFailedRpc) {
status = RpcStatus.FAILED;
} else if (msg.getOneway()) {
status = RpcStatus.SUCCESSFUL;
} else {
status = RpcStatus.DELIVERED;
}
RpcStatus status = msg.getOneway() ? RpcStatus.SUCCESSFUL : RpcStatus.DELIVERED;
TransportProtos.ToDevicePersistedRpcResponseMsg responseMsg = TransportProtos.ToDevicePersistedRpcResponseMsg.newBuilder()
.setRequestId(msg.getRequestId())

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

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

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

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

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

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

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

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

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

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

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

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

2
dao/src/test/java/org/thingsboard/server/dao/service/BaseDashboardServiceTest.java

@ -248,7 +248,7 @@ public abstract class BaseDashboardServiceTest extends AbstractServiceTest {
return o1.getTitle().compareTo(o2.getTitle());
} else if (order1 == null && order2 != null) {
return 1;
} else if (order2 == null) {
} else if (order2 == null) {
return -1;
} else {
return order1 - order2;

2
msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/MqttClientTest.java

@ -267,7 +267,7 @@ public class MqttClientTest extends AbstractContainerTest {
ListenableFuture<ResponseEntity> future = service.submit(() -> {
try {
return restClient.getRestTemplate()
.postForEntity(HTTPS_URL + "/api/plugins/rpc/twoway/{deviceId}",
.postForEntity(HTTPS_URL + "/api/rpc/twoway/{deviceId}",
mapper.readTree(serverRpcPayload.toString()), String.class,
device.getId());
} catch (IOException e) {

2
msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/MqttGatewayClientTest.java

@ -263,7 +263,7 @@ public class MqttGatewayClientTest extends AbstractContainerTest {
ListenableFuture<ResponseEntity> future = service.submit(() -> {
try {
return restClient.getRestTemplate()
.postForEntity(HTTPS_URL + "/api/plugins/rpc/twoway/{deviceId}",
.postForEntity(HTTPS_URL + "/api/rpc/twoway/{deviceId}",
mapper.readTree(serverRpcPayload.toString()), String.class,
createdDevice.getId());
} catch (IOException e) {

6
rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java

@ -1811,12 +1811,12 @@ public class RestClient implements ClientHttpRequestInterceptor, Closeable {
}
public void handleOneWayDeviceRPCRequest(DeviceId deviceId, JsonNode requestBody) {
restTemplate.postForLocation(baseURL + "/api/plugins/rpc/oneway/{deviceId}", requestBody, deviceId.getId());
restTemplate.postForLocation(baseURL + "/api/rpc/oneway/{deviceId}", requestBody, deviceId.getId());
}
public JsonNode handleTwoWayDeviceRPCRequest(DeviceId deviceId, JsonNode requestBody) {
return restTemplate.exchange(
baseURL + "/api/plugins/rpc/twoway/{deviceId}",
baseURL + "/api/rpc/twoway/{deviceId}",
HttpMethod.POST,
new HttpEntity<>(requestBody),
new ParameterizedTypeReference<JsonNode>() {
@ -2034,7 +2034,7 @@ public class RestClient implements ClientHttpRequestInterceptor, Closeable {
params.put("useStrictDataTypes", Boolean.toString(useStrictDataTypes));
StringBuilder urlBuilder = new StringBuilder(baseURL);
urlBuilder.append("/api/plugins/telemetry/{entityType}/{entityId}/values/timeseries?keys={keys}&interval={interval}&agg={agg}&useStrictDataTypes={useStrictDataTypes}&orderBy={orderBy}");
urlBuilder.append("/api/plugins/telemetry/{entityType}/{entityId}/values/timeseries?keys={keys}&interval={interval}&limit={limit}&agg={agg}&useStrictDataTypes={useStrictDataTypes}&orderBy={orderBy}");
if (startTime != null) {
urlBuilder.append("&startTs={startTs}");

34
rest-client/src/main/resources/logback.xml

@ -0,0 +1,34 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!--
Copyright © 2016-2021 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<!DOCTYPE configuration>
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{ISO8601} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<logger name="org.thingsboard.server" level="INFO" />
<root level="INFO">
<appender-ref ref="STDOUT"/>
</root>
</configuration>

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

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

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

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

8
ui-ngx/src/app/core/api/widget-subscription.ts

@ -663,7 +663,7 @@ export class WidgetSubscription implements IWidgetSubscription {
if (!this.rpcEnabled) {
return throwError(new Error('Rpc disabled!'));
} else {
if (this.rpcRejection && this.rpcRejection.status !== 408) {
if (this.rpcRejection && this.rpcRejection.status !== 504) {
this.rpcRejection = null;
this.rpcErrorText = null;
this.callbacks.onRpcErrorCleared(this);
@ -715,12 +715,10 @@ export class WidgetSubscription implements IWidgetSubscription {
}
this.executingRpcRequest = this.executingSubjects.length > 0;
this.callbacks.rpcStateChanged(this);
if (!this.executingRpcRequest || rejection.status === 408) {
if (!this.executingRpcRequest || rejection.status === 504) {
this.rpcRejection = rejection;
if (rejection.status === 408) {
if (rejection.status === 504) {
this.rpcErrorText = 'Request Timeout.';
} else if (rejection.status === 409) {
this.rpcErrorText = 'Device is offline.';
} else {
this.rpcErrorText = 'Error : ' + rejection.status + ' - ' + rejection.statusText;
const error = this.extractRejectionErrorText(rejection);

4
ui-ngx/src/app/core/http/device.service.ts

@ -130,11 +130,11 @@ export class DeviceService {
}
public sendOneWayRpcCommand(deviceId: string, requestBody: any, config?: RequestConfig): Observable<any> {
return this.http.post<Device>(`/api/plugins/rpc/oneway/${deviceId}`, requestBody, defaultHttpOptionsFromConfig(config));
return this.http.post<Device>(`/api/rpc/oneway/${deviceId}`, requestBody, defaultHttpOptionsFromConfig(config));
}
public sendTwoWayRpcCommand(deviceId: string, requestBody: any, config?: RequestConfig): Observable<any> {
return this.http.post<Device>(`/api/plugins/rpc/twoway/${deviceId}`, requestBody, defaultHttpOptionsFromConfig(config));
return this.http.post<Device>(`/api/rpc/twoway/${deviceId}`, requestBody, defaultHttpOptionsFromConfig(config));
}
public findByQuery(query: DeviceSearchQuery,

4
ui-ngx/src/app/core/interceptors/global-http-interceptor.ts

@ -47,7 +47,7 @@ export class GlobalHttpInterceptor implements HttpInterceptor {
private internalUrlPrefixes = [
'/api/auth/token',
'/api/plugins/rpc'
'/api/rpc'
];
private activeRequests = 0;
@ -142,7 +142,7 @@ export class GlobalHttpInterceptor implements HttpInterceptor {
}
} else if (errorResponse.status === 0 || errorResponse.status === -1) {
this.showError('Unable to connect');
} else if (!req.url.startsWith('/api/plugins/rpc')) {
} else if (!(req.url.startsWith('/api/rpc') || req.url.startsWith('/api/plugins/rpc'))) {
if (errorResponse.status === 404) {
if (!ignoreErrors) {
this.showError(req.method + ': ' + req.url + '<br/>' +

3
ui-ngx/src/app/modules/home/components/dashboard-page/states/state-controller.component.ts

@ -129,7 +129,8 @@ export abstract class StateControllerComponent implements IStateControllerCompon
protected updateStateParam(newState: string, replaceCurrentHistoryUrl = false) {
this.currentState = newState;
if (this.syncStateWithQueryParam) {
const queryParams: Params = {state: encodeURIComponent(this.currentState)};
const state = this.currentState ? encodeURIComponent(this.currentState) : this.currentState;
const queryParams: Params = {state};
this.ngZone.run(() => {
this.router.navigate(
[],

5
ui-ngx/src/app/modules/home/components/device/device-credentials-lwm2m.component.html

@ -60,10 +60,9 @@
<mat-error *ngIf="lwm2mConfigFormGroup.get('client.key').hasError('pattern')">
{{ 'device.lwm2m-security-config.client-key-pattern' | translate }}
</mat-error>
<mat-error *ngIf="(lwm2mConfigFormGroup.get('client.key').hasError('maxlength') ||
lwm2mConfigFormGroup.get('client.key').hasError('minlength'))">
<mat-error *ngIf="lwm2mConfigFormGroup.get('client.key').hasError('length')">
{{ 'device.lwm2m-security-config.client-key-length' | translate: {
count: lenMaxKeyClient
count: allowLengthKey.join(', ')
} }}
</mat-error>
</mat-form-field>

19
ui-ngx/src/app/modules/home/components/device/device-credentials-lwm2m.component.ts

@ -16,6 +16,7 @@
import { Component, forwardRef, OnDestroy } from '@angular/core';
import {
AbstractControl,
ControlValueAccessor,
FormBuilder,
FormGroup,
@ -64,6 +65,7 @@ export class DeviceCredentialsLwm2mComponent implements ControlValueAccessor, Va
securityConfigLwM2MTypes = Object.keys(Lwm2mSecurityType);
credentialTypeLwM2MNamesMap = Lwm2mSecurityTypeTranslationMap;
lenMaxKeyClient = LEN_MAX_PSK;
allowLengthKey: number[];
private destroy$ = new Subject();
private propagateChange = (v: any) => {};
@ -119,7 +121,7 @@ export class DeviceCredentialsLwm2mComponent implements ControlValueAccessor, Va
config.key = this.lwm2mConfigFormGroup.get('client.key').value;
break;
}
this.lwm2mConfigFormGroup.get('client').patchValue(config, {emitEvent: false});
this.lwm2mConfigFormGroup.get('client').reset(config, {emitEvent: false});
this.securityConfigClientUpdateValidators(type);
}
@ -135,11 +137,13 @@ export class DeviceCredentialsLwm2mComponent implements ControlValueAccessor, Va
break;
case Lwm2mSecurityType.PSK:
this.lenMaxKeyClient = LEN_MAX_PSK;
this.allowLengthKey = [32, 64, LEN_MAX_PSK];
this.setValidatorsPskRpk(mode);
this.lwm2mConfigFormGroup.get('client.identity').enable({emitEvent: false});
break;
case Lwm2mSecurityType.RPK:
this.lenMaxKeyClient = LEN_MAX_PUBLIC_KEY_RPK;
this.allowLengthKey = [LEN_MAX_PUBLIC_KEY_RPK];
this.setValidatorsPskRpk(mode);
this.lwm2mConfigFormGroup.get('client.identity').disable({emitEvent: false});
break;
@ -164,13 +168,22 @@ export class DeviceCredentialsLwm2mComponent implements ControlValueAccessor, Va
this.lwm2mConfigFormGroup.get('client.key').setValidators([
Validators.required,
Validators.pattern(KEY_REGEXP_HEX_DEC),
Validators.maxLength(this.lenMaxKeyClient),
Validators.minLength(this.lenMaxKeyClient)
this.maxLength(this.allowLengthKey)
]);
this.lwm2mConfigFormGroup.get('client.key').enable({emitEvent: false});
this.lwm2mConfigFormGroup.get('client.cert').disable({emitEvent: false});
}
private maxLength(keyLengths: number[]) {
return (control: AbstractControl): ValidationErrors | null => {
const value = control.value;
if (keyLengths.some(len => value.length === len)) {
return null;
}
return {length: true};
};
}
private initLwm2mConfigForm = (): FormGroup => {
const formGroup = this.fb.group({
client: this.fb.group({

2
ui-ngx/src/app/modules/home/components/device/device-credentials.component.ts

@ -168,7 +168,7 @@ export class DeviceCredentialsComponent implements ControlValueAccessor, OnInit,
const credentialsType = this.deviceCredentialsFormGroup.get('credentialsType').value as DeviceCredentialsType;
switch (credentialsType) {
case DeviceCredentialsType.ACCESS_TOKEN:
this.deviceCredentialsFormGroup.get('credentialsId').setValidators([Validators.required, Validators.pattern(/^.{1,20}$/)]);
this.deviceCredentialsFormGroup.get('credentialsId').setValidators([Validators.required, Validators.pattern(/^.{1,32}$/)]);
this.deviceCredentialsFormGroup.get('credentialsId').updateValueAndValidity({emitEvent: false});
this.deviceCredentialsFormGroup.get('credentialsValue').setValidators([]);
this.deviceCredentialsFormGroup.get('credentialsValue').updateValueAndValidity({emitEvent: false});

6
ui-ngx/src/app/modules/home/components/profile/device/coap-device-profile-transport-configuration.component.ts

@ -101,9 +101,9 @@ export class CoapDeviceProfileTransportConfigurationComponent implements Control
}),
clientSettings: this.fb.group({
powerMode: [PowerMode.DRX, Validators.required],
edrxCycle: [{disabled: true, value: 0}, [Validators.required, Validators.min(0), Validators.pattern('[0-9]*')]],
psmActivityTimer: [{disabled: true, value: 0}, [Validators.required, Validators.min(0), Validators.pattern('[0-9]*')]],
pagingTransmissionWindow: [{disabled: true, value: 0}, [Validators.required, Validators.min(0), Validators.pattern('[0-9]*')]]
edrxCycle: [{disabled: true, value: 0}, Validators.required],
psmActivityTimer: [{disabled: true, value: 0}, Validators.required],
pagingTransmissionWindow: [{disabled: true, value: 0}, Validators.required]
})}
);
this.coapTransportConfigurationFormGroup.get('coapDeviceTypeConfiguration.coapDeviceType').valueChanges.pipe(

6
ui-ngx/src/app/modules/home/components/profile/device/common/device-profile-common.module.ts

@ -15,13 +15,15 @@
///
import { NgModule } from '@angular/core';
import { PowerModeSettingComponent } from '@home/components/profile/device/common/power-mode-setting.component';
import { PowerModeSettingComponent } from './power-mode-setting.component';
import { SharedModule } from '@shared/shared.module';
import { CommonModule } from '@angular/common';
import { TimeUnitSelectComponent } from './time-unit-select.component';
@NgModule({
declarations: [
PowerModeSettingComponent
PowerModeSettingComponent,
TimeUnitSelectComponent
],
imports: [
CommonModule,

55
ui-ngx/src/app/modules/home/components/profile/device/common/power-mode-setting.component.html

@ -26,38 +26,27 @@
</mat-select>
</mat-form-field>
<section class="mat-block" fxFlex *ngIf="parentForm.get('powerMode').value === 'E_DRX'">
<mat-form-field class="mat-block" fxFlex>
<mat-label>{{ 'device-profile.edrx-cycle' | translate }}</mat-label>
<input matInput type="number" min="0" formControlName="edrxCycle" required>
<mat-error *ngIf="parentForm.get('edrxCycle').hasError('required')">
{{ 'device-profile.edrx-cycle-required' | translate }}
</mat-error>
<mat-error *ngIf="parentForm.get('edrxCycle').hasError('pattern') ||
parentForm.get('edrxCycle').hasError('min')">
{{ 'device-profile.edrx-cycle-pattern' | translate }}
</mat-error>
</mat-form-field>
<mat-form-field class="mat-block" fxFlex>
<mat-label>{{ 'device-profile.paging-transmission-window' | translate }}</mat-label>
<input matInput type="number" min="0" formControlName="pagingTransmissionWindow" required>
<mat-error *ngIf="parentForm.get('pagingTransmissionWindow').hasError('required')">
{{ 'device-profile.paging-transmission-window-required' | translate }}
</mat-error>
<mat-error *ngIf="parentForm.get('pagingTransmissionWindow').hasError('pattern') ||
parentForm.get('pagingTransmissionWindow').hasError('min')">
{{ 'device-profile.paging-transmission-window-pattern' | translate }}
</mat-error>
</mat-form-field>
<tb-time-unit-select [labelText]="'device-profile.edrx-cycle'"
[requiredText]="'device-profile.edrx-cycle-required'"
[patternText]="'device-profile.edrx-cycle-pattern'"
[minTime]="20480"
[minText]="'device-profile.edrx-cycle-min'"
formControlName="edrxCycle">
</tb-time-unit-select>
<tb-time-unit-select [labelText]="'device-profile.paging-transmission-window'"
[requiredText]="'device-profile.paging-transmission-window-required'"
[patternText]="'device-profile.paging-transmission-window-pattern'"
[minTime]="2000"
[minText]="'device-profile.paging-transmission-window-min'"
formControlName="pagingTransmissionWindow">
</tb-time-unit-select>
</section>
<mat-form-field class="mat-block" fxFlex *ngIf="parentForm.get('powerMode').value === 'PSM'">
<mat-label>{{ 'device-profile.psm-activity-timer' | translate }}</mat-label>
<input matInput type="number" min="0" formControlName="psmActivityTimer" required>
<mat-error *ngIf="parentForm.get('psmActivityTimer').hasError('required')">
{{ 'device-profile.psm-activity-timer-required' | translate }}
</mat-error>
<mat-error *ngIf="parentForm.get('psmActivityTimer').hasError('pattern') ||
parentForm.get('psmActivityTimer').hasError('min')">
{{ 'device-profile.psm-activity-timer-pattern' | translate }}
</mat-error>
</mat-form-field>
<tb-time-unit-select *ngIf="parentForm.get('powerMode').value === 'PSM'"
[labelText]="'device-profile.psm-activity-timer'"
[requiredText]="'device-profile.psm-activity-timer-required'"
[patternText]="'device-profile.psm-activity-timer-pattern'"
[minTime]="2000"
[minText]="'device-profile.psm-activity-timer-min'"
formControlName="psmActivityTimer">
</tb-time-unit-select>
</section>

13
ui-ngx/src/app/modules/home/components/profile/device/common/power-mode-setting.component.ts

@ -16,7 +16,12 @@
import { Component, Input, OnDestroy, OnInit } from '@angular/core';
import { FormGroup } from '@angular/forms';
import { PowerMode, PowerModeTranslationMap } from '@home/components/profile/device/lwm2m/lwm2m-profile-config.models';
import {
DEFAULT_EDRX_CYCLE,
DEFAULT_PAGING_TRANSMISSION_WINDOW, DEFAULT_PSM_ACTIVITY_TIMER,
PowerMode,
PowerModeTranslationMap
} from '@home/components/profile/device/lwm2m/lwm2m-profile-config.models';
import { takeUntil } from 'rxjs/operators';
import { Subject } from 'rxjs';
@ -63,13 +68,13 @@ export class PowerModeSettingComponent implements OnInit, OnDestroy {
private disablePSKMode() {
this.parentForm.get('psmActivityTimer').disable({emitEvent: false});
this.parentForm.get('psmActivityTimer').reset(0, {emitEvent: false});
this.parentForm.get('psmActivityTimer').reset(DEFAULT_PSM_ACTIVITY_TIMER, {emitEvent: false});
}
private disableEdrxMode() {
this.parentForm.get('edrxCycle').disable({emitEvent: false});
this.parentForm.get('edrxCycle').reset(0, {emitEvent: false});
this.parentForm.get('edrxCycle').reset(DEFAULT_EDRX_CYCLE, {emitEvent: false});
this.parentForm.get('pagingTransmissionWindow').disable({emitEvent: false});
this.parentForm.get('pagingTransmissionWindow').reset(0, {emitEvent: false});
this.parentForm.get('pagingTransmissionWindow').reset(DEFAULT_PAGING_TRANSMISSION_WINDOW, {emitEvent: false});
}
}

40
ui-ngx/src/app/modules/home/components/profile/device/common/time-unit-select.component.html

@ -0,0 +1,40 @@
<!--
Copyright © 2016-2021 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<section [formGroup]="timeUnitSelectFormGroup" fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="8px">
<mat-form-field class="mat-block" fxFlex>
<mat-label>{{ labelText | translate }}</mat-label>
<input matInput type="number" min="0" formControlName="time" required>
<mat-error *ngIf="timeUnitSelectFormGroup.get('time').hasError('required')">
{{ requiredText | translate }}
</mat-error>
<mat-error *ngIf="timeUnitSelectFormGroup.get('time').hasError('pattern')">
{{ patternText | translate }}
</mat-error>
<mat-error *ngIf="timeUnitSelectFormGroup.get('time').hasError('min')">
{{ (minText || patternText) | translate : {min: minTime/1000} }}
</mat-error>
</mat-form-field>
<mat-form-field class="mat-block" fxFlex>
<mat-label translate>device-profile.condition-duration-time-unit</mat-label>
<mat-select formControlName="unit">
<mat-option *ngFor="let timeUnit of timeUnits" [value]="timeUnit">
{{ timeUnitTranslations.get(timeUnit) | translate }}
</mat-option>
</mat-select>
</mat-form-field>
</section>

194
ui-ngx/src/app/modules/home/components/profile/device/common/time-unit-select.component.ts

@ -0,0 +1,194 @@
///
/// Copyright © 2016-2021 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { Component, forwardRef, Input, OnDestroy, OnInit } from '@angular/core';
import {
ControlValueAccessor,
FormBuilder,
FormGroup,
NG_VALIDATORS,
NG_VALUE_ACCESSOR,
ValidationErrors,
Validator,
Validators
} from '@angular/forms';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
import {
FullTimeUnit,
HOUR,
MINUTE,
SECOND,
TimeUnit,
TimeUnitMilli,
timeUnitTranslationMap
} from '@shared/models/time/time.models';
import { isDefinedAndNotNull, isNumber } from '@core/utils';
interface FormGroupModel {
time: number;
unit: FullTimeUnit;
}
@Component({
selector: 'tb-time-unit-select',
templateUrl: './time-unit-select.component.html',
styleUrls: [],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => TimeUnitSelectComponent),
multi: true
},
{
provide: NG_VALIDATORS,
useExisting: forwardRef(() => TimeUnitSelectComponent),
multi: true
}
]
})
export class TimeUnitSelectComponent implements OnInit, OnDestroy, ControlValueAccessor, Validator {
timeUnitSelectFormGroup: FormGroup;
timeUnits = Object.values({...TimeUnitMilli, ...TimeUnit}).filter(item => item !== TimeUnit.DAYS);
timeUnitTranslations = timeUnitTranslationMap;
private destroy$ = new Subject();
private timeUnitToTimeMap = new Map<FullTimeUnit, number>(
[
[TimeUnitMilli.MILLISECONDS, 1],
[TimeUnit.SECONDS, SECOND],
[TimeUnit.MINUTES, MINUTE],
[TimeUnit.HOURS, HOUR]
]
);
private timeToTimeUnitMap = new Map<number, FullTimeUnit>(
[
[SECOND, TimeUnitMilli.MILLISECONDS],
[MINUTE, TimeUnit.SECONDS],
[HOUR, TimeUnit.MINUTES]
]
);
@Input()
disabled: boolean;
@Input()
labelText: string;
@Input()
requiredText: string;
@Input()
patternText: string;
@Input()
minTime = 0;
@Input()
minText: string;
private propagateChange = (v: any) => {
}
constructor(private fb: FormBuilder) {
}
ngOnInit() {
this.timeUnitSelectFormGroup = this.fb.group({
time: [0, [Validators.required, Validators.min(this.minTime), Validators.pattern('[0-9]*')]],
unit: [TimeUnitMilli.MILLISECONDS]
});
this.timeUnitSelectFormGroup.valueChanges.pipe(
takeUntil(this.destroy$)
).subscribe((value) => {
this.updateModel(value);
});
this.timeUnitSelectFormGroup.get('unit').valueChanges.pipe(
takeUntil(this.destroy$)
).subscribe((unit: FullTimeUnit) => {
if (this.minTime > 0) {
const unitTime = this.timeUnitToTimeMap.get(unit);
const validationTime = Math.ceil(this.minTime / unitTime);
this.timeUnitSelectFormGroup.get('time').setValidators([Validators.required, Validators.min(validationTime), Validators.pattern('[0-9]*')]);
this.timeUnitSelectFormGroup.get('time').updateValueAndValidity({emitEvent: false});
}
});
}
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}
registerOnChange(fn: any) {
this.propagateChange = fn;
}
registerOnTouched(fn: any) {
}
setDisabledState(isDisabled: boolean) {
this.disabled = isDisabled;
if (this.disabled) {
this.timeUnitSelectFormGroup.disable({emitEvent: false});
} else {
this.timeUnitSelectFormGroup.enable({emitEvent: false});
}
}
writeValue(value: number) {
const formValue: FormGroupModel = {
time: 0,
unit: TimeUnitMilli.MILLISECONDS
};
if (isDefinedAndNotNull(value) && isNumber(value) && value >= 0) {
formValue.unit = this.calculateTimeUnit(value);
formValue.time = value / this.timeUnitToTimeMap.get(formValue.unit);
}
this.timeUnitSelectFormGroup.reset(formValue, {emitEvent: false});
this.timeUnitSelectFormGroup.get('unit').updateValueAndValidity({onlySelf: true});
}
validate(): ValidationErrors | null {
return this.timeUnitSelectFormGroup.valid ? null : {
timeUnitSelect: false
};
}
private updateModel(value: FormGroupModel) {
const time = value.time * this.timeUnitToTimeMap.get(value.unit);
this.propagateChange(time);
}
private calculateTimeUnit(value: number): FullTimeUnit {
if (value === 0) {
return TimeUnitMilli.MILLISECONDS;
}
const iterators = this.timeToTimeUnitMap[Symbol.iterator]();
let iterator = iterators.next();
while (!iterator.done) {
if (!Number.isInteger(value / iterator.value[0])) {
return iterator.value[1];
}
iterator = iterators.next();
}
return TimeUnit.HOURS;
}
}

2
ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-profile-transport-configuration.component.html

@ -23,11 +23,9 @@
<tb-profile-lwm2m-object-list
(addList)="addObjectsList($event)"
(removeList)="removeObjectsList($event)"
[required]="required"
formControlName="objectIds">
</tb-profile-lwm2m-object-list>
<tb-profile-lwm2m-observe-attr-telemetry
[required]="required"
formControlName="observeAttrTelemetry">
</tb-profile-lwm2m-observe-attr-telemetry>
</section>

28
ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-profile-transport-configuration.component.ts

@ -18,7 +18,8 @@ import { Component, forwardRef, Input, OnDestroy } from '@angular/core';
import {
ControlValueAccessor,
FormBuilder,
FormGroup, NG_VALIDATORS,
FormGroup,
NG_VALIDATORS,
NG_VALUE_ACCESSOR,
ValidationErrors,
Validator,
@ -30,11 +31,14 @@ import {
BingingMode,
BingingModeTranslationsMap,
DEFAULT_BINDING,
DEFAULT_EDRX_CYCLE,
DEFAULT_FW_UPDATE_RESOURCE,
DEFAULT_ID_SERVER,
DEFAULT_LIFE_TIME,
DEFAULT_MIN_PERIOD,
DEFAULT_NOTIF_IF_DESIBLED,
DEFAULT_PAGING_TRANSMISSION_WINDOW,
DEFAULT_PSM_ACTIVITY_TIMER,
DEFAULT_SW_UPDATE_RESOURCE,
getDefaultBootstrapServerSecurityConfig,
getDefaultLwM2MServerSecurityConfig,
@ -50,7 +54,7 @@ import {
TELEMETRY
} from './lwm2m-profile-config.models';
import { DeviceProfileService } from '@core/http/device-profile.service';
import { deepClone, isDefinedAndNotNull, isEmpty } from '@core/utils';
import { deepClone, isDefinedAndNotNull, isEmpty, isUndefined } from '@core/utils';
import { JsonArray, JsonObject } from '@angular/compiler-cli/ngcc/src/packages/entry_point';
import { Direction } from '@shared/models/page/sort-order';
import _ from 'lodash';
@ -100,8 +104,8 @@ export class Lwm2mDeviceProfileTransportConfigurationComponent implements Contro
constructor(private fb: FormBuilder,
private deviceProfileService: DeviceProfileService) {
this.lwm2mDeviceProfileFormGroup = this.fb.group({
objectIds: [null, Validators.required],
observeAttrTelemetry: [null, Validators.required],
objectIds: [null],
observeAttrTelemetry: [null],
bootstrap: this.fb.group({
servers: this.fb.group({
binding: [DEFAULT_BINDING],
@ -120,9 +124,9 @@ export class Lwm2mDeviceProfileTransportConfigurationComponent implements Contro
fwUpdateResource: [{value: '', disabled: true}, []],
swUpdateResource: [{value: '', disabled: true}, []],
powerMode: [PowerMode.DRX, Validators.required],
edrxCycle: [{disabled: true, value: 0}, [Validators.required, Validators.min(0), Validators.pattern('[0-9]*')]],
psmActivityTimer: [{disabled: true, value: 0}, [Validators.required, Validators.min(0), Validators.pattern('[0-9]*')]],
pagingTransmissionWindow: [{disabled: true, value: 0}, [Validators.required, Validators.min(0), Validators.pattern('[0-9]*')]],
edrxCycle: [{disabled: true, value: 0}, Validators.required],
psmActivityTimer: [{disabled: true, value: 0}, Validators.required],
pagingTransmissionWindow: [{disabled: true, value: 0}, Validators.required],
compositeOperationsSupport: [false]
})
});
@ -183,7 +187,8 @@ export class Lwm2mDeviceProfileTransportConfigurationComponent implements Contro
async writeValue(value: Lwm2mProfileConfigModels | null) {
if (isDefinedAndNotNull(value) && (value?.clientLwM2mSettings || value?.observeAttr || value?.bootstrap)) {
this.configurationValue = value;
const defaultFormSettings = !(value.observeAttr.attribute.length && value.observeAttr.telemetry.length);
const defaultFormSettings = value.clientLwM2mSettings.fwUpdateStrategy === 1 &&
isUndefined(value.clientLwM2mSettings.fwUpdateResource);
if (defaultFormSettings) {
await this.defaultProfileConfig();
}
@ -246,9 +251,10 @@ export class Lwm2mDeviceProfileTransportConfigurationComponent implements Contro
fwUpdateResource: this.configurationValue.clientLwM2mSettings.fwUpdateResource || '',
swUpdateResource: this.configurationValue.clientLwM2mSettings.swUpdateResource || '',
powerMode: this.configurationValue.clientLwM2mSettings.powerMode || PowerMode.DRX,
edrxCycle: this.configurationValue.clientLwM2mSettings.edrxCycle || 0,
pagingTransmissionWindow: this.configurationValue.clientLwM2mSettings.pagingTransmissionWindow || 0,
psmActivityTimer: this.configurationValue.clientLwM2mSettings.psmActivityTimer || 0,
edrxCycle: this.configurationValue.clientLwM2mSettings.edrxCycle || DEFAULT_EDRX_CYCLE,
pagingTransmissionWindow:
this.configurationValue.clientLwM2mSettings.pagingTransmissionWindow || DEFAULT_PAGING_TRANSMISSION_WINDOW,
psmActivityTimer: this.configurationValue.clientLwM2mSettings.psmActivityTimer || DEFAULT_PSM_ACTIVITY_TIMER,
compositeOperationsSupport: this.configurationValue.clientLwM2mSettings.compositeOperationsSupport || false
}
},

11
ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-object-list.component.html

@ -44,9 +44,14 @@
<span [innerHTML]="objectLwm2m.keyId + ': ' + (objectLwm2m.name | highlight: searchText)"></span>
</mat-option>
<mat-option *ngIf="!(filteredObjectsList | async)?.length" [value]="null">
<span>
{{ 'device-profile.lwm2m.no-objects-matching' | translate: {object: searchText} }}
</span>
<div *ngIf="!textIsNotEmpty(searchText); else searchNotEmpty">
<span translate>device-profile.lwm2m.no-objects-found</span>
</div>
<ng-template #searchNotEmpty>
<span>
{{ 'device-profile.lwm2m.no-objects-matching' | translate:{object: truncate.transform(searchText, true, 6, &apos;...&apos;)} }}
</span>
</ng-template>
</mat-option>
</mat-autocomplete>
<mat-error *ngIf="lwm2mListFormGroup.get('objectsList').hasError('required')">

8
ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-object-list.component.ts

@ -33,6 +33,7 @@ import { DeviceProfileService } from '@core/http/device-profile.service';
import { Direction } from '@shared/models/page/sort-order';
import { isDefined, isDefinedAndNotNull, isString } from '@core/utils';
import { PageLink } from '@shared/models/page/page-link';
import { TruncatePipe } from '@shared/pipe/truncate.pipe';
@Component({
selector: 'tb-profile-lwm2m-object-list',
@ -82,7 +83,8 @@ export class Lwm2mObjectListComponent implements ControlValueAccessor, OnInit, V
private propagateChange = (v: any) => {
}
constructor(private deviceProfileService: DeviceProfileService,
constructor(public truncate: TruncatePipe,
private deviceProfileService: DeviceProfileService,
private fb: FormBuilder) {
this.lwm2mListFormGroup = this.fb.group({
objectsList: [this.objectsList],
@ -196,6 +198,10 @@ export class Lwm2mObjectListComponent implements ControlValueAccessor, OnInit, V
}
}
textIsNotEmpty(text: string): boolean {
return (text && text.length > 0);
}
private clear(value = '', emitEvent = true) {
this.objectInput.nativeElement.value = value;
this.lwm2mListFormGroup.get('objectLwm2m').patchValue(value, {emitEvent});

20
ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-profile-config.models.ts

@ -42,7 +42,9 @@ export const INSTANCES_ID_VALUE_MAX = 65535;
export const DEFAULT_OTA_UPDATE_PROTOCOL = 'coap://';
export const DEFAULT_FW_UPDATE_RESOURCE = DEFAULT_OTA_UPDATE_PROTOCOL + DEFAULT_LOCAL_HOST_NAME + ':' + DEFAULT_PORT_SERVER_NO_SEC;
export const DEFAULT_SW_UPDATE_RESOURCE = DEFAULT_OTA_UPDATE_PROTOCOL + DEFAULT_LOCAL_HOST_NAME + ':' + DEFAULT_PORT_SERVER_NO_SEC;
export const DEFAULT_PSM_ACTIVITY_TIMER = 10000;
export const DEFAULT_EDRX_CYCLE = 81000;
export const DEFAULT_PAGING_TRANSMISSION_WINDOW = 10000;
export enum BingingMode {
U = 'U',
@ -164,8 +166,8 @@ export interface ClientLwM2mSettings {
clientOnlyObserveAfterConnect: number;
fwUpdateStrategy: number;
swUpdateStrategy: number;
fwUpdateResource: string;
swUpdateResource: string;
fwUpdateResource?: string;
swUpdateResource?: string;
powerMode: PowerMode;
edrxCycle?: number;
pagingTransmissionWindow?: number;
@ -178,7 +180,7 @@ export interface ObservableAttributes {
attribute: string[];
telemetry: string[];
keyName: {};
attributeLwm2m?: AttributesNameValueMap[];
attributeLwm2m: AttributesNameValueMap;
}
export function getDefaultBootstrapServersSecurityConfig(): BootstrapServersSecurityConfig {
@ -193,13 +195,13 @@ export function getDefaultBootstrapServersSecurityConfig(): BootstrapServersSecu
export function getDefaultBootstrapServerSecurityConfig(): ServerSecurityConfig {
return {
bootstrapServerAccountTimeout: DEFAULT_BOOTSTRAP_SERVER_ACCOUNT_TIME_OUT,
clientHoldOffTime: DEFAULT_CLIENT_HOLD_OFF_TIME,
host: DEFAULT_LOCAL_HOST_NAME,
port: DEFAULT_PORT_BOOTSTRAP_NO_SEC,
securityMode: securityConfigMode.NO_SEC,
serverPublicKey: '',
clientHoldOffTime: DEFAULT_CLIENT_HOLD_OFF_TIME,
serverId: DEFAULT_ID_BOOTSTRAP,
bootstrapServerAccountTimeout: DEFAULT_BOOTSTRAP_SERVER_ACCOUNT_TIME_OUT
serverPublicKey: ''
};
}
@ -216,7 +218,7 @@ export function getDefaultProfileObserveAttrConfig(): ObservableAttributes {
attribute: [],
telemetry: [],
keyName: {},
attributeLwm2m: []
attributeLwm2m: {}
};
}
@ -225,8 +227,6 @@ export function getDefaultProfileClientLwM2mSettingsConfig(): ClientLwM2mSetting
clientOnlyObserveAfterConnect: 1,
fwUpdateStrategy: 1,
swUpdateStrategy: 1,
fwUpdateResource: DEFAULT_FW_UPDATE_RESOURCE,
swUpdateResource: DEFAULT_SW_UPDATE_RESOURCE,
powerMode: PowerMode.DRX,
compositeOperationsSupport: false
};

4
ui-ngx/src/app/modules/home/components/widget/lib/multiple-input-widget.component.ts

@ -495,8 +495,8 @@ export class MultipleInputWidgetComponent extends PageComponent implements OnIni
const serverAttributes: AttributeData[] = [];
const sharedAttributes: AttributeData[] = [];
const telemetry: AttributeData[] = [];
for (const key of this.visibleKeys(toSave)) {
const currentValue = this.multipleInputFormGroup.get(key.formId).value;
for (const key of toSave.keys) {
const currentValue = key.settings.dataKeyHidden ? key.value : this.multipleInputFormGroup.get(key.formId).value;
if (!isEqual(currentValue, key.value) || this.settings.updateAllValues) {
const attribute: AttributeData = {
key: key.name,

5
ui-ngx/src/app/modules/home/components/widget/lib/qrcode-widget.component.html

@ -16,6 +16,7 @@
-->
<div fxLayout="column" fxLayoutAlign="center center" style="width: 100%; height: 100%;">
<canvas fxFlex #canvas [ngStyle]="{display: qrCodeText ? 'block' : 'none'}"></canvas>
<div *ngIf="!qrCodeText" translate>entity.no-data</div>
<canvas fxFlex #canvas [ngStyle]="{display: qrCodeText && !invalidQrCodeText ? 'block' : 'none'}"></canvas>
<div *ngIf="!qrCodeText && !invalidQrCodeText" translate>entity.no-data</div>
<div *ngIf="invalidQrCodeText" translate>widgets.invalid-qr-code-text</div>
</div>

20
ui-ngx/src/app/modules/home/components/widget/lib/qrcode-widget.component.ts

@ -19,7 +19,6 @@ import { PageComponent } from '@shared/components/page.component';
import { WidgetContext } from '@home/models/widget-component.models';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import QRCode from 'qrcode';
import {
fillPattern,
parseData,
@ -30,6 +29,7 @@ import {
import { FormattedData } from '@home/components/widget/lib/maps/map-models';
import { DatasourceData } from '@shared/models/widget.models';
import { DataKeyType } from '@shared/models/telemetry/telemetry.models';
import { isNumber, isObject } from '@core/utils';
interface QrCodeWidgetSettings {
qrCodeTextPattern: string;
@ -53,6 +53,7 @@ export class QrCodeWidgetComponent extends PageComponent implements OnInit, Afte
ctx: WidgetContext;
qrCodeText: string;
invalidQrCodeText = false;
private viewInited: boolean;
private scheduleUpdateCanvas: boolean;
@ -109,8 +110,13 @@ export class QrCodeWidgetComponent extends PageComponent implements OnInit, Afte
private updateQrCodeText(newQrCodeText: string): void {
if (this.qrCodeText !== newQrCodeText) {
this.qrCodeText = newQrCodeText;
if (this.qrCodeText) {
this.updateCanvas();
if (!(isObject(newQrCodeText) || isNumber(newQrCodeText))) {
this.invalidQrCodeText = false;
if (this.qrCodeText) {
this.updateCanvas();
}
} else {
this.invalidQrCodeText = true;
}
this.cd.detectChanges();
}
@ -118,9 +124,11 @@ export class QrCodeWidgetComponent extends PageComponent implements OnInit, Afte
private updateCanvas() {
if (this.viewInited) {
QRCode.toCanvas(this.canvasRef.nativeElement, this.qrCodeText);
this.canvasRef.nativeElement.style.width = 'auto';
this.canvasRef.nativeElement.style.height = 'auto';
import('qrcode').then((QRCode) => {
QRCode.toCanvas(this.canvasRef.nativeElement, this.qrCodeText);
this.canvasRef.nativeElement.style.width = 'auto';
this.canvasRef.nativeElement.style.height = 'auto';
});
} else {
this.scheduleUpdateCanvas = true;
}

6
ui-ngx/src/app/modules/home/pages/device/data/coap-device-transport-configuration.component.ts

@ -71,9 +71,9 @@ export class CoapDeviceTransportConfigurationComponent implements ControlValueAc
ngOnInit() {
this.coapDeviceTransportForm = this.fb.group({
powerMode: [null],
edrxCycle: [{disabled: true, value: 0}, [Validators.required, Validators.min(0), Validators.pattern('[0-9]*')]],
psmActivityTimer: [{disabled: true, value: 0}, [Validators.required, Validators.min(0), Validators.pattern('[0-9]*')]],
pagingTransmissionWindow: [{disabled: true, value: 0}, [Validators.required, Validators.min(0), Validators.pattern('[0-9]*')]]
edrxCycle: [{disabled: true, value: 0}, Validators.required],
psmActivityTimer: [{disabled: true, value: 0}, Validators.required],
pagingTransmissionWindow: [{disabled: true, value: 0}, Validators.required]
});
this.coapDeviceTransportForm.valueChanges.pipe(
takeUntil(this.destroy$)

6
ui-ngx/src/app/modules/home/pages/device/data/lwm2m-device-transport-configuration.component.ts

@ -71,9 +71,9 @@ export class Lwm2mDeviceTransportConfigurationComponent implements ControlValueA
ngOnInit() {
this.lwm2mDeviceTransportConfigurationFormGroup = this.fb.group({
powerMode: [null],
edrxCycle: [{disabled: true, value: 0}, [Validators.required, Validators.min(0), Validators.pattern('[0-9]*')]],
psmActivityTimer: [{disabled: true, value: 0}, [Validators.required, Validators.min(0), Validators.pattern('[0-9]*')]],
pagingTransmissionWindow: [{disabled: true, value: 0}, [Validators.required, Validators.min(0), Validators.pattern('[0-9]*')]]
edrxCycle: [{disabled: true, value: 0}, Validators.required],
psmActivityTimer: [{disabled: true, value: 0}, Validators.required],
pagingTransmissionWindow: [{disabled: true, value: 0}, Validators.required]
});
this.lwm2mDeviceTransportConfigurationFormGroup.valueChanges.pipe(
takeUntil(this.destroy$)

2
ui-ngx/src/app/shared/models/lwm2m-security-config.models.ts

@ -14,7 +14,7 @@
/// limitations under the License.
///
export const LEN_MAX_PSK = 64;
export const LEN_MAX_PSK = 128;
export const LEN_MAX_PRIVATE_KEY = 134;
export const LEN_MAX_PUBLIC_KEY_RPK = 182;
export const LEN_MAX_PUBLIC_KEY_X509 = 3000;

9
ui-ngx/src/app/shared/models/time/time.models.ts

@ -812,8 +812,15 @@ export enum TimeUnit {
DAYS = 'DAYS'
}
export const timeUnitTranslationMap = new Map<TimeUnit, string>(
export enum TimeUnitMilli {
MILLISECONDS = 'MILLISECONDS'
}
export type FullTimeUnit = TimeUnit | TimeUnitMilli;
export const timeUnitTranslationMap = new Map<FullTimeUnit, string>(
[
[TimeUnitMilli.MILLISECONDS, 'timeunit.milliseconds'],
[TimeUnit.SECONDS, 'timeunit.seconds'],
[TimeUnit.MINUTES, 'timeunit.minutes'],
[TimeUnit.HOURS, 'timeunit.hours'],

6
ui-ngx/src/assets/locale/locale.constant-cs_CZ.json

@ -74,7 +74,7 @@
"admin": {
"general": "Obecné",
"general-settings": "Obecná nastavení",
"home-settings": "Domácí nastavení",
"home-settings": "Nastavení",
"outgoing-mail": "Odchozí email",
"outgoing-mail-settings": "Nastavení odchozího emailu",
"system-settings": "Systémová nastavení",
@ -953,7 +953,7 @@
"credentials-type": "Typ přístupových údajů",
"access-token": "Přístupový token",
"access-token-required": "Přístupový token je povinný.",
"access-token-invalid": "Délka přístupového tokenu musí být od 1 do 20 znaků.",
"access-token-invalid": "Délka přístupového tokenu musí být od 1 do 32 znaků.",
"rsa-key": "RSA veřejný klíč",
"rsa-key-required": "RSA veřejný klíč je povinný.",
"lwm2m-security-config": {
@ -1229,7 +1229,7 @@
"drx": "Přerušovaný přenos (DRX)",
"edrx": "Rozšířený přerušovaný přenos (eDRX)"
},
"edrx-cycle": "eDRX cyklus v milisekudnách",
"edrx-cycle": "eDRX cyklus",
"edrx-cycle-required": "eDRX cyklus je povinný.",
"edrx-cycle-pattern": "eDRX cyklus musí být kladné číslo.",
"lwm2m": {

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

@ -686,7 +686,7 @@
"credentials-type": "Art der Zugangsdaten",
"access-token": "Zugangs-Token",
"access-token-required": "Zugangs-Token ist erforderlich.",
"access-token-invalid": "Die Länge des Zugangs-Tokens muss zwischen 1 und 20 Zeichen betragen.",
"access-token-invalid": "Die Länge des Zugangs-Tokens muss zwischen 1 und 32 Zeichen betragen.",
"rsa-key": "RSA öffentlicher Schlüssel",
"rsa-key-required": "RSA öffentlicher Schlüssel ist erforderlich.",
"secret": "Geheimnis",

2
ui-ngx/src/assets/locale/locale.constant-el_GR.json

@ -798,7 +798,7 @@
"credentials-type": "Τύπος διαπιστευτηρίων",
"access-token": "Διακριτικό πρόσβασης",
"access-token-required": "Απαιτείται διακριτικό πρόσβασης.",
"access-token-invalid": "Access token length must be from 1 to 20 characters.",
"access-token-invalid": "Access token length must be from 1 to 32 characters.",
"rsa-key": "RSA public key",
"rsa-key-required": "Απαιτείται RSA public key.",
"secret": "Secret",

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

@ -956,7 +956,7 @@
"credentials-type": "Credentials type",
"access-token": "Access token",
"access-token-required": "Access token is required.",
"access-token-invalid": "Access token length must be from 1 to 20 characters.",
"access-token-invalid": "Access token length must be from 1 to 32 characters.",
"rsa-key": "RSA public key",
"rsa-key-required": "RSA public key is required.",
"lwm2m-security-config": {
@ -1232,18 +1232,22 @@
"drx": "Discontinuous Reception (DRX)",
"edrx": "Extended Discontinuous Reception (eDRX)"
},
"edrx-cycle": "eDRX cycle in milliseconds",
"edrx-cycle": "eDRX cycle",
"edrx-cycle-required": "eDRX cycle is required.",
"edrx-cycle-pattern": "eDRX cycle must be a positive integer.",
"paging-transmission-window": "Paging Transmission Window in milliseconds",
"edrx-cycle-min": "Minimum number of eDRX cycle is {{ min }} seconds.",
"paging-transmission-window": "Paging Transmission Window",
"paging-transmission-window-required": "Paging transmission window is required.",
"paging-transmission-window-pattern": "Paging transmission window must be a positive integer.",
"psm-activity-timer": "PSM Activity Timer in milliseconds",
"paging-transmission-window-min": "Minimum number ofpPaging transmission window is {{ min }} seconds.",
"psm-activity-timer": "PSM Activity Timer",
"psm-activity-timer-required": "PSM activity timer is required.",
"psm-activity-timer-pattern": "PSM activity timer must be a positive integer.",
"psm-activity-timer-min": "Minimum number of PSM activity timer is {{ min }} seconds.",
"lwm2m": {
"object-list": "Object list",
"object-list-empty": "No objects selected.",
"no-objects-found": "No objects found.",
"no-objects-matching": "No objects matching '{{object}}' were found.",
"model-tab": "LWM2M Model",
"add-new-instances": "Add new instances",
@ -2735,6 +2739,7 @@
}
},
"timeunit": {
"milliseconds": "Milliseconds",
"seconds": "Seconds",
"minutes": "Minutes",
"hours": "Hours",
@ -3123,7 +3128,8 @@
"update-attribute": "Update attribute",
"update-timeseries": "Update timeseries",
"value": "Value"
}
},
"invalid-qr-code-text": "Invalid input text for QR code. Input should have a string type"
},
"icon": {
"icon": "Icon",

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

@ -893,7 +893,7 @@
"credentials-type": "Tipo de credencial",
"access-token": "Tóken de acceso",
"access-token-required": "Access token requerido.",
"access-token-invalid": "Access token debe tener entre 1 a 20 caracteres.",
"access-token-invalid": "Access token debe tener entre 1 a 32 caracteres.",
"rsa-key": "Clave pública RSA",
"rsa-key-required": "Clave pública RSA requerida.",
"client-id": "ID Cliente",

2
ui-ngx/src/assets/locale/locale.constant-fa_IR.json

@ -640,7 +640,7 @@
"credentials-type": "نوع اعتبارنامه ها",
"access-token": "شناسه دسترسي",
"access-token-required": ".شناسه دسترسي مورد نياز است",
"access-token-invalid": ".طول شناسه دسترسي بايد از 1 تا 20 حرف باشد",
"access-token-invalid": ".طول شناسه دسترسي بايد از 1 تا 32 حرف باشد",
"rsa-key": "RSA کليد عمومي",
"rsa-key-required": ".مورد نياز است RSA کليد عمومي",
"secret": "محرمانه",

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

@ -639,7 +639,7 @@
},
"device": {
"access-token": "Jeton d'accès",
"access-token-invalid": "La longueur du jeton d'accès doit être comprise entre 1 et 20 caractéres.",
"access-token-invalid": "La longueur du jeton d'accès doit être comprise entre 1 et 32 caractéres.",
"access-token-required": "Le jeton d'accès est requis.",
"accessTokenCopiedMessage": "Le jeton d'accès au dispositif a été copié dans le presse-papier",
"add": "Ajouter un dispositif",

2
ui-ngx/src/assets/locale/locale.constant-it_IT.json

@ -665,7 +665,7 @@
"credentials-type": "Tipo credenziali",
"access-token": "Token di accesso",
"access-token-required": "Token di accesso obbligatorio.",
"access-token-invalid": "Il token di accesso deve avere una lunghezza compresa tra 1 e 20 caratteri.",
"access-token-invalid": "Il token di accesso deve avere una lunghezza compresa tra 1 e 32 caratteri.",
"rsa-key": "Chiave pubblica RSA",
"rsa-key-required": "Chiave pubblica RSA obbligatoria.",
"secret": "Secret",

2
ui-ngx/src/assets/locale/locale.constant-ja_JA.json

@ -623,7 +623,7 @@
"credentials-type": "資格情報タイプ",
"access-token": "アクセストークン",
"access-token-required": "アクセストークンが必要です。",
"access-token-invalid": "アクセストークンの長さは、1〜20文字でなければなりません。",
"access-token-invalid": "アクセストークンの長さは、1〜32文字でなければなりません。",
"rsa-key": "RSA公開鍵",
"rsa-key-required": "RSA公開鍵が必要です。",
"secret": "秘密",

2
ui-ngx/src/assets/locale/locale.constant-ka_GE.json

@ -683,7 +683,7 @@
"credentials-type": "მომხმარებლის ჩანაწერი",
"access-token": "ტოკენი",
"access-token-required": "ტოკენი სავალდებულოა",
"access-token-invalid": "ტოკენის სიგრძე უნდა იყოს 1 დან 20 სიმბოლომდე",
"access-token-invalid": "ტოკენის სიგრძე უნდა იყოს 1 დან 32 სიმბოლომდე",
"rsa-key": "ღია გასაღები RSA",
"rsa-key-required": "ღია გასაღები RSA აუცილებელია",
"secret": "საიდუმლო",

2
ui-ngx/src/assets/locale/locale.constant-ko_KR.json

@ -869,7 +869,7 @@
"credentials-type": "크리덴셜 타입",
"access-token": "억세스 토큰",
"access-token-required": "액세스 토큰을 입력하세요.",
"access-token-invalid": "액세스 토큰 길이는 1 - 20 자 여야합니다.",
"access-token-invalid": "액세스 토큰 길이는 1 - 32 자 여야합니다.",
"rsa-key": "RSA public key",
"rsa-key-required": "RSA public key 를 입력하세요.",
"client-id": "클라이언트 ID",

2
ui-ngx/src/assets/locale/locale.constant-lv_LV.json

@ -641,7 +641,7 @@
"credentials-type": "Akreditācijas datu tips",
"access-token": "Piekļuves tokens",
"access-token-required": "Piekļuves tokens ir nepieciešams.",
"access-token-invalid": "Piekļuves tokena garumam ir jābūt no 1 līdz 20 rakstzīmēm.",
"access-token-invalid": "Piekļuves tokena garumam ir jābūt no 1 līdz 32 rakstzīmēm.",
"rsa-key": "RSA publiskā atslēga",
"rsa-key-required": "RSA publiskā atslēga ir nepieciešama.",
"secret": "Noslēpums",

2
ui-ngx/src/assets/locale/locale.constant-pt_BR.json

@ -715,7 +715,7 @@
"credentials-type": "Tipo de credencial",
"access-token": "Token de acesso",
"access-token-required": "O token de acesso é obrigatório.",
"access-token-invalid": "O token de acesso deve ter de 1 a 20 caracteres.",
"access-token-invalid": "O token de acesso deve ter de 1 a 32 caracteres.",
"rsa-key": "Chave pública RSA",
"rsa-key-required": "A chave pública RSA é obrigatória.",
"secret": "Segredo",

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

@ -674,7 +674,7 @@
"credentials-type": "Tip Credențiale",
"access-token": "Token Acces",
"access-token-required": "Tokenul de acces este necesar",
"access-token-invalid": "Dimensiunea tokenului de acces trebuie să fie de 1-20 caractere",
"access-token-invalid": "Dimensiunea tokenului de acces trebuie să fie de 1-32 caractere",
"rsa-key": "Cheie RSA publică",
"rsa-key-required": "Cheia RSA publică key este necesară",
"secret": "Cod Secret",
@ -1797,4 +1797,4 @@
"language": {
"language": "Limba"
}
}
}

2
ui-ngx/src/assets/locale/locale.constant-ru_RU.json

@ -677,7 +677,7 @@
"credentials-type": "Тип учетных данных",
"access-token": "Токен",
"access-token-required": "Токен обязателен.",
"access-token-invalid": "Длина токена должна быть от 1 до 20 символов.",
"access-token-invalid": "Длина токена должна быть от 1 до 32 символов.",
"rsa-key": "Открытый ключ RSA",
"rsa-key-required": "Открытый ключ RSA обязателен.",
"secret": "Секрет",

2
ui-ngx/src/assets/locale/locale.constant-sl_SI.json

@ -869,7 +869,7 @@
"credentials-type": "Vrsta poverilnic",
"access-token": "Dostopni žeton",
"access-token-required": "Zahtevan je žeton za dostop.",
"access-token-invalid": "Dolžina žetona za dostop mora biti od 1 do 20 znakov.",
"access-token-invalid": "Dolžina žetona za dostop mora biti od 1 do 32 znakov.",
"rsa-key": "Javni ključ RSA",
"rsa-key-required": "Potreben je javni ključ RSA.",
"client-id": "Client ID",

2
ui-ngx/src/assets/locale/locale.constant-tr_TR.json

@ -667,7 +667,7 @@
"credentials-type": "Kimlik Bilgi Türü",
"access-token": "Erişim şifresi",
"access-token-required": "Erişim şifresi gerekli.",
"access-token-invalid": "Erişim şifresi uzunluğu 1 ile 20 karakter arasında olmalıdır.",
"access-token-invalid": "Erişim şifresi uzunluğu 1 ile 32 karakter arasında olmalıdır.",
"rsa-key": "RSA açık anahtarı",
"rsa-key-required": "RSA açık anahtarı gerekli.",
"secret": "Secret",

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save