diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/AbstractLwM2MIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/AbstractLwM2MIntegrationTest.java index deb8b8230f..4378c3758e 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/AbstractLwM2MIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/AbstractLwM2MIntegrationTest.java @@ -62,7 +62,6 @@ import org.thingsboard.server.common.data.query.SingleEntityFilter; import org.thingsboard.server.common.data.security.DeviceCredentials; import org.thingsboard.server.common.data.security.DeviceCredentialsType; import org.thingsboard.server.dao.service.DaoSqlTest; -import org.thingsboard.server.service.ws.telemetry.cmd.TelemetryCmdsWrapper; import org.thingsboard.server.service.ws.telemetry.cmd.v2.EntityDataCmd; import org.thingsboard.server.service.ws.telemetry.cmd.v2.EntityDataUpdate; import org.thingsboard.server.service.ws.telemetry.cmd.v2.LatestValueCmd; @@ -75,7 +74,6 @@ import java.io.IOException; import java.net.ServerSocket; import java.util.ArrayList; import java.util.Arrays; -import java.util.Base64; import java.util.Collections; import java.util.HashSet; import java.util.List; @@ -148,13 +146,13 @@ public abstract class AbstractLwM2MIntegrationTest extends AbstractTransportInte " {\n" + " \"keyName\": {\n" + - " \"/3_1.0/0/9\": \"batteryLevel\"\n" + + " \"/3_1.2/0/9\": \"batteryLevel\"\n" + " },\n" + " \"observe\": [],\n" + " \"attribute\": [\n" + " ],\n" + " \"telemetry\": [\n" + - " \"/3_1.0/0/9\"\n" + + " \"/3_1.2/0/9\"\n" + " ],\n" + " \"attributeLwm2m\": {}\n" + " }"; diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/Lwm2mTestHelper.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/Lwm2mTestHelper.java index 9e47eab80b..6eb8dab73f 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/Lwm2mTestHelper.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/Lwm2mTestHelper.java @@ -34,6 +34,7 @@ public class Lwm2mTestHelper { public static final int RESOURCE_ID_2 = 2; public static final int RESOURCE_ID_3 = 3; public static final int RESOURCE_ID_4 = 4; + public static final int RESOURCE_ID_6 = 6; public static final int RESOURCE_ID_7 = 7; public static final int RESOURCE_ID_8 = 8; public static final int RESOURCE_ID_9 = 9; diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/attributes/DefaultLwM2mLinkParserTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/attributes/DefaultLwM2mLinkParserTest.java index b5d8fecf10..6f872432ec 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/attributes/DefaultLwM2mLinkParserTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/attributes/DefaultLwM2mLinkParserTest.java @@ -34,7 +34,7 @@ public class DefaultLwM2mLinkParserTest { @Test public void check_invalid_values() throws LinkParseException { - // first check it's OK with valid value + // first check it's OK with valid value (3/0/11 - "errorCodes") LwM2mLink[] parsed = parser.parseLwM2mLinkFromCoreLinkFormat(";dim=255".getBytes(), null); assertEquals(new LwM2mPath(3, 0, 11), parsed[0].getPath()); AttributeSet attResult = new LwM2mAttributeSet(LwM2mAttributes.create(LwM2mAttributes.DIMENSION, 255l)); @@ -69,7 +69,7 @@ public class DefaultLwM2mLinkParserTest { // then check an invalid one assertThrowsExactly(LinkParseException.class, () -> { - // dim should be between 0-255 + // pmin should be with value parser.parseLwM2mLinkFromCoreLinkFormat(";pmin".getBytes(), null); }); } diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/LwM2MTestClient.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/LwM2MTestClient.java index 6e6fa12cf7..19f32d175f 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/LwM2MTestClient.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/LwM2MTestClient.java @@ -36,7 +36,6 @@ import org.eclipse.leshan.client.observer.LwM2mClientObserver; import org.eclipse.leshan.client.resource.DummyInstanceEnabler; import org.eclipse.leshan.client.resource.LwM2mInstanceEnabler; import org.eclipse.leshan.client.resource.LwM2mObjectEnabler; -import org.eclipse.leshan.client.resource.ObjectsInitializer; import org.eclipse.leshan.client.resource.listener.ObjectsListenerAdapter; import org.eclipse.leshan.client.send.ManualDataSender; import org.eclipse.leshan.client.servers.LwM2mServer; @@ -133,7 +132,7 @@ public class LwM2MTestClient { models.addAll(ObjectLoader.loadDdfFile(LwM2MTestClient.class.getClassLoader().getResourceAsStream("lwm2m/" + resourceName), resourceName)); } LwM2mModel model = new StaticModel(models); - ObjectsInitializer initializer = new ObjectsInitializer(model); + TbObjectsInitializer initializer = new TbObjectsInitializer(model); if (securityBs != null && security != null) { // SECURITY diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/SimpleLwM2MDevice.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/SimpleLwM2MDevice.java index e9bc2c9d48..e7e2320b38 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/SimpleLwM2MDevice.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/SimpleLwM2MDevice.java @@ -47,7 +47,7 @@ public class SimpleLwM2MDevice extends BaseInstanceEnabler implements Destroyabl private static final int min = 5; private static final int max = 50; private static final PrimitiveIterator.OfInt randomIterator = new Random().ints(min,max + 1).iterator(); - private static final List supportedResources = Arrays.asList(0, 1, 2, 3, 9, 10, 11, 13, 14, 15, 16, 17, 18, 19, 20, 21); + private static final List supportedResources = Arrays.asList(0, 1, 2, 3, 6, 9, 10, 11, 13, 14, 15, 16, 17, 18, 19, 20, 21); public SimpleLwM2MDevice() { @@ -79,6 +79,8 @@ public class SimpleLwM2MDevice extends BaseInstanceEnabler implements Destroyabl return ReadResponse.success(resourceId, getSerialNumber()); case 3: return ReadResponse.success(resourceId, getFirmwareVersion()); + case 6: + return ReadResponse.success(resourceId, getAvailablePowerSources(), ResourceModel.Type.INTEGER); case 9: return ReadResponse.success(resourceId, getBatteryLevel()); case 10: @@ -157,6 +159,14 @@ public class SimpleLwM2MDevice extends BaseInstanceEnabler implements Destroyabl return 0; } + private Map getAvailablePowerSources() { + Map availablePowerSources = new HashMap<>(); + availablePowerSources.put(0, 1L); + availablePowerSources.put(1, 2L); + availablePowerSources.put(2, 5L); + return availablePowerSources; + } + private int getBatteryLevel() { return randomIterator.nextInt(); // return 42; diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/TbLwm2mObjectEnabler.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/TbLwm2mObjectEnabler.java new file mode 100644 index 0000000000..a0c1333069 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/TbLwm2mObjectEnabler.java @@ -0,0 +1,642 @@ +package org.thingsboard.server.transport.lwm2m.client; + +import org.eclipse.leshan.client.LwM2mClient; +import org.eclipse.leshan.client.resource.BaseObjectEnabler; +import org.eclipse.leshan.client.resource.DummyInstanceEnabler; +import org.eclipse.leshan.client.resource.LwM2mInstanceEnabler; +import org.eclipse.leshan.client.resource.LwM2mInstanceEnablerFactory; +import org.eclipse.leshan.client.resource.LwM2mObjectEnabler; +import org.eclipse.leshan.client.resource.listener.ResourceListener; +import org.eclipse.leshan.client.servers.LwM2mServer; +import org.eclipse.leshan.client.servers.ServersInfoExtractor; +import org.eclipse.leshan.client.util.LinkFormatHelper; +import org.eclipse.leshan.core.Destroyable; +import org.eclipse.leshan.core.LwM2mId; +import org.eclipse.leshan.core.Startable; +import org.eclipse.leshan.core.Stoppable; +import org.eclipse.leshan.core.link.lwm2m.LwM2mLink; +import org.eclipse.leshan.core.link.lwm2m.attributes.LwM2mAttribute; +import org.eclipse.leshan.core.link.lwm2m.attributes.LwM2mAttributes; +import org.eclipse.leshan.core.model.ObjectModel; +import org.eclipse.leshan.core.model.ResourceModel; +import org.eclipse.leshan.core.node.LwM2mMultipleResource; +import org.eclipse.leshan.core.node.LwM2mObject; +import org.eclipse.leshan.core.node.LwM2mObjectInstance; +import org.eclipse.leshan.core.node.LwM2mPath; +import org.eclipse.leshan.core.node.LwM2mResource; +import org.eclipse.leshan.core.node.LwM2mResourceInstance; +import org.eclipse.leshan.core.request.BootstrapDeleteRequest; +import org.eclipse.leshan.core.request.BootstrapReadRequest; +import org.eclipse.leshan.core.request.BootstrapWriteRequest; +import org.eclipse.leshan.core.request.ContentFormat; +import org.eclipse.leshan.core.request.CreateRequest; +import org.eclipse.leshan.core.request.DeleteRequest; +import org.eclipse.leshan.core.request.DiscoverRequest; +import org.eclipse.leshan.core.request.DownlinkRequest; +import org.eclipse.leshan.core.request.ExecuteRequest; +import org.eclipse.leshan.core.request.ObserveRequest; +import org.eclipse.leshan.core.request.ReadRequest; +import org.eclipse.leshan.core.request.WriteAttributesRequest; +import org.eclipse.leshan.core.request.WriteRequest; +import org.eclipse.leshan.core.request.WriteRequest.Mode; +import org.eclipse.leshan.core.response.BootstrapDeleteResponse; +import org.eclipse.leshan.core.response.BootstrapReadResponse; +import org.eclipse.leshan.core.response.BootstrapWriteResponse; +import org.eclipse.leshan.core.response.CreateResponse; +import org.eclipse.leshan.core.response.DeleteResponse; +import org.eclipse.leshan.core.response.DiscoverResponse; +import org.eclipse.leshan.core.response.ExecuteResponse; +import org.eclipse.leshan.core.response.ObserveResponse; +import org.eclipse.leshan.core.response.ReadResponse; +import org.eclipse.leshan.core.response.WriteAttributesResponse; +import org.eclipse.leshan.core.response.WriteResponse; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; + +public class TbLwm2mObjectEnabler extends BaseObjectEnabler implements Destroyable, Startable, Stoppable { + + private static Logger LOG = LoggerFactory.getLogger(DummyInstanceEnabler.class); + + protected Map instances; + protected LwM2mInstanceEnablerFactory instanceFactory; + protected ContentFormat defaultContentFormat; + + private LinkFormatHelper tbLinkFormatHelper; + + public TbLwm2mObjectEnabler(int id, ObjectModel objectModel, Map instances, + LwM2mInstanceEnablerFactory instanceFactory, ContentFormat defaultContentFormat) { + super(id, objectModel); + this.instances = new HashMap<>(instances); + this.instanceFactory = instanceFactory; + this.defaultContentFormat = defaultContentFormat; + for (Entry entry : this.instances.entrySet()) { + instances.put(entry.getKey(), entry.getValue()); + listenInstance(entry.getValue(), entry.getKey()); + } + } + + public TbLwm2mObjectEnabler(int id, ObjectModel objectModel) { + super(id, objectModel); + } + + @Override + public synchronized List getAvailableInstanceIds() { + List ids = new ArrayList<>(instances.keySet()); + Collections.sort(ids); + return ids; + } + + @Override + public synchronized List getAvailableResourceIds(int instanceId) { + LwM2mInstanceEnabler instanceEnabler = instances.get(instanceId); + if (instanceEnabler != null) { + return instanceEnabler.getAvailableResourceIds(getObjectModel()); + } else { + return Collections.emptyList(); + } + } + + public synchronized void addInstance(int instanceId, LwM2mInstanceEnabler newInstance) { + instances.put(instanceId, newInstance); + listenInstance(newInstance, instanceId); + fireInstancesAdded(instanceId); + } + + public synchronized LwM2mInstanceEnabler getInstance(int instanceId) { + return instances.get(instanceId); + } + + public synchronized LwM2mInstanceEnabler removeInstance(int instanceId) { + LwM2mInstanceEnabler removedInstance = instances.remove(instanceId); + if (removedInstance != null) { + fireInstancesRemoved(removedInstance.getId()); + } + return removedInstance; + } + + @Override + protected CreateResponse doCreate(LwM2mServer server, CreateRequest request) { + if (!getObjectModel().multiple && instances.size() > 0) { + return CreateResponse.badRequest("an instance already exist for this single instance object"); + } + + if (request.unknownObjectInstanceId()) { + // create instance + LwM2mInstanceEnabler newInstance = createInstance(server, getObjectModel().multiple ? null : 0, + request.getResources()); + + // add new instance to this object + instances.put(newInstance.getId(), newInstance); + listenInstance(newInstance, newInstance.getId()); + fireInstancesAdded(newInstance.getId()); + + return CreateResponse + .success(new LwM2mPath(request.getPath().getObjectId(), newInstance.getId()).toString()); + } else { + List instanceNodes = request.getObjectInstances(); + + // checks single object instances + if (!getObjectModel().multiple) { + if (request.getObjectInstances().size() > 1) { + return CreateResponse.badRequest("can not create several instances on this single instance object"); + } + if (request.getObjectInstances().get(0).getId() != 0) { + return CreateResponse.badRequest("single instance object must use 0 as ID"); + } + } + // ensure instance does not already exists + for (LwM2mObjectInstance instance : instanceNodes) { + if (instances.containsKey(instance.getId())) { + return CreateResponse.badRequest(String.format("instance %d already exists", instance.getId())); + } + } + + // create the new instances + int[] instanceIds = new int[request.getObjectInstances().size()]; + int i = 0; + for (LwM2mObjectInstance instance : request.getObjectInstances()) { + // create instance + LwM2mInstanceEnabler newInstance = createInstance(server, instance.getId(), + instance.getResources().values()); + + // add new instance to this object + instances.put(newInstance.getId(), newInstance); + listenInstance(newInstance, newInstance.getId()); + + // store instance ids + instanceIds[i] = newInstance.getId(); + i++; + } + fireInstancesAdded(instanceIds); + return CreateResponse.success(); + } + } + + protected LwM2mInstanceEnabler createInstance(LwM2mServer server, Integer instanceId, + Collection resources) { + // create the new instance + LwM2mInstanceEnabler newInstance = instanceFactory.create(getObjectModel(), instanceId, instances.keySet()); + newInstance.setLwM2mClient(getLwm2mClient()); + + // add/write resource + for (LwM2mResource resource : resources) { + newInstance.write(server, true, resource.getId(), resource); + } + + return newInstance; + } + + @Override + protected ReadResponse doRead(LwM2mServer server, ReadRequest request) { + LwM2mPath path = request.getPath(); + + // Manage Object case + if (path.isObject()) { + List lwM2mObjectInstances = new ArrayList<>(); + for (LwM2mInstanceEnabler instance : instances.values()) { + ReadResponse response = instance.read(server); + if (response.isSuccess()) { + lwM2mObjectInstances.add((LwM2mObjectInstance) response.getContent()); + } + } + return ReadResponse.success(new LwM2mObject(getId(), lwM2mObjectInstances)); + } + + // Manage Instance case + LwM2mInstanceEnabler instance = instances.get(path.getObjectInstanceId()); + if (instance == null) + return ReadResponse.notFound(); + + if (path.getResourceId() == null) { + return instance.read(server); + } + + // Manage Resource case + if (path.getResourceInstanceId() == null) { + return instance.read(server, path.getResourceId()); + } + + // Manage Resource Instance case + return instance.read(server, path.getResourceId(), path.getResourceInstanceId()); + } + + @Override + protected BootstrapReadResponse doRead(LwM2mServer server, BootstrapReadRequest request) { + // Basic implementation we delegate to classic Read Request + ReadResponse response = doRead(server, + new ReadRequest(request.getContentFormat(), request.getPath(), request.getCoapRequest())); + return new BootstrapReadResponse(response.getCode(), response.getContent(), response.getErrorMessage()); + } + + @Override + protected ObserveResponse doObserve(final LwM2mServer server, final ObserveRequest request) { + final LwM2mPath path = request.getPath(); + + // Manage Object case + if (path.isObject()) { + List lwM2mObjectInstances = new ArrayList<>(); + for (LwM2mInstanceEnabler instance : instances.values()) { + ReadResponse response = instance.observe(server); + if (response.isSuccess()) { + lwM2mObjectInstances.add((LwM2mObjectInstance) response.getContent()); + } + } + return ObserveResponse.success(new LwM2mObject(getId(), lwM2mObjectInstances)); + } + + // Manage Instance case + final LwM2mInstanceEnabler instance = instances.get(path.getObjectInstanceId()); + if (instance == null) + return ObserveResponse.notFound(); + + if (path.getResourceId() == null) { + return instance.observe(server); + } + + // Manage Resource case + if (path.getResourceInstanceId() == null) { + return instance.observe(server, path.getResourceId()); + } + + // Manage Resource Instance case + return instance.observe(server, path.getResourceId(), path.getResourceInstanceId()); + } + + @Override + protected WriteResponse doWrite(LwM2mServer server, WriteRequest request) { + LwM2mPath path = request.getPath(); + + // Manage Instance case + LwM2mInstanceEnabler instance = instances.get(path.getObjectInstanceId()); + if (instance == null) + return WriteResponse.notFound(); + + if (path.isObjectInstance()) { + return instance.write(server, request.isReplaceRequest(), (LwM2mObjectInstance) request.getNode()); + } + + // Manage Resource case + if (path.getResourceInstanceId() == null) { + return instance.write(server, request.isReplaceRequest(), path.getResourceId(), + (LwM2mResource) request.getNode()); + } + + // Manage Resource Instance case + return instance.write(server, false, path.getResourceId(), path.getResourceInstanceId(), + ((LwM2mResourceInstance) request.getNode())); + } + + @Override + protected BootstrapWriteResponse doWrite(LwM2mServer server, BootstrapWriteRequest request) { + LwM2mPath path = request.getPath(); + + // Manage Object case + if (path.isObject()) { + for (LwM2mObjectInstance instanceNode : ((LwM2mObject) request.getNode()).getInstances().values()) { + LwM2mInstanceEnabler instanceEnabler = instances.get(instanceNode.getId()); + if (instanceEnabler == null) { + doCreate(server, new CreateRequest(path.getObjectId(), instanceNode)); + } else { + doWrite(server, new WriteRequest(Mode.REPLACE, path.getObjectId(), instanceEnabler.getId(), + instanceNode.getResources().values())); + } + } + return BootstrapWriteResponse.success(); + } + + // Manage Instance case + if (path.isObjectInstance()) { + LwM2mObjectInstance instanceNode = (LwM2mObjectInstance) request.getNode(); + LwM2mInstanceEnabler instanceEnabler = instances.get(path.getObjectInstanceId()); + if (instanceEnabler == null) { + doCreate(server, new CreateRequest(path.getObjectId(), instanceNode)); + } else { + doWrite(server, new WriteRequest(Mode.REPLACE, request.getContentFormat(), path.getObjectId(), + path.getObjectInstanceId(), instanceNode.getResources().values())); + } + return BootstrapWriteResponse.success(); + } + + // Manage resource case + LwM2mResource resource = (LwM2mResource) request.getNode(); + LwM2mInstanceEnabler instanceEnabler = instances.get(path.getObjectInstanceId()); + if (instanceEnabler == null) { + doCreate(server, new CreateRequest(path.getObjectId(), + new LwM2mObjectInstance(path.getObjectInstanceId(), resource))); + } else { + instanceEnabler.write(server, true, path.getResourceId(), resource); + } + return BootstrapWriteResponse.success(); + } + + @Override + protected ExecuteResponse doExecute(LwM2mServer server, ExecuteRequest request) { + LwM2mPath path = request.getPath(); + LwM2mInstanceEnabler instance = instances.get(path.getObjectInstanceId()); + if (instance == null) { + return ExecuteResponse.notFound(); + } + return instance.execute(server, path.getResourceId(), request.getArguments()); + } + + @Override + protected DeleteResponse doDelete(LwM2mServer server, DeleteRequest request) { + LwM2mInstanceEnabler deletedInstance = instances.remove(request.getPath().getObjectInstanceId()); + if (deletedInstance != null) { + deletedInstance.onDelete(server); + fireInstancesRemoved(deletedInstance.getId()); + return DeleteResponse.success(); + } + return DeleteResponse.notFound(); + } + + @Override + public BootstrapDeleteResponse doDelete(LwM2mServer server, BootstrapDeleteRequest request) { + if (request.getPath().isRoot() || request.getPath().isObject()) { + if (id == LwM2mId.SECURITY) { + // For security object, we clean everything except bootstrap Server account. + + // Get bootstrap account and store removed instances ids + Entry bootstrapServerAccount = null; + int[] instanceIds = new int[instances.size()]; + int i = 0; + for (Entry instance : instances.entrySet()) { + if (ServersInfoExtractor.isBootstrapServer(instance.getValue())) { + bootstrapServerAccount = instance; + } else { + // Store instance ids + instanceIds[i] = instance.getKey(); + i++; + } + } + // Clear everything + instances.clear(); + + // Put bootstrap account again + if (bootstrapServerAccount != null) { + instances.put(bootstrapServerAccount.getKey(), bootstrapServerAccount.getValue()); + } + + fireInstancesRemoved(instanceIds); + return BootstrapDeleteResponse.success(); + } else if (id == LwM2mId.OSCORE) { + // For OSCORE object, we clean everything except OSCORE object link to bootstrap Server account. + + // Get bootstrap account + LwM2mObjectInstance bootstrapInstance = ServersInfoExtractor.getBootstrapSecurityInstance( + getLwm2mClient().getObjectTree().getObjectEnabler(LwM2mId.SECURITY)); + // Get OSCORE instance ID associated to it + Integer bootstrapOscoreInstanceId = bootstrapInstance != null + ? ServersInfoExtractor.getOscoreSecurityMode(bootstrapInstance) + : null; + + // if bootstrap server use OSCORE, + // search the OSCORE instance for this ID and store removed instances ids + if (bootstrapOscoreInstanceId != null) { + Entry bootstrapServerOscore = null; + int[] instanceIds = new int[instances.size()]; + int i = 0; + for (Entry instance : instances.entrySet()) { + if (bootstrapOscoreInstanceId.equals(instance.getKey())) { + bootstrapServerOscore = instance; + } else { + // Store instance ids + instanceIds[i] = instance.getKey(); + i++; + } + } + + // Clear everything + instances.clear(); + + // Put bootstrap OSCORE instance again + if (bootstrapServerOscore != null) { + instances.put(bootstrapServerOscore.getKey(), bootstrapServerOscore.getValue()); + } + fireInstancesRemoved(instanceIds); + return BootstrapDeleteResponse.success(); + } + // else delete everything. + } + + // In all other cases, just delete everything + instances.clear(); + // fired instances removed + int[] instanceIds = new int[instances.size()]; + int i = 0; + for (Entry instance : instances.entrySet()) { + instanceIds[i] = instance.getKey(); + i++; + } + fireInstancesRemoved(instanceIds); + + return BootstrapDeleteResponse.success(); + } else if (request.getPath().isObjectInstance()) { + if (id == LwM2mId.SECURITY) { + // For security object, deleting bootstrap Server account is not allowed + LwM2mInstanceEnabler instance = instances.get(request.getPath().getObjectInstanceId()); + if (instance == null) { + return BootstrapDeleteResponse + .badRequest(String.format("Instance %s not found", request.getPath())); + } else if (ServersInfoExtractor.isBootstrapServer(instance)) { + return BootstrapDeleteResponse.badRequest("bootstrap server can not be deleted"); + } + } else if (id == LwM2mId.OSCORE) { + // For OSCORE object, deleting instance linked to Bootstrap account is not allowed + + // Get bootstrap instance + LwM2mObjectInstance bootstrapInstance = ServersInfoExtractor.getBootstrapSecurityInstance( + getLwm2mClient().getObjectTree().getObjectEnabler(LwM2mId.SECURITY)); + // Get OSCORE instance ID associated to it + Integer bootstrapOscoreInstanceId = bootstrapInstance != null + ? ServersInfoExtractor.getOscoreSecurityMode(bootstrapInstance) + : null; + + if (bootstrapOscoreInstanceId != null + && bootstrapOscoreInstanceId.equals(request.getPath().getObjectInstanceId())) { + return BootstrapDeleteResponse + .badRequest("OSCORE instance linked to bootstrap server can not be deleted"); + } + } + if (null != instances.remove(request.getPath().getObjectInstanceId())) { + fireInstancesRemoved(request.getPath().getObjectInstanceId()); + return BootstrapDeleteResponse.success(); + } else { + return BootstrapDeleteResponse.badRequest(String.format("Instance %s not found", request.getPath())); + } + } + return BootstrapDeleteResponse.badRequest(String.format("unexcepted path %s", request.getPath())); + } + + protected void listenInstance(LwM2mInstanceEnabler instance, final int instanceId) { + instance.addResourceListener(new ResourceListener() { + @Override + public void resourceChanged(LwM2mPath... paths) { + for (LwM2mPath path : paths) { + if (!isValid(instanceId, path)) { + LOG.warn("InstanceEnabler ({}) of object ({}) try to raise a change of {} which seems invalid.", + instanceId, getId(), path); + } + } + fireResourcesChanged(paths); + } + }); + } + + protected boolean isValid(int instanceId, LwM2mPath pathToValidate) { + if (!(pathToValidate.isResource() || pathToValidate.isResourceInstance())) + return false; + + if (pathToValidate.getObjectId() != getId()) { + return false; + } + + if (pathToValidate.getObjectInstanceId() != instanceId) { + return false; + } + + return true; + } + + @Override + public ContentFormat getDefaultEncodingFormat(DownlinkRequest request) { + return defaultContentFormat; + } + + @Override + public void init(LwM2mClient client, LinkFormatHelper linkFormatHelper) { + super.init(client, linkFormatHelper); + this.tbLinkFormatHelper = linkFormatHelper; + for (LwM2mInstanceEnabler instanceEnabler : instances.values()) { + instanceEnabler.setLwM2mClient(client); + } + } + + @Override + public void destroy() { + for (LwM2mInstanceEnabler instanceEnabler : instances.values()) { + if (instanceEnabler instanceof Destroyable) { + ((Destroyable) instanceEnabler).destroy(); + } else if (instanceEnabler instanceof Stoppable) { + ((Stoppable) instanceEnabler).stop(); + } + } + } + + @Override + public void start() { + for (LwM2mInstanceEnabler instanceEnabler : instances.values()) { + if (instanceEnabler instanceof Startable) { + ((Startable) instanceEnabler).start(); + } + } + } + + @Override + public void stop() { + for (LwM2mInstanceEnabler instanceEnabler : instances.values()) { + if (instanceEnabler instanceof Stoppable) { + ((Stoppable) instanceEnabler).stop(); + } + } + } + + @Override + public synchronized WriteAttributesResponse writeAttributes(LwM2mServer server, WriteAttributesRequest request) { + // execute is not supported for bootstrap + if (server.isLwm2mBootstrapServer()) { + return WriteAttributesResponse.methodNotAllowed(); + } + // TODO should be implemented here to be available for all object enabler + // This should be a not implemented error, but this is not defined in the spec. + return WriteAttributesResponse.internalServerError("not implemented"); + } + + + @Override + public synchronized DiscoverResponse discover(LwM2mServer server, DiscoverRequest request) { + + if (server.isLwm2mBootstrapServer()) { + // discover is not supported for bootstrap + return DiscoverResponse.methodNotAllowed(); + } + + if (id == LwM2mId.SECURITY || id == LwM2mId.OSCORE) { + return DiscoverResponse.notFound(); + } + return doDiscover(server, request); + + } + + protected DiscoverResponse doDiscover(LwM2mServer server, DiscoverRequest request) { + + LwM2mPath path = request.getPath(); + if (path.isObject()) { + // Manage discover on object + LwM2mLink[] ObjectLinks = this.tbLinkFormatHelper.getObjectDescription(this, null); + return DiscoverResponse.success(ObjectLinks); + + } else if (path.isObjectInstance()) { + // Manage discover on instance + if (!getAvailableInstanceIds().contains(path.getObjectInstanceId())) + return DiscoverResponse.notFound(); + + LwM2mLink[] instanceLink = this.tbLinkFormatHelper.getInstanceDescription(this, path.getObjectInstanceId(), null); + return DiscoverResponse.success(instanceLink); + + } else if (path.isResource()) { + // Manage discover on resource + if (!getAvailableInstanceIds().contains(path.getObjectInstanceId())) + return DiscoverResponse.notFound(); + + ResourceModel resourceModel = getObjectModel().resources.get(path.getResourceId()); + if (resourceModel == null) + return DiscoverResponse.notFound(); + + if (!getAvailableResourceIds(path.getObjectInstanceId()).contains(path.getResourceId())) + return DiscoverResponse.notFound(); + LwM2mLink resourceLink = this.getResourceAttributes(server, path, resourceModel, this); + if (resourceLink == null) { + resourceLink = this.tbLinkFormatHelper.getResourceDescription(this, path.getObjectInstanceId(), path.getResourceId(), null); + } + return DiscoverResponse.success(new LwM2mLink[] { resourceLink }); + } + return DiscoverResponse.badRequest(null); + } + + protected LwM2mLink getResourceAttributes (LwM2mServer server, LwM2mPath path, ResourceModel resourceModel, LwM2mObjectEnabler objectEnabler) { + LwM2mAttribute attrResource = null; + if (resourceModel.multiple) { + attrResource = getResourceAttributeDim(path, server); + + } + + return attrResource != null ? new LwM2mLink(null, new LwM2mPath(objectEnabler.getId(), path.getObjectInstanceId(), path.getResourceId()), attrResource) : null; + } + + protected LwM2mAttribute getResourceAttributeDim(LwM2mPath path, LwM2mServer server) { + LwM2mInstanceEnabler instance = instances.get(path.getObjectInstanceId()); + try { + ReadResponse readResponse = instance.read(server, path.getResourceId()); + if (readResponse.getCode().getCode()==205 && readResponse.getContent() instanceof LwM2mMultipleResource) { + long valueDim = ((LwM2mMultipleResource)readResponse.getContent()).getInstances().size(); + return LwM2mAttributes.create(LwM2mAttributes.DIMENSION, valueDim); + } else { + return null; + } + } catch (Exception e ){ + return null; + } + + } + +} + diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/TbObjectsInitializer.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/TbObjectsInitializer.java new file mode 100644 index 0000000000..33cfe5e4ef --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/TbObjectsInitializer.java @@ -0,0 +1,56 @@ +package org.thingsboard.server.transport.lwm2m.client; + +import org.eclipse.leshan.client.resource.BaseInstanceEnablerFactory; +import org.eclipse.leshan.client.resource.LwM2mInstanceEnabler; +import org.eclipse.leshan.client.resource.LwM2mObjectEnabler; +import org.eclipse.leshan.client.resource.ObjectsInitializer; +import org.eclipse.leshan.core.model.LwM2mModel; +import org.eclipse.leshan.core.model.ObjectModel; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class TbObjectsInitializer extends ObjectsInitializer { + + + public TbObjectsInitializer(LwM2mModel model) { + super(model); + } + + public List create(int... objectId) { + List enablers = new ArrayList<>(); + for (int anObjectId : objectId) { + LwM2mObjectEnabler objectEnabler = create(anObjectId); + if (objectEnabler != null) + enablers.add(objectEnabler); + } + return enablers; + } + + public LwM2mObjectEnabler create(int objectId) { + ObjectModel objectModel = model.getObjectModel(objectId); + if (objectModel == null) { + throw new IllegalArgumentException( + "Cannot create object for id " + objectId + " because no model is defined for this id."); + } + return createNodeEnabler(objectModel); + } + + protected LwM2mObjectEnabler createNodeEnabler(ObjectModel objectModel) { + Map instances = new HashMap<>(); + LwM2mInstanceEnabler[] newInstances = createInstances(objectModel); + for (LwM2mInstanceEnabler instance : newInstances) { + // set id if not already set + if (instance.getId() == null) { + int id = BaseInstanceEnablerFactory.generateNewInstanceId(instances.keySet()); + instance.setId(id); + } + instance.setModel(objectModel); + instances.put(instance.getId(), instance); + } + return new TbLwm2mObjectEnabler(objectModel.id, objectModel, instances, getFactoryFor(objectModel), + getContentFormat(objectModel.id)); + } +} diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationDiscoverTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationDiscoverTest.java index ea0a1ad4ca..6da48b0a79 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationDiscoverTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationDiscoverTest.java @@ -99,8 +99,8 @@ public class RpcLwm2mIntegrationDiscoverTest extends AbstractRpcLwM2MIntegration * {"result":"CONTENT","value":",,,,,,,,,,,,,,,,,,,,,,,"} * If WriteAttributes implemented and WriteAttributes saved - * Discover {"id":"19/0"} - * {"result":"CONTENT","value":"[;dim=2;pmin=10;pmax=60;gt=50;lt=42.2,;pmax=120, , , , , ;lt=45]"} + * Discover {"id":"19"} + * {"result":"CONTENT","value":"[;ver=1.1,;dim=2;pmin=10;pmax=60;gt=50;lt=42.2,;pmax=120, , , , , ;lt=45]"} */ @Test public void testDiscoverInstance_Return_CONTENT_LinksResourcesOnLyExpectedInstance() throws Exception { @@ -117,12 +117,12 @@ public class RpcLwm2mIntegrationDiscoverTest extends AbstractRpcLwM2MIntegration /** * Discover {"id":"3/0/14"} * If WriteAttributes implemented: - * {"result":"CONTENT","value":";pmax=100, "pmin":10, "ver"=1.0"} + * {"result":"CONTENT","value":";pmax=100, "pmin":10} * If WriteAttributes not implemented: * {"result":"CONTENT","value":""} * Discover {"id":"19_1.1/0/0"} * If WriteAttributes implemented: - * {"result":"CONTENT","value":";pmax=100, "pmin":10, "ver"=1.1"} + * {"result":"CONTENT","value":";pmax=100, "pmin":10} * If WriteAttributes not implemented: * {"result":"CONTENT","value":""} */ diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationWriteAttributesTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationWriteAttributesTest.java index c2d87149de..1e5c9b593a 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationWriteAttributesTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationWriteAttributesTest.java @@ -24,7 +24,8 @@ import org.thingsboard.server.transport.lwm2m.rpc.AbstractRpcLwM2MIntegrationTes import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_14; +import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_15; +import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_6; public class RpcLwm2mIntegrationWriteAttributesTest extends AbstractRpcLwM2MIntegrationTest { @@ -38,7 +39,7 @@ public class RpcLwm2mIntegrationWriteAttributesTest extends AbstractRpcLwM2MInte */ @Test public void testWriteAttributesResourceWithParametersById_Result_INTERNAL_SERVER_ERROR() throws Exception { - String expectedPath = objectInstanceIdVer_3 + "/" + RESOURCE_ID_14; + String expectedPath = objectInstanceIdVer_3 + "/" + RESOURCE_ID_15; sendRPCReadById(expectedPath); String expectedValue = "{\"pmax\":100, \"pmin\":10}"; String actualResult = sendRPCExecuteWithValueById(expectedPath, expectedValue); @@ -74,6 +75,46 @@ public class RpcLwm2mIntegrationWriteAttributesTest extends AbstractRpcLwM2MInte assertTrue(actual.equals(expected)); } + /** + * Class Attributes + * Dimension dim Integer [0:255] + * Number of instances existing for a Multiple-Instance Resource + * 3 + * + * Available Power Sources + * R + * Multiple + * Integer + * 0..7 + * WriteAttributes implemented: Discover {"id":"3/0/6"} -> 'dim' = 3 + * "ver" only for objectId + */ + @Test + public void testDIM_3_0_6_Only_R () throws Exception { + String path = objectInstanceIdVer_3 + "/" + RESOURCE_ID_6; + String actualResult = sendDiscover(path); + ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); + assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText()); + String expected = ";dim=3"; + assertTrue(rpcActualResult.get("value").asText().equals(expected)); + + } + /** + * Class Attributes + * Minimum/Maximum Period + * Notes: The Minimum Period Attribute: + * -- indicates the minimum time in seconds the LwM2M Client MUST wait between two notifications. If a notification of an observed Resource is supposed to be generated but it is before pmin expiry, notification MUST be sent as soon as pmin expires. In the absence of this parameter, the Minimum Period is defined by the Default Minimum Period set in the LwM2M Server Account. + * Notes: The Maximum Period Attribute: + * -- indicates the maximum time in seconds the LwM2M Client MAY wait between two notifications. When this "Maximum Period" expires after the last notification, a new notification MUST be sent. In the absence of this parameter, the "Maximum Period" is defined by the Default Maximum Period when set in the LwM2M Server Account or considered as 0 otherwise. The value of 0, means pmax MUST be ignored. The maximum period parameter MUST be greater than the minimum period parameter otherwise pmax will be ignored for the Resource to which such inconsistent timing conditions are applied. + * Object Id = 1 + * Default Minimum Period Id = 2 300 + * Default Maximum Period Id = 3 6000 + */ + @Test + public void testPeriod () { + + } + private String sendRPCExecuteWithValueById(String path, String value) throws Exception { String setRpcRequest = "{\"method\": \"WriteAttributes\", \"params\": {\"id\": \"" + path + "\", \"attributes\": " + value + " }}"; return doPostAsync("/api/plugins/rpc/twoway/" + deviceId, setRpcRequest, String.class, status().isOk()); @@ -84,4 +125,9 @@ public class RpcLwm2mIntegrationWriteAttributesTest extends AbstractRpcLwM2MInte return doPostAsync("/api/plugins/rpc/twoway/" + deviceId, setRpcRequest, String.class, status().isOk()); } + private String sendDiscover(String path) throws Exception { + String setRpcRequest = "{\"method\": \"Discover\", \"params\": {\"id\": \"" + path + "\"}}"; + return doPostAsync("/api/plugins/rpc/twoway/" + deviceId, setRpcRequest, String.class, status().isOk()); + } + } diff --git a/application/src/test/resources/lwm2m/3.xml b/application/src/test/resources/lwm2m/3.xml index 724fc4cb33..e71c2045c2 100644 --- a/application/src/test/resources/lwm2m/3.xml +++ b/application/src/test/resources/lwm2m/3.xml @@ -64,7 +64,7 @@ LEGAL DISCLAIMER 3 urn:oma:lwm2m:oma:3:1.0 1.1 - 1.0 + 1.2 Single Mandatory