Browse Source

Merge pull request #5220 from thingsboard/feature/bulk-import/device-credentials

[3.3.2] Feature/bulk import/device credentials
pull/5231/head
Igor Kulikov 5 years ago
committed by GitHub
parent
commit
8518b993a3
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 49
      application/src/main/java/org/thingsboard/server/controller/AssetController.java
  2. 12
      application/src/main/java/org/thingsboard/server/controller/BaseController.java
  3. 37
      application/src/main/java/org/thingsboard/server/controller/DeviceController.java
  4. 50
      application/src/main/java/org/thingsboard/server/controller/EdgeController.java
  5. 16
      application/src/main/java/org/thingsboard/server/service/action/EntityActionService.java
  6. 94
      application/src/main/java/org/thingsboard/server/service/asset/AssetBulkImportService.java
  7. 259
      application/src/main/java/org/thingsboard/server/service/device/DeviceBulkImportService.java
  8. 106
      application/src/main/java/org/thingsboard/server/service/edge/EdgeBulkImportService.java
  9. 245
      application/src/main/java/org/thingsboard/server/service/importing/AbstractBulkImportService.java
  10. 77
      application/src/main/java/org/thingsboard/server/service/importing/BulkImportColumnType.java
  11. 41
      application/src/main/java/org/thingsboard/server/service/importing/BulkImportRequest.java
  12. 30
      application/src/main/java/org/thingsboard/server/service/importing/BulkImportResult.java
  13. 26
      application/src/main/java/org/thingsboard/server/service/importing/ImportedEntityInfo.java
  14. 12
      application/src/main/java/org/thingsboard/server/service/ttl/AlarmsCleanUpService.java
  15. 46
      application/src/main/java/org/thingsboard/server/utils/CsvUtils.java
  16. 55
      application/src/main/java/org/thingsboard/server/utils/TypeCastUtil.java
  17. 2
      common/dao-api/src/main/java/org/thingsboard/server/dao/device/DeviceCredentialsService.java
  18. 2
      common/data/src/main/java/org/thingsboard/server/common/data/Device.java
  19. 11
      common/data/src/main/java/org/thingsboard/server/common/data/asset/Asset.java
  20. 45
      common/data/src/main/java/org/thingsboard/server/common/data/device/credentials/lwm2m/AbstractLwM2MServerCredentialsWithKeys.java
  21. 26
      common/data/src/main/java/org/thingsboard/server/common/data/device/credentials/lwm2m/LwM2MBootstrapCredentials.java
  22. 5
      common/data/src/main/java/org/thingsboard/server/common/data/device/credentials/lwm2m/LwM2MClientCredentials.java
  23. 26
      common/data/src/main/java/org/thingsboard/server/common/data/device/credentials/lwm2m/LwM2MDeviceCredentials.java
  24. 37
      common/data/src/main/java/org/thingsboard/server/common/data/device/credentials/lwm2m/LwM2MServerCredentials.java
  25. 24
      common/data/src/main/java/org/thingsboard/server/common/data/device/credentials/lwm2m/NoSecServerCredentials.java
  26. 24
      common/data/src/main/java/org/thingsboard/server/common/data/device/credentials/lwm2m/PSKServerCredentials.java
  27. 24
      common/data/src/main/java/org/thingsboard/server/common/data/device/credentials/lwm2m/RPKServerCredentials.java
  28. 24
      common/data/src/main/java/org/thingsboard/server/common/data/device/credentials/lwm2m/X509ServerCredentials.java
  29. 14
      common/data/src/main/java/org/thingsboard/server/common/data/edge/Edge.java
  30. 8
      common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/adaptor/JsonConverter.java
  31. 5
      common/util/src/main/java/org/thingsboard/common/util/JacksonUtil.java
  32. 4
      dao/pom.xml
  33. 241
      dao/src/main/java/org/thingsboard/server/dao/device/DeviceCredentialsServiceImpl.java
  34. 3
      dao/src/main/java/org/thingsboard/server/dao/device/DeviceServiceImpl.java
  35. 22
      dao/src/main/java/org/thingsboard/server/dao/exception/DeviceCredentialsValidationException.java
  36. 5
      ui-ngx/src/app/core/http/asset.service.ts
  37. 5
      ui-ngx/src/app/core/http/device.service.ts
  38. 7
      ui-ngx/src/app/core/http/edge.service.ts
  39. 69
      ui-ngx/src/app/core/http/entity.service.ts
  40. 27
      ui-ngx/src/app/modules/home/components/import-export/import-dialog-csv.component.html
  41. 25
      ui-ngx/src/app/modules/home/components/import-export/import-dialog-csv.component.scss
  42. 204
      ui-ngx/src/app/modules/home/components/import-export/import-dialog-csv.component.ts
  43. 58
      ui-ngx/src/app/modules/home/components/import-export/import-export.models.ts
  44. 23
      ui-ngx/src/app/modules/home/components/import-export/import-export.service.ts
  45. 13
      ui-ngx/src/app/modules/home/components/import-export/table-columns-assignment.component.html
  46. 7
      ui-ngx/src/app/modules/home/components/import-export/table-columns-assignment.component.scss
  47. 44
      ui-ngx/src/app/modules/home/components/import-export/table-columns-assignment.component.ts
  48. 8
      ui-ngx/src/app/shared/models/device.models.ts
  49. 11
      ui-ngx/src/app/shared/models/entity.models.ts
  50. 14
      ui-ngx/src/app/shared/models/lwm2m-security-config.models.ts
  51. 21
      ui-ngx/src/assets/locale/locale.constant-en_US.json

49
application/src/main/java/org/thingsboard/server/controller/AssetController.java

