From 201476f912e317ce7d7c466c0bffec72189611bb Mon Sep 17 00:00:00 2001 From: nickAS21 Date: Thu, 18 Mar 2021 20:54:17 +0200 Subject: [PATCH 01/31] lwm2m: add objectKeyId to back and front profile --- .../server/controller/ResourceController.java | 41 ++++++- .../service/install/InstallScripts.java | 36 +++--- .../install/SqlDatabaseUpgradeService.java | 1 + .../server/dao/resource/ResourceService.java | 14 ++- common/data/pom.xml | 4 + .../server/common/data/Resource.java | 61 ++++++++++- .../server/common/data/lwm2m/LwM2mObject.java | 1 + dao/pom.xml | 4 + .../server/dao/model/ModelConstants.java | 2 + .../server/dao/model/sql/ResourceEntity.java | 6 + .../dao/resource/BaseResourceService.java | 103 +++++++++++++++++- .../server/dao/resource/ResourceDao.java | 9 +- .../dao/sql/resource/ResourceDaoImpl.java | 32 +++++- .../dao/sql/resource/ResourceRepository.java | 49 +++++++++ .../resources/sql/schema-entities-hsql.sql | 1 + .../main/resources/sql/schema-entities.sql | 1 + .../app/core/http/device-profile.service.ts | 30 ++--- ...ofile-transport-configuration.component.ts | 82 +++++++------- ...m2m-object-add-instances-list.component.ts | 12 +- .../lwm2m-object-add-instances.component.html | 2 +- .../lwm2m-object-add-instances.component.ts | 16 +-- .../lwm2m/lwm2m-object-list.component.html | 2 +- .../lwm2m/lwm2m-object-list.component.ts | 61 ++++++----- ...wm2m-observe-attr-telemetry.component.html | 2 +- .../lwm2m-observe-attr-telemetry.component.ts | 22 ++-- .../device/lwm2m/profile-config.models.ts | 4 +- 26 files changed, 458 insertions(+), 140 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/controller/ResourceController.java b/application/src/main/java/org/thingsboard/server/controller/ResourceController.java index 1277a32fd6..7ce3c196e8 100644 --- a/application/src/main/java/org/thingsboard/server/controller/ResourceController.java +++ b/application/src/main/java/org/thingsboard/server/controller/ResourceController.java @@ -27,11 +27,14 @@ import org.thingsboard.server.common.data.Resource; import org.thingsboard.server.common.data.ResourceType; import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.lwm2m.LwM2mObject; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.dao.resource.ResourceService; import org.thingsboard.server.queue.util.TbCoreComponent; +import java.util.List; + @Slf4j @RestController @TbCoreComponent @@ -59,21 +62,55 @@ public class ResourceController extends BaseController { } @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") - @RequestMapping(value = "/resource", method = RequestMethod.GET) + @RequestMapping(value = "/resource/page", method = RequestMethod.GET) @ResponseBody public PageData getResources(@RequestParam(required = false) boolean system, @RequestParam int pageSize, @RequestParam int page, + @RequestParam(required = false) String textSearch, @RequestParam(required = false) String sortProperty, @RequestParam(required = false) String sortOrder) throws ThingsboardException { try { - PageLink pageLink = createPageLink(pageSize, page, null, sortProperty, sortOrder); +// int[] objectIds; +// ResourceType resourceType + PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); return checkNotNull(resourceService.findResourcesByTenantId(system ? TenantId.SYS_TENANT_ID : getTenantId(), pageLink)); } catch (Exception e) { throw handleException(e); } } + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')") + @RequestMapping(value = "/resource/lwm2m/page", method = RequestMethod.GET) + @ResponseBody + public List getLwm2mListObjectsPage(@RequestParam int pageSize, + @RequestParam int page, + @RequestParam(required = false) String textSearch, + @RequestParam(required = false) String sortProperty, + @RequestParam(required = false) String sortOrder) throws ThingsboardException { + try { + PageLink pageLink = new PageLink(pageSize, page, textSearch); + return checkNotNull(resourceService.findLwM2mObjectPage(getTenantId(), sortProperty, sortOrder, pageLink)); + } catch (Exception e) { + throw handleException(e); + } + } + + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')") + @RequestMapping(value = "/resource/lwm2m", method = RequestMethod.GET) + @ResponseBody + public List getLwm2mListObjects(@RequestParam String sortOrder, + @RequestParam String sortProperty, + @RequestParam(required = false) String[] objectIds, + @RequestParam(required = false) String searchText) + throws ThingsboardException { + try { + return checkNotNull(resourceService.findLwM2mObject(getTenantId(), sortOrder, sortProperty, objectIds, searchText)); + } catch (Exception e) { + throw handleException(e); + } + } + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") @RequestMapping(value = "/resource/{resourceType}/{resourceId}", method = RequestMethod.DELETE) @ResponseBody diff --git a/application/src/main/java/org/thingsboard/server/service/install/InstallScripts.java b/application/src/main/java/org/thingsboard/server/service/install/InstallScripts.java index f9ef8e03e1..b9e9128b11 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/InstallScripts.java +++ b/application/src/main/java/org/thingsboard/server/service/install/InstallScripts.java @@ -37,6 +37,7 @@ import org.thingsboard.server.common.data.rule.RuleChainMetaData; import org.thingsboard.server.common.data.widget.WidgetType; import org.thingsboard.server.common.data.widget.WidgetsBundle; import org.thingsboard.server.dao.dashboard.DashboardService; +import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.oauth2.OAuth2ConfigTemplateService; import org.thingsboard.server.dao.resource.ResourceService; import org.thingsboard.server.dao.rule.RuleChainService; @@ -202,7 +203,6 @@ public class InstallScripts { } public void loadSystemLwm2mResources() throws Exception { -// Path modelsDir = Paths.get("/home/nick/Igor_project/thingsboard_ce_3_2_docker/thingsboard/common/transport/lwm2m/src/main/resources/models/"); Path modelsDir = Paths.get(getDataDir(), MODELS_DIR); if (Files.isDirectory(modelsDir)) { try (DirectoryStream dirStream = Files.newDirectoryStream(modelsDir, path -> path.toString().endsWith(XML_EXT))) { @@ -210,18 +210,16 @@ public class InstallScripts { path -> { try { byte[] fileBytes = Files.readAllBytes(path); - String key = getObjectModelLwm2mValid(fileBytes, path.getFileName().toString(), new DefaultDDFFileValidator()); - if (key != null) { - Resource resource = new Resource(); - resource.setTenantId(TenantId.SYS_TENANT_ID); - resource.setResourceType(ResourceType.LWM2M_MODEL); - resource.setResourceId(key); - resource.setValue(Base64.getEncoder().encodeToString(fileBytes)); - resourceService.saveResource(resource); - } + String source = new String(fileBytes); + Resource resource = new Resource(); + resource.setTenantId(TenantId.SYS_TENANT_ID); + resource.setResourceType(ResourceType.LWM2M_MODEL); + resource.setResourceId(getValueByTeg(source, "ObjectID") + "_" + getValueByTeg(source, "ObjectVersion")); + resource.setTextSearch(resource.getResourceId() + ":" + getValueByTeg(source, "Name")); + resource.setValue(Base64.getEncoder().encodeToString(fileBytes)); + resourceService.saveResource(resource); } catch (Exception e) { - log.error("Unable to load lwm2m model [{}]", path.toString()); - throw new RuntimeException("Unable to load lwm2m model", e); + throw new DataValidationException(String.format("Could not parse the XML of objectModel with name %s", path.toString())); } } ); @@ -234,6 +232,7 @@ public class InstallScripts { resource.setTenantId(TenantId.SYS_TENANT_ID); resource.setResourceType(ResourceType.JKS); resource.setResourceId(jksPath.getFileName().toString()); + resource.setTextSearch(jksPath.getFileName().toString()); resource.setValue(Base64.getEncoder().encodeToString(Files.readAllBytes(jksPath))); resourceService.saveResource(resource); } catch (Exception e) { @@ -242,11 +241,18 @@ public class InstallScripts { } } - private String getObjectModelLwm2mValid(byte[] xmlByte, String streamName, DefaultDDFFileValidator ddfValidator) { + private String getValueByTeg(String source, String tagHtml) { + int lenTag = ("<" + tagHtml + ">").length(); + int indStart = source.indexOf("<" + tagHtml + ">"); + int indEnd = source.indexOf(""); + return (indStart > 0 && indEnd > 0) ? source.substring(indStart + lenTag, indEnd) : null; + + } + + private ObjectModel getObjectModelLwm2mValid(byte[] xmlByte, String streamName, DefaultDDFFileValidator ddfValidator) { try { DDFFileParser ddfFileParser = new DDFFileParser(ddfValidator); - ObjectModel objectModel = ddfFileParser.parseEx(new ByteArrayInputStream(xmlByte), streamName).get(0); - return objectModel.id + "##" + objectModel.getVersion(); + return ddfFileParser.parseEx(new ByteArrayInputStream(xmlByte), streamName).get(0); } catch (IOException | InvalidDDFFileException e) { log.error("Could not parse the XML file [{}]", streamName, e); return null; diff --git a/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java b/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java index 2525ebd186..20fcd44db6 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java +++ b/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java @@ -442,6 +442,7 @@ public class SqlDatabaseUpgradeService implements DatabaseEntitiesUpgradeService " tenant_id uuid NOT NULL," + " resource_type varchar(32) NOT NULL," + " resource_id varchar(255) NOT NULL," + + " text_search varchar(255)," + " resource_value varchar," + " CONSTRAINT resource_unq_key UNIQUE (tenant_id, resource_type, resource_id)" + " );"); diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/resource/ResourceService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/resource/ResourceService.java index 8d84af6e6c..97db9d6418 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/resource/ResourceService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/resource/ResourceService.java @@ -18,6 +18,7 @@ package org.thingsboard.server.dao.resource; import org.thingsboard.server.common.data.Resource; import org.thingsboard.server.common.data.ResourceType; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.lwm2m.LwM2mObject; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; @@ -31,7 +32,18 @@ public interface ResourceService { PageData findResourcesByTenantId(TenantId tenantId, PageLink pageLink); - List findResourcesByTenantIdResourceType(TenantId tenantId, ResourceType resourceType); + List findLwM2mObject(TenantId tenantId, + String sortOrder, + String sortProperty, + String[] objectIds, + String searchText); + + List findLwM2mObjectPage(TenantId tenantId, + String sortProperty, + String sortOrder, + PageLink pageLink); + + List findAllByTenantIdAndResourceType(TenantId tenantId, ResourceType resourceType); void deleteResource(TenantId tenantId, ResourceType resourceType, String resourceId); diff --git a/common/data/pom.xml b/common/data/pom.xml index 79b3bc8e26..d598770823 100644 --- a/common/data/pom.xml +++ b/common/data/pom.xml @@ -79,6 +79,10 @@ org.thingsboard protobuf-dynamic + + org.eclipse.leshan + leshan-core + diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/Resource.java b/common/data/src/main/java/org/thingsboard/server/common/data/Resource.java index 2e7cde8185..1d92d852e6 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/Resource.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/Resource.java @@ -16,10 +16,24 @@ package org.thingsboard.server.common.data; import lombok.Data; +import lombok.extern.slf4j.Slf4j; +import org.eclipse.leshan.core.model.DDFFileParser; +import org.eclipse.leshan.core.model.DefaultDDFFileValidator; +import org.eclipse.leshan.core.model.InvalidDDFFileException; +import org.eclipse.leshan.core.model.ObjectModel; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.lwm2m.LwM2mInstance; +import org.thingsboard.server.common.data.lwm2m.LwM2mObject; +import org.thingsboard.server.common.data.lwm2m.LwM2mResource; +import java.io.ByteArrayInputStream; +import java.io.IOException; import java.io.Serializable; +import java.util.ArrayList; +import java.util.Base64; +import java.util.List; +@Slf4j @Data public class Resource implements HasTenantId, Serializable { @@ -28,14 +42,51 @@ public class Resource implements HasTenantId, Serializable { private TenantId tenantId; private ResourceType resourceType; private String resourceId; + private String textSearch; private String value; @Override public String toString() { - return "Resource{" + - "tenantId=" + tenantId + - ", resourceType=" + resourceType + - ", resourceId='" + resourceId + '\'' + - '}'; + final StringBuilder res = new StringBuilder("Resource{"); + res.append("tenantId=").append(tenantId); + res.append(", resourceType='").append(resourceType).append('\''); + res.append(", resourceId='").append(resourceId).append('\''); + res.append(", textSearch='").append(textSearch).append('\''); + res.append('}'); + return res.toString(); + } + + public LwM2mObject toLwM2mObject () { + try { + DDFFileParser ddfFileParser = new DDFFileParser(new DefaultDDFFileValidator()); + List objectModels = ddfFileParser.parseEx(new ByteArrayInputStream(Base64.getDecoder().decode(this.value)), this.textSearch); + if (objectModels.size() == 0) { + return null; + } + else { + ObjectModel obj = objectModels.get(0); + LwM2mObject lwM2mObject = new LwM2mObject(); + lwM2mObject.setId(obj.id); + lwM2mObject.setKeyId(this.resourceId); + lwM2mObject.setName(obj.name); + lwM2mObject.setMultiple(obj.multiple); + lwM2mObject.setMandatory(obj.mandatory); + LwM2mInstance instance = new LwM2mInstance(); + instance.setId(0); + List resources = new ArrayList<>(); + obj.resources.forEach((k, v) -> { + if (!v.operations.isExecutable()) { + LwM2mResource resource = new LwM2mResource(k, v.name, false, false, false); + resources.add(resource); + } + }); + instance.setResources(resources.stream().toArray(LwM2mResource[]::new)); + lwM2mObject.setInstances(new LwM2mInstance[]{instance}); + return lwM2mObject; + } + } catch (IOException | InvalidDDFFileException e) { + log.error("Could not parse the XML of objectModel with name [{}]", this.textSearch, e); + return null; + } } } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/lwm2m/LwM2mObject.java b/common/data/src/main/java/org/thingsboard/server/common/data/lwm2m/LwM2mObject.java index 6401e8a31a..80174b430f 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/lwm2m/LwM2mObject.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/lwm2m/LwM2mObject.java @@ -20,6 +20,7 @@ import lombok.Data; @Data public class LwM2mObject { int id; + String keyId; String name; boolean multiple; boolean mandatory; diff --git a/dao/pom.xml b/dao/pom.xml index e1a08cb74d..5c8d0d9ef5 100644 --- a/dao/pom.xml +++ b/dao/pom.xml @@ -214,6 +214,10 @@ org.elasticsearch.client rest + + org.eclipse.leshan + leshan-core + diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java b/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java index 3f11a4ed00..1315e3d129 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java @@ -460,8 +460,10 @@ public class ModelConstants { public static final String RESOURCE_TENANT_ID_COLUMN = TENANT_ID_COLUMN; public static final String RESOURCE_TYPE_COLUMN = "resource_type"; public static final String RESOURCE_ID_COLUMN = "resource_id"; + public static final String TEXT_SEARCH_COLUMN = "text_search"; public static final String RESOURCE_VALUE_COLUMN = "resource_value"; + /** * Cassandra attributes and timeseries constants. */ diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/ResourceEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/ResourceEntity.java index fdb24e6159..9f403259e4 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/ResourceEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/ResourceEntity.java @@ -33,6 +33,7 @@ import static org.thingsboard.server.dao.model.ModelConstants.RESOURCE_TABLE_NAM import static org.thingsboard.server.dao.model.ModelConstants.RESOURCE_TENANT_ID_COLUMN; import static org.thingsboard.server.dao.model.ModelConstants.RESOURCE_TYPE_COLUMN; import static org.thingsboard.server.dao.model.ModelConstants.RESOURCE_VALUE_COLUMN; +import static org.thingsboard.server.dao.model.ModelConstants.TEXT_SEARCH_COLUMN; @Data @Entity @@ -52,6 +53,9 @@ public class ResourceEntity implements ToData { @Column(name = RESOURCE_ID_COLUMN) private String resourceId; + @Column(name = TEXT_SEARCH_COLUMN) + private String textSearch; + @Column(name = RESOURCE_VALUE_COLUMN) private String value; @@ -62,6 +66,7 @@ public class ResourceEntity implements ToData { this.tenantId = resource.getTenantId().getId(); this.resourceType = resource.getResourceType().name(); this.resourceId = resource.getResourceId(); + this.textSearch = resource.getTextSearch(); this.value = resource.getValue(); } @@ -71,6 +76,7 @@ public class ResourceEntity implements ToData { resource.setTenantId(new TenantId(tenantId)); resource.setResourceType(ResourceType.valueOf(resourceType)); resource.setResourceId(resourceId); + resource.setTextSearch(textSearch); resource.setValue(value); return resource; } diff --git a/dao/src/main/java/org/thingsboard/server/dao/resource/BaseResourceService.java b/dao/src/main/java/org/thingsboard/server/dao/resource/BaseResourceService.java index a451a4e627..839aec32f4 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/resource/BaseResourceService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/resource/BaseResourceService.java @@ -16,15 +16,28 @@ package org.thingsboard.server.dao.resource; import lombok.extern.slf4j.Slf4j; +import org.eclipse.leshan.core.model.DDFFileParser; +import org.eclipse.leshan.core.model.DefaultDDFFileValidator; +import org.eclipse.leshan.core.model.InvalidDDFFileException; +import org.eclipse.leshan.core.model.ObjectModel; import org.springframework.stereotype.Service; import org.thingsboard.server.common.data.Resource; import org.thingsboard.server.common.data.ResourceType; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.lwm2m.LwM2mInstance; +import org.thingsboard.server.common.data.lwm2m.LwM2mObject; +import org.thingsboard.server.common.data.lwm2m.LwM2mResource; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.dao.exception.DataValidationException; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Base64; +import java.util.Comparator; import java.util.List; +import java.util.stream.Collectors; import static org.thingsboard.server.dao.device.DeviceServiceImpl.INCORRECT_TENANT_ID; import static org.thingsboard.server.dao.service.Validator.validateId; @@ -69,10 +82,35 @@ public class BaseResourceService implements ResourceService { @Override - public List findResourcesByTenantIdResourceType(TenantId tenantId, ResourceType resourceType) { + public List findAllByTenantIdAndResourceType(TenantId tenantId, ResourceType resourceType) { log.trace("Executing findByTenantId [{}]", tenantId); validateId(tenantId, INCORRECT_TENANT_ID + tenantId); - return resourceDao.findAllByTenantIdResourceType(tenantId, resourceType); + return resourceDao.findAllByTenantIdAndResourceType(tenantId, resourceType); + } + + @Override + public List findLwM2mObjectPage(TenantId tenantId, String sortProperty, String sortOrder, PageLink pageLink) { + log.trace("Executing findByTenantId [{}]", tenantId); + validateId(tenantId, INCORRECT_TENANT_ID + tenantId); + PageData resourcePageData = resourceDao.findResourcesByTenantIdAndResourceType( + tenantId, + ResourceType.LWM2M_MODEL, pageLink); + List lwM2mObjects = resourcePageData.getData().stream().map(this::toLwM2mObject).collect(Collectors.toList()); + return lwM2mObjects.size() > 1 ? this.sortList (lwM2mObjects, sortProperty, sortOrder) : lwM2mObjects; + } + + @Override + public List findLwM2mObject(TenantId tenantId, String sortOrder, + String sortProperty, + String[] objectIds, + String searchText) { + log.trace("Executing findByTenantId [{}]", tenantId); + validateId(tenantId, INCORRECT_TENANT_ID + tenantId); + List resources = resourceDao.findResourcesByTenantIdAndResourceType(tenantId, ResourceType.LWM2M_MODEL, + objectIds, + searchText); + List lwM2mObjects = resources.stream().map(this::toLwM2mObject).collect(Collectors.toList()); + return lwM2mObjects.size() > 1 ? this.sortList (lwM2mObjects, sortProperty, sortOrder) : lwM2mObjects; } @Override @@ -86,11 +124,13 @@ public class BaseResourceService implements ResourceService { if (resource == null) { throw new DataValidationException("Resource should be specified!"); } - if (resource.getValue() == null) { throw new DataValidationException("Resource value should be specified!"); } validate(resource.getTenantId(), resource.getResourceType(), resource.getResourceId()); + if (resource.getResourceType().equals(ResourceType.LWM2M_MODEL) && resource.toLwM2mObject() == null) { + throw new DataValidationException(String.format("Could not parse the XML of objectModel with name %s", resource.getTextSearch())); + } } protected void validate(TenantId tenantId, ResourceType resourceType, String resourceId) { @@ -103,4 +143,61 @@ public class BaseResourceService implements ResourceService { validateId(tenantId, "Incorrect tenantId "); } + private LwM2mObject toLwM2mObject(Resource resource) { + try { + DDFFileParser ddfFileParser = new DDFFileParser(new DefaultDDFFileValidator()); + List objectModels = + ddfFileParser.parseEx(new ByteArrayInputStream(Base64.getDecoder().decode(resource.getValue())), resource.getTextSearch()); + if (objectModels.size() == 0) { + return null; + } else { + ObjectModel obj = objectModels.get(0); + LwM2mObject lwM2mObject = new LwM2mObject(); + lwM2mObject.setId(obj.id); + lwM2mObject.setKeyId(resource.getResourceId()); + lwM2mObject.setName(obj.name); + lwM2mObject.setMultiple(obj.multiple); + lwM2mObject.setMandatory(obj.mandatory); + LwM2mInstance instance = new LwM2mInstance(); + instance.setId(0); + List resources = new ArrayList<>(); + obj.resources.forEach((k, v) -> { + if (!v.operations.isExecutable()) { + LwM2mResource lwM2mResource = new LwM2mResource(k, v.name, false, false, false); + resources.add(lwM2mResource); + } + }); + instance.setResources(resources.stream().toArray(LwM2mResource[]::new)); + lwM2mObject.setInstances(new LwM2mInstance[]{instance}); + return lwM2mObject; + } + } catch (IOException | InvalidDDFFileException e) { + log.error("Could not parse the XML of objectModel with name [{}]", resource.getTextSearch(), e); + return null; + } + } + + private List sortList (List lwM2mObjects, String sortProperty, String sortOrder) { + switch (sortProperty) { + case "name": + switch (sortOrder) { + case "ASC": + lwM2mObjects.sort((o1, o2) -> o1.getName().compareTo(o2.getName())); + break; + case "DESC": + lwM2mObjects.stream().sorted(Comparator.comparing(LwM2mObject::getName).reversed()); + break; + } + case "id": + switch (sortOrder) { + case "ASC": + lwM2mObjects.sort((o1, o2) -> Long.compare(o1.getId(), o2.getId())); + break; + case "DESC": + lwM2mObjects.sort((o1, o2) -> Long.compare(o2.getId(), o1.getId())); + } + } + return lwM2mObjects; + } + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/resource/ResourceDao.java b/dao/src/main/java/org/thingsboard/server/dao/resource/ResourceDao.java index 68ad4db4fb..82ea27dc2c 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/resource/ResourceDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/resource/ResourceDao.java @@ -33,8 +33,15 @@ public interface ResourceDao { PageData findAllByTenantId(TenantId tenantId, PageLink pageLink); + PageData findResourcesByTenantIdAndResourceType(TenantId tenantId, + ResourceType resourceType, + PageLink pageLink); - List findAllByTenantIdResourceType(TenantId tenantId, ResourceType resourceType); + List findAllByTenantIdAndResourceType(TenantId tenantId, ResourceType resourceType); + List findResourcesByTenantIdAndResourceType(TenantId tenantId, + ResourceType resourceType, + String[] objectIds, + String searchText); void removeAllByTenantId(TenantId tenantId); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/resource/ResourceDaoImpl.java b/dao/src/main/java/org/thingsboard/server/dao/sql/resource/ResourceDaoImpl.java index e0cddcc555..27d335e737 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/resource/ResourceDaoImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/resource/ResourceDaoImpl.java @@ -29,6 +29,7 @@ import org.thingsboard.server.dao.model.sql.ResourceEntity; import org.thingsboard.server.dao.resource.ResourceDao; import java.util.List; +import java.util.Objects; @Slf4j @Component @@ -73,10 +74,39 @@ public class ResourceDaoImpl implements ResourceDao { } @Override - public List findAllByTenantIdResourceType(TenantId tenantId, ResourceType resourceType) { + public List findAllByTenantIdAndResourceType(TenantId tenantId, ResourceType resourceType) { return DaoUtil.convertDataList(resourceRepository.findAllByTenantIdAndResourceType(tenantId.getId(), resourceType.name())); } + @Override + public PageData findResourcesByTenantIdAndResourceType(TenantId tenantId, + ResourceType resourceType, + PageLink pageLink) { + return DaoUtil.toPageData(resourceRepository.findResourcesPage( + tenantId.getId(), + TenantId.SYS_TENANT_ID.getId(), + resourceType.name(), + Objects.toString(pageLink.getTextSearch(), ""), + DaoUtil.toPageable(pageLink) + )); + } + + @Override + public List findResourcesByTenantIdAndResourceType(TenantId tenantId, ResourceType resourceType, + String[] objectIds, + String searchText) { + return objectIds == null ? + DaoUtil.convertDataList(resourceRepository.findResources( + tenantId.getId(), + TenantId.SYS_TENANT_ID.getId(), + resourceType.name(), + Objects.toString(searchText, ""))) : + DaoUtil.convertDataList(resourceRepository.findResourcesByIds( + tenantId.getId(), + TenantId.SYS_TENANT_ID.getId(), + resourceType.name(), objectIds)); + } + @Override public void removeAllByTenantId(TenantId tenantId) { resourceRepository.removeAllByTenantId(tenantId.getId()); diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/resource/ResourceRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/resource/ResourceRepository.java index c33c9786bd..df1e08fde2 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/resource/ResourceRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/resource/ResourceRepository.java @@ -17,7 +17,9 @@ package org.thingsboard.server.dao.sql.resource; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.CrudRepository; +import org.springframework.data.repository.query.Param; import org.thingsboard.server.dao.model.sql.ResourceCompositeKey; import org.thingsboard.server.dao.model.sql.ResourceEntity; @@ -26,10 +28,57 @@ import java.util.UUID; public interface ResourceRepository extends CrudRepository { + Page findAllByTenantId(UUID tenantId, Pageable pageable); + @Query("SELECT tr FROM ResourceEntity tr " + + "WHERE tr.resourceType = :resourceType " + + "AND LOWER(tr.textSearch) LIKE LOWER(CONCAT('%', :searchText, '%')) " + + "AND (tr.tenantId = :tenantId " + + "OR (tr.tenantId = :systemAdminId " + + "AND NOT EXISTS " + + "(SELECT sr FROM ResourceEntity sr " + + "WHERE sr.tenantId = :tenantId " + + "AND sr.resourceType = :resourceType " + + "AND tr.resourceId = sr.resourceId)))") + Page findResourcesPage( + @Param("tenantId") UUID tenantId, + @Param("systemAdminId") UUID sysAdminId, + @Param("resourceType") String resourceType, + @Param("searchText") String search, + Pageable pageable); List findAllByTenantIdAndResourceType(UUID tenantId, String resourceType); void removeAllByTenantId(UUID tenantId); + + @Query("SELECT tr FROM ResourceEntity tr " + + "WHERE tr.resourceType = :resourceType " + + "AND LOWER(tr.textSearch) LIKE LOWER(CONCAT('%', :searchText, '%')) " + + "AND (tr.tenantId = :tenantId " + + "OR (tr.tenantId = :systemAdminId " + + "AND NOT EXISTS " + + "(SELECT sr FROM ResourceEntity sr " + + "WHERE sr.tenantId = :tenantId " + + "AND sr.resourceType = :resourceType " + + "AND tr.resourceId = sr.resourceId)))") + List findResources(@Param("tenantId") UUID tenantId, + @Param("systemAdminId") UUID sysAdminId, + @Param("resourceType") String resourceType, + @Param("searchText") String search); + + @Query("SELECT tr FROM ResourceEntity tr " + + "WHERE tr.resourceType = :resourceType " + + "AND tr.resourceId in (:resourceIds) " + + "AND (tr.tenantId = :tenantId " + + "OR (tr.tenantId = :systemAdminId " + + "AND NOT EXISTS " + + "(SELECT sr FROM ResourceEntity sr " + + "WHERE sr.tenantId = :tenantId " + + "AND sr.resourceType = :resourceType " + + "AND tr.resourceId = sr.resourceId)))") + List findResourcesByIds(@Param("tenantId") UUID tenantId, + @Param("systemAdminId") UUID sysAdminId, + @Param("resourceType") String resourceType, + @Param("resourceIds") String[] objectIds); } diff --git a/dao/src/main/resources/sql/schema-entities-hsql.sql b/dao/src/main/resources/sql/schema-entities-hsql.sql index 787f457c6a..7e4d3ddcf1 100644 --- a/dao/src/main/resources/sql/schema-entities-hsql.sql +++ b/dao/src/main/resources/sql/schema-entities-hsql.sql @@ -425,6 +425,7 @@ CREATE TABLE IF NOT EXISTS resource ( tenant_id uuid NOT NULL, resource_type varchar(32) NOT NULL, resource_id varchar(255) NOT NULL, + text_search varchar(255), resource_value varchar, CONSTRAINT resource_unq_key UNIQUE (tenant_id, resource_type, resource_id) ); diff --git a/dao/src/main/resources/sql/schema-entities.sql b/dao/src/main/resources/sql/schema-entities.sql index 3bf5ce29c8..21e88fe067 100644 --- a/dao/src/main/resources/sql/schema-entities.sql +++ b/dao/src/main/resources/sql/schema-entities.sql @@ -451,6 +451,7 @@ CREATE TABLE IF NOT EXISTS resource ( tenant_id uuid NOT NULL, resource_type varchar(32) NOT NULL, resource_id varchar(255) NOT NULL, + text_search varchar(255), resource_value varchar, CONSTRAINT resource_unq_key UNIQUE (tenant_id, resource_type, resource_id) ); diff --git a/ui-ngx/src/app/core/http/device-profile.service.ts b/ui-ngx/src/app/core/http/device-profile.service.ts index e0a0bf5174..f7e82f3a41 100644 --- a/ui-ngx/src/app/core/http/device-profile.service.ts +++ b/ui-ngx/src/app/core/http/device-profile.service.ts @@ -14,16 +14,16 @@ /// limitations under the License. /// -import { Injectable } from '@angular/core'; -import { HttpClient } from '@angular/common/http'; -import { PageLink } from '@shared/models/page/page-link'; -import { defaultHttpOptionsFromConfig, RequestConfig } from './http-utils'; -import { Observable } from 'rxjs'; -import { PageData } from '@shared/models/page/page-data'; -import { DeviceProfile, DeviceProfileInfo, DeviceTransportType } from '@shared/models/device.models'; -import { isDefinedAndNotNull, isEmptyStr } from '@core/utils'; -import { ObjectLwM2M, ServerSecurityConfig } from '@home/components/profile/device/lwm2m/profile-config.models'; -import { SortOrder } from '@shared/models/page/sort-order'; +import {Injectable} from '@angular/core'; +import {HttpClient} from '@angular/common/http'; +import {PageLink} from '@shared/models/page/page-link'; +import {defaultHttpOptionsFromConfig, RequestConfig} from './http-utils'; +import {Observable} from 'rxjs'; +import {PageData} from '@shared/models/page/page-data'; +import {DeviceProfile, DeviceProfileInfo, DeviceTransportType} from '@shared/models/device.models'; +import {isDefinedAndNotNull, isEmptyStr} from '@core/utils'; +import {ObjectLwM2M, ServerSecurityConfig} from '@home/components/profile/device/lwm2m/profile-config.models'; +import {SortOrder} from '@shared/models/page/sort-order'; @Injectable({ providedIn: 'root' @@ -43,9 +43,9 @@ export class DeviceProfileService { return this.http.get(`/api/deviceProfile/${deviceProfileId}`, defaultHttpOptionsFromConfig(config)); } - public getLwm2mObjects(sortOrder: SortOrder, objectIds?: number[], searchText?: string, config?: RequestConfig): + public getLwm2mObjects(sortOrder: SortOrder, objectIds?: string[], searchText?: string, config?: RequestConfig): Observable> { - let url = `/api/lwm2m/deviceProfile/?sortProperty=${sortOrder.property}&sortOrder=${sortOrder.direction}`; + let url = `/api/resource/lwm2m/?sortProperty=${sortOrder.property}&sortOrder=${sortOrder.direction}`; if (isDefinedAndNotNull(objectIds) && objectIds.length > 0) { url += `&objectIds=${objectIds}`; } @@ -63,9 +63,9 @@ export class DeviceProfileService { ); } - public getLwm2mObjectsPage(pageLink: PageLink, config?: RequestConfig): Observable> { - return this.http.get>( - `/api/lwm2m/deviceProfile/objects${pageLink.toQuery()}`, + public getLwm2mObjectsPage(pageLink: PageLink, config?: RequestConfig): Observable> { + return this.http.get>( + `/api/resource/lwm2m/page${pageLink.toQuery()}`, defaultHttpOptionsFromConfig(config) ); } diff --git a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-profile-transport-configuration.component.ts b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-profile-transport-configuration.component.ts index b0e7ee8e07..b8130f2702 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-profile-transport-configuration.component.ts +++ b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-profile-transport-configuration.component.ts @@ -172,7 +172,7 @@ export class Lwm2mDeviceProfileTransportConfigurationComponent implements Contro private updateObserveAttrTelemetryObjectFormGroup = (objectsList: ObjectLwM2M[]): void => { this.lwm2mDeviceProfileFormGroup.patchValue({ - observeAttrTelemetry: this.getObserveAttrTelemetryObjects(objectsList) + observeAttrTelemetry: deepClone(this.getObserveAttrTelemetryObjects(objectsList)) }, {emitEvent: false}); } @@ -195,32 +195,32 @@ export class Lwm2mDeviceProfileTransportConfigurationComponent implements Contro } } - private getObserveAttrTelemetryObjects = (listObject: ObjectLwM2M[]): object => { - const clientObserveAttrTelemetry = listObject; - if (this.configurationValue.observeAttr) { + private getObserveAttrTelemetryObjects = (objectList: ObjectLwM2M[]): object => { + const objectLwM2MS = deepClone(objectList); + if (this.configurationValue.observeAttr && objectLwM2MS.length > 0) { const observeArray = this.configurationValue.observeAttr.observe; const attributeArray = this.configurationValue.observeAttr.attribute; const telemetryArray = this.configurationValue.observeAttr.telemetry; let keyNameJson = this.configurationValue.observeAttr.keyName; if (this.includesNotZeroInstance(attributeArray, telemetryArray)) { - this.addInstances(attributeArray, telemetryArray, clientObserveAttrTelemetry); + this.addInstances(attributeArray, telemetryArray, objectLwM2MS); } if (isDefinedAndNotNull(observeArray)) { - this.updateObserveAttrTelemetryObjects(observeArray, clientObserveAttrTelemetry, OBSERVE); + this.updateObserveAttrTelemetryObjects(observeArray, objectLwM2MS, OBSERVE); } if (isDefinedAndNotNull(attributeArray)) { - this.updateObserveAttrTelemetryObjects(attributeArray, clientObserveAttrTelemetry, ATTRIBUTE); + this.updateObserveAttrTelemetryObjects(attributeArray, objectLwM2MS, ATTRIBUTE); } if (isDefinedAndNotNull(telemetryArray)) { - this.updateObserveAttrTelemetryObjects(telemetryArray, clientObserveAttrTelemetry, TELEMETRY); + this.updateObserveAttrTelemetryObjects(telemetryArray, objectLwM2MS, TELEMETRY); } if (isDefinedAndNotNull(keyNameJson)) { this.configurationValue.observeAttr.keyName = this.validateKeyNameObjects(keyNameJson, attributeArray, telemetryArray); this.upDateJsonAllConfig(); - this.updateKeyNameObjects(clientObserveAttrTelemetry); + this.updateKeyNameObjects(objectLwM2MS); } } - return {clientLwM2M: clientObserveAttrTelemetry}; + return {clientLwM2M: objectLwM2MS}; } private includesNotZeroInstance = (attribute: string[], telemetry: string[]): boolean => { @@ -235,35 +235,39 @@ export class Lwm2mDeviceProfileTransportConfigurationComponent implements Contro .sort(this.sortPath); new Set(instancesPath).forEach(path => { - const pathParameter = Array.from(path.split('/'), Number); - const objectLwM2M = clientObserveAttrTelemetry.find(x => x.id === pathParameter[0]); + const pathParameter = Array.from(path.split('/'), String); + const objectLwM2M = clientObserveAttrTelemetry.find(x => x.keyId === pathParameter[0]); if (objectLwM2M) { const instance = deepClone(objectLwM2M.instances[0]); - instance.id = pathParameter[1]; + instance.id = +pathParameter[1]; objectLwM2M.instances.push(instance); } }); } - private updateObserveAttrTelemetryObjects = (parameters: string[], clientObserveAttrTelemetry: ObjectLwM2M[], + private updateObserveAttrTelemetryObjects = (parameters: string[], objectLwM2MS: ObjectLwM2M[], nameParameter: string): void => { parameters.forEach(parameter => { - const [objectId, instanceId, resourceId] = Array.from(parameter.substring(1).split('/'), Number); - clientObserveAttrTelemetry.find(objectLwm2m => objectLwm2m.id === objectId) - .instances.find(itrInstance => itrInstance.id === instanceId) - .resources.find(resource => resource.id === resourceId) - [nameParameter] = true; + const [objectKeyId, instanceId, resourceId] = Array.from(parameter.substring(1).split('/'), String); + const objectLwM2M = objectLwM2MS.find(objectLwm2m => objectLwm2m.keyId === objectKeyId); + if (objectLwM2M) { + objectLwM2M.instances.find(itrInstance => itrInstance.id === +instanceId) + .resources.find(resource => resource.id === +resourceId) + [nameParameter] = true; + } }); + } private updateKeyNameObjects = (clientObserveAttrTelemetry: ObjectLwM2M[]): void => { Object.keys(this.configurationValue.observeAttr.keyName).forEach(key => { - const [objectId, instanceId, resourceId] = Array.from(key.substring(1).split('/'), Number); - clientObserveAttrTelemetry.find(objectLwm2m => objectLwm2m.id === objectId) - .instances.find(instance => instance.id === instanceId) - .resources.find(resource => resource.id === resourceId) + const [objectKeyId, instanceId, resourceId] = Array.from(key.substring(1).split('/'), String); + const objectLwM2M = clientObserveAttrTelemetry.find(objectLwm2m => objectLwm2m.keyId === objectKeyId) + if (objectLwM2M) { + objectLwM2M.instances.find(instance => instance.id === +instanceId) + .resources.find(resource => resource.id === +resourceId) .keyName = this.configurationValue.observeAttr.keyName[key]; - }); + }}); } private validateKeyNameObjects = (nameJson: JsonObject, attributeArray: JsonArray, telemetryArray: JsonArray): {} => { @@ -290,7 +294,7 @@ export class Lwm2mDeviceProfileTransportConfigurationComponent implements Contro if (instance.hasOwnProperty(RESOURCES) && Array.isArray(instance.resources)) { instance.resources.forEach(resource => { if (resource.attribute || resource.telemetry) { - let pathRes = `/${obj.id}/${instance.id}/${resource.id}`; + let pathRes = `/${obj.keyId}/${instance.id}/${resource.id}`; if (resource.observe) { observeArray.push(pathRes); } @@ -345,22 +349,22 @@ export class Lwm2mDeviceProfileTransportConfigurationComponent implements Contro }); } - private getObjectsFromJsonAllConfig = (): number[] => { - const objectsIds = new Set(); + private getObjectsFromJsonAllConfig = (): string[] => { + const objectsIds = new Set(); if (this.configurationValue.observeAttr) { if (this.configurationValue.observeAttr.observe) { this.configurationValue.observeAttr.observe.forEach(obj => { - objectsIds.add(Array.from(obj.substring(1).split('/'), Number)[0]); + objectsIds.add(Array.from(obj.substring(1).split('/'), String)[0]); }); } if (this.configurationValue.observeAttr.attribute) { this.configurationValue.observeAttr.attribute.forEach(obj => { - objectsIds.add(Array.from(obj.substring(1).split('/'), Number)[0]); + objectsIds.add(Array.from(obj.substring(1).split('/'), String)[0]); }); } if (this.configurationValue.observeAttr.telemetry) { this.configurationValue.observeAttr.telemetry.forEach(obj => { - objectsIds.add(Array.from(obj.substring(1).split('/'), Number)[0]); + objectsIds.add(Array.from(obj.substring(1).split('/'), String)[0]); }); } } @@ -379,21 +383,21 @@ export class Lwm2mDeviceProfileTransportConfigurationComponent implements Contro removeObjectsList = (value: ObjectLwM2M): void => { const objectsOld = this.lwm2mDeviceProfileFormGroup.get(OBSERVE_ATTR_TELEMETRY).value.clientLwM2M; - const isIdIndex = (element) => element.id === value.id; + const isIdIndex = (element) => element.keyId === value.keyId; const index = objectsOld.findIndex(isIdIndex); if (index >= 0) { objectsOld.splice(index, 1); } - this.removeObserveAttrTelemetryFromJson(OBSERVE, value.id); - this.removeObserveAttrTelemetryFromJson(TELEMETRY, value.id); - this.removeObserveAttrTelemetryFromJson(ATTRIBUTE, value.id); - this.removeKeyNameFromJson(value.id); + this.removeObserveAttrTelemetryFromJson(OBSERVE, value.keyId); + this.removeObserveAttrTelemetryFromJson(TELEMETRY, value.keyId); + this.removeObserveAttrTelemetryFromJson(ATTRIBUTE, value.keyId); + this.removeKeyNameFromJson(value.keyId); this.updateObserveAttrTelemetryObjectFormGroup(objectsOld); this.upDateJsonAllConfig(); } - private removeObserveAttrTelemetryFromJson = (observeAttrTel: string, id: number): void => { - const isIdIndex = (element) => element.startsWith(`/${id}`); + private removeObserveAttrTelemetryFromJson = (observeAttrTel: string, keyId: string): void => { + const isIdIndex = (element) => element.startsWith(`/${keyId}`); let index = this.configurationValue.observeAttr[observeAttrTel].findIndex(isIdIndex); while (index >= 0) { this.configurationValue.observeAttr[observeAttrTel].splice(index, 1); @@ -401,10 +405,10 @@ export class Lwm2mDeviceProfileTransportConfigurationComponent implements Contro } } - private removeKeyNameFromJson = (id: number): void => { + private removeKeyNameFromJson = (keyId: string): void => { const keyNameJson = this.configurationValue.observeAttr.keyName; Object.keys(keyNameJson).forEach(key => { - if (key.startsWith(`/${id}`)) { + if (key.startsWith(`/${keyId}`)) { delete keyNameJson[key]; } }); diff --git a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-object-add-instances-list.component.ts b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-object-add-instances-list.component.ts index 1a0ab08e11..2b33f9cf3e 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-object-add-instances-list.component.ts +++ b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-object-add-instances-list.component.ts @@ -14,12 +14,11 @@ /// limitations under the License. /// -import { Component, forwardRef, } from '@angular/core'; -import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms'; -import { Store } from '@ngrx/store'; -import { AppState } from '@core/core.state'; -import { INSTANCES_ID_VALUE_MAX, INSTANCES_ID_VALUE_MIN, KEY_REGEXP_NUMBER } from './profile-config.models'; -import { DeviceProfileService } from '@core/http/device-profile.service'; +import {Component, forwardRef,} from '@angular/core'; +import {ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR, Validators} from '@angular/forms'; +import {Store} from '@ngrx/store'; +import {AppState} from '@core/core.state'; +import {INSTANCES_ID_VALUE_MAX, INSTANCES_ID_VALUE_MIN, KEY_REGEXP_NUMBER} from './profile-config.models'; @Component({ selector: 'tb-profile-lwm2m-object-add-instances-list', @@ -43,7 +42,6 @@ export class Lwm2mObjectAddInstancesListComponent implements ControlValueAccesso private propagateChange = (v: any) => { }; constructor(private store: Store, - private deviceProfileService: DeviceProfileService, private fb: FormBuilder) { this.instancesListFormGroup = this.fb.group({ instanceIdInput: [null, [ diff --git a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-object-add-instances.component.html b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-object-add-instances.component.html index 34a8d6a843..b37abd2c79 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-object-add-instances.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-object-add-instances.component.html @@ -17,7 +17,7 @@ -->
- {{data.objectName}}    (object [{{data.objectId}}]) + {{data.objectName}}    (object [{{data.objectKeyId}}])