Repository: eclipse/leshan
Pull request #971 (2021-02-10 16:22:54)

Add Send Operation
This aims to add support of **Send Operation** to Leshan.

See LWM2M specification for more details : 
- [core§6.4.6-Send-Operation](http://www.openmobilealliance.org/release/LightweightM2M/V1_1_1-20190617-A/HTML-Version/OMA-TS-LightweightM2M_Core-V1_1_1-20190617-A.html#6-4-6-0-646-Send-Operation)
 - [transport§6.4.5-Infomation-Reporting-Interface](http://www.openmobilealliance.org/release/LightweightM2M/V1_1_1-20190617-A/HTML-Version/OMA-TS-LightweightM2M_Transport-V1_1_1-20190617-A.html#6-4-5-0-645-Information-Reporting-Interface)
 
 The API look like this :

**Client Side:**
 ```java
// send data
SendResponse r = client.sendData (server,
                                   ContentFormat.SENML_JSON, 
                                   Arrays.asList("/3/0/1", "/3/0/2"),
                                   timeoutInMs);
 ```
 
 **Server Side:**
 ```java
// listen for new data
server.getSendService().addListener(new SendListener() {
   @Override
   public void dataReceived(Registration registration, Map<LwM2mPath, LwM2mNode> data, SendRequest request) {
       System.out.println(data);
   }
});
 ```
 
 List of concerns or potential improvements : 
 - at client side, add more low level function allowing to send `SendRequest` directly.
 - at server side, current Map<LwM2mPath, LwM2mNode> data is not so good. LwM2mPath is largely used internally but generally not so exposed. (we have same problem with WriteComposite)
 - at server side, if an error occurred when handling `SendRequest`, this is not reported to application layer, it should maybe be reported in `SendListener`.
 - at server side, in addition of listener maybe we should add a kind of DataCollector which could be able to report the data handling state. This could be useful to let device know when data was not collected/handled correctly. 


Patch:
diff --git a/leshan-client-cf/src/main/java/org/eclipse/leshan/client/californium/LeshanClient.java b/leshan-client-cf/src/main/java/org/eclipse/leshan/client/californium/LeshanClient.java
index d52fcc6b..4674e709 100644
--- a/leshan-client-cf/src/main/java/org/eclipse/leshan/client/californium/LeshanClient.java
+++ b/leshan-client-cf/src/main/java/org/eclipse/leshan/client/californium/LeshanClient.java
@@ -46,10 +46,21 @@ import org.eclipse.leshan.client.resource.LwM2mRootEnabler;
 import org.eclipse.leshan.client.resource.RootEnabler;
 import org.eclipse.leshan.client.resource.listener.ObjectListener;
 import org.eclipse.leshan.client.resource.listener.ObjectsListenerAdapter;
+import org.eclipse.leshan.client.send.NoDataException;
 import org.eclipse.leshan.client.servers.ServerIdentity;
 import org.eclipse.leshan.core.californium.EndpointFactory;
+import org.eclipse.leshan.core.model.LwM2mModel;
+import org.eclipse.leshan.core.node.LwM2mNode;
+import org.eclipse.leshan.core.node.LwM2mPath;
 import org.eclipse.leshan.core.node.codec.LwM2mNodeDecoder;
 import org.eclipse.leshan.core.node.codec.LwM2mNodeEncoder;
+import org.eclipse.leshan.core.request.ContentFormat;
+import org.eclipse.leshan.core.request.ReadCompositeRequest;
+import org.eclipse.leshan.core.request.SendRequest;
+import org.eclipse.leshan.core.response.ErrorCallback;
+import org.eclipse.leshan.core.response.ReadCompositeResponse;
+import org.eclipse.leshan.core.response.ResponseCallback;
+import org.eclipse.leshan.core.response.SendResponse;
 import org.eclipse.leshan.core.util.Validate;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -113,7 +124,7 @@ public class LeshanClient implements LwM2mClient {
         bootstrapHandler = createBoostrapHandler(objectTree);
         endpointsManager = createEndpointsManager(localAddress, coapConfig, dtlsConfigBuilder, trustStore,
                 endpointFactory);
-        requestSender = createRequestSender(endpointsManager, sharedExecutor);
+        requestSender = createRequestSender(endpointsManager, sharedExecutor, encoder, objectTree.getModel());
         engine = engineFactory.createRegistratioEngine(endpoint, objectTree, endpointsManager, requestSender,
                 bootstrapHandler, observers, additionalAttributes, bsAdditionalAttributes, sharedExecutor);
 
@@ -218,8 +229,8 @@ public class LeshanClient implements LwM2mClient {
     }
 
     protected CaliforniumLwM2mRequestSender createRequestSender(CaliforniumEndpointsManager endpointsManager,
-            ScheduledExecutorService executor) {
-        return new CaliforniumLwM2mRequestSender(endpointsManager, executor);
+            ScheduledExecutorService executor, LwM2mNodeEncoder encoder, LwM2mModel model) {
+        return new CaliforniumLwM2mRequestSender(endpointsManager, executor, encoder, model);
     }
 
     protected RegistrationUpdateHandler createRegistrationUpdateHandler(RegistrationEngine engine,
@@ -278,6 +289,39 @@ public class LeshanClient implements LwM2mClient {
         engine.triggerRegistrationUpdate(server);
     }
 
+    @Override
+    public SendResponse sendData(ServerIdentity server, ContentFormat format, List<String> paths, long timeoutInMs)
+            throws InterruptedException {
+        Validate.notNull(server);
+        Validate.notEmpty(paths);
+
+        Map<LwM2mPath, LwM2mNode> collectedData = collectData(server, paths);
+        return requestSender.send(server, new SendRequest(format, collectedData, null), 2000);
+    }
+
+    @Override
+    public void sendData(ServerIdentity server, ContentFormat format, List<String> paths, long timeoutInMs,
+            ResponseCallback<SendResponse> onResponse, ErrorCallback onError) {
+        Validate.notNull(server);
+        Validate.notEmpty(paths);
+        Validate.notNull(onResponse);
+        Validate.notNull(onError);
+
+        Map<LwM2mPath, LwM2mNode> collectedData = collectData(server, paths);
+        requestSender.send(server, new SendRequest(format, collectedData, null), 2000, onResponse, onError);
+    }
+
+    private Map<LwM2mPath, LwM2mNode> collectData(ServerIdentity server, List<String> paths) {
+        // format is not really used as this is an internal call, kind of HACK :/ ...
+        ContentFormat format = ContentFormat.SENML_CBOR;
+        ReadCompositeResponse response = rootEnabler.read(server, new ReadCompositeRequest(format, format, paths));
+        if (response.isSuccess()) {
+            return response.getContent();
+        }
+        throw new NoDataException("Unable to collect data for %s : %s / %s", paths, response.getCode(),
+                response.getErrorMessage());
+    }
+
     /**
      * A CoAP API, generally needed if you want to access to underlying CoAP layer.
      */
diff --git a/leshan-client-cf/src/main/java/org/eclipse/leshan/client/californium/request/CaliforniumLwM2mRequestSender.java b/leshan-client-cf/src/main/java/org/eclipse/leshan/client/californium/request/CaliforniumLwM2mRequestSender.java
index a568806d..6beb92d5 100644
--- a/leshan-client-cf/src/main/java/org/eclipse/leshan/client/californium/request/CaliforniumLwM2mRequestSender.java
+++ b/leshan-client-cf/src/main/java/org/eclipse/leshan/client/californium/request/CaliforniumLwM2mRequestSender.java
@@ -27,6 +27,8 @@ import org.eclipse.leshan.client.request.LwM2mRequestSender;
 import org.eclipse.leshan.client.servers.ServerIdentity;
 import org.eclipse.leshan.core.californium.AsyncRequestObserver;
 import org.eclipse.leshan.core.californium.SyncRequestObserver;
+import org.eclipse.leshan.core.model.LwM2mModel;
+import org.eclipse.leshan.core.node.codec.LwM2mNodeEncoder;
 import org.eclipse.leshan.core.request.UplinkRequest;
 import org.eclipse.leshan.core.response.ErrorCallback;
 import org.eclipse.leshan.core.response.LwM2mResponse;
@@ -45,9 +47,11 @@ public class CaliforniumLwM2mRequestSender implements LwM2mRequestSender {
     private final ScheduledExecutorService executor;
     private final boolean attached;
     private final CaliforniumEndpointsManager endpointsManager;
+    private final LwM2mNodeEncoder encoder;
+    private final LwM2mModel model;
 
     public CaliforniumLwM2mRequestSender(CaliforniumEndpointsManager endpointsManager,
-            ScheduledExecutorService sharedExecutor) {
+            ScheduledExecutorService sharedExecutor, LwM2mNodeEncoder encoder, LwM2mModel model) {
         this.endpointsManager = endpointsManager;
         if (sharedExecutor == null) {
             this.executor = Executors.newScheduledThreadPool(1, new NamedThreadFactory("Leshan Async Request timeout"));
@@ -56,13 +60,15 @@ public class CaliforniumLwM2mRequestSender implements LwM2mRequestSender {
             this.executor = sharedExecutor;
             this.attached = false;
         }
+        this.model = model;
+        this.encoder = encoder;
     }
 
     @Override
     public <T extends LwM2mResponse> T send(ServerIdentity server, final UplinkRequest<T> request, long timeout)
             throws InterruptedException {
         // Create the CoAP request from LwM2m request
-        CoapRequestBuilder coapClientRequestBuilder = new CoapRequestBuilder(server.getIdentity());
+        CoapRequestBuilder coapClientRequestBuilder = new CoapRequestBuilder(server.getIdentity(), encoder, model);
         request.accept(coapClientRequestBuilder);
         Request coapRequest = coapClientRequestBuilder.getRequest();
 
@@ -89,7 +95,7 @@ public class CaliforniumLwM2mRequestSender implements LwM2mRequestSender {
     public <T extends LwM2mResponse> void send(ServerIdentity server, final UplinkRequest<T> request, long timeout,
             ResponseCallback<T> responseCallback, ErrorCallback errorCallback) {
         // Create the CoAP request from LwM2m request
-        CoapRequestBuilder coapClientRequestBuilder = new CoapRequestBuilder(server.getIdentity());
+        CoapRequestBuilder coapClientRequestBuilder = new CoapRequestBuilder(server.getIdentity(), encoder, model);
         request.accept(coapClientRequestBuilder);
         Request coapRequest = coapClientRequestBuilder.getRequest();
 
diff --git a/leshan-client-cf/src/main/java/org/eclipse/leshan/client/californium/request/CoapRequestBuilder.java b/leshan-client-cf/src/main/java/org/eclipse/leshan/client/californium/request/CoapRequestBuilder.java
index 8d661a1a..c85bb6f8 100644
--- a/leshan-client-cf/src/main/java/org/eclipse/leshan/client/californium/request/CoapRequestBuilder.java
+++ b/leshan-client-cf/src/main/java/org/eclipse/leshan/client/californium/request/CoapRequestBuilder.java
@@ -23,12 +23,15 @@ import org.eclipse.californium.core.coap.Request;
 import org.eclipse.californium.elements.EndpointContext;
 import org.eclipse.leshan.core.Link;
 import org.eclipse.leshan.core.californium.EndpointContextUtil;
+import org.eclipse.leshan.core.model.LwM2mModel;
+import org.eclipse.leshan.core.node.codec.LwM2mNodeEncoder;
 import org.eclipse.leshan.core.request.BindingMode;
 import org.eclipse.leshan.core.request.BootstrapRequest;
 import org.eclipse.leshan.core.request.ContentFormat;
 import org.eclipse.leshan.core.request.DeregisterRequest;
 import org.eclipse.leshan.core.request.Identity;
 import org.eclipse.leshan.core.request.RegisterRequest;
+import org.eclipse.leshan.core.request.SendRequest;
 import org.eclipse.leshan.core.request.UpdateRequest;
 import org.eclipse.leshan.core.request.UplinkRequest;
 import org.eclipse.leshan.core.request.UplinkRequestVisitor;
@@ -42,9 +45,13 @@ public class CoapRequestBuilder implements UplinkRequestVisitor {
 
     protected Request coapRequest;
     protected final Identity server;
+    protected final LwM2mNodeEncoder encoder;
+    protected final LwM2mModel model;
 
-    public CoapRequestBuilder(Identity server) {
+    public CoapRequestBuilder(Identity server, LwM2mNodeEncoder encoder, LwM2mModel model) {
         this.server = server;
+        this.encoder = encoder;
+        this.model = model;
     }
 
     @Override
@@ -143,6 +150,17 @@ public class CoapRequestBuilder implements UplinkRequestVisitor {
         coapRequest.getOptions().setUriPath(request.getRegistrationId());
     }
 
+    @Override
+    public void visit(SendRequest request) {
+        coapRequest = Request.newPost();
+        buildRequestSettings();
+        coapRequest.getOptions().setUriPath("/dp");
+
+        ContentFormat format = request.getFormat();
+        coapRequest.getOptions().setContentFormat(format.getCode());
+        coapRequest.setPayload(encoder.encodeNodes(request.getNodes(), format, model));
+    }
+
     public Request getRequest() {
         return coapRequest;
     }
diff --git a/leshan-client-cf/src/main/java/org/eclipse/leshan/client/californium/request/LwM2mClientResponseBuilder.java b/leshan-client-cf/src/main/java/org/eclipse/leshan/client/californium/request/LwM2mClientResponseBuilder.java
index a7ea6cba..19d67b6f 100644
--- a/leshan-client-cf/src/main/java/org/eclipse/leshan/client/californium/request/LwM2mClientResponseBuilder.java
+++ b/leshan-client-cf/src/main/java/org/eclipse/leshan/client/californium/request/LwM2mClientResponseBuilder.java
@@ -22,6 +22,7 @@ import org.eclipse.leshan.core.request.BootstrapRequest;
 import org.eclipse.leshan.core.request.DeregisterRequest;
 import org.eclipse.leshan.core.request.LwM2mRequest;
 import org.eclipse.leshan.core.request.RegisterRequest;
+import org.eclipse.leshan.core.request.SendRequest;
 import org.eclipse.leshan.core.request.UpdateRequest;
 import org.eclipse.leshan.core.request.UplinkRequestVisitor;
 import org.eclipse.leshan.core.request.exception.InvalidResponseException;
@@ -29,6 +30,7 @@ import org.eclipse.leshan.core.response.BootstrapResponse;
 import org.eclipse.leshan.core.response.DeregisterResponse;
 import org.eclipse.leshan.core.response.LwM2mResponse;
 import org.eclipse.leshan.core.response.RegisterResponse;
+import org.eclipse.leshan.core.response.SendResponse;
 import org.eclipse.leshan.core.response.UpdateResponse;
 
 /**
@@ -93,6 +95,21 @@ public class LwM2mClientResponseBuilder<T extends LwM2mResponse> implements Upli
         }
     }
 
+    @Override
+    public void visit(SendRequest request) {
+        if (coapResponse.isError()) {
+            // handle error response:
+            lwM2mresponse = new SendResponse(toLwM2mResponseCode(coapResponse.getCode()),
+                    coapResponse.getPayloadString());
+        } else if (coapResponse.getCode() == org.eclipse.californium.core.coap.CoAP.ResponseCode.CHANGED) {
+            // handle success response:
+            lwM2mresponse = SendResponse.success();
+        } else {
+            // handle unexpected response:
+            handleUnexpectedResponseCode(request, coapResponse);
+        }
+    }
+
     @Override
     public void visit(BootstrapRequest request) {
         if (coapResponse.isError()) {
diff --git a/leshan-client-core/src/main/java/org/eclipse/leshan/client/LwM2mClient.java b/leshan-client-core/src/main/java/org/eclipse/leshan/client/LwM2mClient.java
index 1feb7480..8f2f3442 100644
--- a/leshan-client-core/src/main/java/org/eclipse/leshan/client/LwM2mClient.java
+++ b/leshan-client-core/src/main/java/org/eclipse/leshan/client/LwM2mClient.java
@@ -15,8 +15,23 @@
  *******************************************************************************/
 package org.eclipse.leshan.client;
 
+import java.util.List;
+
 import org.eclipse.leshan.client.resource.LwM2mObjectTree;
+import org.eclipse.leshan.client.send.NoDataException;
 import org.eclipse.leshan.client.servers.ServerIdentity;
+import org.eclipse.leshan.core.node.codec.CodecException;
+import org.eclipse.leshan.core.request.ContentFormat;
+import org.eclipse.leshan.core.request.exception.InvalidRequestException;
+import org.eclipse.leshan.core.request.exception.InvalidResponseException;
+import org.eclipse.leshan.core.request.exception.RequestCanceledException;
+import org.eclipse.leshan.core.request.exception.RequestRejectedException;
+import org.eclipse.leshan.core.request.exception.SendFailedException;
+import org.eclipse.leshan.core.request.exception.TimeoutException;
+import org.eclipse.leshan.core.request.exception.UnconnectedPeerException;
+import org.eclipse.leshan.core.response.ErrorCallback;
+import org.eclipse.leshan.core.response.ResponseCallback;
+import org.eclipse.leshan.core.response.SendResponse;
 
 /**
  * A Lightweight M2M client.
@@ -52,6 +67,75 @@ public interface LwM2mClient {
      */
     void triggerRegistrationUpdate(ServerIdentity server);
 
+    /**
+     * Send Data synchronously to a LWM2M Server.
+     * <p>
+     * The "Send" operation is used by the LwM2M Client to send data to the LwM2M Server without explicit request by
+     * that Server.
+     * <p>
+     * If some data can not be collected before to send, this will be silently ignored.<br>
+     * If there is not data to send at all, {@link NoDataException} is raised.
+     * 
+     * @param server to which data must be send
+     * @param format {@link ContentFormat} to use. It MUST be {@link ContentFormat#SENML_CBOR} or
+     *        {@link ContentFormat#SENML_JSON}
+     * @param paths the list of LWM2M node path to send.
+     * @param timeoutInMs The global timeout to wait in milliseconds (see
+     *        https://github.com/eclipse/leshan/wiki/Request-Timeout)
+     * @return the LWM2M response. The response can be <code>null</code> if the timeout expires (see
+     *         https://github.com/eclipse/leshan/wiki/Request-Timeout).
+     * 
+     * @throws InterruptedException if the thread was interrupted.
+     * @throws InvalidRequestException if send request can not be created.
+     * @throws CodecException if request payload can not be encoded.
+     * @throws NoDataException if we can not collect data for given list of path.
+     * @throws RequestRejectedException if the request is rejected by foreign peer.
+     * @throws RequestCanceledException if the request is cancelled.
+     * @throws SendFailedException if the request can not be sent. E.g. error at CoAP or DTLS/UDP layer.
+     * @throws InvalidResponseException if the response received is malformed.
+     * @throws UnconnectedPeerException if client is not connected (no dtls connection available).
+     */
+    SendResponse sendData(ServerIdentity server, ContentFormat format, List<String> paths, long timeoutInMs)
+            throws InterruptedException;
+
+    /**
+     * Send Data asynchronously to a LWM2M Server.
+     * <p>
+     * The "Send" operation is used by the LwM2M Client to send data to the LwM2M Server without explicit request by
+     * that Server.
+     * <p>
+     * If some data can not be collected before to send, this will be silently ignored.<br>
+     * If there is not data to send at all, {@link NoDataException} is raised.
+     * <p>
+     * {@link ResponseCallback} and {@link ErrorCallback} are exclusively called.
+     * 
+     * @param server to which data must be send
+     * @param format {@link ContentFormat} to use. It MUST be {@link ContentFormat#SENML_CBOR} or
+     *        {@link ContentFormat#SENML_JSON}
+     * @param paths the list of LWM2M node path to send.
+     * @param timeoutInMs The global timeout to wait in milliseconds (see
+     *        https://github.com/eclipse/leshan/wiki/Request-Timeout)
+     * @param responseCallback a callback called when a response is received (successful or error response). This
+     *        callback MUST NOT be null.
+     * @param errorCallback a callback called when an error or exception occurred when response is received. It can be :
+     *        <ul>
+     *        <li>{@link RequestRejectedException} if the request is rejected by foreign peer.</li>
+     *        <li>{@link RequestCanceledException} if the request is cancelled.</li>
+     *        <li>{@link SendFailedException} if the request can not be sent. E.g. error at CoAP or DTLS/UDP layer.</li>
+     *        <li>{@link InvalidResponseException} if the response received is malformed.</li>
+     *        <li>{@link UnconnectedPeerException} if client is not connected (no dtls connection available).</li>
+     *        <li>{@link TimeoutException} if the timeout expires (see
+     *        https://github.com/eclipse/leshan/wiki/Request-Timeout).</li>
+     *        <li>or any other RuntimeException for unexpected issue.
+     *        </ul>
+     *        This callback MUST NOT be null.
+     * @throws CodecException if request payload can not be encoded.
+     * @throws NoDataException if we can not collect data for given list of path.
+     * @throws InvalidRequestException if send request can not be created.
+     */
+    void sendData(ServerIdentity server, ContentFormat format, List<String> paths, long timeoutInMs,
+            ResponseCallback<SendResponse> responseCallback, ErrorCallback errorCallback);
+
     /**
      * @return the {@link LwM2mObjectTree} containing all the object implemented by this client.
      */
diff --git a/leshan-client-core/src/main/java/org/eclipse/leshan/client/resource/LwM2mObjectTree.java b/leshan-client-core/src/main/java/org/eclipse/leshan/client/resource/LwM2mObjectTree.java
index e1596020..de877f04 100644
--- a/leshan-client-core/src/main/java/org/eclipse/leshan/client/resource/LwM2mObjectTree.java
+++ b/leshan-client-core/src/main/java/org/eclipse/leshan/client/resource/LwM2mObjectTree.java
@@ -28,6 +28,9 @@ import org.eclipse.leshan.client.resource.listener.ObjectsListener;
 import org.eclipse.leshan.core.Destroyable;
 import org.eclipse.leshan.core.Startable;
 import org.eclipse.leshan.core.Stoppable;
+import org.eclipse.leshan.core.model.LwM2mModel;
+import org.eclipse.leshan.core.model.ObjectModel;
+import org.eclipse.leshan.core.model.ResourceModel;
 import org.eclipse.leshan.core.node.LwM2mPath;
 
 /**
@@ -38,9 +41,10 @@ import org.eclipse.leshan.core.node.LwM2mPath;
  */
 public class LwM2mObjectTree implements Startable, Stoppable, Destroyable {
 
-    protected ObjectListener dispatcher = new ObjectListenerDispatcher();
-    protected CopyOnWriteArrayList<ObjectsListener> listeners = new CopyOnWriteArrayList<>();
-    protected ConcurrentHashMap<Integer, LwM2mObjectEnabler> objectEnablers = new ConcurrentHashMap<>();
+    protected final ObjectListener dispatcher = new ObjectListenerDispatcher();
+    protected final CopyOnWriteArrayList<ObjectsListener> listeners = new CopyOnWriteArrayList<>();
+    protected final ConcurrentHashMap<Integer, LwM2mObjectEnabler> objectEnablers = new ConcurrentHashMap<>();
+    protected final LwM2mModel model;
 
     public LwM2mObjectTree(LwM2mClient client, LwM2mObjectEnabler... enablers) {
         this(client, Arrays.asList(enablers));
@@ -58,6 +62,31 @@ public class LwM2mObjectTree implements Startable, Stoppable, Destroyable {
             enabler.addListener(dispatcher);
             enabler.setLwM2mClient(client);
         }
+
+        this.model = new LwM2mModel() {
+
+            @Override
+            public ResourceModel getResourceModel(int objectId, int resourceId) {
+                ObjectModel objectModel = this.getObjectModel(objectId);
+                if (objectModel != null)
+                    return objectModel.resources.get(resourceId);
+                return null;
+            }
+
+            @Override
+            public Collection<ObjectModel> getObjectModels() {
+                // TODO implements this ?
+                throw new UnsupportedOperationException("Not implemented");
+            }
+
+            @Override
+            public ObjectModel getObjectModel(int objectId) {
+                LwM2mObjectEnabler objectEnabler = getObjectEnabler(objectId);
+                if (objectEnabler != null)
+                    return objectEnabler.getObjectModel();
+                return null;
+            }
+        };
     }
 
     public void addListener(ObjectsListener listener) {
@@ -68,6 +97,10 @@ public class LwM2mObjectTree implements Startable, Stoppable, Destroyable {
         listeners.remove(listener);
     }
 
+    public LwM2mModel getModel() {
+        return model;
+    }
+
     public Map<Integer, LwM2mObjectEnabler> getObjectEnablers() {
         return Collections.unmodifiableMap(objectEnablers);
     }
@@ -161,5 +194,4 @@ public class LwM2mObjectTree implements Startable, Stoppable, Destroyable {
             enabler.endTransaction(LwM2mPath.ROOT_DEPTH);
         }
     }
-
 }
diff --git a/leshan-client-core/src/main/java/org/eclipse/leshan/client/resource/RootEnabler.java b/leshan-client-core/src/main/java/org/eclipse/leshan/client/resource/RootEnabler.java
index e4b3a51e..649db34e 100644
--- a/leshan-client-core/src/main/java/org/eclipse/leshan/client/resource/RootEnabler.java
+++ b/leshan-client-core/src/main/java/org/eclipse/leshan/client/resource/RootEnabler.java
@@ -15,7 +15,6 @@
  *******************************************************************************/
 package org.eclipse.leshan.client.resource;
 
-import java.util.Collection;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
@@ -52,34 +51,9 @@ public class RootEnabler implements LwM2mRootEnabler {
     private static final Logger LOG = LoggerFactory.getLogger(RootEnabler.class);
 
     private final LwM2mObjectTree tree;
-    private final LwM2mModel model;
 
     public RootEnabler(final LwM2mObjectTree tree) {
         this.tree = tree;
-        this.model = new LwM2mModel() {
-
-            @Override
-            public ResourceModel getResourceModel(int objectId, int resourceId) {
-                ObjectModel objectModel = this.getObjectModel(objectId);
-                if (objectModel != null)
-                    return objectModel.resources.get(resourceId);
-                return null;
-            }
-
-            @Override
-            public Collection<ObjectModel> getObjectModels() {
-                // TODO implements this ?
-                throw new UnsupportedOperationException("Not implemented");
-            }
-
-            @Override
-            public ObjectModel getObjectModel(int objectId) {
-                LwM2mObjectEnabler objectEnabler = tree.getObjectEnabler(objectId);
-                if (objectEnabler != null)
-                    return objectEnabler.getObjectModel();
-                return null;
-            }
-        };
     }
 
     @Override
@@ -221,6 +195,6 @@ public class RootEnabler implements LwM2mRootEnabler {
 
     @Override
     public LwM2mModel getModel() {
-        return model;
+        return tree.getModel();
     }
 }
diff --git a/leshan-client-core/src/main/java/org/eclipse/leshan/client/send/NoDataException.java b/leshan-client-core/src/main/java/org/eclipse/leshan/client/send/NoDataException.java
new file mode 100644
index 00000000..8d7d2ac7
--- /dev/null
+++ b/leshan-client-core/src/main/java/org/eclipse/leshan/client/send/NoDataException.java
@@ -0,0 +1,46 @@
+/*******************************************************************************
+ * Copyright (c) 2021 Sierra Wireless and others.
+ * 
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v2.0
+ * and Eclipse Distribution License v1.0 which accompany this distribution.
+ * 
+ * The Eclipse Public License is available at
+ *    http://www.eclipse.org/legal/epl-v20.html
+ * and the Eclipse Distribution License is available at
+ *    http://www.eclipse.org/org/documents/edl-v10.html.
+ * 
+ * Contributors:
+ *     Sierra Wireless - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.leshan.client.send;
+
+/**
+ * Raised when no data can be collected before to send data.
+ */
+public class NoDataException extends RuntimeException {
+    private static final long serialVersionUID = 1L;
+
+    public NoDataException() {
+    }
+
+    public NoDataException(String m) {
+        super(m);
+    }
+
+    public NoDataException(String m, Object... args) {
+        super(String.format(m, args));
+    }
+
+    public NoDataException(Throwable e) {
+        super(e);
+    }
+
+    public NoDataException(String m, Throwable e) {
+        super(m, e);
+    }
+
+    public NoDataException(Throwable e, String m, Object... args) {
+        super(String.format(m, args), e);
+    }
+}
diff --git a/leshan-core/src/main/java/org/eclipse/leshan/core/request/SendRequest.java b/leshan-core/src/main/java/org/eclipse/leshan/core/request/SendRequest.java
new file mode 100644
index 00000000..7417b35d
--- /dev/null
+++ b/leshan-core/src/main/java/org/eclipse/leshan/core/request/SendRequest.java
@@ -0,0 +1,142 @@
+/*******************************************************************************
+ * Copyright (c) 2021 Sierra Wireless and others.
+ * 
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v2.0
+ * and Eclipse Distribution License v1.0 which accompany this distribution.
+ * 
+ * The Eclipse Public License is available at
+ *    http://www.eclipse.org/legal/epl-v20.html
+ * and the Eclipse Distribution License is available at
+ *    http://www.eclipse.org/org/documents/edl-v10.html.
+ * 
+ * Contributors:
+ *     Sierra Wireless - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.leshan.core.request;
+
+import java.util.Map;
+import java.util.Map.Entry;
+
+import org.eclipse.leshan.core.node.LwM2mNode;
+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.LwM2mResourceInstance;
+import org.eclipse.leshan.core.node.LwM2mSingleResource;
+import org.eclipse.leshan.core.request.exception.InvalidRequestException;
+import org.eclipse.leshan.core.response.SendResponse;
+import org.eclipse.leshan.core.util.Validate;
+
+/**
+ * The "Send" operation is used by the LwM2M Client to send data to the LwM2M Server without explicit request by that
+ * Server.
+ * <p>
+ * The "Send" operation can be used by the LwM2M Client to report values for Resources and Resource Instances of LwM2M
+ * Object Instance(s) to the LwM2M Server.
+ */
+public class SendRequest implements UplinkRequest<SendResponse> {
+
+    private final ContentFormat format;
+    private final Map<LwM2mPath, LwM2mNode> nodes;
+    private final Object coapRequest;
+
+    /**
+     * @param format {@link ContentFormat} used to encode data. It MUST be {@link ContentFormat#SENML_CBOR} or
+     *        {@link ContentFormat#SENML_JSON}
+     * @param nodes The {@link LwM2mNode} to write as a the Map of string path to {@link LwM2mNode}. Value can not be
+     *        <code>null</code>.
+     */
+    public SendRequest(ContentFormat format, Map<LwM2mPath, LwM2mNode> nodes) {
+        this(format, nodes, null);
+    }
+
+    public SendRequest(ContentFormat format, Map<LwM2mPath, LwM2mNode> nodes, Object coapRequest) {
+        // Validate Format
+        if (format == null || !(format.equals(ContentFormat.SENML_CBOR) || format.equals(ContentFormat.SENML_JSON))) {
+            throw new InvalidRequestException("Content format MUST be SenML_CBOR or SenML_JSON but was " + format);
+        }
+        // Validate Nodes
+        validateNodes(nodes);
+
+        this.format = format;
+        this.nodes = nodes;
+        this.coapRequest = coapRequest;
+    }
+
+    private void validateNodes(Map<LwM2mPath, LwM2mNode> nodes) {
+        Validate.notEmpty(nodes);
+        for (Entry<LwM2mPath, LwM2mNode> entry : nodes.entrySet()) {
+            LwM2mPath path = entry.getKey();
+            LwM2mNode node = entry.getValue();
+            Validate.notNull(path);
+            Validate.notNull(node);
+
+            if (path.isObject() && node instanceof LwM2mObject)
+                return;
+            if (path.isObjectInstance() && node instanceof LwM2mObjectInstance)
+                return;
+            if (path.isResource() && node instanceof LwM2mSingleResource)
+                return;
+            if (path.isResourceInstance() && node instanceof LwM2mResourceInstance)
+                return;
+
+            throw new InvalidRequestException("Invalid value : path (%s) should not refer to a %s value", path,
+                    node.getClass().getSimpleName());
+        }
+    }
+
+    @Override
+    public Object getCoapRequest() {
+        return coapRequest;
+    }
+
+    public Map<LwM2mPath, LwM2mNode> getNodes() {
+        return nodes;
+    }
+
+    public ContentFormat getFormat() {
+        return format;
+    }
+
+    @Override
+    public void accept(UplinkRequestVisitor visitor) {
+        visitor.visit(this);
+    }
+
+    @Override
+    public String toString() {
+        return String.format("SendRequest [format=%s, nodes=%s]", format, nodes);
+    }
+
+    @Override
+    public int hashCode() {
+        final int prime = 31;
+        int result = 1;
+        result = prime * result + ((format == null) ? 0 : format.hashCode());
+        result = prime * result + ((nodes == null) ? 0 : nodes.hashCode());
+        return result;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj)
+            return true;
+        if (obj == null)
+            return false;
+        if (getClass() != obj.getClass())
+            return false;
+        SendRequest other = (SendRequest) obj;
+        if (format == null) {
+            if (other.format != null)
+                return false;
+        } else if (!format.equals(other.format))
+            return false;
+        if (nodes == null) {
+            if (other.nodes != null)
+                return false;
+        } else if (!nodes.equals(other.nodes))
+            return false;
+        return true;
+    }
+}
diff --git a/leshan-core/src/main/java/org/eclipse/leshan/core/request/UplinkRequestVisitor.java b/leshan-core/src/main/java/org/eclipse/leshan/core/request/UplinkRequestVisitor.java
index 4a614c97..7ebc46bc 100644
--- a/leshan-core/src/main/java/org/eclipse/leshan/core/request/UplinkRequestVisitor.java
+++ b/leshan-core/src/main/java/org/eclipse/leshan/core/request/UplinkRequestVisitor.java
@@ -26,4 +26,6 @@ public interface UplinkRequestVisitor {
     void visit(DeregisterRequest request);
 
     void visit(BootstrapRequest request);
+
+    void visit(SendRequest sendRequest);
 }
diff --git a/leshan-core/src/main/java/org/eclipse/leshan/core/response/SendResponse.java b/leshan-core/src/main/java/org/eclipse/leshan/core/response/SendResponse.java
new file mode 100644
index 00000000..7a1d819e
--- /dev/null
+++ b/leshan-core/src/main/java/org/eclipse/leshan/core/response/SendResponse.java
@@ -0,0 +1,63 @@
+/*******************************************************************************
+ * Copyright (c) 2021 Sierra Wireless and others.
+ * 
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v2.0
+ * and Eclipse Distribution License v1.0 which accompany this distribution.
+ * 
+ * The Eclipse Public License is available at
+ *    http://www.eclipse.org/legal/epl-v20.html
+ * and the Eclipse Distribution License is available at
+ *    http://www.eclipse.org/org/documents/edl-v10.html.
+ * 
+ * Contributors:
+ *     Sierra Wireless - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.leshan.core.response;
+
+import org.eclipse.leshan.core.ResponseCode;
+
+public class SendResponse extends AbstractLwM2mResponse {
+
+    public SendResponse(ResponseCode code, String errorMessage) {
+        this(code, errorMessage, null);
+    }
+
+    public SendResponse(ResponseCode code, String errorMessage, Object coapResponse) {
+        super(code, errorMessage, coapResponse);
+    }
+
+    @Override
+    public boolean isSuccess() {
+        return getCode() == ResponseCode.CHANGED;
+    }
+
+    @Override
+    public boolean isValid() {
+        switch (code.getCode()) {
+        case ResponseCode.CHANGED_CODE:
+        case ResponseCode.BAD_REQUEST_CODE:
+        case ResponseCode.NOT_FOUND_CODE:
+        case ResponseCode.INTERNAL_SERVER_ERROR_CODE:
+            return true;
+        default:
+            return false;
+        }
+    }
+
+    public static SendResponse success() {
+        return new SendResponse(ResponseCode.CHANGED, null);
+    }
+
+    public static SendResponse badRequest(String errorMessage) {
+        return new SendResponse(ResponseCode.BAD_REQUEST, errorMessage);
+    }
+
+    public static SendResponse notFound() {
+        return new SendResponse(ResponseCode.NOT_FOUND, null);
+    }
+
+    public static SendResponse internalServerError(String errorMessage) {
+        return new SendResponse(ResponseCode.INTERNAL_SERVER_ERROR, errorMessage);
+    }
+}
diff --git a/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/LeshanServer.java b/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/LeshanServer.java
index 67e81c51..5c393dbd 100644
--- a/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/LeshanServer.java
+++ b/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/LeshanServer.java
@@ -28,12 +28,16 @@ import org.eclipse.californium.core.network.Endpoint;
 import org.eclipse.californium.core.network.config.NetworkConfig;
 import org.eclipse.californium.core.server.resources.Resource;
 import org.eclipse.californium.scandium.DTLSConnector;
+import org.eclipse.leshan.core.Destroyable;
+import org.eclipse.leshan.core.Startable;
+import org.eclipse.leshan.core.Stoppable;
 import org.eclipse.leshan.core.californium.CoapResponseCallback;
 import org.eclipse.leshan.core.node.codec.CodecException;
 import org.eclipse.leshan.core.node.codec.LwM2mNodeDecoder;
 import org.eclipse.leshan.core.node.codec.LwM2mNodeEncoder;
 import org.eclipse.leshan.core.observation.Observation;
 import org.eclipse.leshan.core.request.DownlinkRequest;
+import org.eclipse.leshan.core.request.SendRequest;
 import org.eclipse.leshan.core.request.exception.ClientSleepingException;
 import org.eclipse.leshan.core.request.exception.InvalidResponseException;
 import org.eclipse.leshan.core.request.exception.RequestCanceledException;
@@ -44,15 +48,13 @@ import org.eclipse.leshan.core.response.ErrorCallback;
 import org.eclipse.leshan.core.response.LwM2mResponse;
 import org.eclipse.leshan.core.response.ResponseCallback;
 import org.eclipse.leshan.core.util.Validate;
-import org.eclipse.leshan.core.Destroyable;
-import org.eclipse.leshan.core.Startable;
-import org.eclipse.leshan.core.Stoppable;
 import org.eclipse.leshan.server.californium.observation.ObservationServiceImpl;
 import org.eclipse.leshan.server.californium.registration.CaliforniumRegistrationStore;
 import org.eclipse.leshan.server.californium.registration.RegisterResource;
 import org.eclipse.leshan.server.californium.request.CaliforniumLwM2mRequestSender;
 import org.eclipse.leshan.server.californium.request.CaliforniumQueueModeRequestSender;
 import org.eclipse.leshan.server.californium.request.CoapRequestSender;
+import org.eclipse.leshan.server.californium.send.SendResource;
 import org.eclipse.leshan.server.model.LwM2mModelProvider;
 import org.eclipse.leshan.server.observation.ObservationService;
 import org.eclipse.leshan.server.queue.ClientAwakeTimeProvider;
@@ -75,6 +77,8 @@ import org.eclipse.leshan.server.security.EditableSecurityStore;
 import org.eclipse.leshan.server.security.SecurityInfo;
 import org.eclipse.leshan.server.security.SecurityStore;
 import org.eclipse.leshan.server.security.SecurityStoreListener;
+import org.eclipse.leshan.server.send.SendHandler;
+import org.eclipse.leshan.server.send.SendService;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -105,6 +109,8 @@ public class LeshanServer {
     // LWM2M attributes
     private final RegistrationServiceImpl registrationService;
     private final CaliforniumRegistrationStore registrationStore;
+    private final SendHandler sendService;
+
     /** @since 1.1 */
     protected final ObservationServiceImpl observationService;
     private final SecurityStore securityStore;
@@ -212,6 +218,10 @@ public class LeshanServer {
         // define /rd resource
         coapServer.add(createRegisterResource(registrationService, authorizer, registrationIdProvider));
 
+        // define /dp resource
+        this.sendService = createSendHandler();
+        coapServer.add(createSendResource(sendService, modelProvider, decoder, registrationStore));
+
         // create request sender
         requestSender = createRequestSender(securedEndpoint, unsecuredEndpoint, registrationService, observationService,
                 this.modelProvider, encoder, decoder, presenceService);
@@ -270,6 +280,15 @@ public class LeshanServer {
         return new RegisterResource(new RegistrationHandler(registrationService, authorizer, registrationIdProvider));
     }
 
+    protected SendHandler createSendHandler() {
+        return new SendHandler();
+    }
+
+    protected CoapResource createSendResource(SendHandler sendHandler, LwM2mModelProvider modelProvider,
+            LwM2mNodeDecoder decoder, CaliforniumRegistrationStore registrationStore) {
+        return new SendResource(sendHandler, modelProvider, decoder, registrationStore);
+    }
+
     protected LwM2mRequestSender createRequestSender(Endpoint securedEndpoint, Endpoint unsecuredEndpoint,
             RegistrationServiceImpl registrationService, ObservationServiceImpl observationService,
             LwM2mModelProvider modelProvider, LwM2mNodeEncoder encoder, LwM2mNodeDecoder decoder,
@@ -426,6 +445,14 @@ public class LeshanServer {
         return this.observationService;
     }
 
+    /**
+     * Get the {@link SendService} which can be used to listen data received from LWM2M client which are using
+     * {@link SendRequest}.
+     */
+    public SendService getSendService() {
+        return sendService;
+    }
+
     /**
      * Get the {@link PresenceService} to get status of LWM2M clients connected with binding mode 'Q'.
      * <p>
diff --git a/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/registration/InMemoryRegistrationStore.java b/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/registration/InMemoryRegistrationStore.java
index ae47a8b4..45c41824 100644
--- a/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/registration/InMemoryRegistrationStore.java
+++ b/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/registration/InMemoryRegistrationStore.java
@@ -46,11 +46,12 @@ import org.eclipse.californium.core.coap.Token;
 import org.eclipse.californium.core.observe.ObservationStoreException;
 import org.eclipse.californium.core.observe.ObservationUtil;
 import org.eclipse.californium.elements.EndpointContext;
-import org.eclipse.leshan.core.observation.Observation;
-import org.eclipse.leshan.core.util.NamedThreadFactory;
 import org.eclipse.leshan.core.Destroyable;
 import org.eclipse.leshan.core.Startable;
 import org.eclipse.leshan.core.Stoppable;
+import org.eclipse.leshan.core.observation.Observation;
+import org.eclipse.leshan.core.request.Identity;
+import org.eclipse.leshan.core.util.NamedThreadFactory;
 import org.eclipse.leshan.server.californium.observation.ObserveUtil;
 import org.eclipse.leshan.server.registration.Deregistration;
 import org.eclipse.leshan.server.registration.ExpirationListener;
@@ -70,6 +71,7 @@ public class InMemoryRegistrationStore implements CaliforniumRegistrationStore,
     private final Map<String /* end-point */, Registration> regsByEp = new HashMap<>();
     private final Map<InetSocketAddress, Registration> regsByAddr = new HashMap<>();
     private final Map<String /* reg-id */, Registration> regsByRegId = new HashMap<>();
+    private final Map<Identity, Registration> regsByIdentity = new HashMap<>();
     private Map<Token, org.eclipse.californium.core.observe.Observation> obsByToken = new HashMap<>();
     private Map<String, Set<Token>> tokensByRegId = new HashMap<>();
 
@@ -107,6 +109,7 @@ public class InMemoryRegistrationStore implements CaliforniumRegistrationStore,
 
             Registration registrationRemoved = regsByEp.put(registration.getEndpoint(), registration);
             regsByRegId.put(registration.getId(), registration);
+            regsByIdentity.put(registration.getIdentity(), registration);
             // If a registration is already associated to this address we don't care as we only want to keep the most
             // recent binding.
             regsByAddr.put(registration.getSocketAddress(), registration);
@@ -118,6 +121,9 @@ public class InMemoryRegistrationStore implements CaliforniumRegistrationStore,
                 if (!registrationRemoved.getId().equals(registration.getId())) {
                     removeFromMap(regsByRegId, registrationRemoved.getId(), registrationRemoved);
                 }
+                if (!registrationRemoved.getIdentity().equals(registration.getIdentity())) {
+                    removeFromMap(regsByIdentity, registrationRemoved.getIdentity(), registrationRemoved);
+                }
                 return new Deregistration(registrationRemoved, observationsRemoved);
             }
         } finally {
@@ -143,6 +149,10 @@ public class InMemoryRegistrationStore implements CaliforniumRegistrationStore,
                 if (!registration.getSocketAddress().equals(updatedRegistration.getSocketAddress())) {
                     removeFromMap(regsByAddr, registration.getSocketAddress(), registration);
                 }
+                regsByIdentity.put(updatedRegistration.getIdentity(), updatedRegistration);
+                if (!registration.getIdentity().equals(updatedRegistration.getIdentity())) {
+                    removeFromMap(regsByIdentity, registration.getIdentity(), registration);
+                }
 
                 regsByRegId.put(updatedRegistration.getId(), updatedRegistration);
 
@@ -183,6 +193,16 @@ public class InMemoryRegistrationStore implements CaliforniumRegistrationStore,
         }
     }
 
+    @Override
+    public Registration getRegistrationByIdentity(Identity identity) {
+        try {
+            lock.readLock().lock();
+            return regsByIdentity.get(identity);
+        } finally {
+            lock.readLock().unlock();
+        }
+    }
+
     @Override
     public Iterator<Registration> getAllRegistrations() {
         try {
@@ -204,6 +224,7 @@ public class InMemoryRegistrationStore implements CaliforniumRegistrationStore,
                 regsByEp.remove(registration.getEndpoint());
                 removeFromMap(regsByAddr, registration.getSocketAddress(), registration);
                 removeFromMap(regsByRegId, registration.getId(), registration);
+                removeFromMap(regsByIdentity, registration.getIdentity(), registration);
                 return new Deregistration(registration, observationsRemoved);
             }
             return null;
diff --git a/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/send/SendResource.java b/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/send/SendResource.java
new file mode 100644
index 00000000..6637c6a3
--- /dev/null
+++ b/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/send/SendResource.java
@@ -0,0 +1,99 @@
+/*******************************************************************************
+ * Copyright (c) 2021 Sierra Wireless and others.
+ * 
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v2.0
+ * and Eclipse Distribution License v1.0 which accompany this distribution.
+ * 
+ * The Eclipse Public License is available at
+ *    http://www.eclipse.org/legal/epl-v20.html
+ * and the Eclipse Distribution License is available at
+ *    http://www.eclipse.org/org/documents/edl-v10.html.
+ * 
+ * Contributors:
+ *     Sierra Wireless - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.leshan.server.californium.send;
+
+import static org.eclipse.leshan.core.californium.ResponseCodeUtil.toCoapResponseCode;
+
+import java.util.List;
+import java.util.Map;
+
+import org.eclipse.californium.core.coap.CoAP.ResponseCode;
+import org.eclipse.californium.core.coap.Request;
+import org.eclipse.californium.core.server.resources.CoapExchange;
+import org.eclipse.leshan.core.californium.LwM2mCoapResource;
+import org.eclipse.leshan.core.model.LwM2mModel;
+import org.eclipse.leshan.core.node.LwM2mNode;
+import org.eclipse.leshan.core.node.LwM2mPath;
+import org.eclipse.leshan.core.node.codec.LwM2mNodeDecoder;
+import org.eclipse.leshan.core.request.ContentFormat;
+import org.eclipse.leshan.core.request.Identity;
+import org.eclipse.leshan.core.request.SendRequest;
+import org.eclipse.leshan.core.response.SendResponse;
+import org.eclipse.leshan.core.response.SendableResponse;
+import org.eclipse.leshan.server.model.LwM2mModelProvider;
+import org.eclipse.leshan.server.registration.Registration;
+import org.eclipse.leshan.server.registration.RegistrationStore;
+import org.eclipse.leshan.server.send.SendHandler;
+
+/**
+ * A CoAP Resource used to handle "Send" request sent by LWM2M devices.
+ * 
+ * @see SendRequest
+ */
+public class SendResource extends LwM2mCoapResource {
+    private RegistrationStore registrationStore;
+    private LwM2mNodeDecoder decoder;
+    private LwM2mModelProvider modelProvider;
+    private SendHandler sendHandler;
+
+    public SendResource(SendHandler sendHandler, LwM2mModelProvider modelProvider, LwM2mNodeDecoder decoder,
+            RegistrationStore registrationStore) {
+        super("dp");
+        this.registrationStore = registrationStore;
+        this.decoder = decoder;
+        this.modelProvider = modelProvider;
+        this.sendHandler = sendHandler;
+    }
+
+    @Override
+    public void handlePOST(CoapExchange exchange) {
+        Request coapRequest = exchange.advanced().getRequest();
+        Identity sender = extractIdentity(coapRequest.getSourceContext());
+        Registration registration = registrationStore.getRegistrationByIdentity(sender);
+
+        // check we have a registration for this identity
+        if (registration == null) {
+            exchange.respond(ResponseCode.NOT_FOUND, "no registration found");
+            return;
+        }
+
+        // Decode payload
+        LwM2mModel model = modelProvider.getObjectModel(registration);
+        byte[] payload = exchange.getRequestPayload();
+        ContentFormat contentFormat = ContentFormat.fromCode(exchange.getRequestOptions().getContentFormat());
+        if (!decoder.isSupported(contentFormat)) {
+            exchange.respond(ResponseCode.BAD_REQUEST, "Unsupported content format");
+            return;
+        }
+        Map<LwM2mPath, LwM2mNode> data = decoder.decodeNodes(payload, contentFormat, (List<LwM2mPath>) null, model);
+
+        // Handle "send op request
+        SendRequest sendRequest = new SendRequest(contentFormat, data, coapRequest);
+        SendableResponse<SendResponse> sendableResponse = sendHandler.handleSend(registration, sendRequest);
+        SendResponse response = sendableResponse.getResponse();
+
+        // send reponse
+        if (response.isSuccess()) {
+            exchange.respond(toCoapResponseCode(response.getCode()));
+            sendableResponse.sent();
+            return;
+        } else {
+            exchange.respond(toCoapResponseCode(response.getCode()), response.getErrorMessage());
+            sendableResponse.sent();
+            return;
+        }
+    }
+}
diff --git a/leshan-server-core/src/main/java/org/eclipse/leshan/server/registration/RegistrationStore.java b/leshan-server-core/src/main/java/org/eclipse/leshan/server/registration/RegistrationStore.java
index 3c3ae29b..9558ab6f 100644
--- a/leshan-server-core/src/main/java/org/eclipse/leshan/server/registration/RegistrationStore.java
+++ b/leshan-server-core/src/main/java/org/eclipse/leshan/server/registration/RegistrationStore.java
@@ -20,6 +20,7 @@ import java.util.Collection;
 import java.util.Iterator;
 
 import org.eclipse.leshan.core.observation.Observation;
+import org.eclipse.leshan.core.request.Identity;
 
 /**
  * A store for registrations and observations. This interface is also responsible to handle registration expiration.
@@ -70,6 +71,14 @@ public interface RegistrationStore {
      */
     Registration getRegistrationByAdress(InetSocketAddress address);
 
+    /**
+     * Get the registration by {@link Identity}.
+     * 
+     * @param identity of the client registered.
+     * @return the registration or null if there is no client registered with this identity.
+     */
+    Registration getRegistrationByIdentity(Identity identity);
+
     /**
      * Returns an iterator over the registration of this store. There are no guarantees concerning the order in which
      * the elements are returned (unless the implementation provides a guarantee).
diff --git a/leshan-server-core/src/main/java/org/eclipse/leshan/server/send/SendHandler.java b/leshan-server-core/src/main/java/org/eclipse/leshan/server/send/SendHandler.java
new file mode 100644
index 00000000..fb348817
--- /dev/null
+++ b/leshan-server-core/src/main/java/org/eclipse/leshan/server/send/SendHandler.java
@@ -0,0 +1,71 @@
+/*******************************************************************************
+ * Copyright (c) 2021 Sierra Wireless and others.
+ * 
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v2.0
+ * and Eclipse Distribution License v1.0 which accompany this distribution.
+ * 
+ * The Eclipse Public License is available at
+ *    http://www.eclipse.org/legal/epl-v20.html
+ * and the Eclipse Distribution License is available at
+ *    http://www.eclipse.org/org/documents/edl-v10.html.
+ * 
+ * Contributors:
+ *     Sierra Wireless - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.leshan.server.send;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+import org.eclipse.leshan.core.node.LwM2mNode;
+import org.eclipse.leshan.core.node.LwM2mPath;
+import org.eclipse.leshan.core.request.SendRequest;
+import org.eclipse.leshan.core.response.SendResponse;
+import org.eclipse.leshan.core.response.SendableResponse;
+import org.eclipse.leshan.server.registration.Registration;
+
+/**
+ * Class responsible to handle "Send" request from LWM2M client.
+ * 
+ * @see SendRequest
+ */
+public class SendHandler implements SendService {
+
+    private final List<SendListener> listeners = new CopyOnWriteArrayList<>();;
+
+    @Override
+    public void addListener(SendListener listener) {
+        listeners.add(listener);
+    }
+
+    @Override
+    public void removeListener(SendListener listener) {
+        listeners.remove(listener);
+    }
+
+    public SendableResponse<SendResponse> handleSend(final Registration registration, final SendRequest request) {
+        SendableResponse<SendResponse> response = new SendableResponse<>(SendResponse.success(), new Runnable() {
+            @Override
+            public void run() {
+                fireDataReceived(registration, request.getNodes(), request);
+            }
+        });
+        return response;
+    }
+
+    protected void fireDataReceived(Registration registration, Map<LwM2mPath, LwM2mNode> data, SendRequest request) {
+        HashMap<String, LwM2mNode> nodes = new HashMap<>();
+        for (Entry<LwM2mPath, LwM2mNode> entry : data.entrySet()) {
+            nodes.put(entry.getKey().toString(), entry.getValue());
+        }
+
+        for (SendListener listener : listeners) {
+            listener.dataReceived(registration, Collections.unmodifiableMap(nodes), request);
+        }
+    }
+}
diff --git a/leshan-server-core/src/main/java/org/eclipse/leshan/server/send/SendListener.java b/leshan-server-core/src/main/java/org/eclipse/leshan/server/send/SendListener.java
new file mode 100644
index 00000000..b8d848ea
--- /dev/null
+++ b/leshan-server-core/src/main/java/org/eclipse/leshan/server/send/SendListener.java
@@ -0,0 +1,41 @@
+/*******************************************************************************
+ * Copyright (c) 2021 Sierra Wireless and others.
+ * 
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v2.0
+ * and Eclipse Distribution License v1.0 which accompany this distribution.
+ * 
+ * The Eclipse Public License is available at
+ *    http://www.eclipse.org/legal/epl-v20.html
+ * and the Eclipse Distribution License is available at
+ *    http://www.eclipse.org/org/documents/edl-v10.html.
+ * 
+ * Contributors:
+ *     Sierra Wireless - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.leshan.server.send;
+
+import java.util.Map;
+
+import org.eclipse.leshan.core.node.LwM2mNode;
+import org.eclipse.leshan.core.request.SendRequest;
+import org.eclipse.leshan.server.registration.Registration;
+
+/**
+ * Listener used to be aware of new data sent by LWM2M client with "Send" Request.
+ * 
+ * @see SendRequest
+ */
+public interface SendListener {
+
+    /**
+     * Called when new data are received from a LWM2M client via a {@link SendRequest}
+     * 
+     * @param registration Registration of the client which send the data.
+     * @param data The data received
+     * @param request The request received
+     */
+    void dataReceived(Registration registration, Map<String, LwM2mNode> data, SendRequest request);
+
+    // TODO should we add a listener, if called if something wrong happened when we handle SendRequest ?
+}
diff --git a/leshan-server-core/src/main/java/org/eclipse/leshan/server/send/SendService.java b/leshan-server-core/src/main/java/org/eclipse/leshan/server/send/SendService.java
new file mode 100644
index 00000000..c3ee101d
--- /dev/null
+++ b/leshan-server-core/src/main/java/org/eclipse/leshan/server/send/SendService.java
@@ -0,0 +1,30 @@
+/*******************************************************************************
+ * Copyright (c) 2021 Sierra Wireless and others.
+ * 
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v2.0
+ * and Eclipse Distribution License v1.0 which accompany this distribution.
+ * 
+ * The Eclipse Public License is available at
+ *    http://www.eclipse.org/legal/epl-v20.html
+ * and the Eclipse Distribution License is available at
+ *    http://www.eclipse.org/org/documents/edl-v10.html.
+ * 
+ * Contributors:
+ *     Sierra Wireless - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.leshan.server.send;
+
+import org.eclipse.leshan.core.request.SendRequest;
+
+/**
+ * Service used to monitor "Send Operation" used by LWM2M client.
+ * 
+ * @see SendRequest
+ */
+public interface SendService {
+
+    void addListener(SendListener listener);
+
+    void removeListener(SendListener listener);
+}
diff --git a/leshan-server-redis/src/main/java/org/eclipse/leshan/server/redis/RedisRegistrationStore.java b/leshan-server-redis/src/main/java/org/eclipse/leshan/server/redis/RedisRegistrationStore.java
index cd8ad077..36f4d123 100644
--- a/leshan-server-redis/src/main/java/org/eclipse/leshan/server/redis/RedisRegistrationStore.java
+++ b/leshan-server-redis/src/main/java/org/eclipse/leshan/server/redis/RedisRegistrationStore.java
@@ -38,14 +38,16 @@ import java.util.concurrent.TimeUnit;
 import org.eclipse.californium.core.coap.Token;
 import org.eclipse.californium.core.observe.ObservationStoreException;
 import org.eclipse.californium.elements.EndpointContext;
-import org.eclipse.leshan.core.observation.Observation;
-import org.eclipse.leshan.core.util.NamedThreadFactory;
-import org.eclipse.leshan.core.util.Validate;
 import org.eclipse.leshan.core.Destroyable;
 import org.eclipse.leshan.core.Startable;
 import org.eclipse.leshan.core.Stoppable;
+import org.eclipse.leshan.core.observation.Observation;
+import org.eclipse.leshan.core.request.Identity;
+import org.eclipse.leshan.core.util.NamedThreadFactory;
+import org.eclipse.leshan.core.util.Validate;
 import org.eclipse.leshan.server.californium.observation.ObserveUtil;
 import org.eclipse.leshan.server.californium.registration.CaliforniumRegistrationStore;
+import org.eclipse.leshan.server.redis.serialization.IdentitySerDes;
 import org.eclipse.leshan.server.redis.serialization.ObservationSerDes;
 import org.eclipse.leshan.server.redis.serialization.RegistrationSerDes;
 import org.eclipse.leshan.server.registration.Deregistration;
@@ -79,6 +81,7 @@ public class RedisRegistrationStore implements CaliforniumRegistrationStore, Sta
     private static final String REG_EP = "REG:EP:"; // (Endpoint => Registration)
     private static final String REG_EP_REGID_IDX = "EP:REGID:"; // secondary index key (Registration ID => Endpoint)
     private static final String REG_EP_ADDR_IDX = "EP:ADDR:"; // secondary index key (Socket Address => Endpoint)
+    private static final String REG_EP_IDENTITY = "EP:IDENTITY:"; // secondary index key (Identity => Endpoint)
     private static final String LOCK_EP = "LOCK:EP:";
     private static final byte[] OBS_TKN = "OBS:TKN:".getBytes(UTF_8);
     private static final String OBS_TKNS_REGID_IDX = "TKNS:REGID:"; // secondary index (token list by registration)
@@ -169,6 +172,8 @@ public class RedisRegistrationStore implements CaliforniumRegistrationStore, Sta
                 j.set(regid_idx, registration.getEndpoint().getBytes(UTF_8));
                 byte[] addr_idx = toRegAddrKey(registration.getSocketAddress());
                 j.set(addr_idx, registration.getEndpoint().getBytes(UTF_8));
+                byte[] identity_idx = toRegIdentityKey(registration.getIdentity());
+                j.set(identity_idx, registration.getEndpoint().getBytes(UTF_8));
 
                 // Add or update expiration
                 addOrUpdateExpiration(j, registration);
@@ -181,6 +186,9 @@ public class RedisRegistrationStore implements CaliforniumRegistrationStore, Sta
                     if (!oldRegistration.getSocketAddress().equals(registration.getSocketAddress())) {
                         removeAddrIndex(j, oldRegistration);
                     }
+                    if (!oldRegistration.getIdentity().equals(registration.getIdentity())) {
+                        removeIdentityIndex(j, oldRegistration);
+                    }
                     // remove old observation
                     Collection<Observation> obsRemoved = unsafeRemoveAllObservations(j, oldRegistration.getId());
 
@@ -233,6 +241,9 @@ public class RedisRegistrationStore implements CaliforniumRegistrationStore, Sta
                 if (!r.getSocketAddress().equals(updatedRegistration.getSocketAddress())) {
                     removeAddrIndex(j, r);
                 }
+                if (!r.getIdentity().equals(updatedRegistration.getIdentity())) {
+                    removeIdentityIndex(j, r);
+                }
 
                 return new UpdatedRegistration(r, updatedRegistration);
 
@@ -277,6 +288,22 @@ public class RedisRegistrationStore implements CaliforniumRegistrationStore, Sta
         }
     }
 
+    @Override
+    public Registration getRegistrationByIdentity(Identity identity) {
+        Validate.notNull(identity);
+        try (Jedis j = pool.getResource()) {
+            byte[] ep = j.get(toRegIdentityKey(identity));
+            if (ep == null) {
+                return null;
+            }
+            byte[] data = j.get(toEndpointKey(ep));
+            if (data == null) {
+                return null;
+            }
+            return deserializeReg(data);
+        }
+    }
+
     @Override
     public Iterator<Registration> getAllRegistrations() {
         return new RedisIterator(pool, new ScanParams().match(REG_EP + "*").count(100));
@@ -377,6 +404,7 @@ public class RedisRegistrationStore implements CaliforniumRegistrationStore, Sta
                     j.del(toEndpointKey(r.getEndpoint()));
                     Collection<Observation> obsRemoved = unsafeRemoveAllObservations(j, r.getId());
                     removeAddrIndex(j, r);
+                    removeIdentityIndex(j, r);
                     removeExpiration(j, r);
                     return new Deregistration(r, obsRemoved);
                 }
@@ -387,19 +415,26 @@ public class RedisRegistrationStore implements CaliforniumRegistrationStore, Sta
         }
     }
 
-    private void removeAddrIndex(Jedis j, Registration registration) {
+    private void removeAddrIndex(Jedis j, Registration r) {
+        removeSecondaryIndex(j, toRegAddrKey(r.getSocketAddress()), r.getEndpoint());
+    }
+
+    private void removeIdentityIndex(Jedis j, Registration r) {
+        removeSecondaryIndex(j, toRegIdentityKey(r.getIdentity()), r.getEndpoint());
+    }
+
+    private void removeSecondaryIndex(Jedis j, byte[] indexKey, String endpointName) {
         // Watch the key to remove.
-        byte[] regAddrKey = toRegAddrKey(registration.getSocketAddress());
-        j.watch(regAddrKey);
+        j.watch(indexKey);
 
-        byte[] epFromAddr = j.get(regAddrKey);
+        byte[] epFromAddr = j.get(indexKey);
         // Delete the key if needed.
-        if (Arrays.equals(epFromAddr, registration.getEndpoint().getBytes(UTF_8))) {
+        if (Arrays.equals(epFromAddr, endpointName.getBytes(UTF_8))) {
             // Try to delete the key
             Transaction transaction = j.multi();
-            transaction.del(regAddrKey);
+            transaction.del(indexKey);
             transaction.exec();
-            // if transaction failed this is not an issue as the socket address is probably reused and we don't neeed to
+            // if transaction failed this is not an issue as the index is probably reused and we don't need to
             // delete it anymore.
         } else {
             // the key must not be deleted.
@@ -423,6 +458,10 @@ public class RedisRegistrationStore implements CaliforniumRegistrationStore, Sta
         return toKey(REG_EP_ADDR_IDX, addr.getAddress().toString() + ":" + addr.getPort());
     }
 
+    private byte[] toRegIdentityKey(Identity identity) {
+        return toKey(REG_EP_IDENTITY, IdentitySerDes.serialize(identity).toString());
+    }
+
     private byte[] toEndpointKey(String endpoint) {
         return toKey(REG_EP, endpoint);
     }


Test patch:
diff --git a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/lockstep/LockStepLwM2mClient.java b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/lockstep/LockStepLwM2mClient.java
index 8c14596d..f59fa9bc 100644
--- a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/lockstep/LockStepLwM2mClient.java
+++ b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/lockstep/LockStepLwM2mClient.java
@@ -16,6 +16,7 @@
 package org.eclipse.leshan.integration.tests.lockstep;
 
 import java.net.InetSocketAddress;
+import java.util.List;
 import java.util.Random;
 
 import org.eclipse.californium.core.coap.Message;
@@ -25,6 +26,12 @@ import org.eclipse.californium.core.network.serialization.UdpDataSerializer;
 import org.eclipse.californium.core.test.lockstep.LockstepEndpoint;
 import org.eclipse.californium.elements.RawData;
 import org.eclipse.leshan.client.californium.request.CoapRequestBuilder;
+import org.eclipse.leshan.core.model.LwM2mModel;
+import org.eclipse.leshan.core.model.ObjectLoader;
+import org.eclipse.leshan.core.model.ObjectModel;
+import org.eclipse.leshan.core.model.StaticModel;
+import org.eclipse.leshan.core.node.codec.DefaultLwM2mNodeEncoder;
+import org.eclipse.leshan.core.node.codec.LwM2mNodeEncoder;
 import org.eclipse.leshan.core.request.Identity;
 import org.eclipse.leshan.core.request.UplinkRequest;
 import org.eclipse.leshan.core.response.LwM2mResponse;
@@ -33,15 +40,20 @@ public class LockStepLwM2mClient extends LockstepEndpoint {
 
     private static final Random r = new Random();
     private InetSocketAddress destination;
+    private final LwM2mNodeEncoder encoder;
+    private final LwM2mModel model;
 
     public LockStepLwM2mClient(final InetSocketAddress destination) {
         super(destination);
         this.destination = destination;
+        this.encoder = new DefaultLwM2mNodeEncoder();
+        List<ObjectModel> models = ObjectLoader.loadDefault();
+        this.model = new StaticModel(models);
     }
 
     public Request createCoapRequest(UplinkRequest<? extends LwM2mResponse> lwm2mReq) {
         // create CoAP request
-        CoapRequestBuilder coapRequestBuilder = new CoapRequestBuilder(Identity.unsecure(destination));
+        CoapRequestBuilder coapRequestBuilder = new CoapRequestBuilder(Identity.unsecure(destination), encoder, model);
         lwm2mReq.accept(coapRequestBuilder);
         Request coapReq = coapRequestBuilder.getRequest();
         byte[] token = new byte[8];
diff --git a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/send/RedisSendTest.java b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/send/RedisSendTest.java
new file mode 100644
index 00000000..fc7c70e9
--- /dev/null
+++ b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/send/RedisSendTest.java
@@ -0,0 +1,33 @@
+/*******************************************************************************
+ * Copyright (c) 2021 Sierra Wireless and others.
+ * 
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v2.0
+ * and Eclipse Distribution License v1.0 which accompany this distribution.
+ * 
+ * The Eclipse Public License is available at
+ *    http://www.eclipse.org/legal/epl-v20.html
+ * and the Eclipse Distribution License is available at
+ *    http://www.eclipse.org/org/documents/edl-v10.html.
+ * 
+ * Contributors:
+ *     Sierra Wireless - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.leshan.integration.tests.send;
+
+import org.eclipse.leshan.client.resource.ObjectsInitializer;
+import org.eclipse.leshan.core.model.StaticModel;
+import org.eclipse.leshan.core.request.ContentFormat;
+import org.eclipse.leshan.integration.tests.util.RedisIntegrationTestHelper;
+
+public class RedisSendTest extends SendTest {
+    public RedisSendTest(ContentFormat format) {
+        super(format);
+        helper = new RedisIntegrationTestHelper() {
+            @Override
+            protected ObjectsInitializer createObjectsInitializer() {
+                return new ObjectsInitializer(new StaticModel(createObjectModels()));
+            };
+        };
+    }
+}
diff --git a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/send/SendTest.java b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/send/SendTest.java
new file mode 100644
index 00000000..2dbcd34e
--- /dev/null
+++ b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/send/SendTest.java
@@ -0,0 +1,134 @@
+/*******************************************************************************
+ * Copyright (c) 2021 Sierra Wireless and others.
+ * 
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v2.0
+ * and Eclipse Distribution License v1.0 which accompany this distribution.
+ * 
+ * The Eclipse Public License is available at
+ *    http://www.eclipse.org/legal/epl-v20.html
+ * and the Eclipse Distribution License is available at
+ *    http://www.eclipse.org/org/documents/edl-v10.html.
+ * 
+ * Contributors:
+ *     Sierra Wireless - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.leshan.integration.tests.send;
+
+import static org.junit.Assert.*;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+import org.eclipse.leshan.client.resource.ObjectsInitializer;
+import org.eclipse.leshan.client.servers.ServerIdentity;
+import org.eclipse.leshan.core.model.StaticModel;
+import org.eclipse.leshan.core.node.LwM2mNode;
+import org.eclipse.leshan.core.node.LwM2mResource;
+import org.eclipse.leshan.core.request.ContentFormat;
+import org.eclipse.leshan.core.response.SendResponse;
+import org.eclipse.leshan.integration.tests.util.Callback;
+import org.eclipse.leshan.integration.tests.util.IntegrationTestHelper;
+import org.eclipse.leshan.integration.tests.util.SynchronousSendListener;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class SendTest {
+    protected IntegrationTestHelper helper = new IntegrationTestHelper() {
+        @Override
+        protected ObjectsInitializer createObjectsInitializer() {
+            return new ObjectsInitializer(new StaticModel(createObjectModels()));
+        };
+    };
+
+    @Parameters(name = "{0}{1}")
+    public static Collection<?> contentFormats() {
+        return Arrays.asList(new Object[][] { //
+                                // {content format}
+                                { ContentFormat.SENML_JSON }, //
+                                { ContentFormat.SENML_CBOR } });
+    }
+
+    private ContentFormat contentformat;
+
+    public SendTest(ContentFormat contentformat) {
+        this.contentformat = contentformat;
+    }
+
+    @Before
+    public void start() {
+        helper.initialize();
+        helper.createServer();
+        helper.server.start();
+        helper.createClient();
+        helper.client.start();
+        helper.waitForRegistrationAtServerSide(1);
+    }
+
+    @After
+    public void stop() {
+        helper.client.destroy(false);
+        helper.server.destroy();
+        helper.dispose();
+    }
+
+    @Test
+    public void can_send_resources() throws InterruptedException, TimeoutException {
+        // Define send listener
+        SynchronousSendListener listener = new SynchronousSendListener();
+        helper.server.getSendService().addListener(listener);
+
+        // Send Data
+        helper.waitForRegistrationAtClientSide(1);
+        ServerIdentity server = helper.client.getRegisteredServers().values().iterator().next();
+        SendResponse response = helper.client.sendData(server, contentformat, Arrays.asList("/3/0/1", "/3/0/2"), 1000);
+        assertTrue(response.isSuccess());
+
+        // wait for data and check result
+        listener.waitForData(1, TimeUnit.SECONDS);
+        assertNotNull(listener.getRegistration());
+        Map<String, LwM2mNode> data = listener.getData();
+        LwM2mResource modelnumber = (LwM2mResource) data.get("/3/0/1");
+        assertEquals(modelnumber.getId(), 1);
+        assertEquals(modelnumber.getValue(), "IT-TEST-123");
+
+        LwM2mResource serialnumber = (LwM2mResource) data.get("/3/0/2");
+        assertEquals(serialnumber.getId(), 2);
+        assertEquals(serialnumber.getValue(), "12345");
+    }
+
+    @Test
+    public void can_send_resources_asynchronously() throws InterruptedException, TimeoutException {
+        // Define send listener
+        SynchronousSendListener listener = new SynchronousSendListener();
+        helper.server.getSendService().addListener(listener);
+
+        // Send Data
+        helper.waitForRegistrationAtClientSide(1);
+        Callback<SendResponse> callback = new Callback<>();
+        ServerIdentity server = helper.client.getRegisteredServers().values().iterator().next();
+        helper.client.sendData(server, contentformat, Arrays.asList("/3/0/1", "/3/0/2"), 1000, callback, callback);
+        callback.waitForResponse(1000);
+        assertTrue(callback.getResponse().isSuccess());
+
+        // wait for data and check result
+        listener.waitForData(1, TimeUnit.SECONDS);
+        assertNotNull(listener.getRegistration());
+        Map<String, LwM2mNode> data = listener.getData();
+        LwM2mResource modelnumber = (LwM2mResource) data.get("/3/0/1");
+        assertEquals(modelnumber.getId(), 1);
+        assertEquals(modelnumber.getValue(), "IT-TEST-123");
+
+        LwM2mResource serialnumber = (LwM2mResource) data.get("/3/0/2");
+        assertEquals(serialnumber.getId(), 2);
+        assertEquals(serialnumber.getValue(), "12345");
+    }
+}
diff --git a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/util/IntegrationTestHelper.java b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/util/IntegrationTestHelper.java
index 22bc61ee..2c3d1554 100644
--- a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/util/IntegrationTestHelper.java
+++ b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/util/IntegrationTestHelper.java
@@ -55,7 +55,7 @@ import org.eclipse.leshan.core.request.UplinkRequest;
 import org.eclipse.leshan.core.response.ExecuteResponse;
 import org.eclipse.leshan.server.californium.LeshanServer;
 import org.eclipse.leshan.server.californium.LeshanServerBuilder;
-import org.eclipse.leshan.server.model.StaticModelProvider;
+import org.eclipse.leshan.server.model.VersionedModelProvider;
 import org.eclipse.leshan.server.registration.Registration;
 import org.eclipse.leshan.server.registration.RegistrationServiceImpl;
 import org.eclipse.leshan.server.security.DefaultAuthorizer;
@@ -172,9 +172,13 @@ public class IntegrationTestHelper {
         }
     }
 
+    protected ObjectsInitializer createObjectsInitializer() {
+        return new TestObjectsInitializer(new StaticModel(createObjectModels()));
+    }
+
     public void createClient(Map<String, String> additionalAttributes) {
         // Create objects Enabler
-        ObjectsInitializer initializer = new TestObjectsInitializer(new StaticModel(createObjectModels()));
+        ObjectsInitializer initializer = createObjectsInitializer();
         initializer.setInstancesForObject(LwM2mId.SECURITY, Security.noSec(
                 "coap://" + server.getUnsecuredAddress().getHostString() + ":" + server.getUnsecuredAddress().getPort(),
                 12345));
@@ -205,7 +209,7 @@ public class IntegrationTestHelper {
         LeshanServerBuilder builder = new LeshanServerBuilder();
         builder.setDecoder(new DefaultLwM2mNodeDecoder(true));
         builder.setEncoder(new DefaultLwM2mNodeEncoder(true));
-        builder.setObjectModelProvider(new StaticModelProvider(createObjectModels()));
+        builder.setObjectModelProvider(new VersionedModelProvider(createObjectModels()));
         builder.setLocalAddress(new InetSocketAddress(InetAddress.getLoopbackAddress(), 0));
         builder.setLocalSecureAddress(new InetSocketAddress(InetAddress.getLoopbackAddress(), 0));
         SecurityStore securityStore = createSecurityStore();
diff --git a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/util/SynchronousSendListener.java b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/util/SynchronousSendListener.java
new file mode 100644
index 00000000..569d942d
--- /dev/null
+++ b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/util/SynchronousSendListener.java
@@ -0,0 +1,52 @@
+/*******************************************************************************
+ * Copyright (c) 2021 Sierra Wireless and others.
+ * 
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v2.0
+ * and Eclipse Distribution License v1.0 which accompany this distribution.
+ * 
+ * The Eclipse Public License is available at
+ *    http://www.eclipse.org/legal/epl-v20.html
+ * and the Eclipse Distribution License is available at
+ *    http://www.eclipse.org/org/documents/edl-v10.html.
+ * 
+ * Contributors:
+ *     Sierra Wireless - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.leshan.integration.tests.util;
+
+import java.util.Map;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+import org.eclipse.leshan.core.node.LwM2mNode;
+import org.eclipse.leshan.core.request.SendRequest;
+import org.eclipse.leshan.server.registration.Registration;
+import org.eclipse.leshan.server.send.SendListener;
+
+public class SynchronousSendListener implements SendListener {
+    private CountDownLatch dataLatch = new CountDownLatch(1);
+    private volatile Map<String, LwM2mNode> data;
+    private volatile Registration registration;
+
+    @Override
+    public void dataReceived(Registration registration, Map<String, LwM2mNode> data, SendRequest request) {
+        this.data = data;
+        this.registration = registration;
+        dataLatch.countDown();
+    }
+
+    public Map<String, LwM2mNode> getData() {
+        return data;
+    }
+
+    public Registration getRegistration() {
+        return registration;
+    }
+
+    public void waitForData(int timeout, TimeUnit unit) throws TimeoutException, InterruptedException {
+        if (!dataLatch.await(timeout, unit))
+            throw new TimeoutException("wait for data timeout");
+    }
+}
\ No newline at end of file