@ -16,9 +16,12 @@
package org.thingsboard.server.controller; package org.thingsboard.server.controller;
import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListenableFuture;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestMethod;
@ -34,7 +37,6 @@ import org.thingsboard.server.common.data.asset.AssetInfo;
import org.thingsboard.server.common.data.asset.AssetSearchQuery; import org.thingsboard.server.common.data.asset.AssetSearchQuery;
import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.audit.ActionType;
import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.edge.Edge;
import org.thingsboard.server.common.data.edge.EdgeEventType;
import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
import org.thingsboard.server.common.data.edge.EdgeEventActionType; import org.thingsboard.server.common.data.edge.EdgeEventActionType;
import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.exception.ThingsboardException;
@ -48,6 +50,9 @@ import org.thingsboard.server.common.data.page.TimePageLink;
import org.thingsboard.server.dao.exception.IncorrectParameterException; import org.thingsboard.server.dao.exception.IncorrectParameterException;
import org.thingsboard.server.dao.model.ModelConstants; import org.thingsboard.server.dao.model.ModelConstants;
import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.asset.AssetBulkImportService;
import org.thingsboard.server.service.importing.BulkImportRequest;
import org.thingsboard.server.service.importing.BulkImportResult;
import org.thingsboard.server.service.security.model.SecurityUser; import org.thingsboard.server.service.security.model.SecurityUser;
import org.thingsboard.server.service.security.permission.Operation; import org.thingsboard.server.service.security.permission.Operation;
import org.thingsboard.server.service.security.permission.Resource; import org.thingsboard.server.service.security.permission.Resource;
@ -63,7 +68,10 @@ import static org.thingsboard.server.controller.EdgeController.EDGE_ID;
@RestController @RestController
@TbCoreComponent @TbCoreComponent
@RequestMapping("/api") @RequestMapping("/api")
@RequiredArgsConstructor
@Slf4j
public class AssetController extends BaseController { public class AssetController extends BaseController {
private final AssetBulkImportService assetBulkImportService;
public static final String ASSET_ID = "assetId"; public static final String ASSET_ID = "assetId";
@ -108,13 +116,7 @@ public class AssetController extends BaseController {
Asset savedAsset = checkNotNull(assetService.saveAsset(asset)); Asset savedAsset = checkNotNull(assetService.saveAsset(asset));
logEntityAction(savedAsset.getId(), savedAsset, onAssetCreatedOrUpdated(savedAsset, asset.getId() != null);
savedAsset.getCustomerId(),
asset.getId() == null ? ActionType.ADDED : ActionType.UPDATED, null);
if (asset.getId() != null) {
sendEntityNotificationMsg(savedAsset.getTenantId(), savedAsset.getId(), EdgeEventActionType.UPDATED);
}
return savedAsset; return savedAsset;
} catch (Exception e) { } catch (Exception e) {
@ -124,6 +126,20 @@ public class AssetController extends BaseController {
} }
} }
private void onAssetCreatedOrUpdated(Asset asset, boolean updated) {
try {
logEntityAction(asset.getId(), asset,
asset.getCustomerId(),
updated ? ActionType.UPDATED : ActionType.ADDED, null);
} catch (ThingsboardException e) {
log.error("Failed to log entity action", e);
}
if (updated) {
sendEntityNotificationMsg(asset.getTenantId(), asset.getId(), EdgeEventActionType.UPDATED);
}
}
@PreAuthorize("hasAuthority('TENANT_ADMIN')") @PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/asset/{assetId}", method = RequestMethod.DELETE) @RequestMapping(value = "/asset/{assetId}", method = RequestMethod.DELETE)
@ResponseStatus(value = HttpStatus.OK) @ResponseStatus(value = HttpStatus.OK)
@ -258,7 +274,7 @@ public class AssetController extends BaseController {
try { try {
TenantId tenantId = getCurrentUser().getTenantId(); TenantId tenantId = getCurrentUser().getTenantId();
PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder);
if (type != null && type.trim().length()>0) { if (type != null && type.trim().length() > 0) {
return checkNotNull(assetService.findAssetsByTenantIdAndType(tenantId, type, pageLink)); return checkNotNull(assetService.findAssetsByTenantIdAndType(tenantId, type, pageLink));
} else { } else {
return checkNotNull(assetService.findAssetsByTenantId(tenantId, pageLink)); return checkNotNull(assetService.findAssetsByTenantId(tenantId, pageLink));
@ -321,7 +337,7 @@ public class AssetController extends BaseController {
CustomerId customerId = new CustomerId(toUUID(strCustomerId)); CustomerId customerId = new CustomerId(toUUID(strCustomerId));
checkCustomerId(customerId, Operation.READ); checkCustomerId(customerId, Operation.READ);
PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder);
if (type != null && type.trim().length()>0) { if (type != null && type.trim().length() > 0) {
return checkNotNull(assetService.findAssetsByTenantIdAndCustomerIdAndType(tenantId, customerId, type, pageLink)); return checkNotNull(assetService.findAssetsByTenantIdAndCustomerIdAndType(tenantId, customerId, type, pageLink));
} else { } else {
return checkNotNull(assetService.findAssetsByTenantIdAndCustomerId(tenantId, customerId, pageLink)); return checkNotNull(assetService.findAssetsByTenantIdAndCustomerId(tenantId, customerId, pageLink));
@ -426,7 +442,7 @@ public class AssetController extends BaseController {
@RequestMapping(value = "/edge/{edgeId}/asset/{assetId}", method = RequestMethod.POST) @RequestMapping(value = "/edge/{edgeId}/asset/{assetId}", method = RequestMethod.POST)
@ResponseBody @ResponseBody
public Asset assignAssetToEdge(@PathVariable(EDGE_ID) String strEdgeId, public Asset assignAssetToEdge(@PathVariable(EDGE_ID) String strEdgeId,
@PathVariable(ASSET_ID) String strAssetId) throws ThingsboardException { @PathVariable(ASSET_ID) String strAssetId) throws ThingsboardException {
checkParameter(EDGE_ID, strEdgeId); checkParameter(EDGE_ID, strEdgeId);
checkParameter(ASSET_ID, strAssetId); checkParameter(ASSET_ID, strAssetId);
try { try {
@ -444,7 +460,7 @@ public class AssetController extends BaseController {
sendEntityAssignToEdgeNotificationMsg(getTenantId(), edgeId, savedAsset.getId(), EdgeEventActionType.ASSIGNED_TO_EDGE); sendEntityAssignToEdgeNotificationMsg(getTenantId(), edgeId, savedAsset.getId(), EdgeEventActionType.ASSIGNED_TO_EDGE);
return savedAsset; return savedAsset;
} catch (Exception e) { } catch (Exception e) {
logEntityAction(emptyId(EntityType.ASSET), null, logEntityAction(emptyId(EntityType.ASSET), null,
@ -530,4 +546,13 @@ public class AssetController extends BaseController {
throw handleException(e); throw handleException(e);
} }
} }
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN')")
@PostMapping("/asset/bulk_import")
public BulkImportResult<Asset> processAssetsBulkImport(@RequestBody BulkImportRequest request) throws Exception {
return assetBulkImportService.processBulkImport(request, getCurrentUser(), importedAssetInfo -> {
onAssetCreatedOrUpdated(importedAssetInfo.getEntity(), importedAssetInfo.isUpdated());
});
}
} }

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

@ -121,7 +121,7 @@ import org.thingsboard.server.exception.ThingsboardErrorResponseHandler;
import org.thingsboard.server.queue.discovery.PartitionService; import org.thingsboard.server.queue.discovery.PartitionService;
import org.thingsboard.server.queue.provider.TbQueueProducerProvider; import org.thingsboard.server.queue.provider.TbQueueProducerProvider;
import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.action.RuleEngineEntityActionService; import org.thingsboard.server.service.action.EntityActionService;
import org.thingsboard.server.service.component.ComponentDiscoveryService; import org.thingsboard.server.service.component.ComponentDiscoveryService;
import org.thingsboard.server.service.edge.EdgeLicenseService; import org.thingsboard.server.service.edge.EdgeLicenseService;
import org.thingsboard.server.service.edge.EdgeNotificationService; import org.thingsboard.server.service.edge.EdgeNotificationService;
@ -279,7 +279,7 @@ public abstract class BaseController {
protected EdgeLicenseService edgeLicenseService; protected EdgeLicenseService edgeLicenseService;
@Autowired @Autowired
protected RuleEngineEntityActionService ruleEngineEntityActionService; protected EntityActionService entityActionService;
@Value("${server.log_controller_error_stack_trace}") @Value("${server.log_controller_error_stack_trace}")
@Getter @Getter
@ -819,13 +819,7 @@ public abstract class BaseController {
protected <E extends HasName, I extends EntityId> void logEntityAction(User user, I entityId, E entity, CustomerId customerId, protected <E extends HasName, I extends EntityId> void logEntityAction(User user, I entityId, E entity, CustomerId customerId,
ActionType actionType, Exception e, Object... additionalInfo) throws ThingsboardException { ActionType actionType, Exception e, Object... additionalInfo) throws ThingsboardException {
if (customerId == null || customerId.isNullUid()) { entityActionService.logEntityAction(user, entityId, entity, customerId, actionType, e, additionalInfo);
customerId = user.getCustomerId();
}
if (e == null) {
ruleEngineEntityActionService.pushEntityActionToRuleEngine(entityId, entity, user.getTenantId(), customerId, actionType, user, additionalInfo);
}
auditLogService.logEntityAction(user.getTenantId(), customerId, user.getId(), user.getName(), entityId, entity, actionType, e, additionalInfo);
} }

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

@ -19,10 +19,13 @@ import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors; import com.google.common.util.concurrent.MoreExecutors;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestMethod;
@ -56,7 +59,6 @@ import org.thingsboard.server.common.data.ota.OtaPackageType;
import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.page.TimePageLink; import org.thingsboard.server.common.data.page.TimePageLink;
import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent;
import org.thingsboard.server.common.data.security.DeviceCredentials; import org.thingsboard.server.common.data.security.DeviceCredentials;
import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.common.msg.TbMsgDataType; import org.thingsboard.server.common.msg.TbMsgDataType;
@ -67,6 +69,9 @@ import org.thingsboard.server.dao.device.claim.ReclaimResult;
import org.thingsboard.server.dao.exception.IncorrectParameterException; import org.thingsboard.server.dao.exception.IncorrectParameterException;
import org.thingsboard.server.dao.model.ModelConstants; import org.thingsboard.server.dao.model.ModelConstants;
import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.device.DeviceBulkImportService;
import org.thingsboard.server.service.importing.BulkImportRequest;
import org.thingsboard.server.service.importing.BulkImportResult;
import org.thingsboard.server.service.security.model.SecurityUser; import org.thingsboard.server.service.security.model.SecurityUser;
import org.thingsboard.server.service.security.permission.Operation; import org.thingsboard.server.service.security.permission.Operation;
import org.thingsboard.server.service.security.permission.Resource; import org.thingsboard.server.service.security.permission.Resource;
@ -83,7 +88,10 @@ import static org.thingsboard.server.controller.EdgeController.EDGE_ID;
@RestController @RestController
@TbCoreComponent @TbCoreComponent
@RequestMapping("/api") @RequestMapping("/api")
@RequiredArgsConstructor
@Slf4j
public class DeviceController extends BaseController { public class DeviceController extends BaseController {
private final DeviceBulkImportService deviceBulkImportService;
private static final String DEVICE_ID = "deviceId"; private static final String DEVICE_ID = "deviceId";
private static final String DEVICE_NAME = "deviceName"; private static final String DEVICE_NAME = "deviceName";
@ -133,11 +141,7 @@ public class DeviceController extends BaseController {
Device savedDevice = checkNotNull(deviceService.saveDeviceWithAccessToken(device, accessToken)); Device savedDevice = checkNotNull(deviceService.saveDeviceWithAccessToken(device, accessToken));
tbClusterService.onDeviceUpdated(savedDevice, oldDevice); onDeviceCreatedOrUpdated(savedDevice, oldDevice, !created);
logEntityAction(savedDevice.getId(), savedDevice,
savedDevice.getCustomerId(),
created ? ActionType.ADDED : ActionType.UPDATED, null);
return savedDevice; return savedDevice;
} catch (Exception e) { } catch (Exception e) {
@ -148,6 +152,18 @@ public class DeviceController extends BaseController {
} }
private void onDeviceCreatedOrUpdated(Device savedDevice, Device oldDevice, boolean updated) {
tbClusterService.onDeviceUpdated(savedDevice, oldDevice);
try {
logEntityAction(savedDevice.getId(), savedDevice,
savedDevice.getCustomerId(),
updated ? ActionType.UPDATED : ActionType.ADDED, null);
} catch (ThingsboardException e) {
log.error("Failed to log entity action", e);
}
}
@PreAuthorize("hasAuthority('TENANT_ADMIN')") @PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/device/{deviceId}", method = RequestMethod.DELETE) @RequestMapping(value = "/device/{deviceId}", method = RequestMethod.DELETE)
@ResponseStatus(value = HttpStatus.OK) @ResponseStatus(value = HttpStatus.OK)
@ -776,4 +792,13 @@ public class DeviceController extends BaseController {
throw handleException(e); throw handleException(e);
} }
} }
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN')")
@PostMapping("/device/bulk_import")
public BulkImportResult<Device> processDevicesBulkImport(@RequestBody BulkImportRequest request) throws Exception {
return deviceBulkImportService.processBulkImport(request, getCurrentUser(), importedDeviceInfo -> {
onDeviceCreatedOrUpdated(importedDeviceInfo.getEntity(), importedDeviceInfo.getOldEntity(), importedDeviceInfo.isUpdated());
});
}
} }

50
application/src/main/java/org/thingsboard/server/controller/EdgeController.java

@ -17,11 +17,13 @@ package org.thingsboard.server.controller;
import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonNode;
import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListenableFuture;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestMethod;
@ -52,10 +54,14 @@ import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.dao.exception.IncorrectParameterException; import org.thingsboard.server.dao.exception.IncorrectParameterException;
import org.thingsboard.server.dao.model.ModelConstants; import org.thingsboard.server.dao.model.ModelConstants;
import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.edge.EdgeBulkImportService;
import org.thingsboard.server.service.importing.BulkImportRequest;
import org.thingsboard.server.service.importing.BulkImportResult;
import org.thingsboard.server.service.security.model.SecurityUser; import org.thingsboard.server.service.security.model.SecurityUser;
import org.thingsboard.server.service.security.permission.Operation; import org.thingsboard.server.service.security.permission.Operation;
import org.thingsboard.server.service.security.permission.Resource; import org.thingsboard.server.service.security.permission.Resource;
import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@ -64,7 +70,9 @@ import java.util.stream.Collectors;
@TbCoreComponent @TbCoreComponent
@Slf4j @Slf4j
@RequestMapping("/api") @RequestMapping("/api")
@RequiredArgsConstructor
public class EdgeController extends BaseController { public class EdgeController extends BaseController {
private final EdgeBulkImportService edgeBulkImportService;
public static final String EDGE_ID = "edgeId"; public static final String EDGE_ID = "edgeId";
@ -132,17 +140,8 @@ public class EdgeController extends BaseController {
edge.getId(), edge); edge.getId(), edge);
Edge savedEdge = checkNotNull(edgeService.saveEdge(edge, true)); Edge savedEdge = checkNotNull(edgeService.saveEdge(edge, true));
onEdgeCreatedOrUpdated(tenantId, savedEdge, edgeTemplateRootRuleChain, !created);
if (created) {
ruleChainService.assignRuleChainToEdge(tenantId, edgeTemplateRootRuleChain.getId(), savedEdge.getId());
edgeNotificationService.setEdgeRootRuleChain(tenantId, savedEdge, edgeTemplateRootRuleChain.getId());
edgeService.assignDefaultRuleChainsToEdge(tenantId, savedEdge.getId());
}
tbClusterService.broadcastEntityStateChangeEvent(savedEdge.getTenantId(), savedEdge.getId(),
created ? ComponentLifecycleEvent.CREATED : ComponentLifecycleEvent.UPDATED);
logEntityAction(savedEdge.getId(), savedEdge, null, created ? ActionType.ADDED : ActionType.UPDATED, null);
return savedEdge; return savedEdge;
} catch (Exception e) { } catch (Exception e) {
logEntityAction(emptyId(EntityType.EDGE), edge, logEntityAction(emptyId(EntityType.EDGE), edge,
@ -151,6 +150,19 @@ public class EdgeController extends BaseController {
} }
} }
private void onEdgeCreatedOrUpdated(TenantId tenantId, Edge edge, RuleChain edgeTemplateRootRuleChain, boolean updated) throws IOException, ThingsboardException {
if (!updated) {
ruleChainService.assignRuleChainToEdge(tenantId, edgeTemplateRootRuleChain.getId(), edge.getId());
edgeNotificationService.setEdgeRootRuleChain(tenantId, edge, edgeTemplateRootRuleChain.getId());
edgeService.assignDefaultRuleChainsToEdge(tenantId, edge.getId());
}
tbClusterService.broadcastEntityStateChangeEvent(edge.getTenantId(), edge.getId(),
updated ? ComponentLifecycleEvent.UPDATED : ComponentLifecycleEvent.CREATED);
logEntityAction(edge.getId(), edge, null, updated ? ActionType.UPDATED : ActionType.ADDED, null);
}
@PreAuthorize("hasAuthority('TENANT_ADMIN')") @PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/edge/{edgeId}", method = RequestMethod.DELETE) @RequestMapping(value = "/edge/{edgeId}", method = RequestMethod.DELETE)
@ResponseStatus(value = HttpStatus.OK) @ResponseStatus(value = HttpStatus.OK)
@ -563,6 +575,24 @@ public class EdgeController extends BaseController {
} }
} }
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN')")
@PostMapping("/edge/bulk_import")
public BulkImportResult<Edge> processEdgeBulkImport(@RequestBody BulkImportRequest request) throws Exception {
SecurityUser user = getCurrentUser();
RuleChain edgeTemplateRootRuleChain = ruleChainService.getEdgeTemplateRootRuleChain(user.getTenantId());
if (edgeTemplateRootRuleChain == null) {
throw new DataValidationException("Root edge rule chain is not available!");
}
return edgeBulkImportService.processBulkImport(request, user, importedAssetInfo -> {
try {
onEdgeCreatedOrUpdated(user.getTenantId(), importedAssetInfo.getEntity(), edgeTemplateRootRuleChain, importedAssetInfo.isUpdated());
} catch (Exception e) {
throw new RuntimeException(e);
}
});
}
private void cleanUpLicenseKey(Edge edge) { private void cleanUpLicenseKey(Edge edge) {
edge.setEdgeLicenseKey(null); edge.setEdgeLicenseKey(null);
} }

16
application/src/main/java/org/thingsboard/server/service/action/RuleEngineEntityActionService.java → application/src/main/java/org/thingsboard/server/service/action/EntityActionService.java

@ -28,6 +28,7 @@ import org.thingsboard.server.common.data.HasName;
import org.thingsboard.server.common.data.HasTenantId; import org.thingsboard.server.common.data.HasTenantId;
import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.audit.ActionType;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
@ -38,6 +39,7 @@ import org.thingsboard.server.common.data.kv.TsKvEntry;
import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.common.msg.TbMsgDataType; import org.thingsboard.server.common.msg.TbMsgDataType;
import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.common.msg.TbMsgMetaData;
import org.thingsboard.server.dao.audit.AuditLogService;
import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.cluster.TbClusterService; import org.thingsboard.server.cluster.TbClusterService;
@ -49,8 +51,9 @@ import java.util.stream.Collectors;
@Service @Service
@RequiredArgsConstructor @RequiredArgsConstructor
@Slf4j @Slf4j
public class RuleEngineEntityActionService { public class EntityActionService {
private final TbClusterService tbClusterService; private final TbClusterService tbClusterService;
private final AuditLogService auditLogService;
private static final ObjectMapper json = new ObjectMapper(); private static final ObjectMapper json = new ObjectMapper();
@ -209,6 +212,17 @@ public class RuleEngineEntityActionService {
} }
} }
public <E extends HasName, I extends EntityId> void logEntityAction(User user, I entityId, E entity, CustomerId customerId,
ActionType actionType, Exception e, Object... additionalInfo) {
if (customerId == null || customerId.isNullUid()) {
customerId = user.getCustomerId();
}
if (e == null) {
pushEntityActionToRuleEngine(entityId, entity, user.getTenantId(), customerId, actionType, user, additionalInfo);
}
auditLogService.logEntityAction(user.getTenantId(), customerId, user.getId(), user.getName(), entityId, entity, actionType, e, additionalInfo);
}
private <T> T extractParameter(Class<T> clazz, int index, Object... additionalInfo) { private <T> T extractParameter(Class<T> clazz, int index, Object... additionalInfo) {
T result = null; T result = null;

94
application/src/main/java/org/thingsboard/server/service/asset/AssetBulkImportService.java

@ -0,0 +1,94 @@
/**
* 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.service.asset;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.fasterxml.jackson.databind.node.TextNode;
import org.springframework.stereotype.Service;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.cluster.TbClusterService;
import org.thingsboard.server.common.data.asset.Asset;
import org.thingsboard.server.dao.asset.AssetService;
import org.thingsboard.server.dao.tenant.TbTenantProfileCache;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.action.EntityActionService;
import org.thingsboard.server.service.importing.AbstractBulkImportService;
import org.thingsboard.server.service.importing.BulkImportColumnType;
import org.thingsboard.server.service.importing.BulkImportRequest;
import org.thingsboard.server.service.importing.ImportedEntityInfo;
import org.thingsboard.server.service.security.AccessValidator;
import org.thingsboard.server.service.security.model.SecurityUser;
import org.thingsboard.server.service.security.permission.AccessControlService;
import org.thingsboard.server.service.telemetry.TelemetrySubscriptionService;
import java.util.Map;
import java.util.Optional;
@Service
@TbCoreComponent
public class AssetBulkImportService extends AbstractBulkImportService<Asset> {
private final AssetService assetService;
public AssetBulkImportService(TelemetrySubscriptionService tsSubscriptionService, TbTenantProfileCache tenantProfileCache,
AccessControlService accessControlService, AccessValidator accessValidator,
EntityActionService entityActionService, TbClusterService clusterService, AssetService assetService) {
super(tsSubscriptionService, tenantProfileCache, accessControlService, accessValidator, entityActionService, clusterService);
this.assetService = assetService;
}
@Override
protected ImportedEntityInfo<Asset> saveEntity(BulkImportRequest importRequest, Map<BulkImportColumnType, String> fields, SecurityUser user) {
ImportedEntityInfo<Asset> importedEntityInfo = new ImportedEntityInfo<>();
Asset asset = new Asset();
asset.setTenantId(user.getTenantId());
setAssetFields(asset, fields);
Asset existingAsset = assetService.findAssetByTenantIdAndName(user.getTenantId(), asset.getName());
if (existingAsset != null && importRequest.getMapping().getUpdate()) {
importedEntityInfo.setOldEntity(new Asset(existingAsset));
importedEntityInfo.setUpdated(true);
existingAsset.update(asset);
asset = existingAsset;
}
asset = assetService.saveAsset(asset);
importedEntityInfo.setEntity(asset);
return importedEntityInfo;
}
private void setAssetFields(Asset asset, Map<BulkImportColumnType, String> fields) {
ObjectNode additionalInfo = (ObjectNode) Optional.ofNullable(asset.getAdditionalInfo()).orElseGet(JacksonUtil::newObjectNode);
fields.forEach((columnType, value) -> {
switch (columnType) {
case NAME:
asset.setName(value);
break;
case TYPE:
asset.setType(value);
break;
case LABEL:
asset.setLabel(value);
break;
case DESCRIPTION:
additionalInfo.set("description", new TextNode(value));
break;
}
});
asset.setAdditionalInfo(additionalInfo);
}
}

259
application/src/main/java/org/thingsboard/server/service/device/DeviceBulkImportService.java

@ -0,0 +1,259 @@
/**
* 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.service.device;
import com.fasterxml.jackson.databind.node.BooleanNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.fasterxml.jackson.databind.node.TextNode;
import lombok.SneakyThrows;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang3.RandomStringUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.cluster.TbClusterService;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.DeviceProfile;
import org.thingsboard.server.common.data.DeviceProfileProvisionType;
import org.thingsboard.server.common.data.DeviceProfileType;
import org.thingsboard.server.common.data.DeviceTransportType;
import org.thingsboard.server.common.data.device.credentials.BasicMqttCredentials;
import org.thingsboard.server.common.data.device.credentials.lwm2m.LwM2MClientCredentials;
import org.thingsboard.server.common.data.device.credentials.lwm2m.LwM2MSecurityMode;
import org.thingsboard.server.common.data.device.profile.DefaultDeviceProfileConfiguration;
import org.thingsboard.server.common.data.device.profile.DeviceProfileData;
import org.thingsboard.server.common.data.device.profile.DeviceProfileTransportConfiguration;
import org.thingsboard.server.common.data.device.profile.DisabledDeviceProfileProvisionConfiguration;
import org.thingsboard.server.common.data.device.profile.Lwm2mDeviceProfileTransportConfiguration;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.security.DeviceCredentials;
import org.thingsboard.server.common.data.security.DeviceCredentialsType;
import org.thingsboard.server.dao.device.DeviceCredentialsService;
import org.thingsboard.server.dao.device.DeviceProfileService;
import org.thingsboard.server.dao.device.DeviceService;
import org.thingsboard.server.dao.exception.DeviceCredentialsValidationException;
import org.thingsboard.server.dao.tenant.TbTenantProfileCache;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.action.EntityActionService;
import org.thingsboard.server.service.importing.AbstractBulkImportService;
import org.thingsboard.server.service.importing.BulkImportColumnType;
import org.thingsboard.server.service.importing.BulkImportRequest;
import org.thingsboard.server.service.importing.ImportedEntityInfo;
import org.thingsboard.server.service.security.AccessValidator;
import org.thingsboard.server.service.security.model.SecurityUser;
import org.thingsboard.server.service.security.permission.AccessControlService;
import org.thingsboard.server.service.telemetry.TelemetrySubscriptionService;
import java.util.Collection;
import java.util.EnumSet;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
@Service
@TbCoreComponent
public class DeviceBulkImportService extends AbstractBulkImportService<Device> {
protected final DeviceService deviceService;
protected final DeviceCredentialsService deviceCredentialsService;
protected final DeviceProfileService deviceProfileService;
public DeviceBulkImportService(TelemetrySubscriptionService tsSubscriptionService, TbTenantProfileCache tenantProfileCache,
AccessControlService accessControlService, AccessValidator accessValidator,
EntityActionService entityActionService, TbClusterService clusterService,
DeviceService deviceService, DeviceCredentialsService deviceCredentialsService,
DeviceProfileService deviceProfileService) {
super(tsSubscriptionService, tenantProfileCache, accessControlService, accessValidator, entityActionService, clusterService);
this.deviceService = deviceService;
this.deviceCredentialsService = deviceCredentialsService;
this.deviceProfileService = deviceProfileService;
}
@Override
protected ImportedEntityInfo<Device> saveEntity(BulkImportRequest importRequest, Map<BulkImportColumnType, String> fields, SecurityUser user) {
ImportedEntityInfo<Device> importedEntityInfo = new ImportedEntityInfo<>();
Device device = new Device();
device.setTenantId(user.getTenantId());
setDeviceFields(device, fields);
Device existingDevice = deviceService.findDeviceByTenantIdAndName(user.getTenantId(), device.getName());
if (existingDevice != null && importRequest.getMapping().getUpdate()) {
importedEntityInfo.setOldEntity(new Device(existingDevice));
importedEntityInfo.setUpdated(true);
existingDevice.updateDevice(device);
device = existingDevice;
}
DeviceCredentials deviceCredentials;
try {
deviceCredentials = createDeviceCredentials(fields);
deviceCredentialsService.formatCredentials(deviceCredentials);
} catch (Exception e) {
throw new DeviceCredentialsValidationException("Invalid device credentials: " + e.getMessage());
}
if (deviceCredentials.getCredentialsType() == DeviceCredentialsType.LWM2M_CREDENTIALS) {
setUpLwM2mDeviceProfile(user.getTenantId(), device);
}
device = deviceService.saveDeviceWithCredentials(device, deviceCredentials);
importedEntityInfo.setEntity(device);
return importedEntityInfo;
}
private void setDeviceFields(Device device, Map<BulkImportColumnType, String> fields) {
ObjectNode additionalInfo = (ObjectNode) Optional.ofNullable(device.getAdditionalInfo()).orElseGet(JacksonUtil::newObjectNode);
fields.forEach((columnType, value) -> {
switch (columnType) {
case NAME:
device.setName(value);
break;
case TYPE:
device.setType(value);
break;
case LABEL:
device.setLabel(value);
break;
case DESCRIPTION:
additionalInfo.set("description", new TextNode(value));
break;
case IS_GATEWAY:
additionalInfo.set("gateway", BooleanNode.valueOf(Boolean.parseBoolean(value)));
break;
}
device.setAdditionalInfo(additionalInfo);
});
}
@SneakyThrows
private DeviceCredentials createDeviceCredentials(Map<BulkImportColumnType, String> fields) {
DeviceCredentials credentials = new DeviceCredentials();
if (fields.containsKey(BulkImportColumnType.LWM2M_CLIENT_ENDPOINT)) {
credentials.setCredentialsType(DeviceCredentialsType.LWM2M_CREDENTIALS);
setUpLwm2mCredentials(fields, credentials);
} else if (fields.containsKey(BulkImportColumnType.X509)) {
credentials.setCredentialsType(DeviceCredentialsType.X509_CERTIFICATE);
setUpX509CertificateCredentials(fields, credentials);
} else if (CollectionUtils.containsAny(fields.keySet(), EnumSet.of(BulkImportColumnType.MQTT_CLIENT_ID, BulkImportColumnType.MQTT_USER_NAME, BulkImportColumnType.MQTT_PASSWORD))) {
credentials.setCredentialsType(DeviceCredentialsType.MQTT_BASIC);
setUpBasicMqttCredentials(fields, credentials);
} else {
credentials.setCredentialsType(DeviceCredentialsType.ACCESS_TOKEN);
setUpAccessTokenCredentials(fields, credentials);
}
return credentials;
}
private void setUpAccessTokenCredentials(Map<BulkImportColumnType, String> fields, DeviceCredentials credentials) {
credentials.setCredentialsId(Optional.ofNullable(fields.get(BulkImportColumnType.ACCESS_TOKEN))
.orElseGet(() -> RandomStringUtils.randomAlphanumeric(20)));
}
private void setUpBasicMqttCredentials(Map<BulkImportColumnType, String> fields, DeviceCredentials credentials) {
BasicMqttCredentials basicMqttCredentials = new BasicMqttCredentials();
basicMqttCredentials.setClientId(fields.get(BulkImportColumnType.MQTT_CLIENT_ID));
basicMqttCredentials.setUserName(fields.get(BulkImportColumnType.MQTT_USER_NAME));
basicMqttCredentials.setPassword(fields.get(BulkImportColumnType.MQTT_PASSWORD));
credentials.setCredentialsValue(JacksonUtil.toString(basicMqttCredentials));
}
private void setUpX509CertificateCredentials(Map<BulkImportColumnType, String> fields, DeviceCredentials credentials) {
credentials.setCredentialsValue(fields.get(BulkImportColumnType.X509));
}
private void setUpLwm2mCredentials(Map<BulkImportColumnType, String> fields, DeviceCredentials credentials) throws com.fasterxml.jackson.core.JsonProcessingException {
ObjectNode lwm2mCredentials = JacksonUtil.newObjectNode();
Set.of(BulkImportColumnType.LWM2M_CLIENT_SECURITY_CONFIG_MODE, BulkImportColumnType.LWM2M_BOOTSTRAP_SERVER_SECURITY_MODE,
BulkImportColumnType.LWM2M_SERVER_SECURITY_MODE).stream()
.map(fields::get)
.filter(Objects::nonNull)
.forEach(securityMode -> {
try {
LwM2MSecurityMode.valueOf(securityMode.toUpperCase());
} catch (IllegalArgumentException e) {
throw new DeviceCredentialsValidationException("Unknown LwM2M security mode: " + securityMode + ", (the mode should be: NO_SEC, PSK, RPK, X509)!");
}
});
ObjectNode client = JacksonUtil.newObjectNode();
setValues(client, fields, Set.of(BulkImportColumnType.LWM2M_CLIENT_SECURITY_CONFIG_MODE,
BulkImportColumnType.LWM2M_CLIENT_ENDPOINT, BulkImportColumnType.LWM2M_CLIENT_IDENTITY,
BulkImportColumnType.LWM2M_CLIENT_KEY, BulkImportColumnType.LWM2M_CLIENT_CERT));
LwM2MClientCredentials lwM2MClientCredentials = JacksonUtil.treeToValue(client, LwM2MClientCredentials.class);
// so that only fields needed for specific type of lwM2MClientCredentials were saved in json
lwm2mCredentials.set("client", JacksonUtil.valueToTree(lwM2MClientCredentials));
ObjectNode bootstrapServer = JacksonUtil.newObjectNode();
setValues(bootstrapServer, fields, Set.of(BulkImportColumnType.LWM2M_BOOTSTRAP_SERVER_SECURITY_MODE,
BulkImportColumnType.LWM2M_BOOTSTRAP_SERVER_PUBLIC_KEY_OR_ID, BulkImportColumnType.LWM2M_BOOTSTRAP_SERVER_SECRET_KEY));
ObjectNode lwm2mServer = JacksonUtil.newObjectNode();
setValues(lwm2mServer, fields, Set.of(BulkImportColumnType.LWM2M_SERVER_SECURITY_MODE,
BulkImportColumnType.LWM2M_SERVER_CLIENT_PUBLIC_KEY_OR_ID, BulkImportColumnType.LWM2M_SERVER_CLIENT_SECRET_KEY));
ObjectNode bootstrap = JacksonUtil.newObjectNode();
bootstrap.set("bootstrapServer", bootstrapServer);
bootstrap.set("lwm2mServer", lwm2mServer);
lwm2mCredentials.set("bootstrap", bootstrap);
credentials.setCredentialsValue(lwm2mCredentials.toString());
}
private void setUpLwM2mDeviceProfile(TenantId tenantId, Device device) {
DeviceProfile deviceProfile = deviceProfileService.findDeviceProfileByName(tenantId, device.getType());
if (deviceProfile != null) {
if (deviceProfile.getTransportType() != DeviceTransportType.LWM2M) {
deviceProfile.setTransportType(DeviceTransportType.LWM2M);
deviceProfile.getProfileData().setTransportConfiguration(new Lwm2mDeviceProfileTransportConfiguration());
deviceProfile = deviceProfileService.saveDeviceProfile(deviceProfile);
device.setDeviceProfileId(deviceProfile.getId());
}
} else {
deviceProfile = new DeviceProfile();
deviceProfile.setTenantId(tenantId);
deviceProfile.setType(DeviceProfileType.DEFAULT);
deviceProfile.setName(device.getType());
deviceProfile.setTransportType(DeviceTransportType.LWM2M);
deviceProfile.setProvisionType(DeviceProfileProvisionType.DISABLED);
DeviceProfileData deviceProfileData = new DeviceProfileData();
DefaultDeviceProfileConfiguration configuration = new DefaultDeviceProfileConfiguration();
DeviceProfileTransportConfiguration transportConfiguration = new Lwm2mDeviceProfileTransportConfiguration();
DisabledDeviceProfileProvisionConfiguration provisionConfiguration = new DisabledDeviceProfileProvisionConfiguration(null);
deviceProfileData.setConfiguration(configuration);
deviceProfileData.setTransportConfiguration(transportConfiguration);
deviceProfileData.setProvisionConfiguration(provisionConfiguration);
deviceProfile.setProfileData(deviceProfileData);
deviceProfile = deviceProfileService.saveDeviceProfile(deviceProfile);
device.setDeviceProfileId(deviceProfile.getId());
}
}
private void setValues(ObjectNode objectNode, Map<BulkImportColumnType, String> data, Collection<BulkImportColumnType> columns) {
for (BulkImportColumnType column : columns) {
String value = StringUtils.defaultString(data.get(column), column.getDefaultValue());
if (value != null && column.getKey() != null) {
objectNode.set(column.getKey(), new TextNode(value));
}
}
}
}

106
application/src/main/java/org/thingsboard/server/service/edge/EdgeBulkImportService.java

@ -0,0 +1,106 @@
/**
* 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.service.edge;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.fasterxml.jackson.databind.node.TextNode;
import org.springframework.stereotype.Service;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.cluster.TbClusterService;
import org.thingsboard.server.common.data.edge.Edge;
import org.thingsboard.server.dao.edge.EdgeService;
import org.thingsboard.server.dao.tenant.TbTenantProfileCache;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.action.EntityActionService;
import org.thingsboard.server.service.importing.AbstractBulkImportService;
import org.thingsboard.server.service.importing.BulkImportColumnType;
import org.thingsboard.server.service.importing.BulkImportRequest;
import org.thingsboard.server.service.importing.ImportedEntityInfo;
import org.thingsboard.server.service.security.AccessValidator;
import org.thingsboard.server.service.security.model.SecurityUser;
import org.thingsboard.server.service.security.permission.AccessControlService;
import org.thingsboard.server.service.telemetry.TelemetrySubscriptionService;
import java.util.Map;
import java.util.Optional;
@Service
@TbCoreComponent
public class EdgeBulkImportService extends AbstractBulkImportService<Edge> {
private final EdgeService edgeService;
public EdgeBulkImportService(TelemetrySubscriptionService tsSubscriptionService, TbTenantProfileCache tenantProfileCache,
AccessControlService accessControlService, AccessValidator accessValidator,
EntityActionService entityActionService, TbClusterService clusterService, EdgeService edgeService) {
super(tsSubscriptionService, tenantProfileCache, accessControlService, accessValidator, entityActionService, clusterService);
this.edgeService = edgeService;
}
@Override
protected ImportedEntityInfo<Edge> saveEntity(BulkImportRequest importRequest, Map<BulkImportColumnType, String> fields, SecurityUser user) {
ImportedEntityInfo<Edge> importedEntityInfo = new ImportedEntityInfo<>();
Edge edge = new Edge();
edge.setTenantId(user.getTenantId());
setEdgeFields(edge, fields);
Edge existingEdge = edgeService.findEdgeByTenantIdAndName(user.getTenantId(), edge.getName());
if (existingEdge != null && importRequest.getMapping().getUpdate()) {
importedEntityInfo.setOldEntity(new Edge(existingEdge));
importedEntityInfo.setUpdated(true);
existingEdge.update(edge);
edge = existingEdge;
}
edge = edgeService.saveEdge(edge, true);
importedEntityInfo.setEntity(edge);
return importedEntityInfo;
}
private void setEdgeFields(Edge edge, Map<BulkImportColumnType, String> fields) {
ObjectNode additionalInfo = (ObjectNode) Optional.ofNullable(edge.getAdditionalInfo()).orElseGet(JacksonUtil::newObjectNode);
fields.forEach((columnType, value) -> {
switch (columnType) {
case NAME:
edge.setName(value);
break;
case TYPE:
edge.setType(value);
break;
case LABEL:
edge.setLabel(value);
break;
case DESCRIPTION:
additionalInfo.set("description", new TextNode(value));
break;
case EDGE_LICENSE_KEY:
edge.setEdgeLicenseKey(value);
break;
case CLOUD_ENDPOINT:
edge.setCloudEndpoint(value);
break;
case ROUTING_KEY:
edge.setRoutingKey(value);
break;
case SECRET:
edge.setSecret(value);
break;
}
});
edge.setAdditionalInfo(additionalInfo);
}
}

245
application/src/main/java/org/thingsboard/server/service/importing/AbstractBulkImportService.java

@ -0,0 +1,245 @@
/**
* 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.service.importing;
import com.google.common.util.concurrent.FutureCallback;
import com.google.gson.JsonObject;
import com.google.gson.JsonPrimitive;
import lombok.Data;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import org.apache.commons.lang3.StringUtils;
import org.thingsboard.server.cluster.TbClusterService;
import org.thingsboard.server.common.data.BaseData;
import org.thingsboard.server.common.data.TenantProfile;
import org.thingsboard.server.common.data.audit.ActionType;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.UUIDBased;
import org.thingsboard.server.common.data.kv.AttributeKvEntry;
import org.thingsboard.server.common.data.kv.BasicTsKvEntry;
import org.thingsboard.server.common.data.kv.DataType;
import org.thingsboard.server.common.data.kv.TsKvEntry;
import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration;
import org.thingsboard.server.common.transport.adaptor.JsonConverter;
import org.thingsboard.server.controller.BaseController;
import org.thingsboard.server.dao.tenant.TbTenantProfileCache;
import org.thingsboard.server.service.action.EntityActionService;
import org.thingsboard.server.service.importing.BulkImportRequest.ColumnMapping;
import org.thingsboard.server.service.security.AccessValidator;
import org.thingsboard.server.service.security.model.SecurityUser;
import org.thingsboard.server.service.security.permission.AccessControlService;
import org.thingsboard.server.service.security.permission.Operation;
import org.thingsboard.server.service.telemetry.TelemetrySubscriptionService;
import org.thingsboard.server.utils.CsvUtils;
import org.thingsboard.server.utils.TypeCastUtil;
import javax.annotation.Nullable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@RequiredArgsConstructor
public abstract class AbstractBulkImportService<E extends BaseData<? extends EntityId>> {
protected final TelemetrySubscriptionService tsSubscriptionService;
protected final TbTenantProfileCache tenantProfileCache;
protected final AccessControlService accessControlService;
protected final AccessValidator accessValidator;
protected final EntityActionService entityActionService;
protected final TbClusterService clusterService;
public final BulkImportResult<E> processBulkImport(BulkImportRequest request, SecurityUser user, Consumer<ImportedEntityInfo<E>> onEntityImported) throws Exception {
BulkImportResult<E> result = new BulkImportResult<>();
AtomicInteger i = new AtomicInteger(0);
if (request.getMapping().getHeader()) {
i.incrementAndGet();
}
parseData(request).forEach(entityData -> {
i.incrementAndGet();
try {
ImportedEntityInfo<E> importedEntityInfo = saveEntity(request, entityData.getFields(), user);
onEntityImported.accept(importedEntityInfo);
E entity = importedEntityInfo.getEntity();
saveKvs(user, entity, entityData.getKvs());
if (importedEntityInfo.getRelatedError() != null) {
throw new RuntimeException(importedEntityInfo.getRelatedError());
}
if (importedEntityInfo.isUpdated()) {
result.setUpdated(result.getUpdated() + 1);
} else {
result.setCreated(result.getCreated() + 1);
}
} catch (Exception e) {
result.setErrors(result.getErrors() + 1);
result.getErrorsList().add(String.format("Line %d: %s", i.get(), e.getMessage()));
}
});
return result;
}
protected abstract ImportedEntityInfo<E> saveEntity(BulkImportRequest importRequest, Map<BulkImportColumnType, String> fields, SecurityUser user);
/*
* Attributes' values are firstly added to JsonObject in order to then make some type cast,
* because we get all values as strings from CSV
* */
private void saveKvs(SecurityUser user, E entity, Map<ColumnMapping, ParsedValue> data) {
Arrays.stream(BulkImportColumnType.values())
.filter(BulkImportColumnType::isKv)
.map(kvType -> {
JsonObject kvs = new JsonObject();
data.entrySet().stream()
.filter(dataEntry -> dataEntry.getKey().getType() == kvType &&
StringUtils.isNotEmpty(dataEntry.getKey().getKey()))
.forEach(dataEntry -> kvs.add(dataEntry.getKey().getKey(), dataEntry.getValue().toJsonPrimitive()));
return Map.entry(kvType, kvs);
})
.filter(kvsEntry -> kvsEntry.getValue().entrySet().size() > 0)
.forEach(kvsEntry -> {
BulkImportColumnType kvType = kvsEntry.getKey();
if (kvType == BulkImportColumnType.SHARED_ATTRIBUTE || kvType == BulkImportColumnType.SERVER_ATTRIBUTE) {
saveAttributes(user, entity, kvsEntry, kvType);
} else {
saveTelemetry(user, entity, kvsEntry);
}
});
}
@SneakyThrows
private void saveTelemetry(SecurityUser user, E entity, Map.Entry<BulkImportColumnType, JsonObject> kvsEntry) {
List<TsKvEntry> timeseries = JsonConverter.convertToTelemetry(kvsEntry.getValue(), System.currentTimeMillis())
.entrySet().stream()
.flatMap(entry -> entry.getValue().stream().map(kvEntry -> new BasicTsKvEntry(entry.getKey(), kvEntry)))
.collect(Collectors.toList());
accessValidator.validateEntityAndCallback(user, Operation.WRITE_TELEMETRY, entity.getId(), (result, tenantId, entityId) -> {
TenantProfile tenantProfile = tenantProfileCache.get(tenantId);
long tenantTtl = TimeUnit.DAYS.toSeconds(((DefaultTenantProfileConfiguration) tenantProfile.getProfileData().getConfiguration()).getDefaultStorageTtlDays());
tsSubscriptionService.saveAndNotify(tenantId, user.getCustomerId(), entityId, timeseries, tenantTtl, new FutureCallback<Void>() {
@Override
public void onSuccess(@Nullable Void tmp) {
entityActionService.logEntityAction(user, (UUIDBased & EntityId) entityId, null, null,
ActionType.TIMESERIES_UPDATED, null, timeseries);
}
@Override
public void onFailure(Throwable t) {
entityActionService.logEntityAction(user, (UUIDBased & EntityId) entityId, null, null,
ActionType.TIMESERIES_UPDATED, BaseController.toException(t), timeseries);
throw new RuntimeException(t);
}
});
});
}
@SneakyThrows
private void saveAttributes(SecurityUser user, E entity, Map.Entry<BulkImportColumnType, JsonObject> kvsEntry, BulkImportColumnType kvType) {
String scope = kvType.getKey();
List<AttributeKvEntry> attributes = new ArrayList<>(JsonConverter.convertToAttributes(kvsEntry.getValue()));
accessValidator.validateEntityAndCallback(user, Operation.WRITE_ATTRIBUTES, entity.getId(), (result, tenantId, entityId) -> {
tsSubscriptionService.saveAndNotify(tenantId, entityId, scope, attributes, new FutureCallback<>() {
@Override
public void onSuccess(Void unused) {
entityActionService.logEntityAction(user, (UUIDBased & EntityId) entityId, null,
null, ActionType.ATTRIBUTES_UPDATED, null, scope, attributes);
}
@Override
public void onFailure(Throwable throwable) {
entityActionService.logEntityAction(user, (UUIDBased & EntityId) entityId, null,
null, ActionType.ATTRIBUTES_UPDATED, BaseController.toException(throwable),
scope, attributes);
throw new RuntimeException(throwable);
}
});
});
}
private List<EntityData> parseData(BulkImportRequest request) throws Exception {
List<List<String>> records = CsvUtils.parseCsv(request.getFile(), request.getMapping().getDelimiter());
if (request.getMapping().getHeader()) {
records.remove(0);
}
List<ColumnMapping> columnsMappings = request.getMapping().getColumns();
return records.stream()
.map(record -> {
EntityData entityData = new EntityData();
Stream.iterate(0, i -> i < record.size(), i -> i + 1)
.map(i -> Map.entry(columnsMappings.get(i), record.get(i)))
.filter(entry -> StringUtils.isNotEmpty(entry.getValue()))
.forEach(entry -> {
if (!entry.getKey().getType().isKv()) {
entityData.getFields().put(entry.getKey().getType(), entry.getValue());
} else {
Map.Entry<DataType, Object> castResult = TypeCastUtil.castValue(entry.getValue());
entityData.getKvs().put(entry.getKey(), new ParsedValue(castResult.getValue(), castResult.getKey()));
}
});
return entityData;
})
.collect(Collectors.toList());
}
@Data
protected static class EntityData {
private final Map<BulkImportColumnType, String> fields = new LinkedHashMap<>();
private final Map<ColumnMapping, ParsedValue> kvs = new LinkedHashMap<>();
}
@Data
protected static class ParsedValue {
private final Object value;
private final DataType dataType;
public JsonPrimitive toJsonPrimitive() {
switch (dataType) {
case STRING:
return new JsonPrimitive((String) value);
case LONG:
return new JsonPrimitive((Long) value);
case DOUBLE:
return new JsonPrimitive((Double) value);
case BOOLEAN:
return new JsonPrimitive((Boolean) value);
default:
return null;
}
}
public String stringValue() {
return value.toString();
}
}
}

77
application/src/main/java/org/thingsboard/server/service/importing/BulkImportColumnType.java

@ -0,0 +1,77 @@
/**
* 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.service.importing;
import lombok.Getter;
import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.device.credentials.lwm2m.LwM2MSecurityMode;
@Getter
public enum BulkImportColumnType {
NAME,
TYPE,
LABEL,
SHARED_ATTRIBUTE(DataConstants.SHARED_SCOPE, true),
SERVER_ATTRIBUTE(DataConstants.SERVER_SCOPE, true),
TIMESERIES(true),
ACCESS_TOKEN,
X509,
MQTT_CLIENT_ID,
MQTT_USER_NAME,
MQTT_PASSWORD,
LWM2M_CLIENT_ENDPOINT("endpoint"),
LWM2M_CLIENT_SECURITY_CONFIG_MODE("securityConfigClientMode", LwM2MSecurityMode.NO_SEC.name()),
LWM2M_CLIENT_IDENTITY("identity"),
LWM2M_CLIENT_KEY("key"),
LWM2M_CLIENT_CERT("cert"),
LWM2M_BOOTSTRAP_SERVER_SECURITY_MODE("securityMode", LwM2MSecurityMode.NO_SEC.name()),
LWM2M_BOOTSTRAP_SERVER_PUBLIC_KEY_OR_ID("clientPublicKeyOrId"),
LWM2M_BOOTSTRAP_SERVER_SECRET_KEY("clientSecretKey"),
LWM2M_SERVER_SECURITY_MODE("securityMode", LwM2MSecurityMode.NO_SEC.name()),
LWM2M_SERVER_CLIENT_PUBLIC_KEY_OR_ID("clientPublicKeyOrId"),
LWM2M_SERVER_CLIENT_SECRET_KEY("clientSecretKey"),
IS_GATEWAY,
DESCRIPTION,
EDGE_LICENSE_KEY,
CLOUD_ENDPOINT,
ROUTING_KEY,
SECRET;
private String key;
private String defaultValue;
private boolean isKv = false;
BulkImportColumnType() {
}
BulkImportColumnType(String key) {
this.key = key;
}
BulkImportColumnType(String key, String defaultValue) {
this.key = key;
this.defaultValue = defaultValue;
}
BulkImportColumnType(boolean isKv) {
this.isKv = isKv;
}
BulkImportColumnType(String key, boolean isKv) {
this.key = key;
this.isKv = isKv;
}
}

41
application/src/main/java/org/thingsboard/server/service/importing/BulkImportRequest.java

@ -0,0 +1,41 @@
/**
* 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.service.importing;
import lombok.Data;
import java.util.List;
@Data
public class BulkImportRequest {
private String file;
private Mapping mapping;
@Data
public static class Mapping {
private List<ColumnMapping> columns;
private Character delimiter;
private Boolean update;
private Boolean header;
}
@Data
public static class ColumnMapping {
private BulkImportColumnType type;
private String key;
}
}

30
application/src/main/java/org/thingsboard/server/service/importing/BulkImportResult.java

@ -0,0 +1,30 @@
/**
* 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.service.importing;
import lombok.Data;
import java.util.LinkedList;
import java.util.List;
@Data
public class BulkImportResult<E> {
private int created = 0;
private int updated = 0;
private int errors = 0;
private List<String> errorsList = new LinkedList<>();
}

26
application/src/main/java/org/thingsboard/server/service/importing/ImportedEntityInfo.java

@ -0,0 +1,26 @@
/**
* 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.service.importing;
import lombok.Data;
@Data
public class ImportedEntityInfo<E> {
private E entity;
private boolean isUpdated;
private E oldEntity;
private String relatedError;
}

12
application/src/main/java/org/thingsboard/server/service/ttl/AlarmsCleanUpService.java

@ -26,7 +26,6 @@ import org.thingsboard.server.common.data.id.AlarmId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.page.SortOrder;
import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration;
import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.msg.queue.ServiceType;
import org.thingsboard.server.dao.alarm.AlarmDao; import org.thingsboard.server.dao.alarm.AlarmDao;
@ -34,17 +33,12 @@ import org.thingsboard.server.dao.alarm.AlarmService;
import org.thingsboard.server.dao.relation.RelationService; import org.thingsboard.server.dao.relation.RelationService;
import org.thingsboard.server.dao.tenant.TbTenantProfileCache; import org.thingsboard.server.dao.tenant.TbTenantProfileCache;
import org.thingsboard.server.dao.tenant.TenantDao; import org.thingsboard.server.dao.tenant.TenantDao;
import org.thingsboard.server.dao.util.PsqlDao;
import org.thingsboard.server.queue.discovery.PartitionService; import org.thingsboard.server.queue.discovery.PartitionService;
import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.action.RuleEngineEntityActionService; import org.thingsboard.server.service.action.EntityActionService;
import org.thingsboard.server.service.ttl.AbstractCleanUpService;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.Date; import java.util.Date;
import java.util.Optional; import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
@TbCoreComponent @TbCoreComponent
@ -60,7 +54,7 @@ public class AlarmsCleanUpService {
private final AlarmDao alarmDao; private final AlarmDao alarmDao;
private final AlarmService alarmService; private final AlarmService alarmService;
private final RelationService relationService; private final RelationService relationService;
private final RuleEngineEntityActionService ruleEngineEntityActionService; private final EntityActionService entityActionService;
private final PartitionService partitionService; private final PartitionService partitionService;
private final TbTenantProfileCache tenantProfileCache; private final TbTenantProfileCache tenantProfileCache;
@ -90,7 +84,7 @@ public class AlarmsCleanUpService {
toRemove.getData().forEach(alarmId -> { toRemove.getData().forEach(alarmId -> {
relationService.deleteEntityRelations(tenantId, alarmId); relationService.deleteEntityRelations(tenantId, alarmId);
Alarm alarm = alarmService.deleteAlarm(tenantId, alarmId).getAlarm(); Alarm alarm = alarmService.deleteAlarm(tenantId, alarmId).getAlarm();
ruleEngineEntityActionService.pushEntityActionToRuleEngine(alarm.getOriginator(), alarm, tenantId, null, ActionType.ALARM_DELETE, null); entityActionService.pushEntityActionToRuleEngine(alarm.getOriginator(), alarm, tenantId, null, ActionType.ALARM_DELETE, null);
}); });
totalRemoved += toRemove.getTotalElements(); totalRemoved += toRemove.getTotalElements();

46
application/src/main/java/org/thingsboard/server/utils/CsvUtils.java

@ -0,0 +1,46 @@
/**
* 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.utils;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import org.apache.commons.csv.CSVFormat;
import org.apache.commons.csv.CSVRecord;
import org.apache.commons.io.input.CharSequenceReader;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class CsvUtils {
public static List<List<String>> parseCsv(String content, Character delimiter) throws Exception {
CSVFormat csvFormat = delimiter.equals(',') ? CSVFormat.DEFAULT : CSVFormat.DEFAULT.withDelimiter(delimiter);
List<CSVRecord> records;
try (CharSequenceReader reader = new CharSequenceReader(content)) {
records = csvFormat.parse(reader).getRecords();
}
return records.stream()
.map(record -> Stream.iterate(0, i -> i < record.size(), i -> i + 1)
.map(record::get)
.collect(Collectors.toList()))
.collect(Collectors.toList());
}
}

55
application/src/main/java/org/thingsboard/server/utils/TypeCastUtil.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.utils;
import org.apache.commons.lang3.math.NumberUtils;
import org.thingsboard.server.common.data.kv.DataType;
import java.math.BigDecimal;
import java.util.Map;
public class TypeCastUtil {
private TypeCastUtil() {}
public static Map.Entry<DataType, Object> castValue(String value) {
if (isNumber(value)) {
String formattedValue = value.replace(',', '.');
try {
BigDecimal bd = new BigDecimal(formattedValue);
if (bd.stripTrailingZeros().scale() > 0 || isSimpleDouble(formattedValue)) {
if (bd.scale() <= 16) {
return Map.entry(DataType.DOUBLE, bd.doubleValue());
}
} else {
return Map.entry(DataType.LONG, bd.longValueExact());
}
} catch (RuntimeException ignored) {}
} else if (value.equalsIgnoreCase("true") || value.equalsIgnoreCase("false")) {
return Map.entry(DataType.BOOLEAN, Boolean.parseBoolean(value));
}
return Map.entry(DataType.STRING, value);
}
private static boolean isNumber(String value) {
return NumberUtils.isNumber(value.replace(',', '.'));
}
private static boolean isSimpleDouble(String valueAsString) {
return valueAsString.contains(".") && !valueAsString.contains("E") && !valueAsString.contains("e");
}
}

2
common/dao-api/src/main/java/org/thingsboard/server/dao/device/DeviceCredentialsService.java

@ -29,5 +29,7 @@ public interface DeviceCredentialsService {
DeviceCredentials createDeviceCredentials(TenantId tenantId, DeviceCredentials deviceCredentials); DeviceCredentials createDeviceCredentials(TenantId tenantId, DeviceCredentials deviceCredentials);
void formatCredentials(DeviceCredentials deviceCredentials);
void deleteDeviceCredentials(TenantId tenantId, DeviceCredentials deviceCredentials); void deleteDeviceCredentials(TenantId tenantId, DeviceCredentials deviceCredentials);
} }

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

@ -29,6 +29,7 @@ import org.thingsboard.server.common.data.validation.NoXss;
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
import java.io.IOException; import java.io.IOException;
import java.util.Optional;
@EqualsAndHashCode(callSuper = true) @EqualsAndHashCode(callSuper = true)
@Slf4j @Slf4j
@ -83,6 +84,7 @@ public class Device extends SearchTextBasedWithAdditionalInfo<DeviceId> implemen
this.setDeviceData(device.getDeviceData()); this.setDeviceData(device.getDeviceData());
this.setFirmwareId(device.getFirmwareId()); this.setFirmwareId(device.getFirmwareId());
this.setSoftwareId(device.getSoftwareId()); this.setSoftwareId(device.getSoftwareId());
Optional.ofNullable(device.getAdditionalInfo()).ifPresent(this::setAdditionalInfo);
return this; return this;
} }

11
common/data/src/main/java/org/thingsboard/server/common/data/asset/Asset.java

@ -25,6 +25,8 @@ import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.validation.NoXss; import org.thingsboard.server.common.data.validation.NoXss;
import java.util.Optional;
@EqualsAndHashCode(callSuper = true) @EqualsAndHashCode(callSuper = true)
public class Asset extends SearchTextBasedWithAdditionalInfo<AssetId> implements HasName, HasTenantId, HasCustomerId { public class Asset extends SearchTextBasedWithAdditionalInfo<AssetId> implements HasName, HasTenantId, HasCustomerId {
@ -56,6 +58,15 @@ public class Asset extends SearchTextBasedWithAdditionalInfo<AssetId> implements
this.label = asset.getLabel(); this.label = asset.getLabel();
} }
public void update(Asset asset) {
this.tenantId = asset.getTenantId();
this.customerId = asset.getCustomerId();
this.name = asset.getName();
this.type = asset.getType();
this.label = asset.getLabel();
Optional.ofNullable(asset.getAdditionalInfo()).ifPresent(this::setAdditionalInfo);
}
public TenantId getTenantId() { public TenantId getTenantId() {
return tenantId; return tenantId;
} }

45
common/data/src/main/java/org/thingsboard/server/common/data/device/credentials/lwm2m/AbstractLwM2MServerCredentialsWithKeys.java

@ -0,0 +1,45 @@
/**
* 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.common.data.device.credentials.lwm2m;
import com.fasterxml.jackson.annotation.JsonIgnore;
import lombok.Getter;
import lombok.Setter;
import lombok.SneakyThrows;
import org.apache.commons.codec.binary.Hex;
@Getter
@Setter
public abstract class AbstractLwM2MServerCredentialsWithKeys implements LwM2MServerCredentials {
private String clientPublicKeyOrId;
private String clientSecretKey;
@JsonIgnore
public byte[] getDecodedClientPublicKeyOrId() {
return getDecoded(clientPublicKeyOrId);
}
@JsonIgnore
public byte[] getDecodedClientSecretKey() {
return getDecoded(clientSecretKey);
}
@SneakyThrows
private static byte[] getDecoded(String key) {
return Hex.decodeHex(key.toLowerCase().toCharArray());
}
}

26
common/data/src/main/java/org/thingsboard/server/common/data/device/credentials/lwm2m/LwM2MBootstrapCredentials.java

@ -0,0 +1,26 @@
/**
* 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.common.data.device.credentials.lwm2m;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class LwM2MBootstrapCredentials {
private LwM2MServerCredentials bootstrapServer;
private LwM2MServerCredentials lwm2mServer;
}

5
common/data/src/main/java/org/thingsboard/server/common/data/device/credentials/lwm2m/LwM2MClientCredentials.java

@ -16,6 +16,7 @@
package org.thingsboard.server.common.data.device.credentials.lwm2m; package org.thingsboard.server.common.data.device.credentials.lwm2m;
import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo; import com.fasterxml.jackson.annotation.JsonTypeInfo;
@ -26,7 +27,9 @@ import com.fasterxml.jackson.annotation.JsonTypeInfo;
@JsonSubTypes.Type(value = NoSecClientCredentials.class, name = "NO_SEC"), @JsonSubTypes.Type(value = NoSecClientCredentials.class, name = "NO_SEC"),
@JsonSubTypes.Type(value = PSKClientCredentials.class, name = "PSK"), @JsonSubTypes.Type(value = PSKClientCredentials.class, name = "PSK"),
@JsonSubTypes.Type(value = RPKClientCredentials.class, name = "RPK"), @JsonSubTypes.Type(value = RPKClientCredentials.class, name = "RPK"),
@JsonSubTypes.Type(value = X509ClientCredentials.class, name = "X509")}) @JsonSubTypes.Type(value = X509ClientCredentials.class, name = "X509")
})
@JsonIgnoreProperties(ignoreUnknown = true)
public interface LwM2MClientCredentials { public interface LwM2MClientCredentials {
@JsonIgnore @JsonIgnore

26
common/data/src/main/java/org/thingsboard/server/common/data/device/credentials/lwm2m/LwM2MDeviceCredentials.java

@ -0,0 +1,26 @@
/**
* 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.common.data.device.credentials.lwm2m;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class LwM2MDeviceCredentials {
private LwM2MClientCredentials client;
private LwM2MBootstrapCredentials bootstrap;
}

37
common/data/src/main/java/org/thingsboard/server/common/data/device/credentials/lwm2m/LwM2MServerCredentials.java

@ -0,0 +1,37 @@
/**
* 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.common.data.device.credentials.lwm2m;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
@JsonTypeInfo(
use = JsonTypeInfo.Id.NAME,
property = "securityMode")
@JsonSubTypes({
@JsonSubTypes.Type(value = NoSecServerCredentials.class, name = "NO_SEC"),
@JsonSubTypes.Type(value = PSKServerCredentials.class, name = "PSK"),
@JsonSubTypes.Type(value = RPKServerCredentials.class, name = "RPK"),
@JsonSubTypes.Type(value = X509ServerCredentials.class, name = "X509")
})
@JsonIgnoreProperties(ignoreUnknown = true)
public interface LwM2MServerCredentials {
@JsonIgnore
LwM2MSecurityMode getSecurityMode();
}

24
common/data/src/main/java/org/thingsboard/server/common/data/device/credentials/lwm2m/NoSecServerCredentials.java

@ -0,0 +1,24 @@
/**
* 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.common.data.device.credentials.lwm2m;
public class NoSecServerCredentials implements LwM2MServerCredentials {
@Override
public LwM2MSecurityMode getSecurityMode() {
return LwM2MSecurityMode.NO_SEC;
}
}

24
common/data/src/main/java/org/thingsboard/server/common/data/device/credentials/lwm2m/PSKServerCredentials.java

@ -0,0 +1,24 @@
/**
* 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.common.data.device.credentials.lwm2m;
public class PSKServerCredentials extends AbstractLwM2MServerCredentialsWithKeys {
@Override
public LwM2MSecurityMode getSecurityMode() {
return LwM2MSecurityMode.PSK;
}
}

24
common/data/src/main/java/org/thingsboard/server/common/data/device/credentials/lwm2m/RPKServerCredentials.java

@ -0,0 +1,24 @@
/**
* 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.common.data.device.credentials.lwm2m;
public class RPKServerCredentials extends AbstractLwM2MServerCredentialsWithKeys {
@Override
public LwM2MSecurityMode getSecurityMode() {
return LwM2MSecurityMode.RPK;
}
}

24
common/data/src/main/java/org/thingsboard/server/common/data/device/credentials/lwm2m/X509ServerCredentials.java

@ -0,0 +1,24 @@
/**
* 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.common.data.device.credentials.lwm2m;
public class X509ServerCredentials extends AbstractLwM2MServerCredentialsWithKeys {
@Override
public LwM2MSecurityMode getSecurityMode() {
return LwM2MSecurityMode.X509;
}
}

14
common/data/src/main/java/org/thingsboard/server/common/data/edge/Edge.java

@ -15,7 +15,6 @@
*/ */
package org.thingsboard.server.common.data.edge; package org.thingsboard.server.common.data.edge;
import com.fasterxml.jackson.databind.JsonNode;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;
import lombok.Getter; import lombok.Getter;
import lombok.Setter; import lombok.Setter;
@ -70,6 +69,19 @@ public class Edge extends SearchTextBasedWithAdditionalInfo<EdgeId> implements H
this.cloudEndpoint = edge.getCloudEndpoint(); this.cloudEndpoint = edge.getCloudEndpoint();
} }
public void update(Edge edge) {
this.tenantId = edge.getTenantId();
this.customerId = edge.getCustomerId();
this.rootRuleChainId = edge.getRootRuleChainId();
this.type = edge.getType();
this.label = edge.getLabel();
this.name = edge.getName();
this.routingKey = edge.getRoutingKey();
this.secret = edge.getSecret();
this.edgeLicenseKey = edge.getEdgeLicenseKey();
this.cloudEndpoint = edge.getCloudEndpoint();
}
@Override @Override
public String getSearchText() { public String getSearchText() {
return getName(); return getName();

8
common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/adaptor/JsonConverter.java

@ -577,6 +577,14 @@ public class JsonConverter {
return GSON.toJson(element); return GSON.toJson(element);
} }
public static JsonObject toJsonObject(Object o) {
return (JsonObject) GSON.toJsonTree(o);
}
public static <T> T fromJson(JsonElement element, Class<T> type) {
return GSON.fromJson(element, type);
}
public static void setTypeCastEnabled(boolean enabled) { public static void setTypeCastEnabled(boolean enabled) {
isTypeCastEnabled = enabled; isTypeCastEnabled = enabled;
} }

5
common/util/src/main/java/org/thingsboard/common/util/JacksonUtil.java

@ -118,4 +118,9 @@ public class JacksonUtil {
public static <T> JsonNode valueToTree(T value) { public static <T> JsonNode valueToTree(T value) {
return OBJECT_MAPPER.valueToTree(value); return OBJECT_MAPPER.valueToTree(value);
} }
public static <T> T treeToValue(JsonNode tree, Class<T> type) throws JsonProcessingException {
return OBJECT_MAPPER.treeToValue(tree, type);
}
} }

4
dao/pom.xml

@ -227,6 +227,10 @@
<groupId>org.elasticsearch.client</groupId> <groupId>org.elasticsearch.client</groupId>
<artifactId>rest</artifactId> <artifactId>rest</artifactId>
</dependency> </dependency>
<dependency>
<groupId>org.eclipse.leshan</groupId>
<artifactId>leshan-core</artifactId>
</dependency>
</dependencies> </dependencies>
<build> <build>
<plugins> <plugins>

241
dao/src/main/java/org/thingsboard/server/dao/device/DeviceCredentialsServiceImpl.java

@ -16,20 +16,28 @@
package org.thingsboard.server.dao.device; package org.thingsboard.server.dao.device;
import com.fasterxml.jackson.databind.node.ObjectNode;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.binary.Hex;
import org.eclipse.leshan.core.util.SecurityUtil;
import org.hibernate.exception.ConstraintViolationException; import org.hibernate.exception.ConstraintViolationException;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.CacheEvict; import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable; import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.device.credentials.BasicMqttCredentials; import org.thingsboard.server.common.data.device.credentials.BasicMqttCredentials;
import org.thingsboard.server.common.data.device.credentials.lwm2m.LwM2MBootstrapCredentials;
import org.thingsboard.server.common.data.device.credentials.lwm2m.LwM2MClientCredentials; import org.thingsboard.server.common.data.device.credentials.lwm2m.LwM2MClientCredentials;
import org.thingsboard.server.common.data.device.credentials.lwm2m.LwM2MDeviceCredentials;
import org.thingsboard.server.common.data.device.credentials.lwm2m.LwM2MServerCredentials;
import org.thingsboard.server.common.data.device.credentials.lwm2m.PSKClientCredentials; import org.thingsboard.server.common.data.device.credentials.lwm2m.PSKClientCredentials;
import org.thingsboard.server.common.data.device.credentials.lwm2m.PSKServerCredentials;
import org.thingsboard.server.common.data.device.credentials.lwm2m.RPKClientCredentials;
import org.thingsboard.server.common.data.device.credentials.lwm2m.RPKServerCredentials;
import org.thingsboard.server.common.data.device.credentials.lwm2m.X509ClientCredentials; import org.thingsboard.server.common.data.device.credentials.lwm2m.X509ClientCredentials;
import org.thingsboard.server.common.data.device.credentials.lwm2m.X509ServerCredentials;
import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
@ -37,6 +45,7 @@ import org.thingsboard.server.common.data.security.DeviceCredentials;
import org.thingsboard.server.common.msg.EncryptionUtil; import org.thingsboard.server.common.msg.EncryptionUtil;
import org.thingsboard.server.dao.entity.AbstractEntityService; import org.thingsboard.server.dao.entity.AbstractEntityService;
import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.dao.exception.DeviceCredentialsValidationException;
import org.thingsboard.server.dao.service.DataValidator; import org.thingsboard.server.dao.service.DataValidator;
import static org.thingsboard.server.common.data.CacheConstants.DEVICE_CREDENTIALS_CACHE; import static org.thingsboard.server.common.data.CacheConstants.DEVICE_CREDENTIALS_CACHE;
@ -83,17 +92,7 @@ public class DeviceCredentialsServiceImpl extends AbstractEntityService implemen
if (deviceCredentials.getCredentialsType() == null) { if (deviceCredentials.getCredentialsType() == null) {
throw new DataValidationException("Device credentials type should be specified"); throw new DataValidationException("Device credentials type should be specified");
} }
switch (deviceCredentials.getCredentialsType()) { formatCredentials(deviceCredentials);
case X509_CERTIFICATE:
formatCertData(deviceCredentials);
break;
case MQTT_BASIC:
formatSimpleMqttCredentials(deviceCredentials);
break;
case LWM2M_CREDENTIALS:
formatSimpleLwm2mCredentials(deviceCredentials);
break;
}
log.trace("Executing updateDeviceCredentials [{}]", deviceCredentials); log.trace("Executing updateDeviceCredentials [{}]", deviceCredentials);
credentialsValidator.validate(deviceCredentials, id -> tenantId); credentialsValidator.validate(deviceCredentials, id -> tenantId);
try { try {
@ -109,6 +108,21 @@ public class DeviceCredentialsServiceImpl extends AbstractEntityService implemen
} }
} }
@Override
public void formatCredentials(DeviceCredentials deviceCredentials) {
switch (deviceCredentials.getCredentialsType()) {
case X509_CERTIFICATE:
formatCertData(deviceCredentials);
break;
case MQTT_BASIC:
formatSimpleMqttCredentials(deviceCredentials);
break;
case LWM2M_CREDENTIALS:
formatSimpleLwm2mCredentials(deviceCredentials);
break;
}
}
private void formatSimpleMqttCredentials(DeviceCredentials deviceCredentials) { private void formatSimpleMqttCredentials(DeviceCredentials deviceCredentials) {
BasicMqttCredentials mqttCredentials; BasicMqttCredentials mqttCredentials;
try { try {
@ -117,11 +131,16 @@ public class DeviceCredentialsServiceImpl extends AbstractEntityService implemen
throw new IllegalArgumentException(); throw new IllegalArgumentException();
} }
} catch (IllegalArgumentException e) { } catch (IllegalArgumentException e) {
throw new DataValidationException("Invalid credentials body for simple mqtt credentials!"); throw new DeviceCredentialsValidationException("Invalid credentials body for simple mqtt credentials!");
} }
if (StringUtils.isEmpty(mqttCredentials.getClientId()) && StringUtils.isEmpty(mqttCredentials.getUserName())) { if (StringUtils.isEmpty(mqttCredentials.getClientId()) && StringUtils.isEmpty(mqttCredentials.getUserName())) {
throw new DataValidationException("Both mqtt client id and user name are empty!"); throw new DeviceCredentialsValidationException("Both mqtt client id and user name are empty!");
} }
if (StringUtils.isNotEmpty(mqttCredentials.getClientId()) && StringUtils.isNotEmpty(mqttCredentials.getPassword()) && StringUtils.isEmpty(mqttCredentials.getUserName())) {
throw new DeviceCredentialsValidationException("Password cannot be specified along with client id");
}
if (StringUtils.isEmpty(mqttCredentials.getClientId())) { if (StringUtils.isEmpty(mqttCredentials.getClientId())) {
deviceCredentials.setCredentialsId(mqttCredentials.getUserName()); deviceCredentials.setCredentialsId(mqttCredentials.getUserName());
} else if (StringUtils.isEmpty(mqttCredentials.getUserName())) { } else if (StringUtils.isEmpty(mqttCredentials.getUserName())) {
@ -129,7 +148,7 @@ public class DeviceCredentialsServiceImpl extends AbstractEntityService implemen
} else { } else {
deviceCredentials.setCredentialsId(EncryptionUtil.getSha3Hash("|", mqttCredentials.getClientId(), mqttCredentials.getUserName())); deviceCredentials.setCredentialsId(EncryptionUtil.getSha3Hash("|", mqttCredentials.getClientId(), mqttCredentials.getUserName()));
} }
if (!StringUtils.isEmpty(mqttCredentials.getPassword())) { if (StringUtils.isNotEmpty(mqttCredentials.getPassword())) {
mqttCredentials.setPassword(mqttCredentials.getPassword()); mqttCredentials.setPassword(mqttCredentials.getPassword());
} }
deviceCredentials.setCredentialsValue(JacksonUtil.toString(mqttCredentials)); deviceCredentials.setCredentialsValue(JacksonUtil.toString(mqttCredentials));
@ -143,22 +162,16 @@ public class DeviceCredentialsServiceImpl extends AbstractEntityService implemen
} }
private void formatSimpleLwm2mCredentials(DeviceCredentials deviceCredentials) { private void formatSimpleLwm2mCredentials(DeviceCredentials deviceCredentials) {
LwM2MClientCredentials clientCredentials; LwM2MDeviceCredentials lwM2MCredentials;
ObjectNode json;
try { try {
json = JacksonUtil.fromString(deviceCredentials.getCredentialsValue(), ObjectNode.class); lwM2MCredentials = JacksonUtil.fromString(deviceCredentials.getCredentialsValue(), LwM2MDeviceCredentials.class);
if (json == null) { validateLwM2MDeviceCredentials(lwM2MCredentials);
throw new IllegalArgumentException();
}
clientCredentials = JacksonUtil.convertValue(json.get("client"), LwM2MClientCredentials.class);
if (clientCredentials == null) {
throw new IllegalArgumentException();
}
} catch (IllegalArgumentException e) { } catch (IllegalArgumentException e) {
throw new DataValidationException("Invalid credentials body for LwM2M credentials!"); throw new DeviceCredentialsValidationException("Invalid credentials body for LwM2M credentials!");
} }
String credentialsId = null; String credentialsId = null;
LwM2MClientCredentials clientCredentials = lwM2MCredentials.getClient();
switch (clientCredentials.getSecurityConfigClientMode()) { switch (clientCredentials.getSecurityConfigClientMode()) {
case NO_SEC: case NO_SEC:
@ -174,8 +187,8 @@ public class DeviceCredentialsServiceImpl extends AbstractEntityService implemen
String cert = EncryptionUtil.trimNewLines(x509Config.getCert()); String cert = EncryptionUtil.trimNewLines(x509Config.getCert());
String sha3Hash = EncryptionUtil.getSha3Hash(cert); String sha3Hash = EncryptionUtil.getSha3Hash(cert);
x509Config.setCert(cert); x509Config.setCert(cert);
((ObjectNode) json.get("client")).put("cert", cert); ((X509ClientCredentials) clientCredentials).setCert(cert);
deviceCredentials.setCredentialsValue(JacksonUtil.toString(json)); deviceCredentials.setCredentialsValue(JacksonUtil.toString(lwM2MCredentials));
credentialsId = sha3Hash; credentialsId = sha3Hash;
} else { } else {
credentialsId = x509Config.getEndpoint(); credentialsId = x509Config.getEndpoint();
@ -183,11 +196,163 @@ public class DeviceCredentialsServiceImpl extends AbstractEntityService implemen
break; break;
} }
if (credentialsId == null) { if (credentialsId == null) {
throw new DataValidationException("Invalid credentials body for LwM2M credentials!"); throw new DeviceCredentialsValidationException("Invalid credentials body for LwM2M credentials!");
} }
deviceCredentials.setCredentialsId(credentialsId); deviceCredentials.setCredentialsId(credentialsId);
} }
private void validateLwM2MDeviceCredentials(LwM2MDeviceCredentials lwM2MCredentials) {
if (lwM2MCredentials == null) {
throw new DeviceCredentialsValidationException("LwM2M credentials should be specified!");
}
LwM2MClientCredentials clientCredentials = lwM2MCredentials.getClient();
if (clientCredentials == null) {
throw new DeviceCredentialsValidationException("LwM2M client credentials should be specified!");
}
validateLwM2MClientCredentials(clientCredentials);
LwM2MBootstrapCredentials bootstrapCredentials = lwM2MCredentials.getBootstrap();
if (bootstrapCredentials == null) {
throw new DeviceCredentialsValidationException("LwM2M bootstrap credentials should be specified!");
}
LwM2MServerCredentials bootstrapServerCredentials = bootstrapCredentials.getBootstrapServer();
if (bootstrapServerCredentials == null) {
throw new DeviceCredentialsValidationException("LwM2M bootstrap server credentials should be specified!");
}
validateServerCredentials(bootstrapServerCredentials, "Bootstrap server");
LwM2MServerCredentials lwm2mServerCredentials = bootstrapCredentials.getLwm2mServer();
if (lwm2mServerCredentials == null) {
throw new DeviceCredentialsValidationException("LwM2M lwm2m server credentials should be specified!");
}
validateServerCredentials(lwm2mServerCredentials, "LwM2M server");
}
private void validateLwM2MClientCredentials(LwM2MClientCredentials clientCredentials) {
if (StringUtils.isEmpty(clientCredentials.getEndpoint())) {
throw new DeviceCredentialsValidationException("LwM2M client endpoint should be specified!");
}
switch (clientCredentials.getSecurityConfigClientMode()) {
case NO_SEC:
break;
case PSK:
PSKClientCredentials pskCredentials = (PSKClientCredentials) clientCredentials;
if (StringUtils.isEmpty(pskCredentials.getIdentity())) {
throw new DeviceCredentialsValidationException("LwM2M client PSK identity should be specified!");
}
String pskKey = pskCredentials.getKey();
if (StringUtils.isEmpty(pskKey)) {
throw new DeviceCredentialsValidationException("LwM2M client PSK key should be specified!");
}
if (!pskKey.matches("-?[0-9a-fA-F]+")) {
throw new DeviceCredentialsValidationException("LwM2M client PSK key should be HexDecimal format!");
}
if (pskKey.length() % 32 != 0 || pskKey.length() > 128) {
throw new DeviceCredentialsValidationException("LwM2M client PSK key must be 32, 64, 128 characters!");
}
break;
case RPK:
RPKClientCredentials rpkCredentials = (RPKClientCredentials) clientCredentials;
if (StringUtils.isEmpty(rpkCredentials.getKey())) {
throw new DeviceCredentialsValidationException("LwM2M client RPK key should be specified!");
}
try {
SecurityUtil.publicKey.decode(rpkCredentials.getDecodedKey());
} catch (Exception e) {
throw new DeviceCredentialsValidationException("LwM2M client RPK key should be in RFC7250 standard!");
}
break;
case X509:
X509ClientCredentials x509CCredentials = (X509ClientCredentials) clientCredentials;
if (x509CCredentials.getCert() != null) {
try {
SecurityUtil.certificate.decode(Hex.decodeHex(x509CCredentials.getCert().toLowerCase().toCharArray()));
} catch (Exception e) {
throw new DeviceCredentialsValidationException("LwM2M client X509 certificate should be in DER-encoded X.509 format!");
}
}
break;
}
}
private void validateServerCredentials(LwM2MServerCredentials serverCredentials, String server) {
switch (serverCredentials.getSecurityMode()) {
case NO_SEC:
break;
case PSK:
PSKServerCredentials pskCredentials = (PSKServerCredentials) serverCredentials;
if (StringUtils.isEmpty(pskCredentials.getClientPublicKeyOrId())) {
throw new DeviceCredentialsValidationException(server + " client PSK public key or id should be specified!");
}
String pskKey = pskCredentials.getClientSecretKey();
if (StringUtils.isEmpty(pskKey)) {
throw new DeviceCredentialsValidationException(server + " client PSK key should be specified!");
}
if (!pskKey.matches("-?[0-9a-fA-F]+")) {
throw new DeviceCredentialsValidationException(server + " client PSK key should be HexDecimal format!");
}
if (pskKey.length() % 32 != 0 || pskKey.length() > 128) {
throw new DeviceCredentialsValidationException(server + " client PSK key must be 32, 64, 128 characters!");
}
break;
case RPK:
RPKServerCredentials rpkCredentials = (RPKServerCredentials) serverCredentials;
if (StringUtils.isEmpty(rpkCredentials.getClientPublicKeyOrId())) {
throw new DeviceCredentialsValidationException(server + " client RPK public key or id should be specified!");
}
try {
SecurityUtil.publicKey.decode(rpkCredentials.getDecodedClientPublicKeyOrId());
} catch (Exception e) {
throw new DeviceCredentialsValidationException(server + " client RPK public key or id should be in RFC7250 standard!");
}
if (StringUtils.isEmpty(rpkCredentials.getClientSecretKey())) {
throw new DeviceCredentialsValidationException(server + " client RPK secret key should be specified!");
}
try {
SecurityUtil.privateKey.decode(rpkCredentials.getDecodedClientSecretKey());
} catch (Exception e) {
throw new DeviceCredentialsValidationException(server + " client RPK secret key should be in RFC5958 standard!");
}
break;
case X509:
X509ServerCredentials x509CCredentials = (X509ServerCredentials) serverCredentials;
if (StringUtils.isEmpty(x509CCredentials.getClientPublicKeyOrId())) {
throw new DeviceCredentialsValidationException(server + " client X509 public key or id should be specified!");
}
try {
SecurityUtil.certificate.decode(x509CCredentials.getDecodedClientPublicKeyOrId());
} catch (Exception e) {
throw new DeviceCredentialsValidationException(server + " client X509 public key or id should be in DER-encoded X.509 format!");
}
if (StringUtils.isEmpty(x509CCredentials.getClientSecretKey())) {
throw new DeviceCredentialsValidationException(server + " client X509 secret key should be specified!");
}
try {
SecurityUtil.privateKey.decode(x509CCredentials.getDecodedClientSecretKey());
} catch (Exception e) {
throw new DeviceCredentialsValidationException(server + " client X509 secret key should be in RFC5958 standard!");
}
break;
}
}
@Override @Override
@CacheEvict(cacheNames = DEVICE_CREDENTIALS_CACHE, key = "'deviceCredentials_' + #deviceCredentials.credentialsId") @CacheEvict(cacheNames = DEVICE_CREDENTIALS_CACHE, key = "'deviceCredentials_' + #deviceCredentials.credentialsId")
public void deleteDeviceCredentials(TenantId tenantId, DeviceCredentials deviceCredentials) { public void deleteDeviceCredentials(TenantId tenantId, DeviceCredentials deviceCredentials) {
@ -201,38 +366,38 @@ public class DeviceCredentialsServiceImpl extends AbstractEntityService implemen
@Override @Override
protected void validateCreate(TenantId tenantId, DeviceCredentials deviceCredentials) { protected void validateCreate(TenantId tenantId, DeviceCredentials deviceCredentials) {
if (deviceCredentialsDao.findByDeviceId(tenantId, deviceCredentials.getDeviceId().getId()) != null) { if (deviceCredentialsDao.findByDeviceId(tenantId, deviceCredentials.getDeviceId().getId()) != null) {
throw new DataValidationException("Credentials for this device are already specified!"); throw new DeviceCredentialsValidationException("Credentials for this device are already specified!");
} }
if (deviceCredentialsDao.findByCredentialsId(tenantId, deviceCredentials.getCredentialsId()) != null) { if (deviceCredentialsDao.findByCredentialsId(tenantId, deviceCredentials.getCredentialsId()) != null) {
throw new DataValidationException("Device credentials are already assigned to another device!"); throw new DeviceCredentialsValidationException("Device credentials are already assigned to another device!");
} }
} }
@Override @Override
protected void validateUpdate(TenantId tenantId, DeviceCredentials deviceCredentials) { protected void validateUpdate(TenantId tenantId, DeviceCredentials deviceCredentials) {
if (deviceCredentialsDao.findById(tenantId, deviceCredentials.getUuidId()) == null) { if (deviceCredentialsDao.findById(tenantId, deviceCredentials.getUuidId()) == null) {
throw new DataValidationException("Unable to update non-existent device credentials!"); throw new DeviceCredentialsValidationException("Unable to update non-existent device credentials!");
} }
DeviceCredentials existingCredentials = deviceCredentialsDao.findByCredentialsId(tenantId, deviceCredentials.getCredentialsId()); DeviceCredentials existingCredentials = deviceCredentialsDao.findByCredentialsId(tenantId, deviceCredentials.getCredentialsId());
if (existingCredentials != null && !existingCredentials.getId().equals(deviceCredentials.getId())) { if (existingCredentials != null && !existingCredentials.getId().equals(deviceCredentials.getId())) {
throw new DataValidationException("Device credentials are already assigned to another device!"); throw new DeviceCredentialsValidationException("Device credentials are already assigned to another device!");
} }
} }
@Override @Override
protected void validateDataImpl(TenantId tenantId, DeviceCredentials deviceCredentials) { protected void validateDataImpl(TenantId tenantId, DeviceCredentials deviceCredentials) {
if (deviceCredentials.getDeviceId() == null) { if (deviceCredentials.getDeviceId() == null) {
throw new DataValidationException("Device credentials should be assigned to device!"); throw new DeviceCredentialsValidationException("Device credentials should be assigned to device!");
} }
if (deviceCredentials.getCredentialsType() == null) { if (deviceCredentials.getCredentialsType() == null) {
throw new DataValidationException("Device credentials type should be specified!"); throw new DeviceCredentialsValidationException("Device credentials type should be specified!");
} }
if (StringUtils.isEmpty(deviceCredentials.getCredentialsId())) { if (StringUtils.isEmpty(deviceCredentials.getCredentialsId())) {
throw new DataValidationException("Device credentials id should be specified!"); throw new DeviceCredentialsValidationException("Device credentials id should be specified!");
} }
Device device = deviceService.findDeviceById(tenantId, deviceCredentials.getDeviceId()); Device device = deviceService.findDeviceById(tenantId, deviceCredentials.getDeviceId());
if (device == null) { if (device == null) {
throw new DataValidationException("Can't assign device credentials to non-existent device!"); throw new DeviceCredentialsValidationException("Can't assign device credentials to non-existent device!");
} }
} }
}; };

