From 69afda634b25f10cb092104138859eb158f5c0e1 Mon Sep 17 00:00:00 2001 From: nickAS21 Date: Fri, 23 Jan 2026 15:22:33 +0200 Subject: [PATCH 1/3] lwm2m - execute with params --- .../lwm2m/client/SimpleLwM2MDevice.java | 9 +++- .../sql/RpcLwm2mIntegrationExecuteTest.java | 51 +++++++++++++++++-- .../DefaultLwM2mDownlinkMsgHandler.java | 12 +++-- .../rpc/DefaultLwM2MRpcRequestHandler.java | 6 +++ .../lwm2m/server/rpc/RpcExecuteRequest.java | 27 ++++++++++ 5 files changed, 96 insertions(+), 9 deletions(-) create mode 100644 common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/rpc/RpcExecuteRequest.java 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 191bc97579..fb94fa2a91 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 @@ -160,7 +160,14 @@ public class SimpleLwM2MDevice extends BaseInstanceEnabler implements Destroyabl if (!arguments.isEmpty()) withArguments = " with arguments " + arguments; log.info("Execute on Device resource /{}/{}/{} {}", getModel().id, getId(), resourceId, withArguments); - return ExecuteResponse.success(); + switch (resourceId) { + case 4: + return ExecuteResponse.success(); + case 5: + return ExecuteResponse.success(); + default: + return super.execute(identity, resourceId, arguments); + } } @Override diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationExecuteTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationExecuteTest.java index f5b4538c1c..6de02d6a5e 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationExecuteTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationExecuteTest.java @@ -29,6 +29,7 @@ import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.OBJECT_INST import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_2; import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_3; import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_4; +import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_5; import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_8; import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_9; @@ -90,14 +91,54 @@ public class RpcLwm2mIntegrationExecuteTest extends AbstractRpcLwM2MIntegrationT /** - * execute_resource_with_parameters (execute reboot after 60 seconds on device) - * Execute {"id":"3/0/4","value":60} + * execute_resource_with_parameters (execute reboot if digit = 5 on device) + * Execute {"id":"3/0/4","value":5} * {"result":"CHANGED"} */ @Test - public void testExecuteResourceWithParametersById_Result_CHANGED() throws Exception { + public void testExecuteResourceWithParametersOnlyOneDigitValueNullById_Result_Ok() throws Exception { String expectedPath = objectInstanceIdVer_3 + "/" + RESOURCE_ID_4; - Object expectedValue = 60; + Object expectedValue = 5; + String actualResult = sendRPCExecuteWithValueById(expectedPath, expectedValue); + ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); + assertEquals(ResponseCode.CHANGED.getName(), rpcActualResult.get("result").asText()); + } + + /** + * execute_resource_with_parameters (execute Factory Reset if digit = 2 -> after 60 seconds on device) + * Execute {"id":"3/0/5","value":"2='60'"} + + */ + @Test + public void testExecuteResourceWithParametersDigit2Value60ById_Result_Ok() throws Exception { + String expectedPath = objectInstanceIdVer_3 + "/" + RESOURCE_ID_5; + Object expectedValue = "2='60'"; + String actualResult = sendRPCExecuteWithValueById(expectedPath, expectedValue); + ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); + assertEquals(ResponseCode.CHANGED.getName(), rpcActualResult.get("result").asText()); + } + + /** + * execute_resource_with_parameters (execute Factory Reset after connect with link on device) + * Execute {"id":"3/0/5","value":"2,0='https://thingsboard.io/docs/reference/lwm2m-api/'"} + */ + @Test + public void testExecuteResourceWithParametersDigit2_0_ValueLinkById_Result_Ok() throws Exception { + String expectedPath = objectInstanceIdVer_3 + "/" + RESOURCE_ID_5; + Object expectedValue = "2,0='https://thingsboard.io/docs/reference/lwm2m-api/'"; + String actualResult = sendRPCExecuteWithValueById(expectedPath, expectedValue); + ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); + assertEquals(ResponseCode.CHANGED.getName(), rpcActualResult.get("result").asText()); + } + + /** + * execute_resource_with_parameters (execute Factory Reset after connect with link on device) + * Execute {"id":"3/0/5","value":"0,1,2,3,4,5,6,7,8,9"} + */ + @Test + public void testExecuteResourceWithParametersDigitManyValueNullById_Result_Ok() throws Exception { + String expectedPath = objectInstanceIdVer_3 + "/" + RESOURCE_ID_5; + Object expectedValue = "0,1,2,3,4,5,6,7,8,9"; String actualResult = sendRPCExecuteWithValueById(expectedPath, expectedValue); ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); assertEquals(ResponseCode.CHANGED.getName(), rpcActualResult.get("result").asText()); @@ -178,7 +219,7 @@ public class RpcLwm2mIntegrationExecuteTest extends AbstractRpcLwM2MIntegrationT } private String sendRPCExecuteWithValueById(String path, Object value) throws Exception { - String setRpcRequest = "{\"method\": \"Execute\", \"params\": {\"id\": \"" + path + "\", \"value\": " + value + " }}"; + String setRpcRequest = "{\"method\": \"Execute\", \"params\": {\"id\": \"" + path + "\", \"value\": \"" + value + "\"}}"; return doPostAsync("/api/plugins/rpc/twoway/" + lwM2MTestClient.getDeviceIdStr(), setRpcRequest, String.class, status().isOk()); } 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 8f638cfa2a..9576ab7228 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 @@ -50,6 +50,8 @@ import org.eclipse.leshan.core.request.SimpleDownlinkRequest; import org.eclipse.leshan.core.request.WriteAttributesRequest; import org.eclipse.leshan.core.request.WriteCompositeRequest; import org.eclipse.leshan.core.request.WriteRequest; +import org.eclipse.leshan.core.request.argument.Arguments; +import org.eclipse.leshan.core.request.argument.InvalidArgumentException; import org.eclipse.leshan.core.request.exception.ClientSleepingException; import org.eclipse.leshan.core.request.exception.InvalidRequestException; import org.eclipse.leshan.core.request.exception.TimeoutException; @@ -116,6 +118,7 @@ import static org.eclipse.leshan.core.model.ResourceModel.Type.OPAQUE; import static org.thingsboard.server.common.transport.util.JsonUtils.isBase64; import static org.thingsboard.server.transport.lwm2m.utils.LwM2MTransportUtil.convertMultiResourceValuesFromRpcBody; import static org.thingsboard.server.transport.lwm2m.utils.LwM2MTransportUtil.createModelsDefault; +import static org.thingsboard.server.transport.lwm2m.utils.LwM2MTransportUtil.equalsResourceTypeGetSimpleName; import static org.thingsboard.server.transport.lwm2m.utils.LwM2MTransportUtil.fromVersionedIdToObjectId; import static org.thingsboard.server.transport.lwm2m.utils.LwM2MTransportUtil.getVerFromPathIdVerOrId; import static org.thingsboard.server.transport.lwm2m.utils.LwM2MTransportUtil.validateVersionedId; @@ -281,8 +284,11 @@ public class DefaultLwM2mDownlinkMsgHandler extends LwM2MExecutorAwareService im } else if (resourceModelExecute.operations.isExecutable()) { ExecuteRequest downlink; if (request.getParams() != null && !resourceModelExecute.multiple) { - downlink = new ExecuteRequest(request.getObjectId(), (String) this.converter.convertValue(request.getParams(), - resourceModelExecute.type, ResourceModel.Type.STRING, new LwM2mPath(request.getObjectId()))); + Object params = request.getParams(); + ResourceModel.Type resourceModel = resourceModelExecute.type == ResourceModel.Type.NONE ? equalsResourceTypeGetSimpleName(params) : resourceModelExecute.type; + String args = (String) this.converter.convertValue(params, resourceModel, ResourceModel.Type.STRING, new LwM2mPath(request.getObjectId())); + Arguments arguments = Arguments.parse(args); + downlink = new ExecuteRequest(request.getObjectId(), arguments); } else { downlink = new ExecuteRequest(request.getObjectId()); } @@ -290,7 +296,7 @@ public class DefaultLwM2mDownlinkMsgHandler extends LwM2MExecutorAwareService im } else { callback.onValidationError(request.toString(), "Resource with " + request.getVersionedId() + " is not executable."); } - } catch (InvalidRequestException e) { + } catch (InvalidRequestException | InvalidArgumentException e) { callback.onValidationError(request.toString(), e.getMessage()); } } diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/rpc/DefaultLwM2MRpcRequestHandler.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/rpc/DefaultLwM2MRpcRequestHandler.java index 6980fb6341..e3bf6e8daa 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/rpc/DefaultLwM2MRpcRequestHandler.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/rpc/DefaultLwM2MRpcRequestHandler.java @@ -241,7 +241,13 @@ public class DefaultLwM2MRpcRequestHandler implements LwM2MRpcRequestHandler { } private void sendExecuteRequest(LwM2mClient client, TransportProtos.ToDeviceRpcRequestMsg requestMsg, String versionedId) { + RpcExecuteRequest requestBody = JacksonUtil.fromString(requestMsg.getParams(), RpcExecuteRequest.class); TbLwM2MExecuteRequest downlink = TbLwM2MExecuteRequest.builder().versionedId(versionedId).timeout(clientContext.getRequestTimeout(client)).build(); + if (!requestMsg.getParams().isEmpty()) { + downlink = TbLwM2MExecuteRequest.builder().versionedId(versionedId) + .params(requestBody.getValue()) + .timeout(clientContext.getRequestTimeout(client)).build(); + } var mainCallback = new TbLwM2MExecuteCallback(logService, client, versionedId); var rpcCallback = new RpcEmptyResponseCallback<>(transportService, client, requestMsg, mainCallback); downlinkHandler.sendExecuteRequest(client, downlink, rpcCallback); diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/rpc/RpcExecuteRequest.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/rpc/RpcExecuteRequest.java new file mode 100644 index 0000000000..e0360c6f71 --- /dev/null +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/rpc/RpcExecuteRequest.java @@ -0,0 +1,27 @@ +/** + * 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.server.rpc; + +import lombok.Data; +import lombok.EqualsAndHashCode; + +@Data +@EqualsAndHashCode(callSuper = true) +public class RpcExecuteRequest extends LwM2MRpcRequestHeader { + + private Object value; + +} From c66379204aa4c1e70702de76cf4a1e13db76abdb Mon Sep 17 00:00:00 2001 From: nickAS21 Date: Wed, 13 May 2026 14:30:10 +0300 Subject: [PATCH 2/3] lwm2m - refactoring review - 01? add tests --- .../lwm2m/client/SimpleLwM2MDevice.java | 1 - .../sql/RpcLwm2mIntegrationExecuteTest.java | 50 ++++++++++++++++--- .../DefaultLwM2mDownlinkMsgHandler.java | 16 ++++-- 3 files changed, 54 insertions(+), 13 deletions(-) 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 fb94fa2a91..7d4c4b75e5 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 @@ -162,7 +162,6 @@ public class SimpleLwM2MDevice extends BaseInstanceEnabler implements Destroyabl log.info("Execute on Device resource /{}/{}/{} {}", getModel().id, getId(), resourceId, withArguments); switch (resourceId) { case 4: - return ExecuteResponse.success(); case 5: return ExecuteResponse.success(); default: diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationExecuteTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationExecuteTest.java index 6de02d6a5e..7999081bd6 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationExecuteTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationExecuteTest.java @@ -96,7 +96,7 @@ public class RpcLwm2mIntegrationExecuteTest extends AbstractRpcLwM2MIntegrationT * {"result":"CHANGED"} */ @Test - public void testExecuteResourceWithParametersOnlyOneDigitValueNullById_Result_Ok() throws Exception { + public void testExecuteResourceWithParametersSingleDigitValueById_Result_Ok() throws Exception { String expectedPath = objectInstanceIdVer_3 + "/" + RESOURCE_ID_4; Object expectedValue = 5; String actualResult = sendRPCExecuteWithValueById(expectedPath, expectedValue); @@ -110,7 +110,7 @@ public class RpcLwm2mIntegrationExecuteTest extends AbstractRpcLwM2MIntegrationT */ @Test - public void testExecuteResourceWithParametersDigit2Value60ById_Result_Ok() throws Exception { + public void testExecuteResourceWithParametersArgumentIdAndValueById_Result_Ok() throws Exception { String expectedPath = objectInstanceIdVer_3 + "/" + RESOURCE_ID_5; Object expectedValue = "2='60'"; String actualResult = sendRPCExecuteWithValueById(expectedPath, expectedValue); @@ -123,7 +123,7 @@ public class RpcLwm2mIntegrationExecuteTest extends AbstractRpcLwM2MIntegrationT * Execute {"id":"3/0/5","value":"2,0='https://thingsboard.io/docs/reference/lwm2m-api/'"} */ @Test - public void testExecuteResourceWithParametersDigit2_0_ValueLinkById_Result_Ok() throws Exception { + public void testExecuteResourceWithParametersMultipleArgumentsIncludingLinkById_Result_Ok() throws Exception { String expectedPath = objectInstanceIdVer_3 + "/" + RESOURCE_ID_5; Object expectedValue = "2,0='https://thingsboard.io/docs/reference/lwm2m-api/'"; String actualResult = sendRPCExecuteWithValueById(expectedPath, expectedValue); @@ -136,7 +136,7 @@ public class RpcLwm2mIntegrationExecuteTest extends AbstractRpcLwM2MIntegrationT * Execute {"id":"3/0/5","value":"0,1,2,3,4,5,6,7,8,9"} */ @Test - public void testExecuteResourceWithParametersDigitManyValueNullById_Result_Ok() throws Exception { + public void testExecuteResourceWithParametersMultipleArgumentsById_Result_Ok() throws Exception { String expectedPath = objectInstanceIdVer_3 + "/" + RESOURCE_ID_5; Object expectedValue = "0,1,2,3,4,5,6,7,8,9"; String actualResult = sendRPCExecuteWithValueById(expectedPath, expectedValue); @@ -144,13 +144,47 @@ public class RpcLwm2mIntegrationExecuteTest extends AbstractRpcLwM2MIntegrationT assertEquals(ResponseCode.CHANGED.getName(), rpcActualResult.get("result").asText()); } + + /** + * execute_resource_with_parameters (execute Factory Reset after 60 seconds on device) + * Execute {"id":"3/0/5","value":"'60'"} + + */ + @Test + public void testExecuteResourceWithParametersSingleDigitValueInvalidById_BAD_REQUEST_Error_UintegerBetween_0_And_9_Expected() throws Exception { + String expectedPath = objectInstanceIdVer_3 + "/" + RESOURCE_ID_5; + Object expectedValue = "'60'"; + String actualResult = sendRPCExecuteWithValueById(expectedPath, expectedValue); + ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); + assertEquals(ResponseCode.BAD_REQUEST.getName(), rpcActualResult.get("result").asText()); + String expected = "Unable to parse Arguments [" + expectedValue + "] : Invalid digit ['] (an integer between 0 and 9 is expected)"; + String actual = rpcActualResult.get("error").asText(); + assertTrue(actual.equals(expected)); + } + + /** + * execute_resource_with_parameters (execute Bad with Unable to parse Arguments) + * Execute {"id":"3/0/5","value":"0,1,2,3,4,5,6,7,8,9,60"} + */ + @Test + public void testExecuteResourceWithParametersMultipleArgumentsById_Result_BAD_REQUEST_Error_UnableParseArguments() throws Exception { + String expectedPath = objectInstanceIdVer_3 + "/" + RESOURCE_ID_5; + Object expectedValue = "0,1,2,3,4,5,6,7,8,9,60"; + String actualResult = sendRPCExecuteWithValueById(expectedPath, expectedValue);; + ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); + assertEquals(ResponseCode.BAD_REQUEST.getName(), rpcActualResult.get("result").asText()); + String expected = "Unable to parse Arguments [" + expectedValue + "] : [,] separator expected at index 21 after [0,1,2,3,4,5,6,7,8,9,6]"; + String actual = rpcActualResult.get("error").asText(); + assertTrue(actual.equals(expected)); + } + /** * Bootstrap-Request Trigger * Execute {"id":"1/0/9"} * {"result":"BAD_REQUEST","error":"probably no bootstrap server configured"} */ @Test - public void testExecuteBootstrapRequestTriggerById_Result_BAD_REQUEST_Error_NoBootstrapServerConfigured() throws Exception { + public void testExecuteBootstrapRequestTriggerById_Result_BAD_REQUEST_Error_NoBootstrapServer() throws Exception { String expectedPath = objectInstanceIdVer_1 + "/" + RESOURCE_ID_9; String actualResult = sendRPCExecuteById(expectedPath); ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); @@ -166,7 +200,7 @@ public class RpcLwm2mIntegrationExecuteTest extends AbstractRpcLwM2MIntegrationT * {"result":"BAD_REQUEST","error":"Resource with /5_1.0/0/3 is not executable."} */ @Test - public void testExecuteResourceWithOperationNotExecuteById_Result_METHOD_NOT_ALLOWED() throws Exception { + public void testExecuteResourceWithOperationNotExecuteById_Result_BAD_REQUEST_Error_Is_Not_Executable() throws Exception { String expectedPath = objectInstanceIdVer_5 + "/" + RESOURCE_ID_3; String actualResult = sendRPCExecuteById(expectedPath); ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); @@ -182,7 +216,7 @@ public class RpcLwm2mIntegrationExecuteTest extends AbstractRpcLwM2MIntegrationT * {"result":"BAD_REQUEST","error":"Specified object id 50 absent in the list supported objects of the client or is security object!"} */ @Test - public void testExecuteNonExistingResourceOnNonExistingObjectById_Result_BAD_REQUEST() throws Exception { + public void testExecuteNonExistingResourceOnNonExistingObjectById_Result_BAD_REQUEST_Error_Specified_Object_Absent_List_Supported() throws Exception { String expectedPath = OBJECT_ID_VER_50 + "/" + OBJECT_INSTANCE_ID_0 + "/" + RESOURCE_ID_3; String actualResult = sendRPCExecuteById(expectedPath); ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); @@ -200,7 +234,7 @@ public class RpcLwm2mIntegrationExecuteTest extends AbstractRpcLwM2MIntegrationT * {"result":"BAD_REQUEST","error":"Specified object id 0 absent in the list supported objects of the client or is security object!"} */ @Test - public void testExecuteSecurityObjectById_Result_NOT_FOUND() throws Exception { + public void testExecuteSecurityObjectById_Result_BAD_REQUEST_Error_InvalidDigit() throws Exception { String expectedPath = objectIdVer_0 + "/" + OBJECT_INSTANCE_ID_0 + "/" + RESOURCE_ID_3; String actualResult = sendRPCExecuteById(expectedPath); ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); 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 9576ab7228..768008f65f 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 @@ -279,16 +279,24 @@ public class DefaultLwM2mDownlinkMsgHandler extends LwM2MExecutorAwareService im } if (resourceModelExecute == null) { callback.onValidationError(request.toString(), "ResourceModel with " + request.getVersionedId() + - " is absent in system. Need ddd Lwm2m Model with id=" + pathIds.getObjectId() + " ver=" + + " is absent in system. Need to add Model with id=" + pathIds.getObjectId() + " ver=" + getVerFromPathIdVerOrId(request.getVersionedId()) + " to profile."); } else if (resourceModelExecute.operations.isExecutable()) { ExecuteRequest downlink; if (request.getParams() != null && !resourceModelExecute.multiple) { Object params = request.getParams(); - ResourceModel.Type resourceModel = resourceModelExecute.type == ResourceModel.Type.NONE ? equalsResourceTypeGetSimpleName(params) : resourceModelExecute.type; + ResourceModel.Type resourceModel = equalsResourceTypeGetSimpleName(params); + if (resourceModel == null) { + throw new InvalidArgumentException("Unsupported parameter type: " + params.getClass().getSimpleName() + + ". Only simple types (String, Integer, Boolean, etc.) are allowed for Execute arguments."); + } String args = (String) this.converter.convertValue(params, resourceModel, ResourceModel.Type.STRING, new LwM2mPath(request.getObjectId())); - Arguments arguments = Arguments.parse(args); - downlink = new ExecuteRequest(request.getObjectId(), arguments); + try { + Arguments arguments = Arguments.parse(args); + downlink = new ExecuteRequest(request.getObjectId(), arguments); + } catch (IllegalArgumentException e) { + downlink = new ExecuteRequest(request.getObjectId(), args); + } } else { downlink = new ExecuteRequest(request.getObjectId()); } From 444d856fb878d3fdc002d75ba56048248d4c6688 Mon Sep 17 00:00:00 2001 From: nickAS21 Date: Thu, 14 May 2026 14:34:44 +0300 Subject: [PATCH 3/3] lwm2m - refactoring review - 02 and tests --- .../sql/RpcLwm2mIntegrationExecuteTest.java | 37 +++++++++---- .../DefaultLwM2mDownlinkMsgHandler.java | 52 ++++++++----------- .../rpc/DefaultLwM2MRpcRequestHandler.java | 12 ++--- 3 files changed, 57 insertions(+), 44 deletions(-) diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationExecuteTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationExecuteTest.java index 7999081bd6..92e25a793e 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationExecuteTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationExecuteTest.java @@ -119,7 +119,8 @@ public class RpcLwm2mIntegrationExecuteTest extends AbstractRpcLwM2MIntegrationT } /** - * execute_resource_with_parameters (execute Factory Reset after connect with link on device) + * execute_resource_with_parameters (execute Factory Reset with two arguments: + * digit 2 without a value and digit 0 with the link value on device) * Execute {"id":"3/0/5","value":"2,0='https://thingsboard.io/docs/reference/lwm2m-api/'"} */ @Test @@ -132,7 +133,8 @@ public class RpcLwm2mIntegrationExecuteTest extends AbstractRpcLwM2MIntegrationT } /** - * execute_resource_with_parameters (execute Factory Reset after connect with link on device) + * execute_resource_with_parameters (execute Factory Reset with multiple arguments without values) + * According to the OMA LwM2M execute arguments format, this represents ten arguments (digits 0-9), all without values. * Execute {"id":"3/0/5","value":"0,1,2,3,4,5,6,7,8,9"} */ @Test @@ -148,10 +150,9 @@ public class RpcLwm2mIntegrationExecuteTest extends AbstractRpcLwM2MIntegrationT /** * execute_resource_with_parameters (execute Factory Reset after 60 seconds on device) * Execute {"id":"3/0/5","value":"'60'"} - */ @Test - public void testExecuteResourceWithParametersSingleDigitValueInvalidById_BAD_REQUEST_Error_UintegerBetween_0_And_9_Expected() throws Exception { + public void testExecuteResourceWithParametersSingleDigitValueInvalidById_Result_BAD_REQUEST_Error_IntegerBetween_0_And_9_Expected() throws Exception { String expectedPath = objectInstanceIdVer_3 + "/" + RESOURCE_ID_5; Object expectedValue = "'60'"; String actualResult = sendRPCExecuteWithValueById(expectedPath, expectedValue); @@ -159,7 +160,7 @@ public class RpcLwm2mIntegrationExecuteTest extends AbstractRpcLwM2MIntegrationT assertEquals(ResponseCode.BAD_REQUEST.getName(), rpcActualResult.get("result").asText()); String expected = "Unable to parse Arguments [" + expectedValue + "] : Invalid digit ['] (an integer between 0 and 9 is expected)"; String actual = rpcActualResult.get("error").asText(); - assertTrue(actual.equals(expected)); + assertTrue(actual.contains(expected)); } /** @@ -175,7 +176,7 @@ public class RpcLwm2mIntegrationExecuteTest extends AbstractRpcLwM2MIntegrationT assertEquals(ResponseCode.BAD_REQUEST.getName(), rpcActualResult.get("result").asText()); String expected = "Unable to parse Arguments [" + expectedValue + "] : [,] separator expected at index 21 after [0,1,2,3,4,5,6,7,8,9,6]"; String actual = rpcActualResult.get("error").asText(); - assertTrue(actual.equals(expected)); + assertTrue(actual.contains(expected)); } /** @@ -234,7 +235,7 @@ public class RpcLwm2mIntegrationExecuteTest extends AbstractRpcLwM2MIntegrationT * {"result":"BAD_REQUEST","error":"Specified object id 0 absent in the list supported objects of the client or is security object!"} */ @Test - public void testExecuteSecurityObjectById_Result_BAD_REQUEST_Error_InvalidDigit() throws Exception { + public void testExecuteSecurityObjectById_Result_BAD_REQUEST_Error_SpecifiedObjectAbsent() throws Exception { String expectedPath = objectIdVer_0 + "/" + OBJECT_INSTANCE_ID_0 + "/" + RESOURCE_ID_3; String actualResult = sendRPCExecuteById(expectedPath); ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); @@ -253,8 +254,26 @@ public class RpcLwm2mIntegrationExecuteTest extends AbstractRpcLwM2MIntegrationT } private String sendRPCExecuteWithValueById(String path, Object value) throws Exception { - String setRpcRequest = "{\"method\": \"Execute\", \"params\": {\"id\": \"" + path + "\", \"value\": \"" + value + "\"}}"; - return doPostAsync("/api/plugins/rpc/twoway/" + lwM2MTestClient.getDeviceIdStr(), setRpcRequest, String.class, status().isOk()); + ObjectNode params = JacksonUtil.newObjectNode(); + params.put("id", path); + + // Jackson сам вирішить: ставити лапки (рядок) чи ні (число/boolean/null) + if (value instanceof String) { + params.put("value", (String) value); + } else if (value instanceof Integer) { + params.put("value", (Integer) value); + } else if (value instanceof Boolean) { + params.put("value", (Boolean) value); + } else { + params.set("value", JacksonUtil.valueToTree(value)); + } + + ObjectNode setRpcRequest = JacksonUtil.newObjectNode(); + setRpcRequest.put("method", "Execute"); + setRpcRequest.set("params", params); + + return doPostAsync("/api/plugins/rpc/twoway/" + lwM2MTestClient.getDeviceIdStr(), + JacksonUtil.toString(setRpcRequest), String.class, status().isOk()); } } 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 768008f65f..c5b8779a3f 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 @@ -271,40 +271,34 @@ public class DefaultLwM2mDownlinkMsgHandler extends LwM2MExecutorAwareService im validateVersionedId(client, request); LwM2mPath pathIds = new LwM2mPath(fromVersionedIdToObjectId(request.getVersionedId())); ResourceModel resourceModelExecute = client.getResourceModel(request.getVersionedId(), modelProvider); - if (resourceModelExecute == null) { - LwM2mModel model = createModelsDefault(); - if (pathIds.isResource()) { - resourceModelExecute = model.getResourceModel(pathIds.getObjectId(), pathIds.getResourceId()); - } + if (resourceModelExecute == null && pathIds.isResource()) { + resourceModelExecute = createModelsDefault().getResourceModel(pathIds.getObjectId(), pathIds.getResourceId()); } if (resourceModelExecute == null) { - callback.onValidationError(request.toString(), "ResourceModel with " + request.getVersionedId() + - " is absent in system. Need to add Model with id=" + pathIds.getObjectId() + " ver=" + - getVerFromPathIdVerOrId(request.getVersionedId()) + " to profile."); - } else if (resourceModelExecute.operations.isExecutable()) { - ExecuteRequest downlink; - if (request.getParams() != null && !resourceModelExecute.multiple) { - Object params = request.getParams(); - ResourceModel.Type resourceModel = equalsResourceTypeGetSimpleName(params); - if (resourceModel == null) { - throw new InvalidArgumentException("Unsupported parameter type: " + params.getClass().getSimpleName() + - ". Only simple types (String, Integer, Boolean, etc.) are allowed for Execute arguments."); - } - String args = (String) this.converter.convertValue(params, resourceModel, ResourceModel.Type.STRING, new LwM2mPath(request.getObjectId())); - try { - Arguments arguments = Arguments.parse(args); - downlink = new ExecuteRequest(request.getObjectId(), arguments); - } catch (IllegalArgumentException e) { - downlink = new ExecuteRequest(request.getObjectId(), args); - } - } else { - downlink = new ExecuteRequest(request.getObjectId()); + throw new InvalidArgumentException(String.format("ResourceModel with %s is absent in the system. Need to add Model with id= %s ver=%s to profile.", + request.getVersionedId(), pathIds.getObjectId(), getVerFromPathIdVerOrId(request.getVersionedId()))); + } + if (!resourceModelExecute.operations.isExecutable()) { + throw new InvalidArgumentException(String.format("Resource with %s is not executable.", request.getVersionedId())); + } + + ExecuteRequest downlink; + Object params = request.getParams(); + // 4. Handle parameters if they exist and the resource is not a multiple-instance resource + if (params != null && !resourceModelExecute.multiple) { + ResourceModel.Type resourceModelType = equalsResourceTypeGetSimpleName(params); + if (resourceModelType == null) { + throw new InvalidArgumentException(String.format("Unsupported parameter type: %s. Only simple types (String, Integer, Boolean, etc.) are allowed for Execute arguments.", + params.getClass().getSimpleName())); } - sendSimpleRequest(client, downlink, request.getTimeout(), callback); + String args = (String) this.converter.convertValue(params, resourceModelType, ResourceModel.Type.STRING, pathIds); + downlink = new ExecuteRequest(request.getObjectId(), args); } else { - callback.onValidationError(request.toString(), "Resource with " + request.getVersionedId() + " is not executable."); + downlink = new ExecuteRequest(request.getObjectId()); } - } catch (InvalidRequestException | InvalidArgumentException e) { + sendSimpleRequest(client, downlink, request.getTimeout(), callback); + } catch (Exception e) { + log.error("[{}] Validation failed for Execute request: {}", client.getEndpoint(), e.getMessage()); callback.onValidationError(request.toString(), e.getMessage()); } } diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/rpc/DefaultLwM2MRpcRequestHandler.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/rpc/DefaultLwM2MRpcRequestHandler.java index e3bf6e8daa..a0cbd4f598 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/rpc/DefaultLwM2MRpcRequestHandler.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/rpc/DefaultLwM2MRpcRequestHandler.java @@ -242,12 +242,12 @@ public class DefaultLwM2MRpcRequestHandler implements LwM2MRpcRequestHandler { private void sendExecuteRequest(LwM2mClient client, TransportProtos.ToDeviceRpcRequestMsg requestMsg, String versionedId) { RpcExecuteRequest requestBody = JacksonUtil.fromString(requestMsg.getParams(), RpcExecuteRequest.class); - TbLwM2MExecuteRequest downlink = TbLwM2MExecuteRequest.builder().versionedId(versionedId).timeout(clientContext.getRequestTimeout(client)).build(); - if (!requestMsg.getParams().isEmpty()) { - downlink = TbLwM2MExecuteRequest.builder().versionedId(versionedId) - .params(requestBody.getValue()) - .timeout(clientContext.getRequestTimeout(client)).build(); - } + Object value = requestBody != null ? requestBody.getValue() : null; + TbLwM2MExecuteRequest downlink = TbLwM2MExecuteRequest.builder() + .versionedId(versionedId) + .params(value) + .timeout(clientContext.getRequestTimeout(client)) + .build(); var mainCallback = new TbLwM2MExecuteCallback(logService, client, versionedId); var rpcCallback = new RpcEmptyResponseCallback<>(transportService, client, requestMsg, mainCallback); downlinkHandler.sendExecuteRequest(client, downlink, rpcCallback);