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 a307d53054..5d8f4f1c8b 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 @@ -17,11 +17,12 @@ package org.thingsboard.server.transport.lwm2m; public class Lwm2mTestHelper { - public static final String[] lwm2mClientResources = new String[]{"3-1_2.xml", "5.xml", "6.xml", "9.xml", "19.xml", "3303.xml"}; + public static final String[] lwm2mClientResources = new String[]{"3-1_2.xml", "5.xml", "6.xml", "9.xml", "19.xml", "3303.xml", "3442.xml"}; // Models public static final int BINARY_APP_DATA_CONTAINER = 19; public static final int TEMPERATURE_SENSOR = 3303; + public static final int OBJECT_ID_LWM2M_TEST_OBJECT = 3442; // Ids in Client public static final int OBJECT_INSTANCE_ID_0 = 0; @@ -41,6 +42,8 @@ public class Lwm2mTestHelper { public static final int RESOURCE_ID_11 = 11; public static final int RESOURCE_ID_14 = 14; public static final int RESOURCE_ID_15 = 15; + public static final int RESOURCE_ID_120 = 120; // INTEGER_VALUE + public static final int RESOURCE_ID_125 = 125; // UNSIGNED_INTEGER_VALUE public static final int RESOURCE_ID_5700 = 5700; public static final int RESOURCE_INSTANCE_ID_0 = 0; public static final int RESOURCE_INSTANCE_ID_2 = 2; 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 c9c39bc701..7094f23ac0 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 @@ -118,6 +118,7 @@ import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.OBJECT_INST import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.OBJECT_INSTANCE_ID_1; import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.OBJECT_INSTANCE_ID_12; import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.TEMPERATURE_SENSOR; +import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.OBJECT_ID_LWM2M_TEST_OBJECT; import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.lwm2mClientResources; import static org.thingsboard.server.transport.lwm2m.utils.LwM2MTransportUtil.setDtlsConnectorConfigCidLength; import static org.thingsboard.server.utils.PortFinder.isUDPPortAvailable; @@ -192,7 +193,7 @@ public class LwM2MTestClient { LwM2mTemperatureSensor lwM2mTemperatureSensor0 = new LwM2mTemperatureSensor(executor, OBJECT_INSTANCE_ID_0); lwM2MTemperatureSensor12 = new LwM2mTemperatureSensor(executor, OBJECT_INSTANCE_ID_12); initializer.setInstancesForObject(TEMPERATURE_SENSOR, lwM2mTemperatureSensor0, lwM2MTemperatureSensor12); - + initializer.setInstancesForObject(OBJECT_ID_LWM2M_TEST_OBJECT, new LwM2mTestObject()); List enablers = initializer.createAll(); // Create Californium Endpoints Provider: diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/LwM2mTestObject.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/LwM2mTestObject.java new file mode 100644 index 0000000000..bb4afe8f5c --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/LwM2mTestObject.java @@ -0,0 +1,404 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.transport.lwm2m.client; + +import org.eclipse.leshan.client.resource.SimpleInstanceEnabler; +import org.eclipse.leshan.client.servers.LwM2mServer; +import org.eclipse.leshan.core.link.Link; +import org.eclipse.leshan.core.link.attributes.Attribute; +import org.eclipse.leshan.core.link.attributes.QuotedStringAttribute; +import org.eclipse.leshan.core.link.attributes.ResourceTypeAttribute; +import org.eclipse.leshan.core.link.attributes.UnquotedStringAttribute; +import org.eclipse.leshan.core.link.lwm2m.LwM2mLink; +import org.eclipse.leshan.core.link.lwm2m.MixedLwM2mLink; +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.node.ObjectLink; +import org.eclipse.leshan.core.request.ContentFormat; +import org.eclipse.leshan.core.request.argument.Argument; +import org.eclipse.leshan.core.request.argument.Arguments; +import org.eclipse.leshan.core.response.ErrorCallback; +import org.eclipse.leshan.core.response.ExecuteResponse; +import org.eclipse.leshan.core.response.ResponseCallback; +import org.eclipse.leshan.core.response.SendResponse; +import org.eclipse.leshan.core.util.Hex; +import org.eclipse.leshan.core.util.RandomStringUtils; +import org.eclipse.leshan.core.util.datatype.ULong; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Date; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Random; + +/** + * This aims to implements LWM2M Test Object(Id:3442) from LWM2M registry. + */ +public class LwM2mTestObject extends SimpleInstanceEnabler { + private static final Logger LOG = LoggerFactory.getLogger(LwM2mTestObject.class); + + public static final String INITIAL_STRING_VALUE = "initial value"; + public static final Long INITIAL_INTEGER_VALUE = 1024l; + public static final ULong INITIAL_UNSIGNED_INTEGER_VALUE = ULong.valueOf("9223372036854775808"); + public static final Double INITIAL_FLOAT_VALUE = 3.14159d; + public static final Boolean INITIAL_BOOLEAN_VALUE = true; + public static final byte[] INITIAL_OPAQUE_VALUE = Hex.decodeHex("0123456789ABCDEF".toCharArray()); + public static final Date INITIAL_TIME_VALUE = new Date(946684800000l); + public static final ObjectLink INITIAL_OBJLINK_VALUE = new ObjectLink(3, 0); + public static final Link[] INITIAL_CORELINK_VALUE = new Link[] { new LwM2mLink(null, new LwM2mPath(3442)) }; + + private final Random random = new Random(System.currentTimeMillis()); + + public LwM2mTestObject() { + super(); + initialValues = new HashMap<>(); + + initialValues.put(4, Collections.EMPTY_MAP); + initialValues.put(6, Collections.EMPTY_MAP); + + // single + initialValues.put(110, INITIAL_STRING_VALUE); + initialValues.put(120, INITIAL_INTEGER_VALUE); + initialValues.put(125, INITIAL_UNSIGNED_INTEGER_VALUE); + initialValues.put(130, INITIAL_FLOAT_VALUE); + initialValues.put(140, INITIAL_BOOLEAN_VALUE); + initialValues.put(150, INITIAL_OPAQUE_VALUE); + initialValues.put(160, INITIAL_TIME_VALUE); + initialValues.put(170, INITIAL_OBJLINK_VALUE); + initialValues.put(180, INITIAL_CORELINK_VALUE); + + // multi + initialValues.put(1110, LwM2mResourceInstance.newStringInstance(0, INITIAL_STRING_VALUE)); + initialValues.put(1120, LwM2mResourceInstance.newIntegerInstance(0, INITIAL_INTEGER_VALUE)); + initialValues.put(1125, LwM2mResourceInstance.newUnsignedIntegerInstance(0, INITIAL_UNSIGNED_INTEGER_VALUE)); + initialValues.put(1130, LwM2mResourceInstance.newFloatInstance(0, INITIAL_FLOAT_VALUE)); + initialValues.put(1140, LwM2mResourceInstance.newBooleanInstance(0, INITIAL_BOOLEAN_VALUE)); + initialValues.put(1150, LwM2mResourceInstance.newBinaryInstance(0, INITIAL_OPAQUE_VALUE)); + initialValues.put(1160, LwM2mResourceInstance.newDateInstance(0, INITIAL_TIME_VALUE)); + initialValues.put(1170, LwM2mResourceInstance.newObjectLinkInstance(0, INITIAL_OBJLINK_VALUE)); + initialValues.put(1180, LwM2mResourceInstance.newCoreLinkInstance(0, INITIAL_CORELINK_VALUE)); + } + + private void clearValues() { + Map clearedValues = new HashMap<>(); + + clearedValues.put(4, Collections.EMPTY_MAP); + clearedValues.put(6, Collections.EMPTY_MAP); + + // single + clearedValues.put(110, ""); + clearedValues.put(120, 0l); + clearedValues.put(125, ULong.valueOf(0l)); + clearedValues.put(130, 0.0d); + clearedValues.put(140, false); + clearedValues.put(150, new byte[0]); + clearedValues.put(160, new Date(0l)); + clearedValues.put(170, new ObjectLink()); + clearedValues.put(180, new Link[0]); + + // multi + clearedValues.put(1110, Collections.EMPTY_MAP); + clearedValues.put(1120, Collections.EMPTY_MAP); + clearedValues.put(1125, Collections.EMPTY_MAP); + clearedValues.put(1130, Collections.EMPTY_MAP); + clearedValues.put(1140, Collections.EMPTY_MAP); + clearedValues.put(1150, Collections.EMPTY_MAP); + clearedValues.put(1160, Collections.EMPTY_MAP); + clearedValues.put(1170, Collections.EMPTY_MAP); + clearedValues.put(1180, Collections.EMPTY_MAP); + + fireResourcesChange(applyValues(clearedValues)); + } + + private void resetValues() { + fireResourcesChange(applyValues(initialValues)); + } + + private void randomValues() { + + Map randomValues = new HashMap<>(); + randomValues.put(4, generateResourceInstances(new StringGenerator())); + + // single + randomValues.put(110, new StringGenerator().generate()); + randomValues.put(120, new LongGenerator().generate()); + randomValues.put(125, new ULongGenerator().generate()); + randomValues.put(130, new DoubleGenerator().generate()); + randomValues.put(140, new BooleanGenerator().generate()); + randomValues.put(150, new BytesGenerator().generate()); + randomValues.put(160, new DateGenerator().generate()); + randomValues.put(170, new ObjectLinkGenerator().generate()); + randomValues.put(180, new CoreLinkGenerator().generate()); + + // multi + randomValues.put(1110, generateResourceInstances(new StringGenerator())); + randomValues.put(1120, generateResourceInstances(new LongGenerator())); + randomValues.put(1125, generateResourceInstances(new ULongGenerator())); + randomValues.put(1130, generateResourceInstances(new DoubleGenerator())); + randomValues.put(1140, generateResourceInstances(new BooleanGenerator())); + randomValues.put(1150, generateResourceInstances(new BytesGenerator())); + randomValues.put(1160, generateResourceInstances(new DateGenerator())); + randomValues.put(1170, generateResourceInstances(new ObjectLinkGenerator())); + randomValues.put(1180, generateResourceInstances(new CoreLinkGenerator())); + + fireResourcesChange(applyValues(randomValues)); + } + + private void storeArguments(Arguments arguments) { + // convert Arguments to Map + Map argValues = new LinkedHashMap(); + for (Argument argument : arguments) { + String value = argument.getValue() == null ? "" : argument.getValue(); + argValues.put(argument.getDigit(), value); + } + // put value in resource (4) + Map newParams = new HashMap<>(); + newParams.put(4, argValues); + fireResourcesChange(applyValues(newParams)); + } + + private ExecuteResponse sendData(LwM2mServer server, Arguments arguments) { + // get path of resources to send + List paths = new ArrayList<>(); + LwM2mResource lwM2mResource = resources.get(6); + if (lwM2mResource != null && lwM2mResource.isMultiInstances()) { + for (LwM2mResourceInstance instance : lwM2mResource.getInstances().values()) { + try { + paths.add(new LwM2mPath(instance.getValue().toString()).toString()); + } catch (IllegalArgumentException e) { + LOG.warn("Instance {} of resource 6 contains invalid path", instance.getId()); + } + } + } + if (paths.isEmpty()) { + LOG.info("Unable to Send Data to {} as resources 6 {} doesn't contain any valid LWM2M path", server, + lwM2mResource); + return ExecuteResponse.internalServerError("resources 6 doesn't contain any valid LWM2M path"); + } + + ContentFormat format = ContentFormat.SENML_CBOR; + Argument contenFormatArg = arguments.get(0); + try { + if (contenFormatArg != null) + format = ContentFormat.fromCode(contenFormatArg.getValue()); + } catch (NumberFormatException e) { + return ExecuteResponse.badRequest( + String.format("argument 1 %s is not a valid ContentFormat.", contenFormatArg.getValue())); + } + long timeout = 3000;// ms + Argument timeoutArg = arguments.get(1); + try { + if (timeoutArg != null) + timeout = Long.parseLong(timeoutArg.getValue()); + } catch (NumberFormatException e) { + return ExecuteResponse + .badRequest(String.format("argument 0 %s is not a valid timeout.", timeoutArg.getValue())); + } + + // send data; + LOG.info("Try to send data {} using {} to {} (timeout {}ms)...", paths, format, server, timeout); + getLwM2mClient().getSendService().sendData(server, format, paths, timeout, + new ResponseCallback() { + @Override + public void onResponse(SendResponse response) { + if (response.isSuccess()) { + LOG.info("... Send Succeed {}", response.getCode()); + } else { + LOG.warn("... Send Response : {} {}", response.getCode(), response.getErrorMessage()); + } + + } + }, new ErrorCallback() { + @Override + public void onError(Exception e) { + LOG.error("... Send failed.", e); + } + }); + return ExecuteResponse.success(); + } + + private Map generateResourceInstances(ValueGenerator generator) { + HashMap instances = new HashMap<>(); + for (int i = 0; i < random.nextInt(10); i++) { + instances.put(i, generator.generate()); + } + return instances; + } + + @Override + public ExecuteResponse execute(LwM2mServer server, int resourceid, Arguments arguments) { + switch (resourceid) { + case 0: + resetValues(); + return ExecuteResponse.success(); + case 1: + randomValues(); + return ExecuteResponse.success(); + case 2: + clearValues(); + return ExecuteResponse.success(); + case 3: + storeArguments(arguments); + return ExecuteResponse.success(); + case 5: + return sendData(server, arguments); + default: + return super.execute(server, resourceid, arguments); + } + } + + /* ***************** Random Generator ****************** */ + + static interface ValueGenerator { + T generate(); + } + + class StringGenerator implements ValueGenerator { + @Override + public String generate() { + return RandomStringUtils.randomAlphanumeric(random.nextInt(20)); + } + } + + class LongGenerator implements ValueGenerator { + @Override + public Long generate() { + return random.nextLong(); + } + } + + class ULongGenerator implements ValueGenerator { + @Override + public ULong generate() { + return ULong.valueOf(random.nextLong()); + } + } + + class DoubleGenerator implements ValueGenerator { + @Override + public Double generate() { + return random.nextDouble(); + } + } + + class BooleanGenerator implements ValueGenerator { + @Override + public Boolean generate() { + return random.nextInt(2) == 1; + } + } + + class BytesGenerator implements ValueGenerator { + @Override + public byte[] generate() { + byte[] bytes = new byte[random.nextInt(20)]; + random.nextBytes(bytes); + return bytes; + } + } + + class DateGenerator implements ValueGenerator { + @Override + public Date generate() { + // try to generate random date which is not so out of date. + long rd = System.currentTimeMillis(); + + // remove year randomly + rd -= (random.nextInt(20) - 10) * 31557600000l; + + // add some variance in the year + rd += random.nextInt(3155760) * 1000l; + + return new Date(rd); + } + } + + class ObjectLinkGenerator implements ValueGenerator { + @Override + public ObjectLink generate() { + return new ObjectLink(random.nextInt(ObjectLink.MAXID - 1), random.nextInt(ObjectLink.MAXID - 1)); + } + } + + class CoreLinkGenerator implements ValueGenerator { + + private LwM2mPath generatePath() { + int dice4Value = random.nextInt(4); + switch (dice4Value) { + case 0: + return new LwM2mPath(random.nextInt(ObjectLink.MAXID - 1)); + case 1: + return new LwM2mPath(random.nextInt(ObjectLink.MAXID - 1), random.nextInt(ObjectLink.MAXID - 1)); + case 2: + return new LwM2mPath(random.nextInt(ObjectLink.MAXID - 1), random.nextInt(ObjectLink.MAXID - 1), + random.nextInt(ObjectLink.MAXID - 1)); + case 3: + return new LwM2mPath(random.nextInt(ObjectLink.MAXID - 1), random.nextInt(ObjectLink.MAXID - 1), + random.nextInt(ObjectLink.MAXID - 1), random.nextInt(ObjectLink.MAXID - 1)); + } + return null; // should not happened + } + + private Attribute[] generateAttributes() { + int nbAttributes = random.nextInt(3); + Map attributes = new HashMap<>(nbAttributes); + for (int i = 0; i < nbAttributes; i++) { + int dice2value = random.nextInt(2); + Attribute attr = null; + switch (dice2value) { + case 0: + attr = new QuotedStringAttribute(RandomStringUtils.randomAlphabetic(random.nextInt(5) + 1), + RandomStringUtils.randomAlphanumeric(random.nextInt(5) + 1)); + break; + case 1: + attr = new UnquotedStringAttribute(RandomStringUtils.randomAlphabetic(random.nextInt(5) + 1), + RandomStringUtils.randomAlphanumeric(random.nextInt(5) + 1)); + break; + } + attributes.put(attr.getName(), attr); + } + return attributes.values().toArray(new Attribute[attributes.size()]); + } + + @Override + public Link[] generate() { + // define if root path is used or not + String rootpath = random.nextInt(4) == 0 ? "/" + RandomStringUtils.randomAlphabetic(1) + + RandomStringUtils.randomAlphanumeric(random.nextInt(4)) : null; + + // define number of link + int nbLink = random.nextInt(10); + // create links + Link[] links = new Link[nbLink]; + for (int i = 0; i < links.length; i++) { + // when there is a rootpath first link has oma attribute + if (rootpath != null && i == 0) { + links[i] = new MixedLwM2mLink(rootpath, LwM2mPath.ROOTPATH, new ResourceTypeAttribute("oma.lwm2m")); + } else { + // generate random link with random path and attributes + links[i] = new MixedLwM2mLink(rootpath, generatePath(), generateAttributes()); + } + } + return links; + } + } +} diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/AbstractRpcLwM2MIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/AbstractRpcLwM2MIntegrationTest.java index af0fdf4c09..45bb388c63 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/AbstractRpcLwM2MIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/AbstractRpcLwM2MIntegrationTest.java @@ -53,6 +53,7 @@ import static org.eclipse.leshan.core.LwM2mId.SERVER; import static org.eclipse.leshan.core.LwM2mId.SOFTWARE_MANAGEMENT; import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.BINARY_APP_DATA_CONTAINER; import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.LwM2MProfileBootstrapConfigType.NONE; +import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.OBJECT_ID_LWM2M_TEST_OBJECT; import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.OBJECT_INSTANCE_ID_0; import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.OBJECT_INSTANCE_ID_1; import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.OBJECT_INSTANCE_ID_12; @@ -94,6 +95,8 @@ public abstract class AbstractRpcLwM2MIntegrationTest extends AbstractLwM2MInteg protected String objectIdVer_19; protected final String OBJECT_ID_VER_50 = "/50"; protected String objectIdVer_3303; + protected String objectIdVer_3442; + protected String objectInstanceIdVer_3442; protected static AtomicInteger endpointSequence = new AtomicInteger(); protected static String DEVICE_ENDPOINT_RPC_PREF = "deviceEndpointRpc"; @@ -152,10 +155,12 @@ public abstract class AbstractRpcLwM2MIntegrationTest extends AbstractLwM2MInteg objectIdVer_3 = (String) expectedObjectIdVers.stream().filter(PREDICATE_3).findFirst().get(); objectIdVer_19 = (String) expectedObjectIdVers.stream().filter(path -> ((String) path).startsWith("/" + BINARY_APP_DATA_CONTAINER)).findFirst().get(); objectIdVer_3303 = (String) expectedObjectIdVers.stream().filter(path -> ((String) path).startsWith("/" + TEMPERATURE_SENSOR)).findFirst().get(); + objectIdVer_3442 = (String) expectedObjectIdVers.stream().filter(path -> ((String) path).startsWith("/" + OBJECT_ID_LWM2M_TEST_OBJECT)).findFirst().get(); objectInstanceIdVer_1 = (String) expectedObjectIdVerInstances.stream().filter(path -> (!((String) path).startsWith("/" + BINARY_APP_DATA_CONTAINER) && ((String) path).startsWith("/" + SERVER))).findFirst().get(); objectInstanceIdVer_3 = (String) expectedObjectIdVerInstances.stream().filter(PREDICATE_3).findFirst().get(); objectInstanceIdVer_5 = (String) expectedObjectIdVerInstances.stream().filter(path -> ((String) path).startsWith("/" + FIRMWARE)).findFirst().get(); objectInstanceIdVer_9 = (String) expectedObjectIdVerInstances.stream().filter(path -> ((String) path).startsWith("/" + SOFTWARE_MANAGEMENT)).findFirst().get(); + objectInstanceIdVer_3442 = objectIdVer_3442 + "/" + OBJECT_INSTANCE_ID_0; idVer_3_0_0 = objectIdVer_3 + "/" + OBJECT_INSTANCE_ID_0 + "/" + RESOURCE_ID_0; idVer_3_0_9 = objectIdVer_3 + "/" + OBJECT_INSTANCE_ID_0 + "/" + RESOURCE_ID_9; diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationWriteTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationWriteTest.java index 34a4a68a81..c409d4269d 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationWriteTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationWriteTest.java @@ -20,12 +20,15 @@ import com.google.common.hash.Hashing; import org.eclipse.leshan.client.resource.LwM2mObjectEnabler; import org.eclipse.leshan.core.ResponseCode; import org.eclipse.leshan.core.node.LwM2mPath; +import org.eclipse.leshan.core.util.datatype.ULong; import org.junit.Test; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.script.api.tbel.TbUtils; import org.thingsboard.server.transport.lwm2m.rpc.AbstractRpcLwM2MIntegrationTest; import java.util.Base64; +import static org.eclipse.leshan.core.model.ResourceModel.Type.UNSIGNED_INTEGER; +import static org.eclipse.leshan.core.model.ResourceModel.Type.INTEGER; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; @@ -55,6 +58,65 @@ public class RpcLwm2mIntegrationWriteTest extends AbstractRpcLwM2MIntegrationTes assertTrue(actualValues.contains(expected)); } + /** + * update SingleResource: + * WriteReplace {"id":"3442/0/120","value":-1} + * WriteReplace {"id":"3442/0/120","value":-100} + * WriteReplace {"id":"3442/0/120","value":-2147483648} -> Integer.MIN_VALUE + * WriteReplace {"id":"3442/0/120","value":2147483647} -> Integer.MAX_VALUE + * {"result":"CHANGED"} + */ + @Test + public void testWriteReplaceNegativeIntegerValuesSingleResourceById_Result_CHANGED() throws Exception { + Integer expectedValue = -1; + sendRPC_3442_ResourceId_120(expectedValue); + + expectedValue = -100; + sendRPC_3442_ResourceId_120(expectedValue); + + expectedValue = Integer.MIN_VALUE; + sendRPC_3442_ResourceId_120(expectedValue); + + expectedValue = Integer.MAX_VALUE; + sendRPC_3442_ResourceId_120(expectedValue); + } + + /** + * update SingleResource: + * WriteReplace {"id":"3442/0/120","value":"18446744073709551615} // ULong.MAX + * {"result":"CHANGED"} + */ + @Test + public void testWriteReplaceUnIntegerValuesSingleResourceById_Result_CHANGED() throws Exception { + String expectedPath = objectInstanceIdVer_3442 + "/" + RESOURCE_ID_125; + ULong expectedValue = ULong.MAX; + String actualResult = sendRPCWriteObjectById("WriteReplace", expectedPath, expectedValue); + ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); + assertEquals(ResponseCode.CHANGED.getName(), rpcActualResult.get("result").asText()); + actualResult = sendRPCReadById(expectedPath); + rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); + String actualValues = rpcActualResult.get("value").asText(); + String expected = "LwM2mSingleResource [id=" + RESOURCE_ID_125 + ", value=" + expectedValue + ", type=" + UNSIGNED_INTEGER.name() + "]"; + assertTrue(actualValues.contains(expected)); + } + + /** + * update SingleResource: + * WriteReplace {"id":"3442/0/120","value":"-1"} + * {"result":"BAD_REQUEST"} + */ + @Test + public void testWriteReplaceUnIntegerValuesSingleResourceById_Result__BAD_REQUEST() throws Exception { + String expectedPath = objectInstanceIdVer_3442 + "/" + RESOURCE_ID_125; + Integer expectedValue = -1; + String actualResult = sendRPCWriteObjectById("WriteReplace", expectedPath, expectedValue); + ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); + assertEquals(ResponseCode.BAD_REQUEST.getName(), rpcActualResult.get("result").asText()); + String expected = "Format value is bad. Value for this Single Resource must be UNSIGNED_INTEGER!"; + String actual = rpcActualResult.get("error").asText(); + assertTrue(actual.contains(expected)); + } + /** * key * WriteReplace {"key":"timezone","value":"+10"} @@ -572,4 +634,16 @@ public class RpcLwm2mIntegrationWriteTest extends AbstractRpcLwM2MIntegrationTes String setRpcRequest = "{\"method\": \"Create\", \"params\": {\"id\": \"" + path + "\", \"value\": " + value + " }}"; return doPostAsync("/api/plugins/rpc/twoway/" + lwM2MTestClient.getDeviceIdStr(), setRpcRequest, String.class, status().isOk()); } + + private void sendRPC_3442_ResourceId_120 (Integer expectedValue) throws Exception { + String expectedPath = objectInstanceIdVer_3442 + "/" + RESOURCE_ID_120; + String actualResult = sendRPCWriteObjectById("WriteReplace", expectedPath, expectedValue); + ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); + assertEquals(ResponseCode.CHANGED.getName(), rpcActualResult.get("result").asText()); + actualResult = sendRPCReadById(expectedPath); + rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); + String actualValues = rpcActualResult.get("value").asText(); + String expected = "LwM2mSingleResource [id=" + RESOURCE_ID_120 + ", value=" + expectedValue + ", type=" + INTEGER.name() + "]"; + assertTrue(actualValues.contains(expected)); + } } diff --git a/application/src/test/resources/lwm2m/3442.xml b/application/src/test/resources/lwm2m/3442.xml new file mode 100644 index 0000000000..b4607cead7 --- /dev/null +++ b/application/src/test/resources/lwm2m/3442.xml @@ -0,0 +1,332 @@ + + + + + LwM2M v1.1 Test Object + + 3442 + urn:oma:lwm2m:ext:3442 + 1.1 + 1.0 + Multiple + Optional + + + Reset values + E + Single + Optional + + + + Reset all resources of this object with their initial value. + + + Randomize values + E + Single + Optional + + + + + + + + + Clear values + E + Single + Optional + + + + + + + + + Exec With Arguments + E + Single + Optional + + + + + + + + + Arguments List + R + Multiple + Optional + String + + + List of Arguments from last execute on "Exec With Arguments"(3) resource. This resource is not affected by "Randomize values"(1) executable resource. + + + + Send Data + E + Single + Optional + + + + + + + + + Resources to Send + RW + Multiple + Optional + String + + + List of path to LWM2M node (Object / Object Instance / Resource / Resource Instance) used by "Send Data "(5) resource. This resource is not affected by "Randomize values"(1) executable resource. + + + + String Value + RW + Single + Optional + String + + + Initial value must be "initial value". + + + + Integer Value + RW + Single + Optional + Integer + + + Initial value must be "1024". + + + + Unsigned Integer Value + RW + Single + Optional + Unsigned Integer + + + Initial value must be 2^63 (9223372036854775808). + + + + Float Value + RW + Single + Optional + Float + + + Initial value must be "3.14159". + + + + Boolean Value + RW + Single + Optional + Boolean + + + Initial value must be "true". + + + Opaque Value + RW + Single + Optional + Opaque + + + Initial value must be the bytes sequence "0123456789ABCDEF" (Hexadecimal notation). + + + + Time Value + RW + Single + Optional + Time + + + Initial value must be the time to an 1st, 2000 in the UTC time zone. (Timestamp value : 946684800) + + + ObjLink Value + RW + Single + Optional + Objlnk + + + Initial value must be a link to instance 0 of Device Object 3 (3:0). + + + CoreLnk Value + RW + Single + Optional + Corelnk + + + ".]]> + + + Multiple String Value + RW + Multiple + Optional + String + + + Initial value must be 1 instance with ID 0 and value "initial value". + + + Multiple Integer Value + RW + Multiple + Optional + Integer + + + Initial value must be 1 instance with ID 0 and value "1024". + + + Multiple Unsigned Integer Value + RW + Multiple + Optional + Unsigned Integer + + + Initial value must be 1 instance with ID 0 and value 2^63 (9223372036854775808). + + + + Multiple Float Value + RW + Multiple + Optional + Float + + + Initial value must be 1 instance with ID 0 and value "3.14159". + + + Multiple Boolean Value + RW + Multiple + Optional + Boolean + + + Initial value must be 1 instance with ID 0 and value "true". + + + Multiple Opaque Value + RW + Multiple + Optional + Opaque + + + Initial value must be 1 instance with ID 0 and value "0123456789ABCDEF"(Hexadecimal notation of the bytes sequence). + + + Multiple Time Value + RW + Multiple + Optional + Time + + + Initial value must be 1 instance with ID 0 and value 1st, 2000 in the UTC time zone (Timestamp value : 946684800). + + + Multiple ObjLink Value + RW + Multiple + Optional + Objlnk + + + Initial value must be 1 instance with ID 0 and value "3:0". + + + Multiple CoreLnk Value + RW + Multiple + Optional + Corelnk + + + ".]]> + + + + + diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/downlink/DefaultLwM2mDownlinkMsgHandler.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/downlink/DefaultLwM2mDownlinkMsgHandler.java index ef0d57bb5e..a7ea1212ab 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/downlink/DefaultLwM2mDownlinkMsgHandler.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/downlink/DefaultLwM2mDownlinkMsgHandler.java @@ -68,6 +68,7 @@ import org.eclipse.leshan.core.response.WriteAttributesResponse; import org.eclipse.leshan.core.response.WriteCompositeResponse; import org.eclipse.leshan.core.response.WriteResponse; import org.eclipse.leshan.core.util.Hex; +import org.eclipse.leshan.core.util.datatype.ULong; import org.eclipse.leshan.server.model.LwM2mModelProvider; import org.eclipse.leshan.server.registration.Registration; import org.springframework.stereotype.Service; @@ -87,6 +88,7 @@ import org.thingsboard.server.transport.lwm2m.server.log.LwM2MTelemetryLogServic import org.thingsboard.server.transport.lwm2m.server.rpc.composite.RpcWriteCompositeRequest; import org.thingsboard.server.transport.lwm2m.utils.LwM2mValueConverterImpl; +import java.math.BigInteger; import java.util.Arrays; import java.util.Collection; import java.util.Date; @@ -653,9 +655,17 @@ public class DefaultLwM2mDownlinkMsgHandler extends LwM2MExecutorAwareService im switch (type) { case STRING: // String return new WriteRequest(contentFormat, objectId, instanceId, resourceId, value.toString()); - case INTEGER: // Long - final long valueInt = Integer.toUnsignedLong(Integer.parseInt(value.toString())); + case INTEGER: // Integer: + final int valueInt = Integer.parseInt(value.toString()); return new WriteRequest(contentFormat, objectId, instanceId, resourceId, valueInt); + case UNSIGNED_INTEGER: // ULong + BigInteger validateUnsignedInt = new BigInteger(value.toString()); + if (validateUnsignedInt.signum() < 0 || + validateUnsignedInt.bitLength() > 64) { + throw new IllegalArgumentException("Invalid UNSIGNED_INTEGER"); + } + ULong valueUnsignedInt = ULong.valueOf(validateUnsignedInt); + return new WriteRequest(contentFormat, objectId, instanceId, resourceId, valueUnsignedInt); case OBJLNK: // ObjectLink return new WriteRequest(contentFormat, objectId, instanceId, resourceId, ObjectLink.fromPath(value.toString())); case BOOLEAN: // Boolean