Skip to content

Commit 79bb2b0

Browse files
Merge pull request #14881 from thingsboard/lwm2m_execute_with_args
Support for Execute operation with arguments (OMA LwM2M v1.0.2)
2 parents 9c63434 + 444d856 commit 79bb2b0

5 files changed

Lines changed: 171 additions & 30 deletions

File tree

application/src/test/java/org/thingsboard/server/transport/lwm2m/client/SimpleLwM2MDevice.java

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,13 @@ public ExecuteResponse execute(LwM2mServer identity, int resourceId, Arguments a
160160
if (!arguments.isEmpty())
161161
withArguments = " with arguments " + arguments;
162162
log.info("Execute on Device resource /{}/{}/{} {}", getModel().id, getId(), resourceId, withArguments);
163-
return ExecuteResponse.success();
163+
switch (resourceId) {
164+
case 4:
165+
case 5:
166+
return ExecuteResponse.success();
167+
default:
168+
return super.execute(identity, resourceId, arguments);
169+
}
164170
}
165171

166172
@Override

application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationExecuteTest.java

Lines changed: 104 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_2;
3030
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_3;
3131
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_4;
32+
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_5;
3233
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_8;
3334
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_9;
3435

@@ -90,26 +91,101 @@ public void testExecuteRegistrationUpdateTriggerById_Result_CHANGED() throws Exc
9091

9192

9293
/**
93-
* execute_resource_with_parameters (execute reboot after 60 seconds on device)
94-
* Execute {"id":"3/0/4","value":60}
94+
* execute_resource_with_parameters (execute reboot if digit = 5 on device)
95+
* Execute {"id":"3/0/4","value":5}
9596
* {"result":"CHANGED"}
9697
*/
9798
@Test
98-
public void testExecuteResourceWithParametersById_Result_CHANGED() throws Exception {
99+
public void testExecuteResourceWithParametersSingleDigitValueById_Result_Ok() throws Exception {
99100
String expectedPath = objectInstanceIdVer_3 + "/" + RESOURCE_ID_4;
100-
Object expectedValue = 60;
101+
Object expectedValue = 5;
101102
String actualResult = sendRPCExecuteWithValueById(expectedPath, expectedValue);
102103
ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class);
103104
assertEquals(ResponseCode.CHANGED.getName(), rpcActualResult.get("result").asText());
104105
}
105106