3
dao/src/main/java/org/thingsboard/server/dao/device/DeviceServiceImpl.java

@ -228,6 +228,7 @@ public class DeviceServiceImpl extends AbstractEntityService implements DeviceSe
if (foundDeviceCredentials == null) { if (foundDeviceCredentials == null) {
deviceCredentialsService.createDeviceCredentials(savedDevice.getTenantId(), deviceCredentials); deviceCredentialsService.createDeviceCredentials(savedDevice.getTenantId(), deviceCredentials);
} else { } else {
deviceCredentials.setId(foundDeviceCredentials.getId());
deviceCredentialsService.updateDeviceCredentials(device.getTenantId(), deviceCredentials); deviceCredentialsService.updateDeviceCredentials(device.getTenantId(), deviceCredentials);
} }
} }
@ -241,7 +242,7 @@ public class DeviceServiceImpl extends AbstractEntityService implements DeviceSe
deviceCredentials.setDeviceId(new DeviceId(savedDevice.getUuidId())); deviceCredentials.setDeviceId(new DeviceId(savedDevice.getUuidId()));
deviceCredentials.setCredentialsType(DeviceCredentialsType.ACCESS_TOKEN); deviceCredentials.setCredentialsType(DeviceCredentialsType.ACCESS_TOKEN);
deviceCredentials.setCredentialsId(!StringUtils.isEmpty(accessToken) ? accessToken : RandomStringUtils.randomAlphanumeric(20)); deviceCredentials.setCredentialsId(!StringUtils.isEmpty(accessToken) ? accessToken : RandomStringUtils.randomAlphanumeric(20));
deviceCredentialsService.createDeviceCredentials(device.getTenantId(), deviceCredentials); deviceCredentialsService.createDeviceCredentials(savedDevice.getTenantId(), deviceCredentials);
} }
return savedDevice; return savedDevice;
} }