107+
/**
108+
* execute_resource_with_parameters (execute Factory Reset if digit = 2 -> after 60 seconds on device)
109+
* Execute {"id":"3/0/5","value":"2='60'"}
110+
111+
*/
112+
@Test
113+
public void testExecuteResourceWithParametersArgumentIdAndValueById_Result_Ok() throws Exception {
114+
String expectedPath = objectInstanceIdVer_3 + "/" + RESOURCE_ID_5;
115+
Object expectedValue = "2='60'";
116+
String actualResult = sendRPCExecuteWithValueById(expectedPath, expectedValue);
117+
ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class);
118+
assertEquals(ResponseCode.CHANGED.getName(), rpcActualResult.get("result").asText());
119+
}
120+
121+
/**
122+
* execute_resource_with_parameters (execute Factory Reset with two arguments:
123+
* digit 2 without a value and digit 0 with the link value on device)
124+
* Execute {"id":"3/0/5","value":"2,0='https://thingsboard.io/docs/reference/lwm2m-api/'"}
125+
*/
126+
@Test
127+
public void testExecuteResourceWithParametersMultipleArgumentsIncludingLinkById_Result_Ok() throws Exception {
128+
String expectedPath = objectInstanceIdVer_3 + "/" + RESOURCE_ID_5;
129+
Object expectedValue = "2,0='https://thingsboard.io/docs/reference/lwm2m-api/'";
130+
String actualResult = sendRPCExecuteWithValueById(expectedPath, expectedValue);
131+
ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class);
132+
assertEquals(ResponseCode.CHANGED.getName(), rpcActualResult.get("result").asText());
133+
}
134+
135+
/**
136+
* execute_resource_with_parameters (execute Factory Reset with multiple arguments without values)
137+
* According to the OMA LwM2M execute arguments format, this represents ten arguments (digits 0-9), all without values.
138+
* Execute {"id":"3/0/5","value":"0,1,2,3,4,5,6,7,8,9"}
139+
*/
140+
@Test
141+
public void testExecuteResourceWithParametersMultipleArgumentsById_Result_Ok() throws Exception {
142+
String expectedPath = objectInstanceIdVer_3 + "/" + RESOURCE_ID_5;
143+
Object expectedValue = "0,1,2,3,4,5,6,7,8,9";
144+
String actualResult = sendRPCExecuteWithValueById(expectedPath, expectedValue);
145+
ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class);
146+
assertEquals(ResponseCode.CHANGED.getName(), rpcActualResult.get("result").asText());
147+
}
148+
149+
150+
/**
151+
* execute_resource_with_parameters (execute Factory Reset after 60 seconds on device)
152+
* Execute {"id":"3/0/5","value":"'60'"}
153+
*/
154+
@Test
155+
public void testExecuteResourceWithParametersSingleDigitValueInvalidById_Result_BAD_REQUEST_Error_IntegerBetween_0_And_9_Expected() throws Exception {
156+
String expectedPath = objectInstanceIdVer_3 + "/" + RESOURCE_ID_5;
157+
Object expectedValue = "'60'";
158+
String actualResult = sendRPCExecuteWithValueById(expectedPath, expectedValue);
159+
ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class);
160+
assertEquals(ResponseCode.BAD_REQUEST.getName(), rpcActualResult.get("result").asText());
161+
String expected = "Unable to parse Arguments [" + expectedValue + "] : Invalid digit ['] (an integer between 0 and 9 is expected)";
162+
String actual = rpcActualResult.get("error").asText();
163+
assertTrue(actual.contains(expected));
164+
}
165+
166+
/**
167+
* execute_resource_with_parameters (execute Bad with Unable to parse Arguments)
168+
* Execute {"id":"3/0/5","value":"0,1,2,3,4,5,6,7,8,9,60"}
169+
*/
170+
@Test
171+
public void testExecuteResourceWithParametersMultipleArgumentsById_Result_BAD_REQUEST_Error_UnableParseArguments() throws Exception {
172+
String expectedPath = objectInstanceIdVer_3 + "/" + RESOURCE_ID_5;
173+
Object expectedValue = "0,1,2,3,4,5,6,7,8,9,60";
174+
String actualResult = sendRPCExecuteWithValueById(expectedPath, expectedValue);;
175+
ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class);
176+
assertEquals(ResponseCode.BAD_REQUEST.getName(), rpcActualResult.get("result").asText());
177+
String expected = "Unable to parse Arguments [" + expectedValue + "] : [,] separator expected at index 21 after [0,1,2,3,4,5,6,7,8,9,6]";
178+
String actual = rpcActualResult.get("error").asText();
179+
assertTrue(actual.contains(expected));
180+
}
181+
106182
/**
107183
* Bootstrap-Request Trigger
108184
* Execute {"id":"1/0/9"}
109185
* {"result":"BAD_REQUEST","error":"probably no bootstrap server configured"}
110186
*/
111187
@Test
112-
public void testExecuteBootstrapRequestTriggerById_Result_BAD_REQUEST_Error_NoBootstrapServerConfigured() throws Exception {
188+
public void testExecuteBootstrapRequestTriggerById_Result_BAD_REQUEST_Error_NoBootstrapServer() throws Exception {
113189
String expectedPath = objectInstanceIdVer_1 + "/" + RESOURCE_ID_9;
114190
String actualResult = sendRPCExecuteById(expectedPath);
115191
ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class);
@@ -125,7 +201,7 @@ public void testExecuteBootstrapRequestTriggerById_Result_BAD_REQUEST_Error_NoBo
125201
* {"result":"BAD_REQUEST","error":"Resource with /5_1.0/0/3 is not executable."}
126202
*/
127203
@Test
128-
public void testExecuteResourceWithOperationNotExecuteById_Result_METHOD_NOT_ALLOWED() throws Exception {
204+
public void testExecuteResourceWithOperationNotExecuteById_Result_BAD_REQUEST_Error_Is_Not_Executable() throws Exception {
129205
String expectedPath = objectInstanceIdVer_5 + "/" + RESOURCE_ID_3;
130206
String actualResult = sendRPCExecuteById(expectedPath);
131207
ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class);
@@ -141,7 +217,7 @@ public void testExecuteResourceWithOperationNotExecuteById_Result_METHOD_NOT_ALL
141217
* {"result":"BAD_REQUEST","error":"Specified object id 50 absent in the list supported objects of the client or is security object!"}
142218
*/
143219
@Test
144-
public void testExecuteNonExistingResourceOnNonExistingObjectById_Result_BAD_REQUEST() throws Exception {
220+
public void testExecuteNonExistingResourceOnNonExistingObjectById_Result_BAD_REQUEST_Error_Specified_Object_Absent_List_Supported() throws Exception {
145221
String expectedPath = OBJECT_ID_VER_50 + "/" + OBJECT_INSTANCE_ID_0 + "/" + RESOURCE_ID_3;
146222
String actualResult = sendRPCExecuteById(expectedPath);
147223
ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class);
@@ -159,7 +235,7 @@ public void testExecuteNonExistingResourceOnNonExistingObjectById_Result_BAD_REQ
159235
* {"result":"BAD_REQUEST","error":"Specified object id 0 absent in the list supported objects of the client or is security object!"}
160236
*/
161237
@Test
162-
public void testExecuteSecurityObjectById_Result_NOT_FOUND() throws Exception {
238+
public void testExecuteSecurityObjectById_Result_BAD_REQUEST_Error_SpecifiedObjectAbsent() throws Exception {
163239
String expectedPath = objectIdVer_0 + "/" + OBJECT_INSTANCE_ID_0 + "/" + RESOURCE_ID_3;
164240
String actualResult = sendRPCExecuteById(expectedPath);
165241
ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class);
@@ -178,8 +254,26 @@ private String sendRPCExecuteById(String path) throws Exception {
178254
}
179255

180256
private String sendRPCExecuteWithValueById(String path, Object value) throws Exception {
181-
String setRpcRequest = "{\"method\": \"Execute\", \"params\": {\"id\": \"" + path + "\", \"value\": " + value + " }}";
182-
return doPostAsync("/api/plugins/rpc/twoway/" + lwM2MTestClient.getDeviceIdStr(), setRpcRequest, String.class, status().isOk());
257+
ObjectNode params = JacksonUtil.newObjectNode();
258+
params.put("id", path);
259+
260+
// Jackson сам вирішить: ставити лапки (рядок) чи ні (число/boolean/null)
261+
if (value instanceof String) {
262+
params.put("value", (String) value);
263+
} else if (value instanceof Integer) {
264+
params.put("value", (Integer) value);
265+
} else if (value instanceof Boolean) {
266+
params.put("value", (Boolean) value);
267+
} else {
268+
params.set("value", JacksonUtil.valueToTree(value));
269+
}
270+
271+
ObjectNode setRpcRequest = JacksonUtil.newObjectNode();
272+
setRpcRequest.put("method", "Execute");
273+
setRpcRequest.set("params", params);
274+
275+
return doPostAsync("/api/plugins/rpc/twoway/" + lwM2MTestClient.getDeviceIdStr(),
276+
JacksonUtil.toString(setRpcRequest), String.class, status().isOk());
183277
}
184278

185279
}

common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/downlink/DefaultLwM2mDownlinkMsgHandler.java

Lines changed: 26 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,8 @@
5050
import org.eclipse.leshan.core.request.WriteAttributesRequest;
5151
import org.eclipse.leshan.core.request.WriteCompositeRequest;
5252
import org.eclipse.leshan.core.request.WriteRequest;
53+
import org.eclipse.leshan.core.request.argument.Arguments;
54+
import org.eclipse.leshan.core.request.argument.InvalidArgumentException;
5355
import org.eclipse.leshan.core.request.exception.ClientSleepingException;
5456
import org.eclipse.leshan.core.request.exception.InvalidRequestException;
5557
import org.eclipse.leshan.core.request.exception.TimeoutException;
@@ -116,6 +118,7 @@
116118
import static org.thingsboard.server.common.transport.util.JsonUtils.isBase64;
117119
import static org.thingsboard.server.transport.lwm2m.utils.LwM2MTransportUtil.convertMultiResourceValuesFromRpcBody;
118120
import static org.thingsboard.server.transport.lwm2m.utils.LwM2MTransportUtil.createModelsDefault;
121+
import static org.thingsboard.server.transport.lwm2m.utils.LwM2MTransportUtil.equalsResourceTypeGetSimpleName;
119122
import static org.thingsboard.server.transport.lwm2m.utils.LwM2MTransportUtil.fromVersionedIdToObjectId;
120123
import static org.thingsboard.server.transport.lwm2m.utils.LwM2MTransportUtil.getVerFromPathIdVerOrId;
121124
import static org.thingsboard.server.transport.lwm2m.utils.LwM2MTransportUtil.validateVersionedId;
@@ -268,29 +271,34 @@ public void sendExecuteRequest(LwM2mClient client, TbLwM2MExecuteRequest request
268271
validateVersionedId(client, request);
269272
LwM2mPath pathIds = new LwM2mPath(fromVersionedIdToObjectId(request.getVersionedId()));
270273
ResourceModel resourceModelExecute = client.getResourceModel(request.getVersionedId(), modelProvider);
271-
if (resourceModelExecute == null) {
272-
LwM2mModel model = createModelsDefault();
273-
if (pathIds.isResource()) {
274-
resourceModelExecute = model.getResourceModel(pathIds.getObjectId(), pathIds.getResourceId());
275-
}
274+
if (resourceModelExecute == null && pathIds.isResource()) {
275+
resourceModelExecute = createModelsDefault().getResourceModel(pathIds.getObjectId(), pathIds.getResourceId());
276276
}
277277
if (resourceModelExecute == null) {
278-
callback.onValidationError(request.toString(), "ResourceModel with " + request.getVersionedId() +
279-
" is absent in system. Need ddd Lwm2m Model with id=" + pathIds.getObjectId() + " ver=" +
280-
getVerFromPathIdVerOrId(request.getVersionedId()) + " to profile.");
281-
} else if (resourceModelExecute.operations.isExecutable()) {
282-
ExecuteRequest downlink;
283-
if (request.getParams() != null && !resourceModelExecute.multiple) {
284-
downlink = new ExecuteRequest(request.getObjectId(), (String) this.converter.convertValue(request.getParams(),
285-
resourceModelExecute.type, ResourceModel.Type.STRING, new LwM2mPath(request.getObjectId())));
286-
} else {
287-
downlink = new ExecuteRequest(request.getObjectId());
278+
throw new InvalidArgumentException(String.format("ResourceModel with %s is absent in the system. Need to add Model with id= %s ver=%s to profile.",
279+
request.getVersionedId(), pathIds.getObjectId(), getVerFromPathIdVerOrId(request.getVersionedId())));
280+
}
281+
if (!resourceModelExecute.operations.isExecutable()) {
282+
throw new InvalidArgumentException(String.format("Resource with %s is not executable.", request.getVersionedId()));
283+
}
284+
285+
ExecuteRequest downlink;
286+
Object params = request.getParams();
287+
// 4. Handle parameters if they exist and the resource is not a multiple-instance resource
288+
if (params != null && !resourceModelExecute.multiple) {
289+
ResourceModel.Type resourceModelType = equalsResourceTypeGetSimpleName(params);
290+
if (resourceModelType == null) {
291+
throw new InvalidArgumentException(String.format("Unsupported parameter type: %s. Only simple types (String, Integer, Boolean, etc.) are allowed for Execute arguments.",
292+
params.getClass().getSimpleName()));
288293
}
289-
sendSimpleRequest(client, downlink, request.getTimeout(), callback);
294+
String args = (String) this.converter.convertValue(params, resourceModelType, ResourceModel.Type.STRING, pathIds);
295+
downlink = new ExecuteRequest(request.getObjectId(), args);
290296
} else {
291-
callback.onValidationError(request.toString(), "Resource with " + request.getVersionedId() + " is not executable.");
297+
downlink = new ExecuteRequest(request.getObjectId());
292298
}
293-
} catch (InvalidRequestException e) {
299+
sendSimpleRequest(client, downlink, request.getTimeout(), callback);
300+
} catch (Exception e) {
301+
log.error("[{}] Validation failed for Execute request: {}", client.getEndpoint(), e.getMessage());
294302
callback.onValidationError(request.toString(), e.getMessage());
295303
}
296304
}

common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/rpc/DefaultLwM2MRpcRequestHandler.java

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -241,7 +241,13 @@ private void sendDiscoverRequest(LwM2mClient client, TransportProtos.ToDeviceRpc
241241
}
242242

243243
private void sendExecuteRequest(LwM2mClient client, TransportProtos.ToDeviceRpcRequestMsg requestMsg, String versionedId) {
244-
TbLwM2MExecuteRequest downlink = TbLwM2MExecuteRequest.builder().versionedId(versionedId).timeout(clientContext.getRequestTimeout(client)).build();
244+
RpcExecuteRequest requestBody = JacksonUtil.fromString(requestMsg.getParams(), RpcExecuteRequest.class);
245+
Object value = requestBody != null ? requestBody.getValue() : null;
246+
TbLwM2MExecuteRequest downlink = TbLwM2MExecuteRequest.builder()
247+
.versionedId(versionedId)
248+
.params(value)
249+
.timeout(clientContext.getRequestTimeout(client))
250+
.build();
245251
var mainCallback = new TbLwM2MExecuteCallback(logService, client, versionedId);
246252
var rpcCallback = new RpcEmptyResponseCallback<>(transportService, client, requestMsg, mainCallback);
247253
downlinkHandler.sendExecuteRequest(client, downlink, rpcCallback);
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
/**
2+
* Copyright © 2016-2026 The Thingsboard Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.thingsboard.server.transport.lwm2m.server.rpc;
17+
18+
import lombok.Data;
19+
import lombok.EqualsAndHashCode;
20+
21+
@Data
22+
@EqualsAndHashCode(callSuper = true)
23+
public class RpcExecuteRequest extends LwM2MRpcRequestHeader {
24+
25+
private Object value;
26+
27+
}

0 commit comments

Comments
 (0)