22
dao/src/main/java/org/thingsboard/server/dao/exception/DeviceCredentialsValidationException.java

@ -0,0 +1,22 @@
/**
* 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.dao.exception;
public class DeviceCredentialsValidationException extends DataValidationException {
public DeviceCredentialsValidationException(String message) {
super(message);
}
}

5
ui-ngx/src/app/core/http/asset.service.ts

@ -22,6 +22,7 @@ import { PageLink } from '@shared/models/page/page-link';
import { PageData } from '@shared/models/page/page-data'; import { PageData } from '@shared/models/page/page-data';
import { EntitySubtype } from '@app/shared/models/entity-type.models'; import { EntitySubtype } from '@app/shared/models/entity-type.models';
import { Asset, AssetInfo, AssetSearchQuery } from '@app/shared/models/asset.models'; import { Asset, AssetInfo, AssetSearchQuery } from '@app/shared/models/asset.models';
import { BulkImportRequest, BulkImportResult } from '@home/components/import-export/import-export.models';
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
@ -105,4 +106,8 @@ export class AssetService {
defaultHttpOptionsFromConfig(config)); defaultHttpOptionsFromConfig(config));
} }
public bulkImportAssets(entitiesData: BulkImportRequest, config?: RequestConfig): Observable<BulkImportResult> {
return this.http.post<BulkImportResult>('/api/asset/bulk_import', entitiesData, defaultHttpOptionsFromConfig(config));
}
} }

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

@ -30,6 +30,7 @@ import {
} from '@app/shared/models/device.models'; } from '@app/shared/models/device.models';
import { EntitySubtype } from '@app/shared/models/entity-type.models'; import { EntitySubtype } from '@app/shared/models/entity-type.models';
import { AuthService } from '@core/auth/auth.service'; import { AuthService } from '@core/auth/auth.service';
import { BulkImportRequest, BulkImportResult } from '@home/components/import-export/import-export.models';
import { PersistentRpc } from '@shared/models/rpc.models'; import { PersistentRpc } from '@shared/models/rpc.models';
@Injectable({ @Injectable({
@ -178,4 +179,8 @@ export class DeviceService {
defaultHttpOptionsFromConfig(config)); defaultHttpOptionsFromConfig(config));
} }
public bulkImportDevices(entitiesData: BulkImportRequest, config?: RequestConfig): Observable<BulkImportResult> {
return this.http.post<BulkImportResult>('/api/device/bulk_import', entitiesData, defaultHttpOptionsFromConfig(config));
}
} }

7
ui-ngx/src/app/core/http/edge.service.ts

@ -23,6 +23,7 @@ import { PageData } from '@shared/models/page/page-data';
import { EntitySubtype } from '@app/shared/models/entity-type.models'; import { EntitySubtype } from '@app/shared/models/entity-type.models';
import { Edge, EdgeEvent, EdgeInfo, EdgeSearchQuery } from '@shared/models/edge.models'; import { Edge, EdgeEvent, EdgeInfo, EdgeSearchQuery } from '@shared/models/edge.models';
import { EntityId } from '@shared/models/id/entity-id'; import { EntityId } from '@shared/models/id/entity-id';
import { BulkImportRequest, BulkImportResult } from '@home/components/import-export/import-export.models';
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
@ -59,7 +60,7 @@ export class EdgeService {
} }
public getCustomerEdgeInfos(customerId: string, pageLink: PageLink, type: string = '', public getCustomerEdgeInfos(customerId: string, pageLink: PageLink, type: string = '',
config?: RequestConfig): Observable<PageData<EdgeInfo>> { config?: RequestConfig): Observable<PageData<EdgeInfo>> {
return this.http.get<PageData<EdgeInfo>>(`/api/customer/${customerId}/edgeInfos${pageLink.toQuery()}&type=${type}`, return this.http.get<PageData<EdgeInfo>>(`/api/customer/${customerId}/edgeInfos${pageLink.toQuery()}&type=${type}`,
defaultHttpOptionsFromConfig(config)); defaultHttpOptionsFromConfig(config));
} }
@ -108,4 +109,8 @@ export class EdgeService {
public findByName(edgeName: string, config?: RequestConfig): Observable<Edge> { public findByName(edgeName: string, config?: RequestConfig): Observable<Edge> {
return this.http.get<Edge>(`/api/tenant/edges?edgeName=${edgeName}`, defaultHttpOptionsFromConfig(config)); return this.http.get<Edge>(`/api/tenant/edges?edgeName=${edgeName}`, defaultHttpOptionsFromConfig(config));
} }
public bulkImportEdges(entitiesData: BulkImportRequest, config?: RequestConfig): Observable<BulkImportResult> {
return this.http.post<BulkImportResult>('/api/edge/bulk_import', entitiesData, defaultHttpOptionsFromConfig(config));
}
} }

69
ui-ngx/src/app/core/http/entity.service.ts

@ -59,7 +59,7 @@ import {
ImportEntityData ImportEntityData
} from '@shared/models/entity.models'; } from '@shared/models/entity.models';
import { EntityRelationService } from '@core/http/entity-relation.service'; import { EntityRelationService } from '@core/http/entity-relation.service';
import { deepClone, generateSecret, guid, isDefined, isDefinedAndNotNull } from '@core/utils'; import { deepClone, generateSecret, guid, isDefined, isDefinedAndNotNull, isNotEmptyStr } from '@core/utils';
import { Asset } from '@shared/models/asset.models'; import { Asset } from '@shared/models/asset.models';
import { Device, DeviceCredentialsType } from '@shared/models/device.models'; import { Device, DeviceCredentialsType } from '@shared/models/device.models';
import { AttributeService } from '@core/http/attribute.service'; import { AttributeService } from '@core/http/attribute.service';
@ -964,7 +964,12 @@ export class EntityService {
map(() => { map(() => {
return { create: { entity: 1 } } as ImportEntitiesResultInfo; return { create: { entity: 1 } } as ImportEntitiesResultInfo;
}), }),
catchError(err => of({ error: { entity: 1 } } as ImportEntitiesResultInfo)) catchError(err => of({
error: {
entity: 1,
errors: err.message
}
} as ImportEntitiesResultInfo))
); );
}), }),
catchError(err => { catchError(err => {
@ -988,13 +993,28 @@ export class EntityService {
map(() => { map(() => {
return { update: { entity: 1 } } as ImportEntitiesResultInfo; return { update: { entity: 1 } } as ImportEntitiesResultInfo;
}), }),
catchError(updateError => of({ error: { entity: 1 } } as ImportEntitiesResultInfo)) catchError(updateError => of({
error: {
entity: 1,
errors: updateError.message
}
} as ImportEntitiesResultInfo))
); );
}), }),
catchError(findErr => of({ error: { entity: 1 } } as ImportEntitiesResultInfo)) catchError(findErr => of({
error: {
entity: 1,
errors: `Line: ${entityData.lineNumber}; Error: ${findErr.error.message}`
}
} as ImportEntitiesResultInfo))
); );
} else { } else {
return of({ error: { entity: 1 } } as ImportEntitiesResultInfo); return of({
error: {
entity: 1,
errors: `Line: ${entityData.lineNumber}; Error: ${err.error.message}`
}
} as ImportEntitiesResultInfo);
} }
}) })
); );
@ -1050,7 +1070,6 @@ export class EntityService {
break; break;
} }
return saveEntityObservable; return saveEntityObservable;
} }
private getUpdateEntityTasks(entityType: EntityType, entityData: ImportEntityData | EdgeImportEntityData, private getUpdateEntityTasks(entityType: EntityType, entityData: ImportEntityData | EdgeImportEntityData,
@ -1123,15 +1142,31 @@ export class EntityService {
public saveEntityData(entityId: EntityId, entityData: ImportEntityData, config?: RequestConfig): Observable<any> { public saveEntityData(entityId: EntityId, entityData: ImportEntityData, config?: RequestConfig): Observable<any> {
const observables: Observable<string>[] = []; const observables: Observable<string>[] = [];
let observable: Observable<string>; let observable: Observable<string>;
if (entityData.accessToken && entityData.accessToken !== '') { if (Object.keys(entityData.credential).length) {
let credentialsType: DeviceCredentialsType;
let credentialsId: string = null;
let credentialsValue: string = null;
if (isDefinedAndNotNull(entityData.credential.mqtt)) {
credentialsType = DeviceCredentialsType.MQTT_BASIC;
credentialsValue = JSON.stringify(entityData.credential.mqtt);
} else if (isDefinedAndNotNull(entityData.credential.lwm2m)) {
credentialsType = DeviceCredentialsType.LWM2M_CREDENTIALS;
credentialsValue = JSON.stringify(entityData.credential.lwm2m);
} else if (isNotEmptyStr(entityData.credential.x509)) {
credentialsType = DeviceCredentialsType.X509_CERTIFICATE;
credentialsValue = entityData.credential.x509;
} else {
credentialsType = DeviceCredentialsType.ACCESS_TOKEN;
credentialsId = entityData.credential.accessToken;
}
observable = this.deviceService.getDeviceCredentials(entityId.id, false, config).pipe( observable = this.deviceService.getDeviceCredentials(entityId.id, false, config).pipe(
mergeMap((credentials) => { mergeMap((credentials) => {
credentials.credentialsId = entityData.accessToken; credentials.credentialsId = credentialsId;
credentials.credentialsType = DeviceCredentialsType.ACCESS_TOKEN; credentials.credentialsType = credentialsType;
credentials.credentialsValue = null; credentials.credentialsValue = credentialsValue;
return this.deviceService.saveDeviceCredentials(credentials, config).pipe( return this.deviceService.saveDeviceCredentials(credentials, config).pipe(
map(() => 'ok'), map(() => 'ok'),
catchError(err => of('error')) catchError(err => of(`Line: ${entityData.lineNumber}; Error: ${err.error.message}`))
); );
}) })
); );
@ -1141,7 +1176,7 @@ export class EntityService {
observable = this.attributeService.saveEntityAttributes(entityId, AttributeScope.SHARED_SCOPE, observable = this.attributeService.saveEntityAttributes(entityId, AttributeScope.SHARED_SCOPE,
entityData.attributes.shared, config).pipe( entityData.attributes.shared, config).pipe(
map(() => 'ok'), map(() => 'ok'),
catchError(err => of('error')) catchError(err => of(`Line: ${entityData.lineNumber}; Error: ${err.error.message}`))
); );
observables.push(observable); observables.push(observable);
} }
@ -1149,23 +1184,23 @@ export class EntityService {
observable = this.attributeService.saveEntityAttributes(entityId, AttributeScope.SERVER_SCOPE, observable = this.attributeService.saveEntityAttributes(entityId, AttributeScope.SERVER_SCOPE,
entityData.attributes.server, config).pipe( entityData.attributes.server, config).pipe(
map(() => 'ok'), map(() => 'ok'),
catchError(err => of('error')) catchError(err => of(`Line: ${entityData.lineNumber}; Error: ${err.error.message}`))
); );
observables.push(observable); observables.push(observable);
} }
if (entityData.timeseries && entityData.timeseries.length) { if (entityData.timeseries && entityData.timeseries.length) {
observable = this.attributeService.saveEntityTimeseries(entityId, 'time', entityData.timeseries, config).pipe( observable = this.attributeService.saveEntityTimeseries(entityId, 'time', entityData.timeseries, config).pipe(
map(() => 'ok'), map(() => 'ok'),
catchError(err => of('error')) catchError(err => of(`Line: ${entityData.lineNumber}; Error: ${err.error.message}`))
); );
observables.push(observable); observables.push(observable);
} }
if (observables.length) { if (observables.length) {
return forkJoin(observables).pipe( return forkJoin(observables).pipe(
map((response) => { map((response) => {
const hasError = response.filter((status) => status === 'error').length > 0; const hasError = response.filter((status) => status !== 'ok');
if (hasError) { if (hasError.length > 0) {
throw Error(); throw Error(hasError.join('\n'));
} else { } else {
return response; return response;
} }

27
ui-ngx/src/app/modules/home/components/import-export/import-dialog-csv.component.html

@ -94,7 +94,7 @@
<mat-step [stepControl]="columnTypesFormGroup"> <mat-step [stepControl]="columnTypesFormGroup">
<form [formGroup]="columnTypesFormGroup"> <form [formGroup]="columnTypesFormGroup">
<ng-template matStepLabel>{{ 'import.stepper-text.column-type' | translate }}</ng-template> <ng-template matStepLabel>{{ 'import.stepper-text.column-type' | translate }}</ng-template>
<tb-table-columns-assignment formControlName="columnsParam" [entityType]="entityType"></tb-table-columns-assignment> <tb-table-columns-assignment #columnsAssignmentComponent formControlName="columnsParam" [entityType]="entityType"></tb-table-columns-assignment>
</form> </form>
<div fxLayout="row wrap" fxLayoutAlign="space-between center"> <div fxLayout="row wrap" fxLayoutAlign="space-between center">
<button mat-button <button mat-button
@ -113,21 +113,32 @@
</mat-step> </mat-step>
<mat-step> <mat-step>
<ng-template matStepLabel>{{ 'import.stepper-text.creat-entities' | translate }}</ng-template> <ng-template matStepLabel>{{ 'import.stepper-text.creat-entities' | translate }}</ng-template>
<mat-progress-bar color="warn" class="tb-import-progress" mode="determinate" [value]="progressCreate"> <mat-progress-bar color="warn" class="tb-import-progress" mode="indeterminate">
</mat-progress-bar> </mat-progress-bar>
</mat-step> </mat-step>
<mat-step> <mat-step>
<ng-template matStepLabel>{{ 'import.stepper-text.done' | translate }}</ng-template> <ng-template matStepLabel>{{ 'import.stepper-text.done' | translate }}</ng-template>
<div fxLayout="column"> <div fxLayout="column">
<p class="mat-body-1" *ngIf="this.statistical?.create && this.statistical?.create.entity"> <p class="mat-body-1" *ngIf="this.statistical?.created">
{{ translate.instant('import.message.create-entities', {count: this.statistical.create.entity}) }} {{ translate.instant('import.message.create-entities', {count: this.statistical.created}) }}
</p> </p>
<p class="mat-body-1" *ngIf="this.statistical?.update && this.statistical?.update.entity"> <p class="mat-body-1" *ngIf="this.statistical?.updated">
{{ translate.instant('import.message.update-entities', {count: this.statistical.update.entity}) }} {{ translate.instant('import.message.update-entities', {count: this.statistical.updated}) }}
</p> </p>
<p class="mat-body-1" *ngIf="this.statistical?.error && this.statistical?.error.entity"> <p class="mat-body-1" style="margin-bottom: 0.8em" *ngIf="this.statistical?.errors">
{{ translate.instant('import.message.error-entities', {count: this.statistical.error.entity}) }} {{ translate.instant('import.message.error-entities', {count: this.statistical.errors}) }}
</p> </p>
<mat-expansion-panel class="advanced-logs" [expanded]="false"
*ngIf="this.statistical?.errorsList?.length"
(opened)="initEditor()">
<mat-expansion-panel-header [collapsedHeight]="'38px'" [expandedHeight]="'38px'">
<mat-panel-title>
<div class="tb-small" translate>import.details</div>
</mat-panel-title>
</mat-expansion-panel-header>
<mat-divider></mat-divider>
<div #failureDetailsEditor class="tb-failure-details"></div>
</mat-expansion-panel>
</div> </div>
<div fxLayout="row" fxLayoutAlign="end center" fxLayoutGap="20px"> <div fxLayout="row" fxLayoutAlign="end center" fxLayoutGap="20px">
<button mat-raised-button <button mat-raised-button

25
ui-ngx/src/app/modules/home/components/import-export/import-dialog-csv.component.scss

@ -26,5 +26,30 @@
.tb-import-progress{ .tb-import-progress{
margin: 7px 0; margin: 7px 0;
} }
.tb-failure-details {
width: 100%;
min-width: 300px;
height: 100%;
min-height: 50px;
margin-top: 8px;
}
.mat-expansion-panel {
box-shadow: none;
&.advanced-logs {
border: 1px groove rgba(0, 0, 0, .25);
padding: 0;
margin-bottom: 1.6em;
.mat-expansion-panel-header {
padding: 0 8px;
}
.mat-expansion-panel-body {
padding: 0;
}
}
}
} }
} }

204
ui-ngx/src/app/modules/home/components/import-export/import-dialog-csv.component.ts

@ -14,7 +14,7 @@
/// limitations under the License. /// limitations under the License.
/// ///
import { Component, Inject, OnInit, ViewChild } from '@angular/core'; import { AfterViewInit, Component, ElementRef, Inject, Renderer2, ViewChild } from '@angular/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state'; import { AppState } from '@core/core.state';
@ -26,14 +26,20 @@ import { TranslateService } from '@ngx-translate/core';
import { ActionNotificationShow } from '@core/notification/notification.actions'; import { ActionNotificationShow } from '@core/notification/notification.actions';
import { MatVerticalStepper } from '@angular/material/stepper'; import { MatVerticalStepper } from '@angular/material/stepper';
import { import {
BulkImportRequest,
BulkImportResult,
ColumnMapping,
convertCSVToJson, convertCSVToJson,
CsvColumnParam, CsvColumnParam,
CSVDelimiter,
CsvToJsonConfig, CsvToJsonConfig,
CsvToJsonResult, CsvToJsonResult,
ImportEntityColumnType ImportEntityColumnType
} from '@home/components/import-export/import-export.models'; } from '@home/components/import-export/import-export.models';
import { EdgeImportEntityData, ImportEntitiesResultInfo, ImportEntityData } from '@app/shared/models/entity.models';
import { ImportExportService } from '@home/components/import-export/import-export.service'; import { ImportExportService } from '@home/components/import-export/import-export.service';
import { TableColumnsAssignmentComponent } from '@home/components/import-export/table-columns-assignment.component';
import { Ace } from 'ace-builds';
import { getAce } from '@shared/models/ace/ace.models';
export interface ImportDialogCsvData { export interface ImportDialogCsvData {
entityType: EntityType; entityType: EntityType;
@ -48,15 +54,21 @@ export interface ImportDialogCsvData {
styleUrls: ['./import-dialog-csv.component.scss'] styleUrls: ['./import-dialog-csv.component.scss']
}) })
export class ImportDialogCsvComponent extends DialogComponent<ImportDialogCsvComponent, boolean> export class ImportDialogCsvComponent extends DialogComponent<ImportDialogCsvComponent, boolean>
implements OnInit { implements AfterViewInit {
@ViewChild('importStepper', {static: true}) importStepper: MatVerticalStepper; @ViewChild('importStepper', {static: true}) importStepper: MatVerticalStepper;
@ViewChild('columnsAssignmentComponent', {static: true})
columnsAssignmentComponent: TableColumnsAssignmentComponent;
@ViewChild('failureDetailsEditor')
failureDetailsEditorElmRef: ElementRef;
entityType: EntityType; entityType: EntityType;
importTitle: string; importTitle: string;
importFileLabel: string; importFileLabel: string;
delimiters: {key: string, value: string}[] = [{ delimiters: { key: CSVDelimiter, value: string }[] = [{
key: ',', key: ',',
value: ',' value: ','
}, { }, {
@ -77,9 +89,10 @@ export class ImportDialogCsvComponent extends DialogComponent<ImportDialogCsvCom
columnTypesFormGroup: FormGroup; columnTypesFormGroup: FormGroup;
isImportData = false; isImportData = false;
progressCreate = 0; statistical: BulkImportResult;
statistical: ImportEntitiesResultInfo;
private allowAssignColumn: ImportEntityColumnType[];
private initEditorComponent = false;
private parseData: CsvToJsonResult; private parseData: CsvToJsonResult;
constructor(protected store: Store<AppState>, constructor(protected store: Store<AppState>,
@ -88,7 +101,8 @@ export class ImportDialogCsvComponent extends DialogComponent<ImportDialogCsvCom
public dialogRef: MatDialogRef<ImportDialogCsvComponent, boolean>, public dialogRef: MatDialogRef<ImportDialogCsvComponent, boolean>,
public translate: TranslateService, public translate: TranslateService,
private importExport: ImportExportService, private importExport: ImportExportService,
private fb: FormBuilder) { private fb: FormBuilder,
private renderer: Renderer2) {
super(store, router, dialogRef); super(store, router, dialogRef);
this.entityType = data.entityType; this.entityType = data.entityType;
this.importTitle = data.importTitle; this.importTitle = data.importTitle;
@ -109,7 +123,12 @@ export class ImportDialogCsvComponent extends DialogComponent<ImportDialogCsvCom
}); });
} }
ngOnInit(): void { ngAfterViewInit() {
let columns = this.columnsAssignmentComponent.columnTypes;
if (this.entityType === EntityType.DEVICE) {
columns = columns.concat(this.columnsAssignmentComponent.columnDeviceCredentials);
}
this.allowAssignColumn = columns.map(column => column.value);
} }
cancel(): void { cancel(): void {
@ -157,8 +176,10 @@ export class ImportDialogCsvComponent extends DialogComponent<ImportDialogCsvCom
return convertCSVToJson(importData, config, return convertCSVToJson(importData, config,
(messageId, params) => { (messageId, params) => {
this.store.dispatch(new ActionNotificationShow( this.store.dispatch(new ActionNotificationShow(
{message: this.translate.instant(messageId, params), {
type: 'error'})); message: this.translate.instant(messageId, params),
type: 'error'
}));
} }
); );
} }
@ -168,9 +189,14 @@ export class ImportDialogCsvComponent extends DialogComponent<ImportDialogCsvCom
const isHeader: boolean = this.importParametersFormGroup.get('isHeader').value; const isHeader: boolean = this.importParametersFormGroup.get('isHeader').value;
for (let i = 0; i < this.parseData.headers.length; i++) { for (let i = 0; i < this.parseData.headers.length; i++) {
let columnParam: CsvColumnParam; let columnParam: CsvColumnParam;
if (isHeader && this.parseData.headers[i].search(/^(name|type|label)$/im) === 0) { let findEntityColumnType: ImportEntityColumnType;
if (isHeader) {
const headerColumnName = this.parseData.headers[i].toUpperCase();
findEntityColumnType = this.allowAssignColumn.find(column => column === headerColumnName);
}
if (isHeader && findEntityColumnType) {
columnParam = { columnParam = {
type: ImportEntityColumnType[this.parseData.headers[i].toLowerCase()], type: findEntityColumnType,
key: this.parseData.headers[i].toLowerCase(), key: this.parseData.headers[i].toLowerCase(),
sampleData: this.parseData.rows[0][i] sampleData: this.parseData.rows[0][i]
}; };
@ -188,76 +214,16 @@ export class ImportDialogCsvComponent extends DialogComponent<ImportDialogCsvCom
private addEntities() { private addEntities() {
const importData = this.parseData; const entitiesData: BulkImportRequest = {
const parameterColumns: CsvColumnParam[] = this.columnTypesFormGroup.get('columnsParam').value; file: this.selectFileFormGroup.get('importData').value,
const entitiesData: ImportEntityData[] = []; mapping: {
let sentDataLength = 0; columns: this.processingColumnsParams(),
for (let row = 0; row < importData.rows.length; row++) { delimiter: this.importParametersFormGroup.get('delim').value,
const entityData: ImportEntityData = this.constructDraftImportEntityData(); header: this.importParametersFormGroup.get('isHeader').value,
const i = row; update: this.importParametersFormGroup.get('isUpdate').value
for (let j = 0; j < parameterColumns.length; j++) {
switch (parameterColumns[j].type) {
case ImportEntityColumnType.serverAttribute:
entityData.attributes.server.push({
key: parameterColumns[j].key,
value: importData.rows[i][j]
});
break;
case ImportEntityColumnType.timeseries:
entityData.timeseries.push({
key: parameterColumns[j].key,
value: importData.rows[i][j]
});
break;
case ImportEntityColumnType.sharedAttribute:
entityData.attributes.shared.push({
key: parameterColumns[j].key,
value: importData.rows[i][j]
});
break;
case ImportEntityColumnType.accessToken:
entityData.accessToken = importData.rows[i][j];
break;
case ImportEntityColumnType.name:
entityData.name = importData.rows[i][j];
break;
case ImportEntityColumnType.type:
entityData.type = importData.rows[i][j];
break;
case ImportEntityColumnType.label:
entityData.label = importData.rows[i][j];
break;
case ImportEntityColumnType.isGateway:
entityData.gateway = importData.rows[i][j];
break;
case ImportEntityColumnType.description:
entityData.description = importData.rows[i][j];
break;
case ImportEntityColumnType.edgeLicenseKey:
(entityData as EdgeImportEntityData).edgeLicenseKey = importData.rows[i][j];
break;
case ImportEntityColumnType.cloudEndpoint:
(entityData as EdgeImportEntityData).cloudEndpoint = importData.rows[i][j];
break;
case ImportEntityColumnType.routingKey:
(entityData as EdgeImportEntityData).routingKey = importData.rows[i][j];
break;
case ImportEntityColumnType.secret:
(entityData as EdgeImportEntityData).secret = importData.rows[i][j];
break;
}
} }
entitiesData.push(entityData);
}
const createImportEntityCompleted = () => {
sentDataLength++;
this.progressCreate = Math.round((sentDataLength / importData.rows.length) * 100);
}; };
this.importExport.bulkImportEntities(entitiesData, this.entityType, {ignoreErrors: true}).subscribe(
const isUpdate: boolean = this.importParametersFormGroup.get('isUpdate').value;
this.importExport.importEntities(entitiesData, this.entityType, isUpdate,
createImportEntityCompleted, {ignoreErrors: true, resendRequest: true}).subscribe(
(result) => { (result) => {
this.statistical = result; this.statistical = result;
this.isImportData = false; this.isImportData = false;
@ -266,31 +232,63 @@ export class ImportDialogCsvComponent extends DialogComponent<ImportDialogCsvCom
); );
} }
private constructDraftImportEntityData(): ImportEntityData { private processingColumnsParams(): Array<ColumnMapping> {
const entityData: ImportEntityData = { const parameterColumns: CsvColumnParam[] = this.columnTypesFormGroup.get('columnsParam').value;
name: '', const allowKeyForTypeColumns: ImportEntityColumnType[] = [
type: '', ImportEntityColumnType.serverAttribute,
description: '', ImportEntityColumnType.timeseries,
gateway: null, ImportEntityColumnType.sharedAttribute
label: '', ];
accessToken: '', return parameterColumns.map(column => ({
attributes: { type: column.type,
server: [], key: allowKeyForTypeColumns.some(type => type === column.type) ? column.key : undefined
shared: [] }));
}, }
timeseries: []
}; initEditor() {
if (this.entityType === EntityType.EDGE) { if (!this.initEditorComponent) {
const edgeEntityData: EdgeImportEntityData = entityData as EdgeImportEntityData; this.createEditor(this.failureDetailsEditorElmRef, this.statistical.errorsList);
edgeEntityData.edgeLicenseKey = '';
edgeEntityData.cloudEndpoint = '';
edgeEntityData.routingKey = '';
edgeEntityData.secret = '';
return edgeEntityData;
} else {
return entityData;
} }
} }
private createEditor(editorElementRef: ElementRef, contents: string[]): void {
const editorElement = editorElementRef.nativeElement;
let editorOptions: Partial<Ace.EditorOptions> = {
mode: 'ace/mode/java',
theme: 'ace/theme/github',
showGutter: false,
showPrintMargin: false,
readOnly: true
};
const advancedOptions = {
enableSnippets: false,
enableBasicAutocompletion: false,
enableLiveAutocompletion: false
};
editorOptions = {...editorOptions, ...advancedOptions};
const content = contents.map(error => error.replace('\n', '')).join('\n');
getAce().subscribe(
(ace) => {
const editor = ace.edit(editorElement, editorOptions);
editor.session.setUseWrapMode(false);
editor.setValue(content, -1);
this.updateEditorSize(editorElement, content, editor);
}
);
}
private updateEditorSize(editorElement: any, content: string, editor: Ace.Editor) {
let newHeight = 200;
if (content && content.length > 0) {
const lines = content.split('\n');
newHeight = 16 * lines.length + 24;
}
const minHeight = Math.min(200, newHeight);
this.renderer.setStyle(editorElement, 'minHeight', minHeight.toString() + 'px');
this.renderer.setStyle(editorElement, 'height', newHeight.toString() + 'px');
editor.resize();
}
} }

58
ui-ngx/src/app/modules/home/components/import-export/import-export.models.ts

@ -14,7 +14,7 @@
/// limitations under the License. /// limitations under the License.
/// ///
import { Widget, WidgetType, WidgetTypeDetails } from '@app/shared/models/widget.models'; import { Widget, WidgetTypeDetails } from '@app/shared/models/widget.models';
import { DashboardLayoutId } from '@shared/models/dashboard.models'; import { DashboardLayoutId } from '@shared/models/dashboard.models';
import { WidgetsBundle } from '@shared/models/widgets-bundle.model'; import { WidgetsBundle } from '@shared/models/widgets-bundle.model';
@ -38,6 +38,8 @@ export interface CsvToJsonResult {
rows?: any[][]; rows?: any[][];
} }
export type CSVDelimiter = ',' | ';' | '|' | '\t';
export enum ImportEntityColumnType { export enum ImportEntityColumnType {
name = 'NAME', name = 'NAME',
type = 'TYPE', type = 'TYPE',
@ -46,8 +48,22 @@ export enum ImportEntityColumnType {
sharedAttribute = 'SHARED_ATTRIBUTE', sharedAttribute = 'SHARED_ATTRIBUTE',
serverAttribute = 'SERVER_ATTRIBUTE', serverAttribute = 'SERVER_ATTRIBUTE',
timeseries = 'TIMESERIES', timeseries = 'TIMESERIES',
entityField = 'ENTITY_FIELD',
accessToken = 'ACCESS_TOKEN', accessToken = 'ACCESS_TOKEN',
x509 = 'X509',
mqttClientId = 'MQTT_CLIENT_ID',
mqttUserName = 'MQTT_USER_NAME',
mqttPassword = 'MQTT_PASSWORD',
lwm2mClientEndpoint = 'LWM2M_CLIENT_ENDPOINT',
lwm2mClientSecurityConfigMode = 'LWM2M_CLIENT_SECURITY_CONFIG_MODE',
lwm2mClientIdentity = 'LWM2M_CLIENT_IDENTITY',
lwm2mClientKey = 'LWM2M_CLIENT_KEY',
lwm2mClientCert = 'LWM2M_CLIENT_CERT',
lwm2mBootstrapServerSecurityMode = 'LWM2M_BOOTSTRAP_SERVER_SECURITY_MODE',
lwm2mBootstrapServerClientPublicKeyOrId = 'LWM2M_BOOTSTRAP_SERVER_PUBLIC_KEY_OR_ID',
lwm2mBootstrapServerClientSecretKey = 'LWM2M_BOOTSTRAP_SERVER_SECRET_KEY',
lwm2mServerSecurityMode = 'LWM2M_SERVER_SECURITY_MODE',
lwm2mServerClientPublicKeyOrId = 'LWM2M_SERVER_CLIENT_PUBLIC_KEY_OR_ID',
lwm2mServerClientSecretKey = 'LWM2M_SERVER_CLIENT_SECRET_KEY',
isGateway = 'IS_GATEWAY', isGateway = 'IS_GATEWAY',
description = 'DESCRIPTION', description = 'DESCRIPTION',
edgeLicenseKey = 'EDGE_LICENSE_KEY', edgeLicenseKey = 'EDGE_LICENSE_KEY',
@ -68,8 +84,22 @@ export const importEntityColumnTypeTranslations = new Map<ImportEntityColumnType
[ImportEntityColumnType.sharedAttribute, 'import.column-type.shared-attribute'], [ImportEntityColumnType.sharedAttribute, 'import.column-type.shared-attribute'],
[ImportEntityColumnType.serverAttribute, 'import.column-type.server-attribute'], [ImportEntityColumnType.serverAttribute, 'import.column-type.server-attribute'],
[ImportEntityColumnType.timeseries, 'import.column-type.timeseries'], [ImportEntityColumnType.timeseries, 'import.column-type.timeseries'],
[ImportEntityColumnType.entityField, 'import.column-type.entity-field'],
[ImportEntityColumnType.accessToken, 'import.column-type.access-token'], [ImportEntityColumnType.accessToken, 'import.column-type.access-token'],
[ImportEntityColumnType.x509, 'import.column-type.x509'],
[ImportEntityColumnType.mqttClientId, 'import.column-type.mqtt.client-id'],
[ImportEntityColumnType.mqttUserName, 'import.column-type.mqtt.user-name'],
[ImportEntityColumnType.mqttPassword, 'import.column-type.mqtt.password'],
[ImportEntityColumnType.lwm2mClientEndpoint, 'import.column-type.lwm2m.client-endpoint'],
[ImportEntityColumnType.lwm2mClientSecurityConfigMode, 'import.column-type.lwm2m.security-config-mode'],
[ImportEntityColumnType.lwm2mClientIdentity, 'import.column-type.lwm2m.client-identity'],
[ImportEntityColumnType.lwm2mClientKey, 'import.column-type.lwm2m.client-key'],
[ImportEntityColumnType.lwm2mClientCert, 'import.column-type.lwm2m.client-cert'],
[ImportEntityColumnType.lwm2mBootstrapServerSecurityMode, 'import.column-type.lwm2m.bootstrap-server-security-mode'],
[ImportEntityColumnType.lwm2mBootstrapServerClientPublicKeyOrId, 'import.column-type.lwm2m.bootstrap-server-public-key-id'],
[ImportEntityColumnType.lwm2mBootstrapServerClientSecretKey, 'import.column-type.lwm2m.bootstrap-server-secret-key'],
[ImportEntityColumnType.lwm2mServerSecurityMode, 'import.column-type.lwm2m.lwm2m-server-security-mode'],
[ImportEntityColumnType.lwm2mServerClientPublicKeyOrId, 'import.column-type.lwm2m.lwm2m-server-public-key-id'],
[ImportEntityColumnType.lwm2mServerClientSecretKey, 'import.column-type.lwm2m.lwm2m-server-secret-key'],
[ImportEntityColumnType.isGateway, 'import.column-type.isgateway'], [ImportEntityColumnType.isGateway, 'import.column-type.isgateway'],
[ImportEntityColumnType.description, 'import.column-type.description'], [ImportEntityColumnType.description, 'import.column-type.description'],
[ImportEntityColumnType.edgeLicenseKey, 'import.column-type.edge-license-key'], [ImportEntityColumnType.edgeLicenseKey, 'import.column-type.edge-license-key'],
@ -85,6 +115,28 @@ export interface CsvColumnParam {
sampleData: any; sampleData: any;
} }
export interface ColumnMapping {
type: ImportEntityColumnType;
key?: string;
}
export interface BulkImportRequest {
file: string;
mapping: {
columns: Array<ColumnMapping>;
delimiter: CSVDelimiter;
header: boolean;
update: boolean;
};
}
export interface BulkImportResult {
created: number;
updated: number;
errors: number;
errorsList: Array<string>;
}
export interface FileType { export interface FileType {
mimeType: string; mimeType: string;
extension: string; extension: string;

23
ui-ngx/src/app/modules/home/components/import-export/import-export.service.ts

@ -21,7 +21,7 @@ import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state'; import { AppState } from '@core/core.state';
import { ActionNotificationShow } from '@core/notification/notification.actions'; import { ActionNotificationShow } from '@core/notification/notification.actions';
import { Dashboard, DashboardLayoutId } from '@shared/models/dashboard.models'; import { Dashboard, DashboardLayoutId } from '@shared/models/dashboard.models';
import { deepClone, isDefined, isObject, isUndefined } from '@core/utils'; import { deepClone, isDefined, isObject, isString, isUndefined } from '@core/utils';
import { WINDOW } from '@core/services/window.service'; import { WINDOW } from '@core/services/window.service';
import { DOCUMENT } from '@angular/common'; import { DOCUMENT } from '@angular/common';
import { import {
@ -44,7 +44,7 @@ import {
EntityAliasesDialogData EntityAliasesDialogData
} from '@home/components/alias/entity-aliases-dialog.component'; } from '@home/components/alias/entity-aliases-dialog.component';
import { ItemBufferService, WidgetItem } from '@core/services/item-buffer.service'; import { ItemBufferService, WidgetItem } from '@core/services/item-buffer.service';
import { FileType, ImportWidgetResult, JSON_TYPE, WidgetsBundleItem, ZIP_TYPE } from './import-export.models'; import { FileType, ImportWidgetResult, JSON_TYPE, WidgetsBundleItem, ZIP_TYPE, BulkImportRequest, BulkImportResult } from './import-export.models';
import { EntityType } from '@shared/models/entity-type.models'; import { EntityType } from '@shared/models/entity-type.models';
import { UtilsService } from '@core/services/utils.service'; import { UtilsService } from '@core/services/utils.service';
import { WidgetService } from '@core/http/widget.service'; import { WidgetService } from '@core/http/widget.service';
@ -59,6 +59,9 @@ import { DeviceProfileService } from '@core/http/device-profile.service';
import { DeviceProfile } from '@shared/models/device.models'; import { DeviceProfile } from '@shared/models/device.models';
import { TenantProfile } from '@shared/models/tenant.model'; import { TenantProfile } from '@shared/models/tenant.model';
import { TenantProfileService } from '@core/http/tenant-profile.service'; import { TenantProfileService } from '@core/http/tenant-profile.service';
import { DeviceService } from '@core/http/device.service';
import { AssetService } from '@core/http/asset.service';
import { EdgeService } from '@core/http/edge.service';
// @dynamic // @dynamic
@Injectable() @Injectable()
@ -75,6 +78,9 @@ export class ImportExportService {
private tenantProfileService: TenantProfileService, private tenantProfileService: TenantProfileService,
private entityService: EntityService, private entityService: EntityService,
private ruleChainService: RuleChainService, private ruleChainService: RuleChainService,
private deviceService: DeviceService,
private assetService: AssetService,
private edgeService: EdgeService,
private utils: UtilsService, private utils: UtilsService,
private itembuffer: ItemBufferService, private itembuffer: ItemBufferService,
private dialog: MatDialog) { private dialog: MatDialog) {
@ -342,6 +348,17 @@ export class ImportExportService {
); );
} }
public bulkImportEntities(entitiesData: BulkImportRequest, entityType: EntityType, config?: RequestConfig): Observable<BulkImportResult> {
switch (entityType) {
case EntityType.DEVICE:
return this.deviceService.bulkImportDevices(entitiesData, config);
case EntityType.ASSET:
return this.assetService.bulkImportAssets(entitiesData, config);
case EntityType.EDGE:
return this.edgeService.bulkImportEdges(entitiesData, config);
}
}
public importEntities(entitiesData: ImportEntityData[], entityType: EntityType, updateData: boolean, public importEntities(entitiesData: ImportEntityData[], entityType: EntityType, updateData: boolean,
importEntityCompleted?: () => void, config?: RequestConfig): Observable<ImportEntitiesResultInfo> { importEntityCompleted?: () => void, config?: RequestConfig): Observable<ImportEntitiesResultInfo> {
let partSize = 100; let partSize = 100;
@ -563,6 +580,8 @@ export class ImportExportService {
if (isObject(obj2[key])) { if (isObject(obj2[key])) {
obj1[key] = obj1[key] || {}; obj1[key] = obj1[key] || {};
obj1[key] = {...obj1[key], ...this.sumObject(obj1[key], obj2[key])}; obj1[key] = {...obj1[key], ...this.sumObject(obj1[key], obj2[key])};
} else if (isString(obj2[key])) {
obj1[key] = (obj1[key] || '') + `${obj2[key]}\n`;
} else { } else {
obj1[key] = (obj1[key] || 0) + obj2[key]; obj1[key] = (obj1[key] || 0) + obj2[key];
} }

13
ui-ngx/src/app/modules/home/components/import-export/table-columns-assignment.component.html

@ -23,23 +23,28 @@
</mat-cell> </mat-cell>
</ng-container> </ng-container>
<ng-container matColumnDef="sampleData"> <ng-container matColumnDef="sampleData">
<mat-header-cell *matHeaderCellDef style="flex: 0 0 30%;" class="mat-column-sampleData"> {{ 'import.column-example' | translate }} </mat-header-cell> <mat-header-cell *matHeaderCellDef class="mat-column-sampleData"> {{ 'import.column-example' | translate }} </mat-header-cell>
<mat-cell *matCellDef="let column"> <mat-cell *matCellDef="let column">
{{column.sampleData}} {{column.sampleData}}
</mat-cell> </mat-cell>
</ng-container> </ng-container>
<ng-container matColumnDef="type"> <ng-container matColumnDef="type">
<mat-header-cell *matHeaderCellDef style="flex: 0 0 40%" class="mat-column-type"> {{ 'import.column-type.column-type' | translate }} </mat-header-cell> <mat-header-cell *matHeaderCellDef class="mat-column-type"> {{ 'import.column-type.column-type' | translate }} </mat-header-cell>
<mat-cell *matCellDef="let column"> <mat-cell *matCellDef="let column">
<mat-select matInput [(ngModel)]="column.type" (ngModelChange)="columnsUpdated()"> <mat-select [(ngModel)]="column.type" (ngModelChange)="columnsUpdated()">
<mat-option *ngFor="let type of columnTypes" [value]="type.value" [disabled]="type.disabled"> <mat-option *ngFor="let type of columnTypes" [value]="type.value" [disabled]="type.disabled">
{{ columnTypesTranslations.get(type.value) | translate }} {{ columnTypesTranslations.get(type.value) | translate }}
</mat-option> </mat-option>
<mat-optgroup label="{{ 'import.credentials' | translate }}" *ngIf="entityType === entityTypeDevice">
<mat-option *ngFor="let credential of columnDeviceCredentials" [value]="credential.value" [disabled]="credential.disabled">
{{ columnTypesTranslations.get(credential.value) | translate }}
</mat-option>
</mat-optgroup>
</mat-select> </mat-select>
</mat-cell> </mat-cell>
</ng-container> </ng-container>
<ng-container matColumnDef="key"> <ng-container matColumnDef="key">
<mat-header-cell *matHeaderCellDef style="flex: 0 0 30%"> {{ 'import.column-key' | translate }} </mat-header-cell> <mat-header-cell *matHeaderCellDef class="mat-column-key"> {{ 'import.column-key' | translate }} </mat-header-cell>
<mat-cell *matCellDef="let column"> <mat-cell *matCellDef="let column">
<mat-form-field floatLabel="always" hideRequiredMarker <mat-form-field floatLabel="always" hideRequiredMarker
*ngIf="isColumnTypeDiffers(column.type)"> *ngIf="isColumnTypeDiffers(column.type)">

7
ui-ngx/src/app/modules/home/components/import-export/table-columns-assignment.component.scss

@ -14,14 +14,21 @@
* limitations under the License. * limitations under the License.
*/ */
:host { :host {
overflow-x: auto;
.mat-column-order { .mat-column-order {
flex: 0 0 40px; flex: 0 0 40px;
} }
.mat-column-sampleData { .mat-column-sampleData {
flex: 0 0 120px; flex: 0 0 120px;
min-width: 120px; min-width: 120px;
max-width: 230px;
} }
.mat-column-type { .mat-column-type {
flex: 0 0 180px;
min-width: 180px;
}
.mat-column-key {
flex: 0 0 120px; flex: 0 0 120px;
min-width: 120px; min-width: 120px;
} }

44
ui-ngx/src/app/modules/home/components/import-export/table-columns-assignment.component.ts

@ -57,8 +57,12 @@ export class TableColumnsAssignmentComponent implements OnInit, ControlValueAcce
columnTypes: AssignmentColumnType[] = []; columnTypes: AssignmentColumnType[] = [];
columnDeviceCredentials: AssignmentColumnType[] = [];
columnTypesTranslations = importEntityColumnTypeTranslations; columnTypesTranslations = importEntityColumnTypeTranslations;
readonly entityTypeDevice = EntityType.DEVICE;
private columns: CsvColumnParam[]; private columns: CsvColumnParam[];
private valid = true; private valid = true;
@ -83,9 +87,26 @@ export class TableColumnsAssignmentComponent implements OnInit, ControlValueAcce
{ value: ImportEntityColumnType.sharedAttribute }, { value: ImportEntityColumnType.sharedAttribute },
{ value: ImportEntityColumnType.serverAttribute }, { value: ImportEntityColumnType.serverAttribute },
{ value: ImportEntityColumnType.timeseries }, { value: ImportEntityColumnType.timeseries },
{ value: ImportEntityColumnType.accessToken },
{ value: ImportEntityColumnType.isGateway } { value: ImportEntityColumnType.isGateway }
); );
this.columnDeviceCredentials.push(
{ value: ImportEntityColumnType.accessToken },
{ value: ImportEntityColumnType.x509 },
{ value: ImportEntityColumnType.mqttClientId },
{ value: ImportEntityColumnType.mqttUserName },
{ value: ImportEntityColumnType.mqttPassword },
{ value: ImportEntityColumnType.lwm2mClientEndpoint },
{ value: ImportEntityColumnType.lwm2mClientSecurityConfigMode },
{ value: ImportEntityColumnType.lwm2mClientIdentity },
{ value: ImportEntityColumnType.lwm2mClientKey },
{ value: ImportEntityColumnType.lwm2mClientCert },
{ value: ImportEntityColumnType.lwm2mBootstrapServerSecurityMode },
{ value: ImportEntityColumnType.lwm2mBootstrapServerClientPublicKeyOrId },
{ value: ImportEntityColumnType.lwm2mBootstrapServerClientSecretKey },
{ value: ImportEntityColumnType.lwm2mServerSecurityMode },
{ value: ImportEntityColumnType.lwm2mServerClientPublicKeyOrId },
{ value: ImportEntityColumnType.lwm2mServerClientSecretKey },
);
break; break;
case EntityType.ASSET: case EntityType.ASSET:
this.columnTypes.push( this.columnTypes.push(
@ -123,8 +144,6 @@ export class TableColumnsAssignmentComponent implements OnInit, ControlValueAcce
const isSelectName = this.columns.findIndex((column) => column.type === ImportEntityColumnType.name) > -1; const isSelectName = this.columns.findIndex((column) => column.type === ImportEntityColumnType.name) > -1;
const isSelectType = this.columns.findIndex((column) => column.type === ImportEntityColumnType.type) > -1; const isSelectType = this.columns.findIndex((column) => column.type === ImportEntityColumnType.type) > -1;
const isSelectLabel = this.columns.findIndex((column) => column.type === ImportEntityColumnType.label) > -1; const isSelectLabel = this.columns.findIndex((column) => column.type === ImportEntityColumnType.label) > -1;
const isSelectCredentials = this.columns.findIndex((column) => column.type === ImportEntityColumnType.accessToken) > -1;
const isSelectGateway = this.columns.findIndex((column) => column.type === ImportEntityColumnType.isGateway) > -1;
const isSelectDescription = this.columns.findIndex((column) => column.type === ImportEntityColumnType.description) > -1; const isSelectDescription = this.columns.findIndex((column) => column.type === ImportEntityColumnType.description) > -1;
const isSelectEdgeLicenseKey = this.columns.findIndex((column) => column.type === ImportEntityColumnType.edgeLicenseKey) > -1; const isSelectEdgeLicenseKey = this.columns.findIndex((column) => column.type === ImportEntityColumnType.edgeLicenseKey) > -1;
const isSelectCloudEndpoint = this.columns.findIndex((column) => column.type === ImportEntityColumnType.cloudEndpoint) > -1; const isSelectCloudEndpoint = this.columns.findIndex((column) => column.type === ImportEntityColumnType.cloudEndpoint) > -1;
@ -139,14 +158,19 @@ export class TableColumnsAssignmentComponent implements OnInit, ControlValueAcce
this.columnTypes.find((columnType) => columnType.value === ImportEntityColumnType.label).disabled = isSelectLabel; this.columnTypes.find((columnType) => columnType.value === ImportEntityColumnType.label).disabled = isSelectLabel;
this.columnTypes.find((columnType) => columnType.value === ImportEntityColumnType.description).disabled = isSelectDescription; this.columnTypes.find((columnType) => columnType.value === ImportEntityColumnType.description).disabled = isSelectDescription;
const isGatewayColumnType = this.columnTypes.find((columnType) => columnType.value === ImportEntityColumnType.isGateway); if (this.entityType === EntityType.DEVICE) {
if (isGatewayColumnType) { const isSelectGateway = this.columns.findIndex((column) => column.type === ImportEntityColumnType.isGateway) > -1;
isGatewayColumnType.disabled = isSelectGateway;
} const isGatewayColumnType = this.columnTypes.find((columnType) => columnType.value === ImportEntityColumnType.isGateway);
const accessTokenColumnType = this.columnTypes.find((columnType) => columnType.value === ImportEntityColumnType.accessToken); if (isGatewayColumnType) {
if (accessTokenColumnType) { isGatewayColumnType.disabled = isSelectGateway;
accessTokenColumnType.disabled = isSelectCredentials; }
this.columnDeviceCredentials.forEach((columnCredential) => {
columnCredential.disabled = this.columns.findIndex(column => column.type === columnCredential.value) > -1;
});
} }
const edgeLicenseKeyColumnType = this.columnTypes.find((columnType) => columnType.value === ImportEntityColumnType.edgeLicenseKey); const edgeLicenseKeyColumnType = this.columnTypes.find((columnType) => columnType.value === ImportEntityColumnType.edgeLicenseKey);
if (edgeLicenseKeyColumnType) { if (edgeLicenseKeyColumnType) {
edgeLicenseKeyColumnType.disabled = isSelectEdgeLicenseKey; edgeLicenseKeyColumnType.disabled = isSelectEdgeLicenseKey;

8
ui-ngx/src/app/shared/models/device.models.ts

@ -743,6 +743,14 @@ export interface DeviceCredentialMQTTBasic {
password: string; password: string;
} }
export function getDeviceCredentialMQTTDefault(): DeviceCredentialMQTTBasic {
return {
clientId: '',
userName: '',
password: ''
};
}
export interface DeviceSearchQuery extends EntitySearchQuery { export interface DeviceSearchQuery extends EntitySearchQuery {
deviceTypes: Array<string>; deviceTypes: Array<string>;
} }

11
ui-ngx/src/app/shared/models/entity.models.ts

@ -17,6 +17,8 @@
import { EntityType } from '@shared/models/entity-type.models'; import { EntityType } from '@shared/models/entity-type.models';
import { AttributeData } from './telemetry/telemetry.models'; import { AttributeData } from './telemetry/telemetry.models';
import { EntityId } from '@shared/models/id/entity-id'; import { EntityId } from '@shared/models/id/entity-id';
import { DeviceCredentialMQTTBasic } from '@shared/models/device.models';
import { Lwm2mSecurityConfigModels } from '@shared/models/lwm2m-security-config.models';
export interface EntityInfo { export interface EntityInfo {
name?: string; name?: string;
@ -32,12 +34,18 @@ export interface EntityInfoData {
} }
export interface ImportEntityData { export interface ImportEntityData {
lineNumber: number;
name: string; name: string;
type: string; type: string;
label: string; label: string;
gateway: boolean; gateway: boolean;
description: string; description: string;
accessToken: string; credential: {
accessToken?: string;
x509?: string;
mqtt?: DeviceCredentialMQTTBasic;
lwm2m?: Lwm2mSecurityConfigModels;
};
attributes: { attributes: {
server: AttributeData[], server: AttributeData[],
shared: AttributeData[] shared: AttributeData[]
@ -61,6 +69,7 @@ export interface ImportEntitiesResultInfo {
}; };
error?: { error?: {
entity: number; entity: number;
errors?: string;
}; };
} }

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

@ -58,6 +58,20 @@ export interface Lwm2mSecurityConfigModels {
bootstrap: BootstrapSecurityConfig; bootstrap: BootstrapSecurityConfig;
} }
export function getLwm2mSecurityConfigModelsDefault(): Lwm2mSecurityConfigModels {
return {
client: {
securityConfigClientMode: Lwm2mSecurityType.NO_SEC,
endpoint: ''
},
bootstrap: {
bootstrapServer: getDefaultServerSecurityConfig(),
lwm2mServer: getDefaultServerSecurityConfig()
}
};
}
export function getDefaultClientSecurityConfig(securityConfigMode: Lwm2mSecurityType, endPoint = ''): ClientSecurityConfig { export function getDefaultClientSecurityConfig(securityConfigMode: Lwm2mSecurityType, endPoint = ''): ClientSecurityConfig {
let security = { let security = {
securityConfigClientMode: securityConfigMode, securityConfigClientMode: securityConfigMode,

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

@ -2187,9 +2187,11 @@
"column-title": "Title", "column-title": "Title",
"column-example": "Example value data", "column-example": "Example value data",
"column-key": "Attribute/telemetry key", "column-key": "Attribute/telemetry key",
"credentials": "Credentials",
"csv-delimiter": "CSV delimiter", "csv-delimiter": "CSV delimiter",
"csv-first-line-header": "First line contains column names", "csv-first-line-header": "First line contains column names",
"csv-update-data": "Update attributes/telemetry", "csv-update-data": "Update attributes/telemetry",
"details": "Details",
"import-csv-number-columns-error": "A file should contain at least two columns", "import-csv-number-columns-error": "A file should contain at least two columns",
"import-csv-invalid-format-error": "Invalid file format. Line: '{{line}}'", "import-csv-invalid-format-error": "Invalid file format. Line: '{{line}}'",
"column-type": { "column-type": {
@ -2203,6 +2205,25 @@
"timeseries": "Timeseries", "timeseries": "Timeseries",
"entity-field": "Entity field", "entity-field": "Entity field",
"access-token": "Access token", "access-token": "Access token",
"x509": "X.509",
"mqtt": {
"client-id": "MQTT client ID",
"user-name": "MQTT user name",
"password": "MQTT password"
},
"lwm2m": {
"client-endpoint": "LwM2M endpoint client name",
"security-config-mode": "LwM2M security config mode",
"client-identity": "LwM2M client identity",
"client-key": "LwM2M client key",
"client-cert": "LwM2M client public key",
"bootstrap-server-security-mode": "LwM2M bootstrap server security mode",
"bootstrap-server-secret-key": "LwM2M bootstrap server secret key",
"bootstrap-server-public-key-id": "LwM2M bootstrap server public key or id",
"lwm2m-server-security-mode": "LwM2M server security mode",
"lwm2m-server-secret-key": "LwM2M server secret key",
"lwm2m-server-public-key-id": "LwM2M server public key or id"
},
"isgateway": "Is Gateway", "isgateway": "Is Gateway",
"activity-time-from-gateway-device": "Activity time from gateway device", "activity-time-from-gateway-device": "Activity time from gateway device",
"description": "Description", "description": "Description",

Loading…
Cancel
Save