Browse Source

Merge pull request #9903 from thingsboard/californium_3_9_1

Californium_3_9_1
pull/10129/head
Andrew Shvayka 3 years ago
committed by GitHub
parent
commit
0ae5061134
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 296
      application/src/main/java/org/eclipse/leshan/server/observation/ObservationServiceImpl.java
  2. 2
      application/src/test/java/org/thingsboard/server/controller/TbResourceControllerTest.java
  3. 12
      application/src/test/java/org/thingsboard/server/edge/DeviceProfileEdgeTest.java
  4. 5
      application/src/test/java/org/thingsboard/server/transport/coap/attributes/AbstractCoapAttributesIntegrationTest.java
  5. 5
      application/src/test/java/org/thingsboard/server/transport/coap/attributes/updates/CoapAttributesUpdatesIntegrationTest.java
  6. 4
      application/src/test/java/org/thingsboard/server/transport/coap/attributes/updates/CoapAttributesUpdatesJsonIntegrationTest.java
  7. 5
      application/src/test/java/org/thingsboard/server/transport/coap/attributes/updates/CoapAttributesUpdatesProtoIntegrationTest.java
  8. 3
      application/src/test/java/org/thingsboard/server/transport/coap/client/CoapClientIntegrationTest.java
  9. 18
      application/src/test/java/org/thingsboard/server/transport/coap/rpc/AbstractCoapServerSideRpcIntegrationTest.java
  10. 3
      application/src/test/java/org/thingsboard/server/transport/coap/rpc/CoapServerSideRpcDefaultIntegrationTest.java
  11. 3
      application/src/test/java/org/thingsboard/server/transport/coap/rpc/CoapServerSideRpcJsonIntegrationTest.java
  12. 3
      application/src/test/java/org/thingsboard/server/transport/coap/rpc/CoapServerSideRpcProtoIntegrationTest.java
  13. 74
      application/src/test/java/org/thingsboard/server/transport/lwm2m/AbstractLwM2MIntegrationTest.java
  14. 3
      application/src/test/java/org/thingsboard/server/transport/lwm2m/Lwm2mTestHelper.java
  15. 147
      application/src/test/java/org/thingsboard/server/transport/lwm2m/attributes/DefaultLwM2mAttributeParserTest.java
  16. 76
      application/src/test/java/org/thingsboard/server/transport/lwm2m/attributes/DefaultLwM2mLinkParserTest.java
  17. 87
      application/src/test/java/org/thingsboard/server/transport/lwm2m/attributes/LwM2mAttributesTest.java
  18. 21
      application/src/test/java/org/thingsboard/server/transport/lwm2m/client/FwLwM2MDevice.java
  19. 245
      application/src/test/java/org/thingsboard/server/transport/lwm2m/client/LwM2MTestClient.java
  20. 6
      application/src/test/java/org/thingsboard/server/transport/lwm2m/client/LwM2mBinaryAppDataContainer.java
  21. 4
      application/src/test/java/org/thingsboard/server/transport/lwm2m/client/LwM2mLocation.java
  22. 9
      application/src/test/java/org/thingsboard/server/transport/lwm2m/client/LwM2mTemperatureSensor.java
  23. 12
      application/src/test/java/org/thingsboard/server/transport/lwm2m/client/Lwm2mServer.java
  24. 33
      application/src/test/java/org/thingsboard/server/transport/lwm2m/client/SimpleLwM2MDevice.java
  25. 20
      application/src/test/java/org/thingsboard/server/transport/lwm2m/client/SwLwM2MDevice.java
  26. 732
      application/src/test/java/org/thingsboard/server/transport/lwm2m/client/TbLwm2mObjectEnabler.java
  27. 71
      application/src/test/java/org/thingsboard/server/transport/lwm2m/client/TbObjectsInitializer.java
  28. 41
      application/src/test/java/org/thingsboard/server/transport/lwm2m/ota/AbstractOtaLwM2MIntegrationTest.java
  29. 52
      application/src/test/java/org/thingsboard/server/transport/lwm2m/ota/sql/OtaLwM2MIntegrationTest.java
  30. 34
      application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/AbstractRpcLwM2MIntegrationObserveTest.java
  31. 14
      application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/AbstractRpcLwM2MIntegrationTest.java
  32. 487
      application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2MIntegrationObserveCompositeTest.java
  33. 4
      application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationCreateTest.java
  34. 30
      application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationDiscoverTest.java
  35. 249
      application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationDiscoverWriteAttributesTest.java
  36. 295
      application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationObserveTest.java
  37. 36
      application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationReadTest.java
  38. 56
      application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationWriteAttributesTest.java
  39. 67
      application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationWriteTest.java
  40. 36
      application/src/test/java/org/thingsboard/server/transport/lwm2m/security/AbstractSecurityLwM2MIntegrationTest.java
  41. 18
      application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/NoSecLwM2MIntegrationTest.java
  42. 9
      application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/PskLwm2mIntegrationTest.java
  43. 10
      application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/RpkLwM2MIntegrationTest.java
  44. 6
      application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/X509_NoTrustLwM2MIntegrationTest.java
  45. 6
      application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/X509_TrustLwM2MIntegrationTest.java
  46. 2
      application/src/test/resources/lwm2m/3.xml
  47. 2
      application/src/test/resources/lwm2m/5.xml
  48. 2
      application/src/test/resources/lwm2m/9.xml
  49. 15
      common/data/src/main/java/org/thingsboard/server/common/data/device/profile/lwm2m/ObjectAttributes.java
  50. 2
      common/data/src/main/java/org/thingsboard/server/common/data/device/profile/lwm2m/bootstrap/LwM2MServerSecurityConfig.java
  51. 6
      common/data/src/main/java/org/thingsboard/server/common/data/util/TbDDFFileParser.java
  52. 129
      common/data/src/main/java/org/thingsboard/server/common/data/util/TbDefaultDDFFileValidator.java
  53. 40
      common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportResource.java
  54. 54
      common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/callback/CoapResponseCallback.java
  55. 4
      common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/callback/CoapResponseCodeCallback.java
  56. 22
      common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/client/DefaultCoapClientContext.java
  57. 85
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/LwM2MTransportBootstrapService.java
  58. 35
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/secure/LwM2mDefaultBootstrapSessionManager.java
  59. 21
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/store/LwM2MBootstrapConfigStoreTaskProvider.java
  60. 8
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/store/LwM2MBootstrapSecurityStore.java
  61. 22
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/store/LwM2MConfigurationChecker.java
  62. 6
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/store/LwM2MInMemoryBootstrapConfigStore.java
  63. 5
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/config/LwM2MTransportServerConfig.java
  64. 2
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/secure/LwM2mCredentialsSecurityInfoValidator.java
  65. 38
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/secure/TbLwM2MAuthorizer.java
  66. 3
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/AbstractLwM2mTransportResource.java
  67. 116
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/DefaultLwM2mTransportService.java
  68. 5
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2MNetworkConfig.java
  69. 33
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mServerListener.java
  70. 2
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mTransportContext.java
  71. 6
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mTransportServerHelper.java
  72. 10
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mVersionedModelProvider.java
  73. 21
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClient.java
  74. 257
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/downlink/DefaultLwM2mDownlinkMsgHandler.java
  75. 18
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/downlink/LwM2mDownlinkMsgHandler.java
  76. 40
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/downlink/composite/TbLwM2MCancelObserveCompositeCallback.java
  77. 32
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/downlink/composite/TbLwM2MCancelObserveCompositeRequest.java
  78. 39
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/downlink/composite/TbLwM2MObserveCompositeCallback.java
  79. 51
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/downlink/composite/TbLwM2MObserveCompositeRequest.java
  80. 55
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/ota/DefaultLwM2MOtaUpdateService.java
  81. 148
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/rpc/DefaultLwM2MRpcRequestHandler.java
  82. 2
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/rpc/RpcDiscoverCallback.java
  83. 37
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/rpc/composite/RpcCancelObserveCompositeCallback.java
  84. 40
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/rpc/composite/RpcObserveResponseCompositeCallback.java
  85. 619
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbInMemoryRegistrationStore.java
  86. 16
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbInMemorySecurityStore.java
  87. 395
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbLwM2mRedisRegistrationStore.java
  88. 22
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbLwM2mRedisSecurityStore.java
  89. 8
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbLwM2mSecurityStore.java
  90. 12
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbLwM2mStoreFactory.java
  91. 383
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/util/LwM2MClientSerDes.java
  92. 33
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/uplink/DefaultLwM2mUplinkMsgHandler.java
  93. 1
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/uplink/LwM2mUplinkMsgHandler.java
  94. 115
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/utils/LwM2MTransportUtil.java
  95. 9
      dao/src/main/java/org/thingsboard/server/dao/service/validator/DeviceProfileDataValidator.java
  96. 81
      msa/black-box-tests/src/test/java/org/thingsboard/server/msa/AbstractCoapClientTest.java
  97. 122
      msa/black-box-tests/src/test/java/org/thingsboard/server/msa/TestCoapClient.java
  98. 68
      msa/black-box-tests/src/test/java/org/thingsboard/server/msa/TestCoapClientCallback.java
  99. 28
      msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/CoapClientTest.java
  100. 13
      pom.xml

296
application/src/main/java/org/eclipse/leshan/server/observation/ObservationServiceImpl.java

@ -0,0 +1,296 @@
/**
* Copyright © 2016-2024 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* Copyright (c) 2016 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
* Michał Wadowski (Orange) - Add Observe-Composite feature.
*/
package org.eclipse.leshan.server.observation;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.leshan.core.node.LwM2mPath;
import org.eclipse.leshan.core.observation.CompositeObservation;
import org.eclipse.leshan.core.observation.Observation;
import org.eclipse.leshan.core.observation.SingleObservation;
import org.eclipse.leshan.core.peer.LwM2mPeer;
import org.eclipse.leshan.core.response.ObserveCompositeResponse;
import org.eclipse.leshan.core.response.ObserveResponse;
import org.eclipse.leshan.server.endpoint.LwM2mServerEndpoint;
import org.eclipse.leshan.server.endpoint.LwM2mServerEndpointsProvider;
import org.eclipse.leshan.server.profile.ClientProfile;
import org.eclipse.leshan.server.registration.Registration;
import org.eclipse.leshan.server.registration.RegistrationStore;
import org.eclipse.leshan.server.registration.RegistrationUpdate;
import org.eclipse.leshan.server.registration.UpdatedRegistration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArrayList;
/**
* Implementation of the {@link ObservationService} accessing the persisted observation via the provided
* {@link RegistrationStore}.
*
* When a new observation is added or changed or canceled, the registered listeners are notified.
*/
@Slf4j
public class ObservationServiceImpl implements ObservationService, LwM2mNotificationReceiver {
private final Logger LOG = LoggerFactory.getLogger(ObservationServiceImpl.class);
private final RegistrationStore registrationStore;
private final LwM2mServerEndpointsProvider endpointProvider;
private final boolean updateRegistrationOnNotification;
private final List<ObservationListener> listeners = new CopyOnWriteArrayList<>();;
/**
* Creates an instance of {@link ObservationServiceImpl}
*/
public ObservationServiceImpl(RegistrationStore store, LwM2mServerEndpointsProvider endpointProvider) {
this(store, endpointProvider, false);
}
/**
* Creates an instance of {@link ObservationServiceImpl}
*
* @param updateRegistrationOnNotification will activate registration update on observe notification.
*
* @since 1.1
*/
public ObservationServiceImpl(RegistrationStore store, LwM2mServerEndpointsProvider endpointProvider,
boolean updateRegistrationOnNotification) {
this.registrationStore = store;
this.updateRegistrationOnNotification = updateRegistrationOnNotification;
this.endpointProvider = endpointProvider;
}
@Override
public int cancelObservations(Registration registration) {
// check registration id
String registrationId = registration.getId();
if (registrationId == null)
return 0;
Collection<Observation> observations = registrationStore.removeObservations(registrationId);
if (observations == null)
return 0;
for (Observation observation : observations) {
cancel(observation);
}
return observations.size();
}
@Override
public int cancelObservations(Registration registration, String nodePath) {
if (registration == null || registration.getId() == null || nodePath == null || nodePath.isEmpty())
return 0;
Set<Observation> observations = getObservationsForCancel(registration.getId(), nodePath);
for (Observation observation : observations) {
cancelObservation(observation);
}
return observations.size();
}
@Override
public int cancelCompositeObservations(Registration registration, String[] nodePaths) {
if (registration == null || registration.getId() == null || nodePaths == null || nodePaths.length == 0)
return 0;
Set<Observation> observations = getCompositeObservationsForCancel(registration.getId(), nodePaths);
for (Observation observation : observations) {
cancelObservation(observation);
}
return observations.size();
}
@Override
public void cancelObservation(Observation observation) {
if (observation == null)
return;
registrationStore.removeObservation(observation.getRegistrationId(), observation.getId());
cancel(observation);
}
private void cancel(Observation observation) {
List<LwM2mServerEndpoint> endpoints = endpointProvider.getEndpoints();
for (LwM2mServerEndpoint lwM2mEndpoint : endpoints) {
lwM2mEndpoint.cancelObservation(observation);
}
for (ObservationListener listener : listeners) {
listener.cancelled(observation);
}
}
@Override
public Set<Observation> getObservations(Registration registration) {
return getObservations(registration.getId());
}
private Set<Observation> getObservations(String registrationId) {
if (registrationId == null)
return Collections.emptySet();
return new HashSet<>(registrationStore.getObservations(registrationId));
}
private Set<Observation> getCompositeObservationsForCancel(String registrationId, String[] nodePaths) {
if (registrationId == null || nodePaths == null)
return Collections.emptySet();
// array of String to array of LWM2M path
List<LwM2mPath> lwPaths = new ArrayList<>(nodePaths.length);
for (int i = 0; i < nodePaths.length; i++) {
lwPaths.add(new LwM2mPath(nodePaths[i]));
}
// search composite-observation
Set<Observation> result = new HashSet<>();
for (Observation obs : getObservations(registrationId)) {
if (obs instanceof CompositeObservation) {
if (lwPaths.equals(((CompositeObservation) obs).getPaths())) {
result.add(obs);
}
}
}
return result;
}
private Set<Observation> getObservationsForCancel(String registrationId, String nodePath) {
if (registrationId == null || nodePath == null)
return Collections.emptySet();
Set<Observation> result = new HashSet<>();
LwM2mPath lwPath = new LwM2mPath(nodePath);
for (Observation obs : getObservations(registrationId)) {
if (obs instanceof SingleObservation) {
LwM2mPath lwPathObs = ((SingleObservation) obs).getPath();
if (lwPath.equals(lwPathObs) || lwPathObs.startWith(lwPath)) { // nodePath = "3", lwPathObs = "3/0/9": cancel for tne all lwPathObs
result.add(obs);
} else if (!lwPath.equals(lwPathObs) && lwPath.startWith(lwPathObs)) { // nodePath = "3/0/9", lwPathObs = "3": error...
String errorMsg = String.format(
"Unexpected error <cancelObservation>: There is registration with id [%s] existing observation [%s] includes input observation [%s]!",
registrationId, lwPathObs, lwPath);
throw new IllegalStateException(errorMsg);
}
}
}
return result;
}
@Override
public void addListener(ObservationListener listener) {
listeners.add(listener);
}
@Override
public void removeListener(ObservationListener listener) {
listeners.remove(listener);
}
private Registration updateRegistrationOnRegistration(Observation observation, LwM2mPeer sender,
ClientProfile profile) {
if (updateRegistrationOnNotification) {
RegistrationUpdate regUpdate = new RegistrationUpdate(observation.getRegistrationId(), sender, null, null,
null, null, null, null, null, null, null, null);
UpdatedRegistration updatedRegistration = registrationStore.updateRegistration(regUpdate);
if (updatedRegistration == null || updatedRegistration.getUpdatedRegistration() == null) {
String errorMsg = String.format(
"Unexpected error: There is no registration with id %s for this observation %s",
observation.getRegistrationId(), observation);
LOG.error(errorMsg);
throw new IllegalStateException(errorMsg);
}
return updatedRegistration.getUpdatedRegistration();
}
return profile.getRegistration();
}
// ********** NotificationListener interface **********//
@Override
public void onNotification(SingleObservation observation, LwM2mPeer sender, ClientProfile profile,
ObserveResponse response) {
try {
Registration updatedRegistration = updateRegistrationOnRegistration(observation, sender, profile);
for (ObservationListener listener : listeners) {
listener.onResponse(observation, updatedRegistration, response);
}
} catch (Exception e) {
for (ObservationListener listener : listeners) {
listener.onError(observation, profile.getRegistration(), e);
}
}
}
@Override
public void onNotification(CompositeObservation observation, LwM2mPeer sender, ClientProfile profile,
ObserveCompositeResponse response) {
try {
Registration updatedRegistration = updateRegistrationOnRegistration(observation, sender, profile);
for (ObservationListener listener : listeners) {
listener.onResponse(observation, updatedRegistration, response);
}
} catch (Exception e) {
for (ObservationListener listener : listeners) {
listener.onError(observation, profile.getRegistration(), e);
}
}
}
@Override
public void onError(Observation observation, LwM2mPeer sender, ClientProfile profile, Exception error) {
for (ObservationListener listener : listeners) {
listener.onError(observation, profile.getRegistration(), error);
}
}
@Override
public void newObservation(Observation observation, Registration registration) {
for (ObservationListener listener : listeners) {
listener.newObservation(observation, registration);
}
}
@Override
public void cancelled(Observation observation) {
for (ObservationListener listener : listeners) {
listener.cancelled(observation);
}
}
}

2
application/src/test/java/org/thingsboard/server/controller/TbResourceControllerTest.java

@ -687,7 +687,7 @@ public class TbResourceControllerTest extends AbstractControllerTest {
List<TbResource> resources = loadLwm2mResources();
List<LwM2mObject> objects =
doGetTyped("/api/resource/lwm2m?sortProperty=id&sortOrder=ASC&objectIds=3_1.0,5_1.0,19_1.1", new TypeReference<>() {});
doGetTyped("/api/resource/lwm2m?sortProperty=id&sortOrder=ASC&objectIds=3_1.2,5_1.2,19_1.1", new TypeReference<>() {});
Assert.assertNotNull(objects);
Assert.assertEquals(3, objects.size());

12
application/src/test/java/org/thingsboard/server/edge/DeviceProfileEdgeTest.java

@ -198,16 +198,16 @@ public class DeviceProfileEdgeTest extends AbstractEdgeTest {
Assert.assertEquals("PUBLIC_KEY", noSecLwM2MBootstrapServerCredential.getServerPublicKey());
Assert.assertEquals(Integer.valueOf(123), noSecLwM2MBootstrapServerCredential.getShortServerId());
Assert.assertTrue(noSecLwM2MBootstrapServerCredential.isBootstrapServerIs());
Assert.assertFalse(noSecLwM2MBootstrapServerCredential.isBootstrapServerIs());
Assert.assertEquals("localhost", noSecLwM2MBootstrapServerCredential.getHost());
Assert.assertEquals(Integer.valueOf(5687), noSecLwM2MBootstrapServerCredential.getPort());
Assert.assertEquals(Integer.valueOf(5685), noSecLwM2MBootstrapServerCredential.getPort());
TelemetryMappingConfiguration observeAttr = transportConfiguration.getObserveAttr();
Assert.assertEquals("batteryLevel", observeAttr.getKeyName().get("/3_1.0/0/9"));
Assert.assertEquals("batteryLevel", observeAttr.getKeyName().get("/3_1.2/0/9"));
Assert.assertTrue(observeAttr.getObserve().isEmpty());
Assert.assertTrue(observeAttr.getAttribute().isEmpty());
Assert.assertFalse(observeAttr.getTelemetry().isEmpty());
Assert.assertTrue(observeAttr.getTelemetry().contains("/3_1.0/0/9"));
Assert.assertTrue(observeAttr.getTelemetry().contains("/3_1.2/0/9"));
Assert.assertTrue(observeAttr.getAttributeLwm2m().isEmpty());
removeDeviceProfileAndDoBasicAssert(deviceProfile);
@ -366,9 +366,9 @@ public class DeviceProfileEdgeTest extends AbstractEdgeTest {
AbstractLwM2MBootstrapServerCredential bootstrapServerCredential = new NoSecLwM2MBootstrapServerCredential();
bootstrapServerCredential.setServerPublicKey("PUBLIC_KEY");
bootstrapServerCredential.setShortServerId(123);
bootstrapServerCredential.setBootstrapServerIs(true);
bootstrapServerCredential.setBootstrapServerIs(false);
bootstrapServerCredential.setHost("localhost");
bootstrapServerCredential.setPort(5687);
bootstrapServerCredential.setPort(5685);
bootstrap.add(bootstrapServerCredential);
transportConfiguration.setBootstrap(bootstrap);

5
application/src/test/java/org/thingsboard/server/transport/coap/attributes/AbstractCoapAttributesIntegrationTest.java

@ -25,6 +25,7 @@ import org.awaitility.Awaitility;
import org.eclipse.californium.core.CoapObserveRelation;
import org.eclipse.californium.core.CoapResponse;
import org.eclipse.californium.core.coap.CoAP;
import org.eclipse.californium.core.coap.CoAP.ResponseCode;
import org.springframework.beans.factory.annotation.Autowired;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.DynamicProtoUtils;
@ -238,7 +239,7 @@ public abstract class AbstractCoapAttributesIntegrationTest extends AbstractCoap
String awaitAlias = "await Json Test Subscribe To AttributesUpdates (client.getObserveRelation)";
await(awaitAlias)
.atMost(DEFAULT_WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS)
.until(() -> CoAP.ResponseCode.CONTENT.equals(callbackCoap.getResponseCode()) &&
.until(() -> ResponseCode.CONTENT.equals(callbackCoap.getResponseCode()) &&
callbackCoap.getObserve() != null &&
0 == callbackCoap.getObserve().intValue());
if (emptyCurrentStateNotification) {
@ -285,7 +286,7 @@ public abstract class AbstractCoapAttributesIntegrationTest extends AbstractCoap
CoapObserveRelation observeRelation = client.getObserveRelation(callbackCoap);
await(awaitAlias)
.atMost(DEFAULT_WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS)
.until(() -> CoAP.ResponseCode.CONTENT.equals(callbackCoap.getResponseCode()) &&
.until(() -> ResponseCode.CONTENT.equals(callbackCoap.getResponseCode()) &&
callbackCoap.getObserve() != null &&
0 == callbackCoap.getObserve().intValue());

5
application/src/test/java/org/thingsboard/server/transport/coap/attributes/updates/CoapAttributesUpdatesIntegrationTest.java

@ -19,6 +19,7 @@ import lombok.extern.slf4j.Slf4j;
import org.eclipse.californium.core.server.resources.Resource;
import org.junit.After;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.thingsboard.server.coapserver.DefaultCoapServerService;
@ -59,11 +60,15 @@ public class CoapAttributesUpdatesIntegrationTest extends AbstractCoapAttributes
processAfterTest();
}
@Ignore // Uncomment when Californium 3.11 is released with https://github.com/eclipse-californium/californium/pull/2215
@Test
public void testSubscribeToAttributesUpdatesFromTheServer() throws Exception {
processJsonTestSubscribeToAttributesUpdates(false);
}
@Ignore // Uncomment when Californium 3.11 is released with https://github.com/eclipse-californium/californium/pull/2215
@Test
public void testSubscribeToAttributesUpdatesFromTheServerWithEmptyCurrentStateNotification() throws Exception {
processJsonTestSubscribeToAttributesUpdates(true);

4
application/src/test/java/org/thingsboard/server/transport/coap/attributes/updates/CoapAttributesUpdatesJsonIntegrationTest.java

@ -18,6 +18,7 @@ package org.thingsboard.server.transport.coap.attributes.updates;
import lombok.extern.slf4j.Slf4j;
import org.junit.After;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;
import org.thingsboard.server.common.data.CoapDeviceType;
import org.thingsboard.server.common.data.TransportPayloadType;
@ -44,11 +45,12 @@ public class CoapAttributesUpdatesJsonIntegrationTest extends AbstractCoapAttrib
processAfterTest();
}
@Ignore // Uncomment when Californium 3.11 is released with https://github.com/eclipse-californium/californium/pull/2215
@Test
public void testSubscribeToAttributesUpdatesFromTheServer() throws Exception {
processJsonTestSubscribeToAttributesUpdates(false);
}
@Ignore // Uncomment when Californium 3.11 is released with https://github.com/eclipse-californium/californium/pull/2215
@Test
public void testSubscribeToAttributesUpdatesFromTheServerWithEmptyCurrentStateNotification() throws Exception {
processJsonTestSubscribeToAttributesUpdates(true);

5
application/src/test/java/org/thingsboard/server/transport/coap/attributes/updates/CoapAttributesUpdatesProtoIntegrationTest.java

@ -18,6 +18,7 @@ package org.thingsboard.server.transport.coap.attributes.updates;
import lombok.extern.slf4j.Slf4j;
import org.junit.After;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;
import org.thingsboard.server.common.data.CoapDeviceType;
import org.thingsboard.server.common.data.TransportPayloadType;
@ -43,12 +44,12 @@ public class CoapAttributesUpdatesProtoIntegrationTest extends AbstractCoapAttri
public void afterTest() throws Exception {
processAfterTest();
}
@Ignore // Uncomment when Californium 3.11 is released with https://github.com/eclipse-californium/californium/pull/2215
@Test
public void testSubscribeToAttributesUpdatesFromTheServer() throws Exception {
processProtoTestSubscribeToAttributesUpdates(false);
}
@Ignore // Uncomment when Californium 3.11 is released with https://github.com/eclipse-californium/californium/pull/2215
@Test
public void testSubscribeToAttributesUpdatesFromTheServerWithEmptyCurrentStateNotification() throws Exception {
processProtoTestSubscribeToAttributesUpdates(true);

3
application/src/test/java/org/thingsboard/server/transport/coap/client/CoapClientIntegrationTest.java

@ -26,6 +26,7 @@ import org.eclipse.californium.core.coap.CoAP;
import org.eclipse.californium.core.coap.MediaTypeRegistry;
import org.junit.After;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.id.DeviceId;
@ -82,6 +83,7 @@ public class CoapClientIntegrationTest extends AbstractCoapIntegrationTest {
processAfterTest();
}
@Ignore // Uncomment when Californium 3.11 is released with https://github.com/eclipse-californium/californium/pull/2215
@Test
public void testConfirmableRequests() throws Exception {
boolean confirmable = true;
@ -90,6 +92,7 @@ public class CoapClientIntegrationTest extends AbstractCoapIntegrationTest {
processTestRequestAttributesValuesFromTheServer(confirmable);
}
@Ignore // Uncomment when Californium 3.11 is released with https://github.com/eclipse-californium/californium/pull/2215
@Test
public void testNonConfirmableRequests() throws Exception {
boolean confirmable = false;

18
application/src/test/java/org/thingsboard/server/transport/coap/rpc/AbstractCoapServerSideRpcIntegrationTest.java

@ -82,15 +82,27 @@ public abstract class AbstractCoapServerSideRpcIntegrationTest extends AbstractC
.until(() -> CoAP.ResponseCode.VALID.equals(callbackCoap.getResponseCode()) &&
callbackCoap.getObserve() != null && 0 == callbackCoap.getObserve());
validateCurrentStateNotification(callbackCoap);
int expectedObserveAfterRpcProcessed = callbackCoap.getObserve() + 1;
String setGpioRequest = "{\"method\":\"setGpio\",\"params\":{\"pin\": \"23\",\"value\": 1}}";
int expectedObserveAfterRpcProcessed1 = callbackCoap.getObserve() + 1;
String setGpioRequest = "{\"method\":\"setGpio1\",\"params\":{\"pin\": \"21\",\"value\": 1}}";
String deviceId = savedDevice.getId().getId().toString();
String result = doPostAsync("/api/rpc/oneway/" + deviceId, setGpioRequest, String.class, status().isOk());
awaitAlias = "await One Way Rpc setGpio(method, params, value)";
await(awaitAlias)
.atMost(DEFAULT_WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS)
.until(() -> CoAP.ResponseCode.CONTENT.equals(callbackCoap.getResponseCode()) &&
callbackCoap.getObserve() != null && expectedObserveAfterRpcProcessed == callbackCoap.getObserve());
callbackCoap.getObserve() != null && expectedObserveAfterRpcProcessed1 == callbackCoap.getObserve());
validateOneWayStateChangedNotification(callbackCoap, result);
int expectedObserveAfterRpcProcessed2 = callbackCoap.getObserve() + 1;
setGpioRequest = "{\"method\":\"setGpio2\",\"params\":{\"pin\": \"22\",\"value\": 2}}";
deviceId = savedDevice.getId().getId().toString();
result = doPostAsync("/api/rpc/oneway/" + deviceId, setGpioRequest, String.class, status().isOk());
awaitAlias = "await One Way Rpc setGpio(method, params, value)";
await(awaitAlias)
.atMost(DEFAULT_WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS)
.until(() -> CoAP.ResponseCode.CONTENT.equals(callbackCoap.getResponseCode()) &&
callbackCoap.getObserve() != null && expectedObserveAfterRpcProcessed2 == callbackCoap.getObserve());
validateOneWayStateChangedNotification(callbackCoap, result);
observeRelation.proactiveCancel();

3
application/src/test/java/org/thingsboard/server/transport/coap/rpc/CoapServerSideRpcDefaultIntegrationTest.java

@ -20,6 +20,7 @@ import lombok.extern.slf4j.Slf4j;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;
import org.thingsboard.server.dao.service.DaoSqlTest;
import org.thingsboard.server.service.security.AccessValidator;
@ -82,11 +83,13 @@ public class CoapServerSideRpcDefaultIntegrationTest extends AbstractCoapServerS
Assert.assertEquals(AccessValidator.DEVICE_WITH_REQUESTED_ID_NOT_FOUND, result);
}
@Ignore // Uncomment when Californium 3.11 is released with https://github.com/eclipse-californium/californium/pull/2215
@Test
public void testServerCoapOneWayRpc() throws Exception {
processOneWayRpcTest(false);
}
@Ignore // Uncomment when Californium 3.11 is released with https://github.com/eclipse-californium/californium/pull/2215
@Test
public void testServerCoapTwoWayRpc() throws Exception {
processTwoWayRpcTest("{\"value1\":\"A\",\"value2\":\"B\"}", false);

3
application/src/test/java/org/thingsboard/server/transport/coap/rpc/CoapServerSideRpcJsonIntegrationTest.java

@ -18,6 +18,7 @@ package org.thingsboard.server.transport.coap.rpc;
import lombok.extern.slf4j.Slf4j;
import org.junit.After;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;
import org.thingsboard.server.common.data.CoapDeviceType;
import org.thingsboard.server.common.data.TransportPayloadType;
@ -43,11 +44,13 @@ public class CoapServerSideRpcJsonIntegrationTest extends AbstractCoapServerSide
processAfterTest();
}
@Ignore // Uncomment when Californium 3.11 is released with https://github.com/eclipse-californium/californium/pull/2215
@Test
public void testServerCoapOneWayRpc() throws Exception {
processOneWayRpcTest(false);
}
@Ignore // Uncomment when Californium 3.11 is released with https://github.com/eclipse-californium/californium/pull/2215
@Test
public void testServerCoapTwoWayRpc() throws Exception {
processTwoWayRpcTest("{\"value1\":\"A\",\"value2\":\"B\"}", false);

3
application/src/test/java/org/thingsboard/server/transport/coap/rpc/CoapServerSideRpcProtoIntegrationTest.java

@ -18,6 +18,7 @@ package org.thingsboard.server.transport.coap.rpc;
import lombok.extern.slf4j.Slf4j;
import org.junit.After;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;
import org.thingsboard.server.common.data.CoapDeviceType;
import org.thingsboard.server.common.data.TransportPayloadType;
@ -44,11 +45,13 @@ public class CoapServerSideRpcProtoIntegrationTest extends AbstractCoapServerSid
processAfterTest();
}
@Ignore // Uncomment when Californium 3.11 is released with https://github.com/eclipse-californium/californium/pull/2215
@Test
public void testServerCoapOneWayRpc() throws Exception {
processOneWayRpcTest(true);
}
@Ignore // Uncomment when Californium 3.11 is released with https://github.com/eclipse-californium/californium/pull/2215
@Test
public void testServerCoapTwoWayRpc() throws Exception {
processTwoWayRpcTest("{\"payload\":\"{\\\"value1\\\":\\\"A\\\",\\\"value2\\\":\\\"B\\\"}\"}", true);

74
application/src/test/java/org/thingsboard/server/transport/lwm2m/AbstractLwM2MIntegrationTest.java

@ -17,10 +17,12 @@ package org.thingsboard.server.transport.lwm2m;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.IOUtils;
import org.eclipse.californium.elements.config.Configuration;
import org.eclipse.leshan.client.californium.LeshanClient;
import org.eclipse.leshan.client.LeshanClient;
import org.eclipse.leshan.client.object.Security;
import org.eclipse.leshan.core.ResponseCode;
import org.junit.After;
@ -61,8 +63,8 @@ import org.thingsboard.server.common.data.query.EntityKeyType;
import org.thingsboard.server.common.data.query.SingleEntityFilter;
import org.thingsboard.server.common.data.security.DeviceCredentials;
import org.thingsboard.server.common.data.security.DeviceCredentialsType;
import org.thingsboard.server.common.transport.util.JsonUtils;
import org.thingsboard.server.dao.service.DaoSqlTest;
import org.thingsboard.server.service.ws.telemetry.cmd.TelemetryCmdsWrapper;
import org.thingsboard.server.service.ws.telemetry.cmd.v2.EntityDataCmd;
import org.thingsboard.server.service.ws.telemetry.cmd.v2.EntityDataUpdate;
import org.thingsboard.server.service.ws.telemetry.cmd.v2.LatestValueCmd;
@ -75,7 +77,6 @@ import java.io.IOException;
import java.net.ServerSocket;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Base64;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
@ -88,8 +89,8 @@ import static org.awaitility.Awaitility.await;
import static org.eclipse.californium.core.config.CoapConfig.COAP_PORT;
import static org.eclipse.californium.core.config.CoapConfig.COAP_SECURE_PORT;
import static org.eclipse.leshan.client.object.Security.noSec;
import static org.eclipse.leshan.client.object.Security.noSecBootstap;
import static org.hamcrest.core.IsInstanceOf.instanceOf;
import static org.junit.Assert.assertEquals;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.LwM2MClientState.ON_BOOTSTRAP_STARTED;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.LwM2MClientState.ON_BOOTSTRAP_SUCCESS;
@ -121,8 +122,11 @@ public abstract class AbstractLwM2MIntegrationTest extends AbstractTransportInte
public static final String host = "localhost";
public static final String hostBs = "localhost";
public static final int shortServerId = 123;
public static final int shortServerIdBs = 111;
public static final Integer shortServerId = 123;
public static final Integer shortServerIdBs0 = 0;
public static final int serverId = 1;
public static final int serverIdBs = 0;
public static final String COAP = "coap://";
public static final String COAPS = "coaps://";
public static final String URI = COAP + host + ":" + port;
@ -132,7 +136,6 @@ public abstract class AbstractLwM2MIntegrationTest extends AbstractTransportInte
public static final Configuration COAP_CONFIG = new Configuration().set(COAP_PORT, port).set(COAP_SECURE_PORT, securityPort);
public static Configuration COAP_CONFIG_BS = new Configuration().set(COAP_PORT, portBs).set(COAP_SECURE_PORT, securityPortBs);
public static final Security SECURITY_NO_SEC = noSec(URI, shortServerId);
public static final Security SECURITY_NO_SEC_BS = noSecBootstap(URI_BS);
protected final String OBSERVE_ATTRIBUTES_WITHOUT_PARAMS =
" {\n" +
@ -142,18 +145,17 @@ public abstract class AbstractLwM2MIntegrationTest extends AbstractTransportInte
" \"telemetry\": [],\n" +
" \"attributeLwm2m\": {}\n" +
" }";
public static final String OBSERVE_ATTRIBUTES_WITH_PARAMS =
public static String OBSERVE_ATTRIBUTES_WITH_PARAMS =
" {\n" +
" \"keyName\": {\n" +
" \"/3_1.0/0/9\": \"batteryLevel\"\n" +
" \"/3_1.2/0/9\": \"batteryLevel\"\n" +
" },\n" +
" \"observe\": [],\n" +
" \"attribute\": [\n" +
" ],\n" +
" \"telemetry\": [\n" +
" \"/3_1.0/0/9\"\n" +
" \"/3_1.2/0/9\"\n" +
" ],\n" +
" \"attributeLwm2m\": {}\n" +
" }";
@ -178,6 +180,8 @@ public abstract class AbstractLwM2MIntegrationTest extends AbstractTransportInte
protected ScheduledExecutorService executor;
protected LwM2MTestClient lwM2MTestClient;
private String[] resources;
protected String deviceId;
protected boolean isWriteAttribute = false;
@Before
public void startInit() throws Exception {
@ -232,8 +236,9 @@ public abstract class AbstractLwM2MIntegrationTest extends AbstractTransportInte
getWsClient().waitForReply();
getWsClient().registerWaitForUpdate();
createNewClient(security, coapConfig, false, endpoint, false, null);
awaitObserveReadAll(0, false, device.getId().getId().toString());
createNewClient(security, null, coapConfig, false, endpoint);
deviceId = device.getId().getId().toString();
awaitObserveReadAll(0, deviceId);
String msg = getWsClient().waitForUpdate();
EntityDataUpdate update = JacksonUtil.fromString(msg, EntityDataUpdate.class);
@ -249,6 +254,8 @@ public abstract class AbstractLwM2MIntegrationTest extends AbstractTransportInte
int expectedMin = 5;
Assert.assertTrue(expectedMax >= Long.parseLong(tsValue.getValue()));
Assert.assertTrue(expectedMin <= Long.parseLong(tsValue.getValue()));
}
protected void createDeviceProfile(Lwm2mDeviceProfileTransportConfiguration transportConfiguration) throws Exception {
@ -297,14 +304,14 @@ public abstract class AbstractLwM2MIntegrationTest extends AbstractTransportInte
this.resources = resources;
}
public void createNewClient(Security security, Configuration coapConfig, boolean isRpc, String endpoint, boolean isBootstrap, Security securityBs) throws Exception {
public void createNewClient(Security security, Security securityBs, Configuration coapConfig, boolean isRpc, String endpoint) throws Exception {
this.clientDestroy();
lwM2MTestClient = new LwM2MTestClient(this.executor, endpoint);
try (ServerSocket socket = new ServerSocket(0)) {
int clientPort = socket.getLocalPort();
lwM2MTestClient.init(security, coapConfig, clientPort, isRpc, isBootstrap, this.shortServerId, this.shortServerIdBs,
securityBs, this.defaultLwM2mUplinkMsgHandlerTest, this.clientContextTest);
lwM2MTestClient.init(security, securityBs, coapConfig, clientPort, isRpc,
this.defaultLwM2mUplinkMsgHandlerTest, this.clientContextTest, isWriteAttribute);
}
}
@ -351,7 +358,7 @@ public abstract class AbstractLwM2MIntegrationTest extends AbstractTransportInte
private AbstractLwM2MBootstrapServerCredential getBootstrapServerCredentialNoSec(boolean isBootstrap) {
AbstractLwM2MBootstrapServerCredential bootstrapServerCredential = new NoSecLwM2MBootstrapServerCredential();
bootstrapServerCredential.setServerPublicKey("");
bootstrapServerCredential.setShortServerId(isBootstrap ? shortServerIdBs : shortServerId);
bootstrapServerCredential.setShortServerId(isBootstrap ? shortServerIdBs0 : shortServerId);
bootstrapServerCredential.setBootstrapServerIs(isBootstrap);
bootstrapServerCredential.setHost(isBootstrap ? hostBs : host);
bootstrapServerCredential.setPort(isBootstrap ? portBs : port);
@ -394,20 +401,25 @@ public abstract class AbstractLwM2MIntegrationTest extends AbstractTransportInte
.until(() -> leshanClient.getRegisteredServers().size() == 0);
}
protected void awaitObserveReadAll(int cntObserve, boolean isBootstrap, String deviceIdStr) throws Exception {
if (!isBootstrap) {
await("ObserveReadAll after start client: countObserve " + cntObserve)
.atMost(40, TimeUnit.SECONDS)
.until(() -> {
String actualResultReadAll = sendObserve("ObserveReadAll", null, deviceIdStr);
ObjectNode rpcActualResultReadAll = JacksonUtil.fromString(actualResultReadAll, ObjectNode.class);
Assert.assertEquals(ResponseCode.CONTENT.getName(), rpcActualResultReadAll.get("result").asText());
String actualValuesReadAll = rpcActualResultReadAll.get("value").asText();
log.warn("ObserveReadAll: [{}]", actualValuesReadAll);
int actualCntObserve = "[]".equals(actualValuesReadAll) ? 0 : actualValuesReadAll.split(",").length;
return cntObserve == actualCntObserve;
});
}
protected void awaitObserveReadAll(int cntObserve, String deviceIdStr) throws Exception {
await("ObserveReadAll after start client/test: countObserve " + cntObserve)
.atMost(40, TimeUnit.SECONDS)
.until(() -> cntObserve == getCntObserveAll(deviceIdStr));
}
protected Integer getCntObserveAll(String deviceIdStr) throws Exception {
String actualResultBefore = sendObserve("ObserveReadAll", null, deviceIdStr);
ObjectNode rpcActualResultBefore = JacksonUtil.fromString(actualResultBefore, ObjectNode.class);
assertEquals(ResponseCode.CONTENT.getName(), rpcActualResultBefore.get("result").asText());
JsonElement element = JsonUtils.parse(rpcActualResultBefore.get("value").asText());
return element.isJsonArray() ? ((JsonArray)element).size() : null;
}
protected void sendCancelObserveAllWithAwait(String deviceIdStr) throws Exception {
String actualResultCancelAll = sendObserve("ObserveCancelAll", null, deviceIdStr);
ObjectNode rpcActualResultCancelAll = JacksonUtil.fromString(actualResultCancelAll, ObjectNode.class);
assertEquals(ResponseCode.CONTENT.getName(), rpcActualResultCancelAll.get("result").asText());
awaitObserveReadAll(0, deviceId);
}
protected String sendObserve(String method, String params, String deviceIdStr) throws Exception {

3
application/src/test/java/org/thingsboard/server/transport/lwm2m/Lwm2mTestHelper.java

@ -34,12 +34,15 @@ public class Lwm2mTestHelper {
public static final int RESOURCE_ID_2 = 2;
public static final int RESOURCE_ID_3 = 3;
public static final int RESOURCE_ID_4 = 4;
public static final int RESOURCE_ID_5 = 5;
public static final int RESOURCE_ID_6 = 6;
public static final int RESOURCE_ID_7 = 7;
public static final int RESOURCE_ID_8 = 8;
public static final int RESOURCE_ID_9 = 9;
public static final int RESOURCE_ID_11 = 11;
public static final int RESOURCE_ID_14 = 14;
public static final int RESOURCE_ID_15 = 15;
public static final int RESOURCE_INSTANCE_ID_0 = 0;
public static final int RESOURCE_INSTANCE_ID_2 = 2;
public static final String RESOURCE_ID_NAME_3_9 = "batteryLevel";

147
application/src/test/java/org/thingsboard/server/transport/lwm2m/attributes/DefaultLwM2mAttributeParserTest.java

@ -0,0 +1,147 @@
/**
* Copyright © 2016-2024 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.transport.lwm2m.attributes;
import org.eclipse.leshan.core.link.LinkParseException;
import org.eclipse.leshan.core.link.attributes.InvalidAttributeException;
import org.eclipse.leshan.core.link.lwm2m.attributes.DefaultLwM2mAttributeParser;
import org.eclipse.leshan.core.link.lwm2m.attributes.LwM2mAttribute;
import org.eclipse.leshan.core.link.lwm2m.attributes.LwM2mAttributeSet;
import org.eclipse.leshan.core.link.lwm2m.attributes.LwM2mAttributes;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import java.util.stream.Stream;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
public class DefaultLwM2mAttributeParserTest {
private final DefaultLwM2mAttributeParser parser = new DefaultLwM2mAttributeParser();
private static Stream<Arguments> validAttributes() throws InvalidAttributeException {
return Stream.of(//
Arguments.of("pmin", LwM2mAttributes.create(LwM2mAttributes.MINIMUM_PERIOD)), //
Arguments.of("pmin=30", LwM2mAttributes.create(LwM2mAttributes.MINIMUM_PERIOD, 30l)), //
Arguments.of("pmax", LwM2mAttributes.create(LwM2mAttributes.MAXIMUM_PERIOD)), //
Arguments.of("pmax=60", LwM2mAttributes.create(LwM2mAttributes.MAXIMUM_PERIOD, 60l)), //
Arguments.of("dim=2", LwM2mAttributes.create(LwM2mAttributes.DIMENSION, 2l)), //
Arguments.of("epmin", LwM2mAttributes.create(LwM2mAttributes.EVALUATE_MINIMUM_PERIOD)), //
Arguments.of("epmin=30", LwM2mAttributes.create(LwM2mAttributes.EVALUATE_MINIMUM_PERIOD, 30l)), //
Arguments.of("epmax", LwM2mAttributes.create(LwM2mAttributes.EVALUATE_MAXIMUM_PERIOD)), //
Arguments.of("epmax=60", LwM2mAttributes.create(LwM2mAttributes.EVALUATE_MAXIMUM_PERIOD, 60l)), //
Arguments.of("lt", LwM2mAttributes.create(LwM2mAttributes.LESSER_THAN)), //
Arguments.of("lt=30", LwM2mAttributes.create(LwM2mAttributes.LESSER_THAN, 30d)), //
Arguments.of("lt=-30", LwM2mAttributes.create(LwM2mAttributes.LESSER_THAN, -30d)), //
Arguments.of("lt=30.55", LwM2mAttributes.create(LwM2mAttributes.LESSER_THAN, 30.55)), //
Arguments.of("lt=-30.55", LwM2mAttributes.create(LwM2mAttributes.LESSER_THAN, -30.55)), //
Arguments.of("gt", LwM2mAttributes.create(LwM2mAttributes.GREATER_THAN)), //
Arguments.of("gt=60", LwM2mAttributes.create(LwM2mAttributes.GREATER_THAN, 60d)), //
Arguments.of("gt=-60", LwM2mAttributes.create(LwM2mAttributes.GREATER_THAN, -60d)), //
Arguments.of("gt=60.55", LwM2mAttributes.create(LwM2mAttributes.GREATER_THAN, 60.55)), //
Arguments.of("gt=-60.55", LwM2mAttributes.create(LwM2mAttributes.GREATER_THAN, -60.55)), //
Arguments.of("st", LwM2mAttributes.create(LwM2mAttributes.STEP)), //
Arguments.of("st=60", LwM2mAttributes.create(LwM2mAttributes.STEP, 60d)), //
Arguments.of("st=60.55", LwM2mAttributes.create(LwM2mAttributes.STEP, 60.55)) //
);
}
@ParameterizedTest(name = "Tests {index} : {0}")
@MethodSource("validAttributes")
public void check_one_valid_attribute(String attributeAsString, LwM2mAttribute<?> expectedValue)
throws LinkParseException, InvalidAttributeException {
LwM2mAttributeSet parsed = new LwM2mAttributeSet(parser.parseUriQuery(attributeAsString));
LwM2mAttributeSet attResult = new LwM2mAttributeSet(expectedValue);
assertEquals(parsed, attResult);
}
private static String[] invalidAttributes() {
return new String[] { //
// MINIMUM_PERIOD
"pmin=", //
"pmin=1.9", //
"pmin=-1.8", //
"pmin=a", //
// MAXIMUM_PERIOD
"pmax=", //
"pmax=-2", //
"pmax=2.7", //
"pmax=-2.6", //
"pmax=bc", //
// DIMENSION
"dim", //
"dim=", //
"dim=-3", //
"dim=3.5", //
"dim=-3.4", //
"dim=def", //
// EVALUATE_MINIMUM_PERIOD
"epmin=", //
"epmin=-4", //
"epmin=4.3", //
"epmin=-4.2", //
"epmin=ghij", //
// EVALUATE_MAXIMUM_PERIOD
"epmax=", //
"epmax=-5", //
"epmax=5.1", //
"epmax=-5.9", //
"epmax=klmno", //
// LESSER_THAN
"lt=", //
"lt=pqrts", //
"lt=0abc", //
// GREATER_THAN
"gt=", //
"gt=uvwxyz", //
"gt=0.xyz", //
// STEP
"st=", //
"st=-6", //
"st=-6.8", //
// Multi-attributes
"&pmin&pmax=60&gt=2&", //
"&pmin&pmax=60&gt=2", //
"pmin&pmax=60&gt=2&", //
"pmin&pmax=60&&gt=2", //
"pmin&pmax=60&&gt=2&&&", //
"&&&pmin&pmax=60&&gt=2", //
};
}
@ParameterizedTest(name = "Test {index} : {0}")
@MethodSource("invalidAttributes")
public void check_invalid_attributes(String invalidAttributesAsString) {
assertThrows(InvalidAttributeException.class,
() -> new LwM2mAttributeSet(parser.parseUriQuery(invalidAttributesAsString)));
}
@Test
public void check_multiple_valid_attributes() throws LinkParseException, InvalidAttributeException {
LwM2mAttributeSet parsed = new LwM2mAttributeSet(parser.parseUriQuery("pmin&pmax=60&gt=2"));
LwM2mAttributeSet attResult = new LwM2mAttributeSet( //
LwM2mAttributes.create(LwM2mAttributes.MINIMUM_PERIOD), //
LwM2mAttributes.create(LwM2mAttributes.MAXIMUM_PERIOD, 60l), //
LwM2mAttributes.create(LwM2mAttributes.GREATER_THAN, 2d));
assertEquals(attResult, parsed);
}
}

76
application/src/test/java/org/thingsboard/server/transport/lwm2m/attributes/DefaultLwM2mLinkParserTest.java

@ -0,0 +1,76 @@
/**
* Copyright © 2016-2024 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.transport.lwm2m.attributes;
import org.eclipse.leshan.core.link.LinkParseException;
import org.eclipse.leshan.core.link.attributes.AttributeSet;
import org.eclipse.leshan.core.link.lwm2m.DefaultLwM2mLinkParser;
import org.eclipse.leshan.core.link.lwm2m.LwM2mLink;
import org.eclipse.leshan.core.link.lwm2m.LwM2mLinkParser;
import org.eclipse.leshan.core.link.lwm2m.attributes.LwM2mAttributeSet;
import org.eclipse.leshan.core.link.lwm2m.attributes.LwM2mAttributes;
import org.eclipse.leshan.core.node.LwM2mPath;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrowsExactly;
public class DefaultLwM2mLinkParserTest {
private final LwM2mLinkParser parser = new DefaultLwM2mLinkParser();
@Test
public void check_invalid_values() throws LinkParseException {
// first check it's OK with valid value (3/0/11 - "errorCodes")
LwM2mLink[] parsed = parser.parseLwM2mLinkFromCoreLinkFormat("</3/0/11>;dim=255".getBytes(), null);
assertEquals(new LwM2mPath(3, 0, 11), parsed[0].getPath());
AttributeSet attResult = new LwM2mAttributeSet(LwM2mAttributes.create(LwM2mAttributes.DIMENSION, 255l));
assertEquals(attResult, parsed[0].getAttributes());
// then check an invalid one
assertThrowsExactly(LinkParseException.class, () -> {
// dim should be between 0-255
parser.parseLwM2mLinkFromCoreLinkFormat("</3/0/11>;dim=256".getBytes(), null);
});
// first check it's OK with valid value
parsed = parser.parseLwM2mLinkFromCoreLinkFormat("</0/1>;ssid=1".getBytes(), null);
assertEquals(new LwM2mPath(0, 1), parsed[0].getPath());
attResult = new LwM2mAttributeSet(LwM2mAttributes.create(LwM2mAttributes.SHORT_SERVER_ID, 1l));
assertEquals(attResult, parsed[0].getAttributes());
// then check an invalid one
assertThrowsExactly(LinkParseException.class, () -> {
// ssid should be between 1-65534
parser.parseLwM2mLinkFromCoreLinkFormat("</0/1>;ssid=0".getBytes(), null);
});
}
@Test
public void check_attribute_with_no_value_failed() throws LinkParseException {
// first check it's OK with value
LwM2mLink[] parsed = parser.parseLwM2mLinkFromCoreLinkFormat("</3/0/11>;pmin=200".getBytes(), null);
assertEquals(new LwM2mPath(3, 0, 11), parsed[0].getPath());
AttributeSet attResult = new LwM2mAttributeSet(LwM2mAttributes.create(LwM2mAttributes.MINIMUM_PERIOD, 200l));
assertEquals(attResult, parsed[0].getAttributes());
// then check an invalid one
assertThrowsExactly(LinkParseException.class, () -> {
// pmin should be with value
parser.parseLwM2mLinkFromCoreLinkFormat("</3/0/11>;pmin".getBytes(), null);
});
}
}

87
application/src/test/java/org/thingsboard/server/transport/lwm2m/attributes/LwM2mAttributesTest.java

@ -0,0 +1,87 @@
/**
* Copyright © 2016-2024 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.transport.lwm2m.attributes;
import org.eclipse.leshan.core.link.attributes.InvalidAttributeException;
import org.eclipse.leshan.core.link.lwm2m.attributes.LwM2mAttribute;
import org.eclipse.leshan.core.link.lwm2m.attributes.LwM2mAttributeModel;
import org.eclipse.leshan.core.link.lwm2m.attributes.LwM2mAttributes;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import java.util.stream.Stream;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
public class LwM2mAttributesTest {
@ParameterizedTest(name = "Tests {index} : {0}")
@MethodSource("supportNullAttributes")
public void check_attribute_can_be_created_with_null_value(LwM2mAttributeModel<?> model)
throws InvalidAttributeException {
LwM2mAttribute<?> attribute = LwM2mAttributes.create(model);
assertNotNull(attribute);
assertFalse(attribute.hasValue());
assertNull(attribute.getValue());
attribute = LwM2mAttributes.create(model, null);
assertNotNull(attribute);
assertFalse(attribute.hasValue());
assertNull(attribute.getValue());
}
@ParameterizedTest(name = "Tests {index} : {0}")
@MethodSource("doesntSupportAttributesWithoutValue")
public void check_attribute_can_not_be_created_without_value(LwM2mAttributeModel<?> model) {
assertThrows(UnsupportedOperationException.class, () -> LwM2mAttributes.create(model));
}
@ParameterizedTest(name = "Tests {index} : {0}")
@MethodSource("doesntSupportAttributesWithValueNull")
public void check_attribute_can_not_be_created_with_null(LwM2mAttributeModel<?> model) {
assertThrows(NullPointerException.class, () -> LwM2mAttributes.create(model, null));
}
private static Stream<Arguments> supportNullAttributes() throws InvalidAttributeException {
return Stream.of(//
Arguments.of(LwM2mAttributes.MINIMUM_PERIOD), //
Arguments.of(LwM2mAttributes.MAXIMUM_PERIOD), //
Arguments.of(LwM2mAttributes.EVALUATE_MINIMUM_PERIOD), //
Arguments.of(LwM2mAttributes.EVALUATE_MAXIMUM_PERIOD), //
Arguments.of(LwM2mAttributes.LESSER_THAN), //
Arguments.of(LwM2mAttributes.GREATER_THAN), //
Arguments.of(LwM2mAttributes.STEP) //
);
}
private static Stream<Arguments> doesntSupportAttributesWithoutValue() throws InvalidAttributeException {
return Stream.of(//
Arguments.of(LwM2mAttributes.ENABLER_VERSION), //
Arguments.of(LwM2mAttributes.OBJECT_VERSION)//
);
}
private static Stream<Arguments> doesntSupportAttributesWithValueNull() throws InvalidAttributeException {
return Stream.of(//
Arguments.of(LwM2mAttributes.DIMENSION), //
Arguments.of(LwM2mAttributes.SHORT_SERVER_ID) //
);
}
}

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

@ -17,9 +17,10 @@ package org.thingsboard.server.transport.lwm2m.client;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.leshan.client.resource.BaseInstanceEnabler;
import org.eclipse.leshan.client.servers.ServerIdentity;
import org.eclipse.leshan.client.servers.LwM2mServer;
import org.eclipse.leshan.core.model.ObjectModel;
import org.eclipse.leshan.core.node.LwM2mResource;
import org.eclipse.leshan.core.request.argument.Arguments;
import org.eclipse.leshan.core.response.ExecuteResponse;
import org.eclipse.leshan.core.response.ReadResponse;
import org.eclipse.leshan.core.response.WriteResponse;
@ -45,7 +46,7 @@ public class FwLwM2MDevice extends BaseInstanceEnabler implements Destroyable {
private final AtomicInteger updateResult = new AtomicInteger(0);
@Override
public ReadResponse read(ServerIdentity identity, int resourceId) {
public ReadResponse read(LwM2mServer identity, int resourceId) {
if (!identity.isSystem())
log.info("Read on Device resource /{}/{}/{}", getModel().id, getId(), resourceId);
switch (resourceId) {
@ -65,24 +66,24 @@ public class FwLwM2MDevice extends BaseInstanceEnabler implements Destroyable {
}
@Override
public ExecuteResponse execute(ServerIdentity identity, int resourceId, String params) {
String withParams = null;
if (params != null && params.length() != 0) {
withParams = " with params " + params;
}
log.info("Execute on Device resource /{}/{}/{} {}", getModel().id, getId(), resourceId, withParams != null ? withParams : "");
public ExecuteResponse execute(LwM2mServer identity, int resourceId, Arguments arguments) {
String withArguments = "";
if (!arguments.isEmpty())
withArguments = " with arguments " + arguments;
log.info("Execute on Device resource /{}/{}/{} {}", getModel().id, getId(), resourceId, withArguments);
switch (resourceId) {
case 2:
startUpdating();
return ExecuteResponse.success();
default:
return super.execute(identity, resourceId, params);
return super.execute(identity, resourceId, arguments);
}
}
@Override
public WriteResponse write(ServerIdentity identity, boolean replace, int resourceId, LwM2mResource value) {
public WriteResponse write(LwM2mServer identity, boolean replace, int resourceId, LwM2mResource value) {
log.info("Write on Device resource /{}/{}/{}", getModel().id, getId(), resourceId);
switch (resourceId) {

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

@ -18,17 +18,28 @@ package org.thingsboard.server.transport.lwm2m.client;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.californium.elements.config.Configuration;
import org.eclipse.californium.scandium.config.DtlsConfig;
import org.eclipse.californium.scandium.config.DtlsConnectorConfig;
import org.eclipse.leshan.client.californium.LeshanClient;
import org.eclipse.leshan.client.californium.LeshanClientBuilder;
import org.eclipse.leshan.client.LeshanClient;
import org.eclipse.leshan.client.LeshanClientBuilder;
import org.eclipse.leshan.client.californium.endpoint.CaliforniumClientEndpointFactory;
import org.eclipse.leshan.client.californium.endpoint.CaliforniumClientEndpointsProvider;
import org.eclipse.leshan.client.californium.endpoint.ClientProtocolProvider;
import org.eclipse.leshan.client.californium.endpoint.coap.CoapOscoreProtocolProvider;
import org.eclipse.leshan.client.californium.endpoint.coaps.CoapsClientEndpointFactory;
import org.eclipse.leshan.client.californium.endpoint.coaps.CoapsClientProtocolProvider;
import org.eclipse.leshan.client.endpoint.LwM2mClientEndpointsProvider;
import org.eclipse.leshan.client.engine.DefaultRegistrationEngineFactory;
import org.eclipse.leshan.client.object.Security;
import org.eclipse.leshan.client.object.Server;
import org.eclipse.leshan.client.observer.LwM2mClientObserver;
import org.eclipse.leshan.client.resource.DummyInstanceEnabler;
import org.eclipse.leshan.client.resource.LwM2mInstanceEnabler;
import org.eclipse.leshan.client.resource.LwM2mObjectEnabler;
import org.eclipse.leshan.client.resource.ObjectsInitializer;
import org.eclipse.leshan.client.servers.ServerIdentity;
import org.eclipse.leshan.client.resource.listener.ObjectsListenerAdapter;
import org.eclipse.leshan.client.send.ManualDataSender;
import org.eclipse.leshan.client.servers.LwM2mServer;
import org.eclipse.leshan.core.ResponseCode;
import org.eclipse.leshan.core.model.InvalidDDFFileException;
import org.eclipse.leshan.core.model.LwM2mModel;
@ -49,13 +60,13 @@ import org.thingsboard.server.transport.lwm2m.server.uplink.LwM2mUplinkMsgHandle
import org.thingsboard.server.transport.lwm2m.utils.LwM2mValueConverterImpl;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.ScheduledExecutorService;
import static org.eclipse.californium.scandium.config.DtlsConfig.DTLS_RECOMMENDED_CIPHER_SUITES_ONLY;
import static org.eclipse.leshan.core.LwM2mId.ACCESS_CONTROL;
import static org.eclipse.leshan.core.LwM2mId.DEVICE;
import static org.eclipse.leshan.core.LwM2mId.FIRMWARE;
@ -63,6 +74,10 @@ import static org.eclipse.leshan.core.LwM2mId.LOCATION;
import static org.eclipse.leshan.core.LwM2mId.SECURITY;
import static org.eclipse.leshan.core.LwM2mId.SERVER;
import static org.eclipse.leshan.core.LwM2mId.SOFTWARE_MANAGEMENT;
import static org.thingsboard.server.transport.lwm2m.AbstractLwM2MIntegrationTest.serverId;
import static org.thingsboard.server.transport.lwm2m.AbstractLwM2MIntegrationTest.serverIdBs;
import static org.thingsboard.server.transport.lwm2m.AbstractLwM2MIntegrationTest.shortServerId;
import static org.thingsboard.server.transport.lwm2m.AbstractLwM2MIntegrationTest.shortServerIdBs0;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.BINARY_APP_DATA_CONTAINER;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.LwM2MClientState;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.LwM2MClientState.ON_BOOTSTRAP_FAILURE;
@ -97,11 +112,6 @@ public class LwM2MTestClient {
private final ScheduledExecutorService executor;
private final String endpoint;
private LeshanClient leshanClient;
private Security lwm2mSecurity;
private Security lwm2mSecurityBs;
private Lwm2mServer lwm2mServer;
private Lwm2mServer lwm2mServerBs;
private SimpleLwM2MDevice lwM2MDevice;
private FwLwM2MDevice fwLwM2MDevice;
private SwLwM2MDevice swLwM2MDevice;
@ -112,10 +122,9 @@ public class LwM2MTestClient {
private LwM2mUplinkMsgHandler defaultLwM2mUplinkMsgHandlerTest;
private LwM2mClientContext clientContext;
public void init(Security security, Configuration coapConfig, int port, boolean isRpc, boolean isBootstrap,
int shortServerId, int shortServerIdBs, Security securityBs,
public void init(Security security, Security securityBs,Configuration coapConfig, int port, boolean isRpc,
LwM2mUplinkMsgHandler defaultLwM2mUplinkMsgHandler,
LwM2mClientContext clientContext) throws InvalidDDFFileException, IOException {
LwM2mClientContext clientContext, boolean isWriteAttribute) throws InvalidDDFFileException, IOException {
Assert.assertNull("client already initialized", leshanClient);
this.defaultLwM2mUplinkMsgHandlerTest = defaultLwM2mUplinkMsgHandler;
this.clientContext = clientContext;
@ -124,31 +133,36 @@ public class LwM2MTestClient {
models.addAll(ObjectLoader.loadDdfFile(LwM2MTestClient.class.getClassLoader().getResourceAsStream("lwm2m/" + resourceName), resourceName));
}
LwM2mModel model = new StaticModel(models);
ObjectsInitializer initializer = new ObjectsInitializer(model);
if (securityBs == null) {
initializer.setInstancesForObject(SECURITY, this.lwm2mSecurity = security);
} else {
securityBs.setId(0);
security.setId(1);
LwM2mInstanceEnabler[] instances = new LwM2mInstanceEnabler[]{this.lwm2mSecurityBs = securityBs, this.lwm2mSecurity = security};
ObjectsInitializer initializer = isWriteAttribute ? new TbObjectsInitializer(model) : new ObjectsInitializer(model);
if (securityBs != null && security != null) {
// SECURITY
security.setId(serverId);
securityBs.setId(serverIdBs);
LwM2mInstanceEnabler[] instances = new LwM2mInstanceEnabler[]{securityBs, security};
initializer.setClassForObject(SECURITY, Security.class);
initializer.setInstancesForObject(SECURITY, instances);
}
if (isBootstrap) {
initializer.setInstancesForObject(SERVER, lwm2mServerBs = new Lwm2mServer(shortServerIdBs, 300));
// SERVER
Server lwm2mServer = new Server(shortServerId, 300);
lwm2mServer.setId(serverId);
Server serverBs = new Server(shortServerIdBs0, 300);
serverBs.setId(serverIdBs);
instances = new LwM2mInstanceEnabler[]{serverBs, lwm2mServer};
initializer.setClassForObject(SERVER, Server.class);
initializer.setInstancesForObject(SERVER, instances);
} else if (securityBs != null) {
// SECURITY
initializer.setInstancesForObject(SECURITY, securityBs);
// SERVER
initializer.setClassForObject(SERVER, Server.class);
} else {
if (securityBs == null) {
initializer.setInstancesForObject(SERVER, lwm2mServer = new Lwm2mServer(shortServerId, 300));
} else {
lwm2mServerBs = new Lwm2mServer(shortServerIdBs, 300);
lwm2mServerBs.setId(0);
lwm2mServer = new Lwm2mServer(shortServerId, 300);
lwm2mServer.setId(1);
LwM2mInstanceEnabler[] instances = new LwM2mInstanceEnabler[]{lwm2mServerBs, lwm2mServer};
initializer.setClassForObject(SERVER, Server.class);
initializer.setInstancesForObject(SERVER, instances);
}
// SECURITY
initializer.setInstancesForObject(SECURITY, security);
// SERVER
Server lwm2mServer = new Server(shortServerId, 300);
lwm2mServer.setId(serverId);
initializer.setInstancesForObject(SERVER, lwm2mServer );
}
initializer.setInstancesForObject(DEVICE, lwM2MDevice = new SimpleLwM2MDevice(executor));
initializer.setInstancesForObject(FIRMWARE, fwLwM2MDevice = new FwLwM2MDevice());
initializer.setInstancesForObject(SOFTWARE_MANAGEMENT, swLwM2MDevice = new SwLwM2MDevice());
@ -160,106 +174,192 @@ public class LwM2MTestClient {
initializer.setInstancesForObject(LOCATION, new LwM2mLocation(locationParams.getLatitude(), locationParams.getLongitude(), locationParams.getScaleFactor(), executor, OBJECT_INSTANCE_ID_0));
initializer.setInstancesForObject(TEMPERATURE_SENSOR, lwM2MTemperatureSensor = new LwM2mTemperatureSensor(executor, OBJECT_INSTANCE_ID_0), new LwM2mTemperatureSensor(executor, OBJECT_INSTANCE_ID_12));
List<LwM2mObjectEnabler> enablers = initializer.createAll();
// Create Californium Endpoints Provider:
// --------------------------------------
// Define Custom CoAPS protocol provider
CoapsClientProtocolProvider customCoapsProtocolProvider = new CoapsClientProtocolProvider() {
@Override
public CaliforniumClientEndpointFactory createDefaultEndpointFactory() {
return new CoapsClientEndpointFactory() {
@Override
protected DtlsConnectorConfig.Builder createRootDtlsConnectorConfigBuilder(
Configuration configuration) {
DtlsConnectorConfig.Builder builder = super.createRootDtlsConnectorConfigBuilder(configuration);
return builder;
};
};
}
};
// Create client endpoints Provider
List<ClientProtocolProvider> protocolProvider = new ArrayList<>();
/**
* "Use java-coap for CoAP protocol instead of Californium."
*/
boolean useJavaCoap = false;
if (!useJavaCoap) protocolProvider.add(new CoapOscoreProtocolProvider());
protocolProvider.add(customCoapsProtocolProvider);
CaliforniumClientEndpointsProvider.Builder endpointsBuilder = new CaliforniumClientEndpointsProvider.Builder(
protocolProvider.toArray(new ClientProtocolProvider[protocolProvider.size()]));
// Create Californium Configuration
Configuration clientCoapConfig = endpointsBuilder.createDefaultConfiguration();
DtlsConnectorConfig.Builder dtlsConfig = new DtlsConnectorConfig.Builder(coapConfig);
dtlsConfig.set(DTLS_RECOMMENDED_CIPHER_SUITES_ONLY, true);
// Set some DTLS stuff
// These configuration values are always overwritten by CLI therefore set them to transient.
clientCoapConfig.setTransient(DtlsConfig.DTLS_RECOMMENDED_CIPHER_SUITES_ONLY);
clientCoapConfig.setTransient(DtlsConfig.DTLS_CONNECTION_ID_LENGTH);
boolean supportDeprecatedCiphers = false;
clientCoapConfig.set(DtlsConfig.DTLS_RECOMMENDED_CIPHER_SUITES_ONLY, !supportDeprecatedCiphers);
/**
* "Control usage of DTLS connection ID.", //
* "- 'on' to activate Connection ID support (same as -cid 0)", //
* "- 'off' to deactivate it", //
* "- Positive value define the size in byte of CID generated.", //
* "- 0 value means we accept to use CID but will not generated one for foreign peer.", //
* "Default: off"
*/
Integer cid = null;
clientCoapConfig.set(DtlsConfig.DTLS_CONNECTION_ID_LENGTH, cid);
// Set Californium Configuration
endpointsBuilder.setConfiguration(clientCoapConfig);
endpointsBuilder.setClientAddress(new InetSocketAddress(port).getAddress());
// creates EndpointsProvider
List<LwM2mClientEndpointsProvider> endpointsProvider = new ArrayList<>();
endpointsProvider.add(endpointsBuilder.build());
/**
* dependency -> org.eclipse.leshan.transport.javacoap.client.endpoint;
*/
// if (useJavaCoap) {endpointsProvider.add(new JavaCoapClientEndpointsProvider());
// Configure Registration Engine
DefaultRegistrationEngineFactory engineFactory = new DefaultRegistrationEngineFactory();
engineFactory.setReconnectOnUpdate(false);
// old
/**
* Force reconnection/rehandshake on registration update.
*/
int comPeriodInSec = 5;
if (comPeriodInSec > 0) engineFactory.setCommunicationPeriod(comPeriodInSec * 1000);
// engineFactory.setCommunicationPeriod(5000); // old
/**
* By default client will try to resume DTLS session by using abbreviated Handshake. This option force to always do a full handshake."
*/
boolean reconnectOnUpdate = false;
engineFactory.setReconnectOnUpdate(reconnectOnUpdate);
engineFactory.setResumeOnConnect(true);
engineFactory.setCommunicationPeriod(5000);
// new
/**
* Client use queue mode (not fully implemented).
*/
boolean queueMode = false;
engineFactory.setQueueMode(queueMode);
// Create client
LeshanClientBuilder builder = new LeshanClientBuilder(endpoint);
builder.setLocalAddress("0.0.0.0", port);
builder.setObjects(initializer.createAll());
builder.setCoapConfig(coapConfig);
builder.setDtlsConfig(dtlsConfig);
builder.setObjects(enablers);
builder.setEndpointsProviders(endpointsProvider.toArray(new LwM2mClientEndpointsProvider[endpointsProvider.size()]));
builder.setDataSenders(new ManualDataSender());
builder.setRegistrationEngineFactory(engineFactory);
boolean supportOldFormat = true;
if (supportOldFormat) {
builder.setDecoder(new DefaultLwM2mDecoder(supportOldFormat));
builder.setEncoder(new DefaultLwM2mEncoder(new LwM2mValueConverterImpl(), supportOldFormat));
}
builder.setRegistrationEngineFactory(engineFactory);
builder.setSharedExecutor(executor);
builder.setDecoder(new DefaultLwM2mDecoder(false));
builder.setEncoder(new DefaultLwM2mEncoder(new LwM2mValueConverterImpl(), false));
clientStates = new HashSet<>();
clientStates.add(ON_INIT);
leshanClient = builder.build();
LwM2mClientObserver observer = new LwM2mClientObserver() {
@Override
public void onBootstrapStarted(ServerIdentity bsserver, BootstrapRequest request) {
public void onBootstrapStarted(LwM2mServer bsserver, BootstrapRequest request) {
clientStates.add(ON_BOOTSTRAP_STARTED);
}
@Override
public void onBootstrapSuccess(ServerIdentity bsserver, BootstrapRequest request) {
public void onBootstrapSuccess(LwM2mServer bsserver, BootstrapRequest request) {
clientStates.add(ON_BOOTSTRAP_SUCCESS);
}
@Override
public void onBootstrapFailure(ServerIdentity bsserver, BootstrapRequest request, ResponseCode responseCode, String errorMessage, Exception cause) {
public void onBootstrapFailure(LwM2mServer bsserver, BootstrapRequest request, ResponseCode responseCode, String errorMessage, Exception cause) {
clientStates.add(ON_BOOTSTRAP_FAILURE);
}
@Override
public void onBootstrapTimeout(ServerIdentity bsserver, BootstrapRequest request) {
public void onBootstrapTimeout(LwM2mServer bsserver, BootstrapRequest request) {
clientStates.add(ON_BOOTSTRAP_TIMEOUT);
}
@Override
public void onRegistrationStarted(ServerIdentity server, RegisterRequest request) {
public void onRegistrationStarted(LwM2mServer server, RegisterRequest request) {
clientStates.add(ON_REGISTRATION_STARTED);
}
@Override
public void onRegistrationSuccess(ServerIdentity server, RegisterRequest request, String registrationID) {
public void onRegistrationSuccess(LwM2mServer server, RegisterRequest request, String registrationID) {
clientStates.add(ON_REGISTRATION_SUCCESS);
}
@Override
public void onRegistrationFailure(ServerIdentity server, RegisterRequest request, ResponseCode responseCode, String errorMessage, Exception cause) {
public void onRegistrationFailure(LwM2mServer server, RegisterRequest request, ResponseCode responseCode, String errorMessage, Exception cause) {
clientStates.add(ON_REGISTRATION_FAILURE);
}
@Override
public void onRegistrationTimeout(ServerIdentity server, RegisterRequest request) {
public void onRegistrationTimeout(LwM2mServer server, RegisterRequest request) {
clientStates.add(ON_REGISTRATION_TIMEOUT);
}
@Override
public void onUpdateStarted(ServerIdentity server, UpdateRequest request) {
public void onUpdateStarted(LwM2mServer server, UpdateRequest request) {
clientStates.add(ON_UPDATE_STARTED);
}
@Override
public void onUpdateSuccess(ServerIdentity server, UpdateRequest request) {
public void onUpdateSuccess(LwM2mServer server, UpdateRequest request) {
clientStates.add(ON_UPDATE_SUCCESS);
}
@Override
public void onUpdateFailure(ServerIdentity server, UpdateRequest request, ResponseCode responseCode, String errorMessage, Exception cause) {
public void onUpdateFailure(LwM2mServer server, UpdateRequest request, ResponseCode responseCode, String errorMessage, Exception cause) {
clientStates.add(ON_UPDATE_FAILURE);
}
@Override
public void onUpdateTimeout(ServerIdentity server, UpdateRequest request) {
public void onUpdateTimeout(LwM2mServer server, UpdateRequest request) {
clientStates.add(ON_UPDATE_TIMEOUT);
}
@Override
public void onDeregistrationStarted(ServerIdentity server, DeregisterRequest request) {
public void onDeregistrationStarted(LwM2mServer server, DeregisterRequest request) {
clientStates.add(ON_DEREGISTRATION_STARTED);
}
@Override
public void onDeregistrationSuccess(ServerIdentity server, DeregisterRequest request) {
public void onDeregistrationSuccess(LwM2mServer server, DeregisterRequest request) {
clientStates.add(ON_DEREGISTRATION_SUCCESS);
}
@Override
public void onDeregistrationFailure(ServerIdentity server, DeregisterRequest request, ResponseCode responseCode, String errorMessage, Exception cause) {
public void onDeregistrationFailure(LwM2mServer server, DeregisterRequest request, ResponseCode responseCode, String errorMessage, Exception cause) {
clientStates.add(ON_DEREGISTRATION_FAILURE);
}
@Override
public void onDeregistrationTimeout(ServerIdentity server, DeregisterRequest request) {
public void onDeregistrationTimeout(LwM2mServer server, DeregisterRequest request) {
clientStates.add(ON_DEREGISTRATION_TIMEOUT);
}
@ -270,6 +370,21 @@ public class LwM2MTestClient {
};
this.leshanClient.addObserver(observer);
// Add some log about object tree life cycle.
this.leshanClient.getObjectTree().addListener(new ObjectsListenerAdapter() {
@Override
public void objectRemoved(LwM2mObjectEnabler object) {
log.info("Object {} v{} disabled.", object.getId(), object.getObjectModel().version);
}
@Override
public void objectAdded(LwM2mObjectEnabler object) {
log.info("Object {} v{} enabled.", object.getId(), object.getObjectModel().version);
}
});
if (!isRpc) {
this.start(true);
}
@ -279,18 +394,6 @@ public class LwM2MTestClient {
if (leshanClient != null) {
leshanClient.destroy(true);
}
if (lwm2mSecurityBs != null) {
lwm2mSecurityBs = null;
}
if (lwm2mSecurity != null) {
lwm2mSecurity = null;
}
if (lwm2mServerBs != null) {
lwm2mServerBs = null;
}
if (lwm2mServer != null) {
lwm2mServer = null;
}
if (lwM2MDevice != null) {
lwM2MDevice.destroy();
}

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

@ -17,7 +17,7 @@ package org.thingsboard.server.transport.lwm2m.client;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.leshan.client.resource.BaseInstanceEnabler;
import org.eclipse.leshan.client.servers.ServerIdentity;
import org.eclipse.leshan.client.servers.LwM2mServer;
import org.eclipse.leshan.core.model.ObjectModel;
import org.eclipse.leshan.core.model.ResourceModel;
import org.eclipse.leshan.core.node.LwM2mMultipleResource;
@ -93,7 +93,7 @@ public class LwM2mBinaryAppDataContainer extends BaseInstanceEnabler implements
}
@Override
public ReadResponse read(ServerIdentity identity, int resourceId) {
public ReadResponse read(LwM2mServer identity, int resourceId) {
try {
switch (resourceId) {
case 0:
@ -118,7 +118,7 @@ public class LwM2mBinaryAppDataContainer extends BaseInstanceEnabler implements
}
@Override
public WriteResponse write(ServerIdentity identity, boolean replace, int resourceId, LwM2mResource value) {
public WriteResponse write(LwM2mServer identity, boolean replace, int resourceId, LwM2mResource value) {
log.info("Write on Device resource /[{}]/[{}]/[{}]", getModel().id, getId(), resourceId);
switch (resourceId) {
case 0:

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

@ -17,7 +17,7 @@ package org.thingsboard.server.transport.lwm2m.client;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.leshan.client.resource.BaseInstanceEnabler;
import org.eclipse.leshan.client.servers.ServerIdentity;
import org.eclipse.leshan.client.servers.LwM2mServer;
import org.eclipse.leshan.core.model.ObjectModel;
import org.eclipse.leshan.core.response.ReadResponse;
@ -85,7 +85,7 @@ public class LwM2mLocation extends BaseInstanceEnabler implements Destroyable {
}
@Override
public ReadResponse read(ServerIdentity identity, int resourceId) {
public ReadResponse read(LwM2mServer identity, int resourceId) {
log.info("Read on Location resource /[{}]/[{}]/[{}]", getModel().id, getId(), resourceId);
switch (resourceId) {
case 0:

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

@ -17,8 +17,9 @@ package org.thingsboard.server.transport.lwm2m.client;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.leshan.client.resource.BaseInstanceEnabler;
import org.eclipse.leshan.client.servers.ServerIdentity;
import org.eclipse.leshan.client.servers.LwM2mServer;
import org.eclipse.leshan.core.model.ObjectModel;
import org.eclipse.leshan.core.request.argument.Arguments;
import org.eclipse.leshan.core.response.ExecuteResponse;
import org.eclipse.leshan.core.response.ReadResponse;
@ -56,7 +57,7 @@ public class LwM2mTemperatureSensor extends BaseInstanceEnabler implements Destr
}
@Override
public synchronized ReadResponse read(ServerIdentity identity, int resourceId) {
public synchronized ReadResponse read(LwM2mServer identity, int resourceId) {
log.info("Read on Temperature resource /[{}]/[{}]/[{}]", getModel().id, getId(), resourceId);
switch (resourceId) {
case 5601:
@ -73,14 +74,14 @@ public class LwM2mTemperatureSensor extends BaseInstanceEnabler implements Destr
}
@Override
public synchronized ExecuteResponse execute(ServerIdentity identity, int resourceId, String params) {
public synchronized ExecuteResponse execute(LwM2mServer identity, int resourceId, Arguments arguments) {
log.info("Execute on Temperature resource /[{}]/[{}]/[{}]", getModel().id, getId(), resourceId);
switch (resourceId) {
case 5605:
resetMinMaxMeasuredValues();
return ExecuteResponse.success();
default:
return super.execute(identity, resourceId, params);
return super.execute(identity, resourceId, arguments);
}
}

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

@ -18,11 +18,12 @@ package org.thingsboard.server.transport.lwm2m.client;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.leshan.client.resource.BaseInstanceEnabler;
import org.eclipse.leshan.client.resource.LwM2mInstanceEnabler;
import org.eclipse.leshan.client.servers.ServerIdentity;
import org.eclipse.leshan.client.servers.LwM2mServer;
import org.eclipse.leshan.core.model.ObjectModel;
import org.eclipse.leshan.core.model.ResourceModel.Type;
import org.eclipse.leshan.core.node.LwM2mResource;
import org.eclipse.leshan.core.request.BindingMode;
import org.eclipse.leshan.core.request.argument.Arguments;
import org.eclipse.leshan.core.response.ExecuteResponse;
import org.eclipse.leshan.core.response.ReadResponse;
import org.eclipse.leshan.core.response.WriteResponse;
@ -70,7 +71,7 @@ public class Lwm2mServer extends BaseInstanceEnabler {
}
@Override
public ReadResponse read(ServerIdentity identity, int resourceid) {
public ReadResponse read(LwM2mServer identity, int resourceid) {
if (!identity.isSystem())
LOG.debug("Read on Server resource /{}/{}/{}", getModel().id, getId(), resourceid);
@ -108,7 +109,7 @@ public class Lwm2mServer extends BaseInstanceEnabler {
}
@Override
public WriteResponse write(ServerIdentity identity, boolean replace, int resourceid, LwM2mResource value) {
public WriteResponse write(LwM2mServer identity, boolean replace, int resourceid, LwM2mResource value) {
if (!identity.isSystem())
log.debug("Write on Server resource /{}/{}/{}", getModel().id, getId(), resourceid);
@ -195,8 +196,7 @@ public class Lwm2mServer extends BaseInstanceEnabler {
}
}
@Override
public ExecuteResponse execute(ServerIdentity identity, int resourceid, String params) {
public ExecuteResponse execute(LwM2mServer identity, int resourceid, Arguments arguments) {
log.info("Execute on Server resource /{}/{}/{}", getModel().id, getId(), resourceid);
if (resourceid == 8) {
getLwM2mClient().triggerRegistrationUpdate(identity);
@ -210,7 +210,7 @@ public class Lwm2mServer extends BaseInstanceEnabler {
return ExecuteResponse.badRequest("probably no bootstrap server configured");
}
} else {
return super.execute(identity, resourceid, params);
return super.execute(identity, resourceid, arguments);
}
}

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

@ -17,11 +17,12 @@ package org.thingsboard.server.transport.lwm2m.client;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.leshan.client.resource.BaseInstanceEnabler;
import org.eclipse.leshan.client.servers.ServerIdentity;
import org.eclipse.leshan.client.servers.LwM2mServer;
import org.eclipse.leshan.core.Destroyable;
import org.eclipse.leshan.core.model.ObjectModel;
import org.eclipse.leshan.core.model.ResourceModel;
import org.eclipse.leshan.core.node.LwM2mResource;
import org.eclipse.leshan.core.request.argument.Arguments;
import org.eclipse.leshan.core.response.ExecuteResponse;
import org.eclipse.leshan.core.response.ReadResponse;
import org.eclipse.leshan.core.response.WriteResponse;
@ -46,7 +47,7 @@ public class SimpleLwM2MDevice extends BaseInstanceEnabler implements Destroyabl
private static final int min = 5;
private static final int max = 50;
private static final PrimitiveIterator.OfInt randomIterator = new Random().ints(min,max + 1).iterator();
private static final List<Integer> supportedResources = Arrays.asList(0, 1, 2, 3, 9, 10, 11, 13, 14, 15, 16, 17, 18, 19, 20, 21);
private static final List<Integer> supportedResources = Arrays.asList(0, 1, 2, 3, 6, 7, 8, 9, 10, 11, 13, 14, 15, 16, 17, 18, 19, 20, 21);
public SimpleLwM2MDevice() {
@ -57,7 +58,8 @@ public class SimpleLwM2MDevice extends BaseInstanceEnabler implements Destroyabl
executorService.scheduleWithFixedDelay(() -> {
fireResourceChange(9);
}
, 1800000, 1800000, TimeUnit.MILLISECONDS); // 30 MIN
, 1, 1, TimeUnit.SECONDS); // 30 MIN
// , 1800000, 1800000, TimeUnit.MILLISECONDS); // 30 MIN
} catch (Throwable e) {
log.error("[{}]Throwable", e.toString());
e.printStackTrace();
@ -66,7 +68,7 @@ public class SimpleLwM2MDevice extends BaseInstanceEnabler implements Destroyabl
@Override
public ReadResponse read(ServerIdentity identity, int resourceId) {
public ReadResponse read(LwM2mServer identity, int resourceId) {
if (!identity.isSystem())
log.info("Read on Device resource /{}/{}/{}", getModel().id, getId(), resourceId);
switch (resourceId) {
@ -78,6 +80,8 @@ public class SimpleLwM2MDevice extends BaseInstanceEnabler implements Destroyabl
return ReadResponse.success(resourceId, getSerialNumber());
case 3:
return ReadResponse.success(resourceId, getFirmwareVersion());
case 6:
return ReadResponse.success(resourceId, getAvailablePowerSources(), ResourceModel.Type.INTEGER);
case 9:
return ReadResponse.success(resourceId, getBatteryLevel());
case 10:
@ -108,17 +112,16 @@ public class SimpleLwM2MDevice extends BaseInstanceEnabler implements Destroyabl
}
@Override
public ExecuteResponse execute(ServerIdentity identity, int resourceId, String params) {
String withParams = null;
if (params != null && params.length() != 0) {
withParams = " with params " + params;
}
log.info("Execute on Device resource /{}/{}/{} {}", getModel().id, getId(), resourceId, withParams != null ? withParams : "");
public ExecuteResponse execute(LwM2mServer identity, int resourceId, Arguments arguments) {
String withArguments = "";
if (!arguments.isEmpty())
withArguments = " with arguments " + arguments;
log.info("Execute on Device resource /{}/{}/{} {}", getModel().id, getId(), resourceId, withArguments);
return ExecuteResponse.success();
}
@Override
public WriteResponse write(ServerIdentity identity, boolean replace, int resourceId, LwM2mResource value) {
public WriteResponse write(LwM2mServer identity, boolean replace, int resourceId, LwM2mResource value) {
log.info("Write on Device resource /{}/{}/{}", getModel().id, getId(), resourceId);
switch (resourceId) {
@ -157,6 +160,14 @@ public class SimpleLwM2MDevice extends BaseInstanceEnabler implements Destroyabl
return 0;
}
private Map<Integer, Long> getAvailablePowerSources() {
Map<Integer, Long> availablePowerSources = new HashMap<>();
availablePowerSources.put(0, 1L);
availablePowerSources.put(1, 2L);
availablePowerSources.put(2, 5L);
return availablePowerSources;
}
private int getBatteryLevel() {
return randomIterator.nextInt();
// return 42;

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

@ -17,9 +17,10 @@ package org.thingsboard.server.transport.lwm2m.client;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.leshan.client.resource.BaseInstanceEnabler;
import org.eclipse.leshan.client.servers.ServerIdentity;
import org.eclipse.leshan.client.servers.LwM2mServer;
import org.eclipse.leshan.core.model.ObjectModel;
import org.eclipse.leshan.core.node.LwM2mResource;
import org.eclipse.leshan.core.request.argument.Arguments;
import org.eclipse.leshan.core.response.ExecuteResponse;
import org.eclipse.leshan.core.response.ReadResponse;
import org.eclipse.leshan.core.response.WriteResponse;
@ -45,7 +46,7 @@ public class SwLwM2MDevice extends BaseInstanceEnabler implements Destroyable {
private final AtomicInteger updateResult = new AtomicInteger(0);
@Override
public ReadResponse read(ServerIdentity identity, int resourceId) {
public ReadResponse read(LwM2mServer identity, int resourceId) {
if (!identity.isSystem())
log.info("Read on Device resource /{}/{}/{}", getModel().id, getId(), resourceId);
switch (resourceId) {
@ -63,12 +64,11 @@ public class SwLwM2MDevice extends BaseInstanceEnabler implements Destroyable {
}
@Override
public ExecuteResponse execute(ServerIdentity identity, int resourceId, String params) {
String withParams = null;
if (params != null && params.length() != 0) {
withParams = " with params " + params;
}
log.info("Execute on Device resource /{}/{}/{} {}", getModel().id, getId(), resourceId, withParams != null ? withParams : "");
public ExecuteResponse execute(LwM2mServer identity, int resourceId, Arguments arguments) {
String withArguments = "";
if (!arguments.isEmpty())
withArguments = " with arguments " + arguments;
log.info("Execute on Device resource /{}/{}/{} {}", getModel().id, getId(), resourceId, withArguments);
switch (resourceId) {
case 4:
@ -77,12 +77,12 @@ public class SwLwM2MDevice extends BaseInstanceEnabler implements Destroyable {
case 6:
return ExecuteResponse.success();
default:
return super.execute(identity, resourceId, params);
return super.execute(identity, resourceId, arguments);
}
}
@Override
public WriteResponse write(ServerIdentity identity, boolean replace, int resourceId, LwM2mResource value) {
public WriteResponse write(LwM2mServer identity, boolean replace, int resourceId, LwM2mResource value) {
log.info("Write on Device resource /{}/{}/{}", getModel().id, getId(), resourceId);
switch (resourceId) {

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

@ -0,0 +1,732 @@
/**
* Copyright © 2016-2024 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.transport.lwm2m.client;
import org.eclipse.leshan.client.LwM2mClient;
import org.eclipse.leshan.client.resource.BaseObjectEnabler;
import org.eclipse.leshan.client.resource.DummyInstanceEnabler;
import org.eclipse.leshan.client.resource.LwM2mInstanceEnabler;
import org.eclipse.leshan.client.resource.LwM2mInstanceEnablerFactory;
import org.eclipse.leshan.client.resource.listener.ResourceListener;
import org.eclipse.leshan.client.servers.LwM2mServer;
import org.eclipse.leshan.client.servers.ServersInfoExtractor;
import org.eclipse.leshan.client.util.LinkFormatHelper;
import org.eclipse.leshan.core.Destroyable;
import org.eclipse.leshan.core.LwM2mId;
import org.eclipse.leshan.core.Startable;
import org.eclipse.leshan.core.Stoppable;
import org.eclipse.leshan.core.link.lwm2m.LwM2mLink;
import org.eclipse.leshan.core.link.lwm2m.attributes.LwM2mAttribute;
import org.eclipse.leshan.core.link.lwm2m.attributes.LwM2mAttributeSet;
import org.eclipse.leshan.core.link.lwm2m.attributes.LwM2mAttributes;
import org.eclipse.leshan.core.model.ObjectModel;
import org.eclipse.leshan.core.model.ResourceModel;
import org.eclipse.leshan.core.node.LwM2mMultipleResource;
import org.eclipse.leshan.core.node.LwM2mObject;
import org.eclipse.leshan.core.node.LwM2mObjectInstance;
import org.eclipse.leshan.core.node.LwM2mPath;
import org.eclipse.leshan.core.node.LwM2mResource;
import org.eclipse.leshan.core.node.LwM2mResourceInstance;
import org.eclipse.leshan.core.request.BootstrapDeleteRequest;
import org.eclipse.leshan.core.request.BootstrapReadRequest;
import org.eclipse.leshan.core.request.BootstrapWriteRequest;
import org.eclipse.leshan.core.request.ContentFormat;
import org.eclipse.leshan.core.request.CreateRequest;
import org.eclipse.leshan.core.request.DeleteRequest;
import org.eclipse.leshan.core.request.DiscoverRequest;
import org.eclipse.leshan.core.request.DownlinkRequest;
import org.eclipse.leshan.core.request.ExecuteRequest;
import org.eclipse.leshan.core.request.ObserveRequest;
import org.eclipse.leshan.core.request.ReadRequest;
import org.eclipse.leshan.core.request.WriteAttributesRequest;
import org.eclipse.leshan.core.request.WriteRequest;
import org.eclipse.leshan.core.request.WriteRequest.Mode;
import org.eclipse.leshan.core.response.BootstrapDeleteResponse;
import org.eclipse.leshan.core.response.BootstrapReadResponse;
import org.eclipse.leshan.core.response.BootstrapWriteResponse;
import org.eclipse.leshan.core.response.CreateResponse;
import org.eclipse.leshan.core.response.DeleteResponse;
import org.eclipse.leshan.core.response.DiscoverResponse;
import org.eclipse.leshan.core.response.ExecuteResponse;
import org.eclipse.leshan.core.response.ObserveResponse;
import org.eclipse.leshan.core.response.ReadResponse;
import org.eclipse.leshan.core.response.WriteAttributesResponse;
import org.eclipse.leshan.core.response.WriteResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
public class TbLwm2mObjectEnabler extends BaseObjectEnabler implements Destroyable, Startable, Stoppable {
private static Logger LOG = LoggerFactory.getLogger(DummyInstanceEnabler.class);
protected Map<Integer, LwM2mInstanceEnabler> instances;
protected LwM2mInstanceEnablerFactory instanceFactory;
protected ContentFormat defaultContentFormat;
private LinkFormatHelper tbLinkFormatHelper;
protected Map<LwM2mPath, LwM2mAttributeSet> lwM2mAttributes;
public TbLwm2mObjectEnabler(int id, ObjectModel objectModel, Map<Integer, LwM2mInstanceEnabler> instances,
LwM2mInstanceEnablerFactory instanceFactory, ContentFormat defaultContentFormat) {
super(id, objectModel);
this.instances = new HashMap<>(instances);
;
this.instanceFactory = instanceFactory;
this.defaultContentFormat = defaultContentFormat;
for (Entry<Integer, LwM2mInstanceEnabler> entry : this.instances.entrySet()) {
instances.put(entry.getKey(), entry.getValue());
listenInstance(entry.getValue(), entry.getKey());
}
this.lwM2mAttributes = new HashMap<>();
}
public TbLwm2mObjectEnabler(int id, ObjectModel objectModel) {
super(id, objectModel);
}
@Override
public synchronized List<Integer> getAvailableInstanceIds() {
List<Integer> ids = new ArrayList<>(instances.keySet());
Collections.sort(ids);
return ids;
}
@Override
public synchronized List<Integer> getAvailableResourceIds(int instanceId) {
LwM2mInstanceEnabler instanceEnabler = instances.get(instanceId);
if (instanceEnabler != null) {
return instanceEnabler.getAvailableResourceIds(getObjectModel());
} else {
return Collections.emptyList();
}
}
public synchronized void addInstance(int instanceId, LwM2mInstanceEnabler newInstance) {
instances.put(instanceId, newInstance);
listenInstance(newInstance, instanceId);
fireInstancesAdded(instanceId);
}
public synchronized LwM2mInstanceEnabler getInstance(int instanceId) {
return instances.get(instanceId);
}
public synchronized LwM2mInstanceEnabler removeInstance(int instanceId) {
LwM2mInstanceEnabler removedInstance = instances.remove(instanceId);
if (removedInstance != null) {
fireInstancesRemoved(removedInstance.getId());
}
return removedInstance;
}
@Override
protected CreateResponse doCreate(LwM2mServer server, CreateRequest request) {
if (!getObjectModel().multiple && instances.size() > 0) {
return CreateResponse.badRequest("an instance already exist for this single instance object");
}
if (request.unknownObjectInstanceId()) {
// create instance
LwM2mInstanceEnabler newInstance = createInstance(server, getObjectModel().multiple ? null : 0,
request.getResources());
// add new instance to this object
instances.put(newInstance.getId(), newInstance);
listenInstance(newInstance, newInstance.getId());
fireInstancesAdded(newInstance.getId());
return CreateResponse
.success(new LwM2mPath(request.getPath().getObjectId(), newInstance.getId()).toString());
} else {
List<LwM2mObjectInstance> instanceNodes = request.getObjectInstances();
// checks single object instances
if (!getObjectModel().multiple) {
if (request.getObjectInstances().size() > 1) {
return CreateResponse.badRequest("can not create several instances on this single instance object");
}
if (request.getObjectInstances().get(0).getId() != 0) {
return CreateResponse.badRequest("single instance object must use 0 as ID");
}
}
// ensure instance does not already exists
for (LwM2mObjectInstance instance : instanceNodes) {
if (instances.containsKey(instance.getId())) {
return CreateResponse.badRequest(String.format("instance %d already exists", instance.getId()));
}
}
// create the new instances
int[] instanceIds = new int[request.getObjectInstances().size()];
int i = 0;
for (LwM2mObjectInstance instance : request.getObjectInstances()) {
// create instance
LwM2mInstanceEnabler newInstance = createInstance(server, instance.getId(),
instance.getResources().values());
// add new instance to this object
instances.put(newInstance.getId(), newInstance);
listenInstance(newInstance, newInstance.getId());
// store instance ids
instanceIds[i] = newInstance.getId();
i++;
}
fireInstancesAdded(instanceIds);
return CreateResponse.success();
}
}
protected LwM2mInstanceEnabler createInstance(LwM2mServer server, Integer instanceId,
Collection<LwM2mResource> resources) {
// create the new instance
LwM2mInstanceEnabler newInstance = instanceFactory.create(getObjectModel(), instanceId, instances.keySet());
newInstance.setLwM2mClient(getLwm2mClient());
// add/write resource
for (LwM2mResource resource : resources) {
newInstance.write(server, true, resource.getId(), resource);
}
return newInstance;
}
@Override
protected ReadResponse doRead(LwM2mServer server, ReadRequest request) {
LwM2mPath path = request.getPath();
// Manage Object case
if (path.isObject()) {
List<LwM2mObjectInstance> lwM2mObjectInstances = new ArrayList<>();
for (LwM2mInstanceEnabler instance : instances.values()) {
ReadResponse response = instance.read(server);
if (response.isSuccess()) {
lwM2mObjectInstances.add((LwM2mObjectInstance) response.getContent());
}
}
return ReadResponse.success(new LwM2mObject(getId(), lwM2mObjectInstances));
}
// Manage Instance case
LwM2mInstanceEnabler instance = instances.get(path.getObjectInstanceId());
if (instance == null)
return ReadResponse.notFound();
if (path.getResourceId() == null) {
return instance.read(server);
}
// Manage Resource case
if (path.getResourceInstanceId() == null) {
return instance.read(server, path.getResourceId());
}
// Manage Resource Instance case
return instance.read(server, path.getResourceId(), path.getResourceInstanceId());
}
@Override
protected BootstrapReadResponse doRead(LwM2mServer server, BootstrapReadRequest request) {
// Basic implementation we delegate to classic Read Request
ReadResponse response = doRead(server,
new ReadRequest(request.getContentFormat(), request.getPath(), request.getCoapRequest()));
return new BootstrapReadResponse(response.getCode(), response.getContent(), response.getErrorMessage());
}
@Override
protected ObserveResponse doObserve(final LwM2mServer server, final ObserveRequest request) {
final LwM2mPath path = request.getPath();
// Manage Object case
if (path.isObject()) {
List<LwM2mObjectInstance> lwM2mObjectInstances = new ArrayList<>();
for (LwM2mInstanceEnabler instance : instances.values()) {
ReadResponse response = instance.observe(server);
if (response.isSuccess()) {
lwM2mObjectInstances.add((LwM2mObjectInstance) response.getContent());
}
}
return ObserveResponse.success(new LwM2mObject(getId(), lwM2mObjectInstances));
}
// Manage Instance case
final LwM2mInstanceEnabler instance = instances.get(path.getObjectInstanceId());
if (instance == null)
return ObserveResponse.notFound();
if (path.getResourceId() == null) {
return instance.observe(server);
}
// Manage Resource case
if (path.getResourceInstanceId() == null) {
return instance.observe(server, path.getResourceId());
}
// Manage Resource Instance case
return instance.observe(server, path.getResourceId(), path.getResourceInstanceId());
}
@Override
protected WriteResponse doWrite(LwM2mServer server, WriteRequest request) {
LwM2mPath path = request.getPath();
// Manage Instance case
LwM2mInstanceEnabler instance = instances.get(path.getObjectInstanceId());
if (instance == null)
return WriteResponse.notFound();
if (path.isObjectInstance()) {
return instance.write(server, request.isReplaceRequest(), (LwM2mObjectInstance) request.getNode());
}
// Manage Resource case
if (path.getResourceInstanceId() == null) {
return instance.write(server, request.isReplaceRequest(), path.getResourceId(),
(LwM2mResource) request.getNode());
}
// Manage Resource Instance case
return instance.write(server, false, path.getResourceId(), path.getResourceInstanceId(),
((LwM2mResourceInstance) request.getNode()));
}
@Override
protected BootstrapWriteResponse doWrite(LwM2mServer server, BootstrapWriteRequest request) {
LwM2mPath path = request.getPath();
// Manage Object case
if (path.isObject()) {
for (LwM2mObjectInstance instanceNode : ((LwM2mObject) request.getNode()).getInstances().values()) {
LwM2mInstanceEnabler instanceEnabler = instances.get(instanceNode.getId());
if (instanceEnabler == null) {
doCreate(server, new CreateRequest(path.getObjectId(), instanceNode));
} else {
doWrite(server, new WriteRequest(Mode.REPLACE, path.getObjectId(), instanceEnabler.getId(),
instanceNode.getResources().values()));
}
}
return BootstrapWriteResponse.success();
}
// Manage Instance case
if (path.isObjectInstance()) {
LwM2mObjectInstance instanceNode = (LwM2mObjectInstance) request.getNode();
LwM2mInstanceEnabler instanceEnabler = instances.get(path.getObjectInstanceId());
if (instanceEnabler == null) {
doCreate(server, new CreateRequest(path.getObjectId(), instanceNode));
} else {
doWrite(server, new WriteRequest(Mode.REPLACE, request.getContentFormat(), path.getObjectId(),
path.getObjectInstanceId(), instanceNode.getResources().values()));
}
return BootstrapWriteResponse.success();
}
// Manage resource case
LwM2mResource resource = (LwM2mResource) request.getNode();
LwM2mInstanceEnabler instanceEnabler = instances.get(path.getObjectInstanceId());
if (instanceEnabler == null) {
doCreate(server, new CreateRequest(path.getObjectId(),
new LwM2mObjectInstance(path.getObjectInstanceId(), resource)));
} else {
instanceEnabler.write(server, true, path.getResourceId(), resource);
}
return BootstrapWriteResponse.success();
}
@Override
protected ExecuteResponse doExecute(LwM2mServer server, ExecuteRequest request) {
LwM2mPath path = request.getPath();
LwM2mInstanceEnabler instance = instances.get(path.getObjectInstanceId());
if (instance == null) {
return ExecuteResponse.notFound();
}
return instance.execute(server, path.getResourceId(), request.getArguments());
}
@Override
protected DeleteResponse doDelete(LwM2mServer server, DeleteRequest request) {
LwM2mInstanceEnabler deletedInstance = instances.remove(request.getPath().getObjectInstanceId());
if (deletedInstance != null) {
deletedInstance.onDelete(server);
fireInstancesRemoved(deletedInstance.getId());
return DeleteResponse.success();
}
return DeleteResponse.notFound();
}
@Override
public BootstrapDeleteResponse doDelete(LwM2mServer server, BootstrapDeleteRequest request) {
if (request.getPath().isRoot() || request.getPath().isObject()) {
if (id == LwM2mId.SECURITY) {
// For security object, we clean everything except bootstrap Server account.
// Get bootstrap account and store removed instances ids
Entry<Integer, LwM2mInstanceEnabler> bootstrapServerAccount = null;
int[] instanceIds = new int[instances.size()];
int i = 0;
for (Entry<Integer, LwM2mInstanceEnabler> instance : instances.entrySet()) {
if (ServersInfoExtractor.isBootstrapServer(instance.getValue())) {
bootstrapServerAccount = instance;
} else {
// Store instance ids
instanceIds[i] = instance.getKey();
i++;
}
}
// Clear everything
instances.clear();
// Put bootstrap account again
if (bootstrapServerAccount != null) {
instances.put(bootstrapServerAccount.getKey(), bootstrapServerAccount.getValue());
}
fireInstancesRemoved(instanceIds);
return BootstrapDeleteResponse.success();
} else if (id == LwM2mId.OSCORE) {
// For OSCORE object, we clean everything except OSCORE object link to bootstrap Server account.
// Get bootstrap account
LwM2mObjectInstance bootstrapInstance = ServersInfoExtractor.getBootstrapSecurityInstance(
getLwm2mClient().getObjectTree().getObjectEnabler(LwM2mId.SECURITY));
// Get OSCORE instance ID associated to it
Integer bootstrapOscoreInstanceId = bootstrapInstance != null
? ServersInfoExtractor.getOscoreSecurityMode(bootstrapInstance)
: null;
// if bootstrap server use OSCORE,
// search the OSCORE instance for this ID and store removed instances ids
if (bootstrapOscoreInstanceId != null) {
Entry<Integer, LwM2mInstanceEnabler> bootstrapServerOscore = null;
int[] instanceIds = new int[instances.size()];
int i = 0;
for (Entry<Integer, LwM2mInstanceEnabler> instance : instances.entrySet()) {
if (bootstrapOscoreInstanceId.equals(instance.getKey())) {
bootstrapServerOscore = instance;
} else {
// Store instance ids
instanceIds[i] = instance.getKey();
i++;
}
}
// Clear everything
instances.clear();
// Put bootstrap OSCORE instance again
if (bootstrapServerOscore != null) {
instances.put(bootstrapServerOscore.getKey(), bootstrapServerOscore.getValue());
}
fireInstancesRemoved(instanceIds);
return BootstrapDeleteResponse.success();
}
// else delete everything.
}
// In all other cases, just delete everything
instances.clear();
// fired instances removed
int[] instanceIds = new int[instances.size()];
int i = 0;
for (Entry<Integer, LwM2mInstanceEnabler> instance : instances.entrySet()) {
instanceIds[i] = instance.getKey();
i++;
}
fireInstancesRemoved(instanceIds);
return BootstrapDeleteResponse.success();
} else if (request.getPath().isObjectInstance()) {
if (id == LwM2mId.SECURITY) {
// For security object, deleting bootstrap Server account is not allowed
LwM2mInstanceEnabler instance = instances.get(request.getPath().getObjectInstanceId());
if (instance == null) {
return BootstrapDeleteResponse
.badRequest(String.format("Instance %s not found", request.getPath()));
} else if (ServersInfoExtractor.isBootstrapServer(instance)) {
return BootstrapDeleteResponse.badRequest("bootstrap server can not be deleted");
}
} else if (id == LwM2mId.OSCORE) {
// For OSCORE object, deleting instance linked to Bootstrap account is not allowed
// Get bootstrap instance
LwM2mObjectInstance bootstrapInstance = ServersInfoExtractor.getBootstrapSecurityInstance(
getLwm2mClient().getObjectTree().getObjectEnabler(LwM2mId.SECURITY));
// Get OSCORE instance ID associated to it
Integer bootstrapOscoreInstanceId = bootstrapInstance != null
? ServersInfoExtractor.getOscoreSecurityMode(bootstrapInstance)
: null;
if (bootstrapOscoreInstanceId != null
&& bootstrapOscoreInstanceId.equals(request.getPath().getObjectInstanceId())) {
return BootstrapDeleteResponse
.badRequest("OSCORE instance linked to bootstrap server can not be deleted");
}
}
if (null != instances.remove(request.getPath().getObjectInstanceId())) {
fireInstancesRemoved(request.getPath().getObjectInstanceId());
return BootstrapDeleteResponse.success();
} else {
return BootstrapDeleteResponse.badRequest(String.format("Instance %s not found", request.getPath()));
}
}
return BootstrapDeleteResponse.badRequest(String.format("unexcepted path %s", request.getPath()));
}
protected void listenInstance(LwM2mInstanceEnabler instance, final int instanceId) {
instance.addResourceListener(new ResourceListener() {
@Override
public void resourceChanged(LwM2mPath... paths) {
for (LwM2mPath path : paths) {
if (!isValid(instanceId, path)) {
LOG.warn("InstanceEnabler ({}) of object ({}) try to raise a change of {} which seems invalid.",
instanceId, getId(), path);
}
}
fireResourcesChanged(paths);
}
});
}
protected boolean isValid(int instanceId, LwM2mPath pathToValidate) {
if (!(pathToValidate.isResource() || pathToValidate.isResourceInstance()))
return false;
if (pathToValidate.getObjectId() != getId()) {
return false;
}
if (pathToValidate.getObjectInstanceId() != instanceId) {
return false;
}
return true;
}
@Override
public ContentFormat getDefaultEncodingFormat(DownlinkRequest<?> request) {
return defaultContentFormat;
}
@Override
public void init(LwM2mClient client, LinkFormatHelper linkFormatHelper) {
super.init(client, linkFormatHelper);
this.tbLinkFormatHelper = linkFormatHelper;
for (LwM2mInstanceEnabler instanceEnabler : instances.values()) {
instanceEnabler.setLwM2mClient(client);
}
}
@Override
public void destroy() {
for (LwM2mInstanceEnabler instanceEnabler : instances.values()) {
if (instanceEnabler instanceof Destroyable) {
((Destroyable) instanceEnabler).destroy();
} else if (instanceEnabler instanceof Stoppable) {
((Stoppable) instanceEnabler).stop();
}
}
}
@Override
public void start() {
for (LwM2mInstanceEnabler instanceEnabler : instances.values()) {
if (instanceEnabler instanceof Startable) {
((Startable) instanceEnabler).start();
}
}
}
@Override
public void stop() {
for (LwM2mInstanceEnabler instanceEnabler : instances.values()) {
if (instanceEnabler instanceof Stoppable) {
((Stoppable) instanceEnabler).stop();
}
}
}
@Override
public synchronized WriteAttributesResponse writeAttributes(LwM2mServer server, WriteAttributesRequest request) {
// execute is not supported for bootstrap
if (server.isLwm2mBootstrapServer()) {
return WriteAttributesResponse.methodNotAllowed();
}
// return WriteAttributesResponse.internalServerError("not implemented");
return doWriteAttributes(server, request);
}
/**
* <NOTIFICATION> Class Attributes
* - pmin (def = 0(sec)) Integer Resource/Object Instance/Object Readable Resource
* - pmax (def = -- ) Integer Resource/Object Instance/Object Readable Resource
* - Greater Than gt (def = -- ) Float Resource Numerical&Readable Resource
* - Less Than lt (def = -- ) Float Resource Numerical&Readable Resource
* - Step st (def = -- ) Float Resource Numerical&Readable Resource
*/
public WriteAttributesResponse doWriteAttributes(LwM2mServer server, WriteAttributesRequest request) {
LwM2mPath lwM2mPath = request.getPath();
LwM2mAttributeSet attributeSet = lwM2mAttributes.get(lwM2mPath);
Map <String, LwM2mAttribute<?>> attributes = new HashMap<>();
for (LwM2mAttribute attr : request.getAttributes().getLwM2mAttributes()) {
if (attr.getName().equals("pmax") || attr.getName().equals("pmin")) {
if (lwM2mPath.isObject() || lwM2mPath.isObjectInstance() || lwM2mPath.isResource()) {
attributes.put(attr.getName(), attr);
} else {
return WriteAttributesResponse.badRequest("Attribute " + attr.getName() + " can be used for only Resource/Object Instance/Object.");
}
} else if (attr.getName().equals("gt") || attr.getName().equals("lt") || attr.getName().equals("st")) {
if (lwM2mPath.isResource()) {
attributes.put(attr.getName(), attr);
} else {
return WriteAttributesResponse.badRequest("Attribute " + attr.getName() + " can be used for only Resource.");
}
}
}
if (attributes.size()>0){
if (attributeSet == null) {
attributeSet = new LwM2mAttributeSet(attributes.values());
} else {
Iterable<LwM2mAttribute<?>> lwM2mAttributeIterable = attributeSet.getLwM2mAttributes();
Map <String, LwM2mAttribute<?>> attributesOld = new HashMap<>();
for (LwM2mAttribute<?> attr : lwM2mAttributeIterable) {
attributesOld.put(attr.getName(), attr);
}
attributesOld.putAll(attributes);
attributeSet = new LwM2mAttributeSet(attributesOld.values());
}
lwM2mAttributes.put(lwM2mPath, attributeSet);
return WriteAttributesResponse.success();
}
return WriteAttributesResponse.internalServerError("not implemented");
}
@Override
public synchronized DiscoverResponse discover(LwM2mServer server, DiscoverRequest request) {
if (server.isLwm2mBootstrapServer()) {
// discover is not supported for bootstrap
return DiscoverResponse.methodNotAllowed();
}
if (id == LwM2mId.SECURITY || id == LwM2mId.OSCORE) {
return DiscoverResponse.notFound();
}
return doDiscover(server, request);
}
protected DiscoverResponse doDiscover(LwM2mServer server, DiscoverRequest request) {
LwM2mPath path = request.getPath();
if (path.isObject()) {
LwM2mLink[] ObjectLinks = linkUpdateAttributes(this.tbLinkFormatHelper.getObjectDescription(this, null), server);
return DiscoverResponse.success(ObjectLinks);
} else if (path.isObjectInstance()) {
// Manage discover on instance
if (!getAvailableInstanceIds().contains(path.getObjectInstanceId()))
return DiscoverResponse.notFound();
LwM2mLink[] instanceLink = linkUpdateAttributes(this.tbLinkFormatHelper.getInstanceDescription(this, path.getObjectInstanceId(), null), server);
return DiscoverResponse.success(instanceLink);
} else if (path.isResource()) {
// Manage discover on resource
if (!getAvailableInstanceIds().contains(path.getObjectInstanceId()))
return DiscoverResponse.notFound();
ResourceModel resourceModel = getObjectModel().resources.get(path.getResourceId());
if (resourceModel == null)
return DiscoverResponse.notFound();
if (!getAvailableResourceIds(path.getObjectInstanceId()).contains(path.getResourceId()))
return DiscoverResponse.notFound();
LwM2mLink resourceLink = linkAddAttribute(
this.tbLinkFormatHelper.getResourceDescription(this, path.getObjectInstanceId(), path.getResourceId(), null),
server);
return DiscoverResponse.success(new LwM2mLink[] { resourceLink });
}
return DiscoverResponse.badRequest(null);
}
private LwM2mLink[] linkUpdateAttributes(LwM2mLink[] links, LwM2mServer server) {
return Arrays.stream(links)
.map(link -> linkAddAttribute(link, server))
.toArray(LwM2mLink[]::new);
}
private LwM2mLink linkAddAttribute(LwM2mLink link, LwM2mServer server) {
LwM2mAttributeSet lwM2mAttributeSetDop = null;
if (this.lwM2mAttributes.get(link.getPath())!= null){
lwM2mAttributeSetDop = this.lwM2mAttributes.get(link.getPath());
}
LwM2mAttribute resourceAttributeDim = getResourceAttributes (server, link.getPath());
Map <String, LwM2mAttribute<?>> attributes = new HashMap<>();
if (link.getAttributes() != null) {
for (LwM2mAttribute attr : link.getAttributes().getLwM2mAttributes()) {
attributes.put(attr.getName(), attr);
}
}
if (lwM2mAttributeSetDop != null) {
for (LwM2mAttribute attr : lwM2mAttributeSetDop.getLwM2mAttributes()) {
attributes.put(attr.getName(), attr);
}
}
if (resourceAttributeDim != null) {
attributes.put(resourceAttributeDim.getName(), resourceAttributeDim);
}
return new LwM2mLink(link.getRootPath(), link.getPath(), attributes.values());
}
protected LwM2mAttribute getResourceAttributes (LwM2mServer server, LwM2mPath path) {
ResourceModel resourceModel = getObjectModel().resources.get(path.getResourceId());
if (path.isResource() && resourceModel.multiple) {
return getResourceAttributeDim(path, server);
}
return null;
}
protected LwM2mAttribute getResourceAttributeDim(LwM2mPath path, LwM2mServer server) {
LwM2mInstanceEnabler instance = instances.get(path.getObjectInstanceId());
try {
ReadResponse readResponse = instance.read(server, path.getResourceId());
if (readResponse.getCode().getCode()==205 && readResponse.getContent() instanceof LwM2mMultipleResource) {
long valueDim = ((LwM2mMultipleResource)readResponse.getContent()).getInstances().size();
return LwM2mAttributes.create(LwM2mAttributes.DIMENSION, valueDim);
} else {
return null;
}
} catch (Exception e ){
return null;
}
}
}

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

@ -0,0 +1,71 @@
/**
* Copyright © 2016-2024 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.transport.lwm2m.client;
import org.eclipse.leshan.client.resource.BaseInstanceEnablerFactory;
import org.eclipse.leshan.client.resource.LwM2mInstanceEnabler;
import org.eclipse.leshan.client.resource.LwM2mObjectEnabler;
import org.eclipse.leshan.client.resource.ObjectsInitializer;
import org.eclipse.leshan.core.model.LwM2mModel;
import org.eclipse.leshan.core.model.ObjectModel;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class TbObjectsInitializer extends ObjectsInitializer {
public TbObjectsInitializer(LwM2mModel model) {
super(model);
}
public List<LwM2mObjectEnabler> create(int... objectId) {
List<LwM2mObjectEnabler> enablers = new ArrayList<>();
for (int anObjectId : objectId) {
LwM2mObjectEnabler objectEnabler = create(anObjectId);
if (objectEnabler != null)
enablers.add(objectEnabler);
}
return enablers;
}
public LwM2mObjectEnabler create(int objectId) {
ObjectModel objectModel = model.getObjectModel(objectId);
if (objectModel == null) {
throw new IllegalArgumentException(
"Cannot create object for id " + objectId + " because no model is defined for this id.");
}
return createNodeEnabler(objectModel);
}
protected LwM2mObjectEnabler createNodeEnabler(ObjectModel objectModel) {
Map<Integer, LwM2mInstanceEnabler> instances = new HashMap<>();
LwM2mInstanceEnabler[] newInstances = createInstances(objectModel);
for (LwM2mInstanceEnabler instance : newInstances) {
// set id if not already set
if (instance.getId() == null) {
int id = BaseInstanceEnablerFactory.generateNewInstanceId(instances.keySet());
instance.setId(id);
}
instance.setModel(objectModel);
instances.put(instance.getId(), instance);
}
return new TbLwm2mObjectEnabler(objectModel.id, objectModel, instances, getFactoryFor(objectModel),
getContentFormat(objectModel.id));
}
}

41
application/src/test/java/org/thingsboard/server/transport/lwm2m/ota/AbstractOtaLwM2MIntegrationTest.java

@ -34,6 +34,47 @@ public abstract class AbstractOtaLwM2MIntegrationTest extends AbstractLwM2MInteg
protected static final String CLIENT_ENDPOINT_OTA5 = "Ota5_Device";
protected static final String CLIENT_ENDPOINT_OTA9 = "Ota9_Device";
protected final String OBSERVE_ATTRIBUTES_WITH_PARAMS_OTA =
" {\n" +
" \"keyName\": {\n" +
" \"/5_1.2/0/3\": \"state\",\n" +
" \"/5_1.2/0/5\": \"updateResult\",\n" +
" \"/5_1.2/0/6\": \"pkgname\",\n" +
" \"/5_1.2/0/7\": \"pkgversion\",\n" +
" \"/5_1.2/0/9\": \"firmwareUpdateDeliveryMethod\",\n" +
" \"/9_1.1/0/0\": \"pkgname\",\n" +
" \"/9_1.1/0/1\": \"pkgversion\",\n" +
" \"/9_1.1/0/7\": \"updateState\",\n" +
" \"/9_1.1/0/9\": \"updateResult\"\n" +
" },\n" +
" \"observe\": [\n" +
" \"/5_1.2/0/3\",\n" +
" \"/5_1.2/0/5\",\n" +
" \"/5_1.2/0/6\",\n" +
" \"/5_1.2/0/7\",\n" +
" \"/5_1.2/0/9\",\n" +
" \"/9_1.1/0/0\",\n" +
" \"/9_1.1/0/1\",\n" +
" \"/9_1.1/0/7\",\n" +
" \"/9_1.1/0/9\"\n" +
" ],\n" +
" \"attribute\": [],\n" +
" \"telemetry\": [\n" +
" \"/5_1.2/0/3\",\n" +
" \"/5_1.2/0/5\",\n" +
" \"/5_1.2/0/6\",\n" +
" \"/5_1.2/0/7\",\n" +
" \"/5_1.2/0/9\",\n" +
" \"/9_1.1/0/0\",\n" +
" \"/9_1.1/0/1\",\n" +
" \"/9_1.1/0/7\",\n" +
" \"/9_1.1/0/9\"\n" +
" ],\n" +
" \"attributeLwm2m\": {}\n" +
" }";
public AbstractOtaLwM2MIntegrationTest() {
setResources(this.RESOURCES_OTA);
}

52
application/src/test/java/org/thingsboard/server/transport/lwm2m/ota/sql/OtaLwM2MIntegrationTest.java

@ -51,46 +51,6 @@ import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.LwM2MProfil
@Slf4j
public class OtaLwM2MIntegrationTest extends AbstractOtaLwM2MIntegrationTest {
protected final String OBSERVE_ATTRIBUTES_WITH_PARAMS_OTA =
" {\n" +
" \"keyName\": {\n" +
" \"/5_1.0/0/3\": \"state\",\n" +
" \"/5_1.0/0/5\": \"updateResult\",\n" +
" \"/5_1.0/0/6\": \"pkgname\",\n" +
" \"/5_1.0/0/7\": \"pkgversion\",\n" +
" \"/5_1.0/0/9\": \"firmwareUpdateDeliveryMethod\",\n" +
" \"/9_1.0/0/0\": \"pkgname\",\n" +
" \"/9_1.0/0/1\": \"pkgversion\",\n" +
" \"/9_1.0/0/7\": \"updateState\",\n" +
" \"/9_1.0/0/9\": \"updateResult\"\n" +
" },\n" +
" \"observe\": [\n" +
" \"/5_1.0/0/3\",\n" +
" \"/5_1.0/0/5\",\n" +
" \"/5_1.0/0/6\",\n" +
" \"/5_1.0/0/7\",\n" +
" \"/5_1.0/0/9\",\n" +
" \"/9_1.0/0/0\",\n" +
" \"/9_1.0/0/1\",\n" +
" \"/9_1.0/0/7\",\n" +
" \"/9_1.0/0/9\"\n" +
" ],\n" +
" \"attribute\": [],\n" +
" \"telemetry\": [\n" +
" \"/5_1.0/0/3\",\n" +
" \"/5_1.0/0/5\",\n" +
" \"/5_1.0/0/6\",\n" +
" \"/5_1.0/0/7\",\n" +
" \"/5_1.0/0/9\",\n" +
" \"/9_1.0/0/0\",\n" +
" \"/9_1.0/0/1\",\n" +
" \"/9_1.0/0/7\",\n" +
" \"/9_1.0/0/9\"\n" +
" ],\n" +
" \"attributeLwm2m\": {}\n" +
" }";
private List<OtaPackageUpdateStatus> expectedStatuses;
@Test
@ -99,8 +59,8 @@ public class OtaLwM2MIntegrationTest extends AbstractOtaLwM2MIntegrationTest {
createDeviceProfile(transportConfiguration);
LwM2MDeviceCredentials deviceCredentials = getDeviceCredentialsNoSec(createNoSecClientCredentials(this.CLIENT_ENDPOINT_WITHOUT_FW_INFO));
final Device device = createDevice(deviceCredentials, this.CLIENT_ENDPOINT_WITHOUT_FW_INFO);
createNewClient(SECURITY_NO_SEC, COAP_CONFIG, false, this.CLIENT_ENDPOINT_WITHOUT_FW_INFO, false, null);
awaitObserveReadAll(0, false, device.getId().getId().toString());
createNewClient(SECURITY_NO_SEC, null, COAP_CONFIG, false, this.CLIENT_ENDPOINT_WITHOUT_FW_INFO);
awaitObserveReadAll(0, device.getId().getId().toString());
device.setFirmwareId(createFirmware().getId());
final Device savedDevice = doPost("/api/device", device, Device.class);
@ -124,8 +84,8 @@ public class OtaLwM2MIntegrationTest extends AbstractOtaLwM2MIntegrationTest {
createDeviceProfile(transportConfiguration);
LwM2MDeviceCredentials deviceCredentials = getDeviceCredentialsNoSec(createNoSecClientCredentials(this.CLIENT_ENDPOINT_OTA5));
final Device device = createDevice(deviceCredentials, this.CLIENT_ENDPOINT_OTA5);
createNewClient(SECURITY_NO_SEC, COAP_CONFIG, false, this.CLIENT_ENDPOINT_OTA5, false, null);
awaitObserveReadAll(9, false, device.getId().getId().toString());
createNewClient(SECURITY_NO_SEC, null, COAP_CONFIG, false, this.CLIENT_ENDPOINT_OTA5);
awaitObserveReadAll(9, device.getId().getId().toString());
device.setFirmwareId(createFirmware().getId());
final Device savedDevice = doPost("/api/device", device, Device.class);
@ -154,8 +114,8 @@ public class OtaLwM2MIntegrationTest extends AbstractOtaLwM2MIntegrationTest {
createDeviceProfile(transportConfiguration);
LwM2MDeviceCredentials deviceCredentials = getDeviceCredentialsNoSec(createNoSecClientCredentials(this.CLIENT_ENDPOINT_OTA9));
final Device device = createDevice(deviceCredentials, this.CLIENT_ENDPOINT_OTA9);
createNewClient(SECURITY_NO_SEC, COAP_CONFIG, false, this.CLIENT_ENDPOINT_OTA9, false, null);
awaitObserveReadAll(9, false, device.getId().getId().toString());
createNewClient(SECURITY_NO_SEC, null, COAP_CONFIG, false, this.CLIENT_ENDPOINT_OTA9);
awaitObserveReadAll(9, device.getId().getId().toString());
device.setSoftwareId(createSoftware().getId());
final Device savedDevice = doPost("/api/device", device, Device.class); //sync call

34
application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/AbstractRpcLwM2MIntegrationObserveTest.java

@ -0,0 +1,34 @@
/**
* Copyright © 2016-2024 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.transport.lwm2m.rpc;
import org.junit.Before;
import org.thingsboard.server.dao.service.DaoSqlTest;
@DaoSqlTest
public abstract class AbstractRpcLwM2MIntegrationObserveTest extends AbstractRpcLwM2MIntegrationTest{
private final String[] RESOURCES_RPC_MULTIPLE_19 = new String[]{"0.xml", "1.xml", "2.xml", "3.xml", "5.xml", "6.xml", "9.xml", "19.xml", "3303.xml"};
public AbstractRpcLwM2MIntegrationObserveTest() {
setResources(this.RESOURCES_RPC_MULTIPLE_19);
}
@Before
public void initTest () throws Exception {
awaitObserveReadAll(2, deviceId);
}
}

14
application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/AbstractRpcLwM2MIntegrationTest.java

@ -15,6 +15,8 @@
*/
package org.thingsboard.server.transport.lwm2m.rpc;
import org.eclipse.leshan.core.link.LinkParser;
import org.eclipse.leshan.core.link.lwm2m.DefaultLwM2mLinkParser;
import org.junit.Before;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.device.credentials.lwm2m.LwM2MDeviceCredentials;
@ -35,6 +37,7 @@ import static org.eclipse.leshan.core.LwM2mId.SOFTWARE_MANAGEMENT;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.BINARY_APP_DATA_CONTAINER;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.LwM2MProfileBootstrapConfigType.NONE;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.OBJECT_ID_0;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.OBJECT_ID_1;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.OBJECT_INSTANCE_ID_0;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.OBJECT_INSTANCE_ID_1;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_0;
@ -50,8 +53,8 @@ import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.resources;
@DaoSqlTest
public abstract class AbstractRpcLwM2MIntegrationTest extends AbstractLwM2MIntegrationTest {
protected final LinkParser linkParser = new DefaultLwM2mLinkParser();
protected String OBSERVE_ATTRIBUTES_WITH_PARAMS_RPC;
protected String deviceId;
public Set expectedObjects;
public Set expectedObjectIdVers;
public Set expectedInstances;
@ -59,6 +62,7 @@ public abstract class AbstractRpcLwM2MIntegrationTest extends AbstractLwM2MInteg
protected String objectInstanceIdVer_1;
protected String objectIdVer_0;
protected String objectIdVer_1;
protected String objectIdVer_2;
private static final Predicate PREDICATE_3 = path -> (!((String) path).startsWith("/" + TEMPERATURE_SENSOR) && ((String) path).startsWith("/" + DEVICE));
protected String objectIdVer_3;
@ -79,12 +83,15 @@ public abstract class AbstractRpcLwM2MIntegrationTest extends AbstractLwM2MInteg
@Before
public void startInitRPC() throws Exception {
if (this.getClass().getSimpleName().equals("RpcLwm2mIntegrationDiscoverWriteAttributesTest")){
isWriteAttribute = true;
}
initRpc();
}
private void initRpc () throws Exception {
String endpoint = DEVICE_ENDPOINT_RPC_PREF + endpointSequence.incrementAndGet();
createNewClient(SECURITY_NO_SEC, COAP_CONFIG, true, endpoint, false, null);
createNewClient(SECURITY_NO_SEC, null, COAP_CONFIG, true, endpoint);
expectedObjects = ConcurrentHashMap.newKeySet();
expectedObjectIdVers = ConcurrentHashMap.newKeySet();
expectedInstances = ConcurrentHashMap.newKeySet();
@ -103,7 +110,9 @@ public abstract class AbstractRpcLwM2MIntegrationTest extends AbstractLwM2MInteg
}
});
String ver_Id_0 = lwM2MTestClient.getLeshanClient().getObjectTree().getModel().getObjectModel(OBJECT_ID_0).version;
String ver_Id_1 = lwM2MTestClient.getLeshanClient().getObjectTree().getModel().getObjectModel(OBJECT_ID_1).version;
objectIdVer_0 = "/" + OBJECT_ID_0 + "_" + ver_Id_0;
objectIdVer_1 = "/" + OBJECT_ID_1 + "_" + ver_Id_1;
objectIdVer_2 = (String) expectedObjectIdVers.stream().filter(path -> ((String) path).startsWith("/" + ACCESS_CONTROL)).findFirst().get();
objectIdVer_3 = (String) expectedObjectIdVers.stream().filter(PREDICATE_3).findFirst().get();
objectIdVer_19 = (String) expectedObjectIdVers.stream().filter(path -> ((String) path).startsWith("/" + BINARY_APP_DATA_CONTAINER)).findFirst().get();
@ -147,7 +156,6 @@ public abstract class AbstractRpcLwM2MIntegrationTest extends AbstractLwM2MInteg
deviceId = device.getId().getId().toString();
lwM2MTestClient.start(true);
awaitObserveReadAll(2, false, device.getId().getId().toString());
}
protected String pathIdVerToObjectId(String pathIdVer) {

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

@ -0,0 +1,487 @@
/**
* Copyright © 2016-2024 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.transport.lwm2m.rpc.sql;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.eclipse.leshan.core.ResponseCode;
import org.junit.Test;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.transport.lwm2m.rpc.AbstractRpcLwM2MIntegrationObserveTest;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.OBJECT_INSTANCE_ID_0;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.OBJECT_INSTANCE_ID_1;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_0;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_14;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_15;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_2;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_3;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_5;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_7;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_9;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_NAME_19_0_0;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_NAME_19_1_0;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_NAME_3_14;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_NAME_3_9;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_INSTANCE_ID_0;
import static org.thingsboard.server.transport.lwm2m.utils.LwM2MTransportUtil.fromVersionedIdToObjectId;
public class RpcLwm2MIntegrationObserveCompositeTest extends AbstractRpcLwM2MIntegrationObserveTest {
/**
* ObserveComposite {"ids":["5/0/7", "5/0/5", "5/0/3", "3/0/9", "19/1/0/0"]} - Ok
* @throws Exception
*/
@Test
public void testObserveCompositeAnyResources_Result_CONTENT_Value_LwM2mSingleResource_LwM2mResourceInstance() throws Exception {
sendCancelObserveAllWithAwait(deviceId);
String expectedIdVer5_0_7 = objectInstanceIdVer_5 + "/" + RESOURCE_ID_7;
String expectedIdVer5_0_5= objectInstanceIdVer_5 + "/" + RESOURCE_ID_5;
String expectedIdVer5_0_3 = objectInstanceIdVer_5 + "/" + RESOURCE_ID_3;
String expectedIdVer19_1_0_0 = objectIdVer_19 + "/" + OBJECT_INSTANCE_ID_1 + "/" + RESOURCE_ID_0 + "/" + RESOURCE_INSTANCE_ID_0;
String expectedIds = "[\"" + expectedIdVer5_0_7 + "\", \"" + expectedIdVer5_0_5 + "\", \"" + expectedIdVer5_0_3 + "\", \"" + idVer_3_0_9 + "\", \"" + expectedIdVer19_1_0_0 + "\"]";
String actualResult = sendCompositeRPCByIds("ObserveComposite", expectedIds);
ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class);
assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText());
String actualValues = rpcActualResult.get("value").asText();
assertTrue(actualValues.contains(fromVersionedIdToObjectId(expectedIdVer5_0_7) + "=LwM2mSingleResource"));
assertTrue(actualValues.contains(fromVersionedIdToObjectId(expectedIdVer5_0_3) + "=LwM2mSingleResource"));
assertTrue(actualValues.contains(fromVersionedIdToObjectId(expectedIdVer5_0_5) + "=LwM2mSingleResource"));
assertTrue(actualValues.contains(fromVersionedIdToObjectId(idVer_3_0_9) + "=LwM2mSingleResource"));
assertTrue(actualValues.contains(fromVersionedIdToObjectId(expectedIdVer19_1_0_0) + "=LwM2mResourceInstance"));
}
/**
* ObserveComposite {"ids":["19/1/0/0", "5/0"]} - Ok
* @throws Exception
*/
@Test
public void testObserveComposite_ObjectInstanceWithOtherObjectResourceInstance_Result_CONTENT_Ok() throws Exception {
sendCancelObserveAllWithAwait(deviceId);
String expectedIdVer19_1_0 = objectIdVer_19 + "/" + OBJECT_INSTANCE_ID_1 + "/" + RESOURCE_ID_0;
String expectedIdVer5_0 = objectInstanceIdVer_5;
String expectedIds = "[\"" + expectedIdVer19_1_0 + "\", \"" + expectedIdVer5_0 + "\"]";
String actualResult = sendCompositeRPCByIds("ObserveComposite", expectedIds);
ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class);
assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText());
String actual= rpcActualResult.get("value").asText();
assertTrue(actual.contains(fromVersionedIdToObjectId(expectedIdVer19_1_0) + "=LwM2mMultipleResource"));
assertTrue(actual.contains(fromVersionedIdToObjectId(expectedIdVer5_0) + "=LwM2mObjectInstance"));
}
/**
* ObserveComposite {"ids":["5/0/7", "5/0/2"]} - Ok
* "5/0/2" - Execute^ result == null
* @throws Exception
*/
@Test
public void testObserveCompositeAnyResources_Result_CONTENT_Value_LwM2mSingleResource_If_Error_Null() throws Exception {
// sendCancelObserveAllWithAwait(deviceId);
String expectedIdVer5_0_7 = objectInstanceIdVer_5 + "/" + RESOURCE_ID_7;
String expectedIdVer5_0_2 = objectInstanceIdVer_5 + "/" + RESOURCE_ID_2;
String expectedIds = "[\"" + expectedIdVer5_0_7 + "\", \"" + expectedIdVer5_0_2 + "\"]";
String actualResult = sendCompositeRPCByIds("ObserveComposite", expectedIds);
ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class);
assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText());
String actualValues = rpcActualResult.get("value").asText();
assertTrue(actualValues.contains(fromVersionedIdToObjectId(expectedIdVer5_0_7) + "=LwM2mSingleResource"));
assertTrue(actualValues.contains(fromVersionedIdToObjectId(expectedIdVer5_0_2) + "=null"));
}
/**
* ObserveComposite {"ids":["5/0/7", "5/0/2"]} - Ok
* "5/0" contains "5/0/2"
* @throws Exception
*/
@Test
public void testObserveComposite_Result_BAD_REQUEST_ONE_PATH_CONTAINCE_OTHER() throws Exception {
sendCancelObserveAllWithAwait(deviceId);
String expectedIdVer5_0 = objectInstanceIdVer_5;
String expectedIdVer5_0_2 = objectInstanceIdVer_5 + "/" + RESOURCE_ID_2;
String expectedIds = "[\"" + expectedIdVer5_0 + "\", \"" + expectedIdVer5_0_2 + "\"]";
String actualResult = sendCompositeRPCByIds("ObserveComposite", expectedIds);
ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class);
assertEquals(ResponseCode.BAD_REQUEST.getName(), rpcActualResult.get("result").asText());
String actual= rpcActualResult.get("error").asText();
String expected = "Invalid path list : /5/0 and /5/0/2 are overlapped paths";
assertTrue(expected.equals(actual));
}
/**
* Previous -> "3/0/9"
* ObserveComposite {"ids":["5/0/7", "5/0/5", "5/0/3", "3/0/9"]} - CONTENT
* @throws Exception
*/
@Test
public void testObserveCompositeThereAreObservationOneResource_Result_CONTENT_Value_ObservationAddIfAbsent() throws Exception {
String expectedIdVer5_0_7 = objectInstanceIdVer_5 + "/" + RESOURCE_ID_7;
String expectedIdVer5_0_5= objectInstanceIdVer_5 + "/" + RESOURCE_ID_5;
String expectedIdVer5_0_3 = objectInstanceIdVer_5 + "/" + RESOURCE_ID_3;
String expectedIds = "[\"" + expectedIdVer5_0_7 + "\", \"" + expectedIdVer5_0_5 + "\", \"" + expectedIdVer5_0_3 + "\", \"" + idVer_3_0_9 + "\"]";
String actualResult = sendCompositeRPCByIds("ObserveComposite", expectedIds);
ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class);
assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText());
String expectedResult = "/3/0/9=LwM2mSingleResource [id=9";
assertTrue(rpcActualResult.get("value").asText().contains(expectedResult));
}
/**
* ObserveComposite {"ids":["5/0/7", "5/0/5", "5/0/3", "3/0/9", "19/1/0"]} - Ok
* @throws Exception
*/
@Test
public void testObserveCompositeAnyResources_Result_CONTENT_Value_LwM2mSingleResource_LwM2mMultipleResource() throws Exception {
sendCancelObserveAllWithAwait(deviceId);
String expectedIdVer5_0_7 = objectInstanceIdVer_5 + "/" + RESOURCE_ID_7;
String expectedIdVer5_0_5= objectInstanceIdVer_5 + "/" + RESOURCE_ID_5;
String expectedIdVer5_0_3 = objectInstanceIdVer_5 + "/" + RESOURCE_ID_3;
String expectedIdVer19_1_0 = objectIdVer_19 + "/" + OBJECT_INSTANCE_ID_1 + "/" + RESOURCE_ID_0;
String expectedIds = "[\"" + expectedIdVer5_0_7 + "\", \"" + expectedIdVer5_0_5 + "\", \"" + expectedIdVer5_0_3 + "\", \"" + idVer_3_0_9 + "\", \"" + expectedIdVer19_1_0 + "\"]";
String actualResult = sendCompositeRPCByIds("ObserveComposite", expectedIds);
ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class);
assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText());
String actualValues = rpcActualResult.get("value").asText();
assertTrue(actualValues.contains(fromVersionedIdToObjectId(expectedIdVer5_0_7) + "=LwM2mSingleResource"));
assertTrue(actualValues.contains(fromVersionedIdToObjectId(expectedIdVer5_0_3) + "=LwM2mSingleResource"));
assertTrue(actualValues.contains(fromVersionedIdToObjectId(expectedIdVer5_0_5 + "=LwM2mSingleResource")));
assertTrue(actualValues.contains(fromVersionedIdToObjectId(idVer_3_0_9) + "=LwM2mSingleResource"));
assertTrue(actualValues.contains(fromVersionedIdToObjectId(expectedIdVer19_1_0) + "=LwM2mMultipleResource"));
}
/**
* ObserveComposite with keyName {"keys":["batteryLevel", "UtfOffset", "dataRead", "dataWrite"]} - Ok
* @throws Exception
*/
@Test
public void testObserveCompositeWithKeyName_Result_CONTENT_Value_SingleResources() throws Exception {
sendCancelObserveAllWithAwait(deviceId);
String expectedKey3_0_9 = RESOURCE_ID_NAME_3_9;
String expectedKey3_0_14 = RESOURCE_ID_NAME_3_14;
String expectedKey19_0_0 = RESOURCE_ID_NAME_19_0_0;
String expectedKey19_1_0 = RESOURCE_ID_NAME_19_1_0;
String expectedKeys = "[\"" + expectedKey3_0_9 + "\", \"" + expectedKey3_0_14 + "\", \"" + expectedKey19_0_0 + "\", \"" + expectedKey19_1_0 + "\"]";
String actualResult = sendCompositeRPCByKeys("ObserveComposite", expectedKeys);
ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class);
assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText());
String actualValues = rpcActualResult.get("value").asText();
String expectedIdVer3_0_14 = objectInstanceIdVer_3 + "/" + RESOURCE_ID_14;
String expectedIdVer19_0_0 = objectIdVer_19 + "/" + OBJECT_INSTANCE_ID_0 + "/" + RESOURCE_ID_0;
String expectedIdVer19_1_0 = objectIdVer_19 + "/" + OBJECT_INSTANCE_ID_1 + "/" + RESOURCE_ID_0;
assertTrue(actualValues.contains(fromVersionedIdToObjectId(expectedIdVer3_0_14)));
assertTrue(actualValues.contains(fromVersionedIdToObjectId(expectedIdVer19_0_0)));
assertTrue(actualValues.contains(fromVersionedIdToObjectId(expectedIdVer19_1_0)));
assertTrue(actualValues.contains(fromVersionedIdToObjectId(idVer_3_0_9)));
}
/**
* ObserveComposite with keyName {"keys":["batteryLevel", "UtfOffset", "dataRead", "dataWrite"]} - - BAD_REQUEST
* @throws Exception
*/
@Test
public void testObserveCompositeWithKeyNameThereAreObservationOneResource_Result_CONTENT_Value_ObservationAddIfAbsent() throws Exception {
String expectedKey3_0_9 = RESOURCE_ID_NAME_3_9;
String expectedKey3_0_14 = RESOURCE_ID_NAME_3_14;
String expectedKey19_0_0 = RESOURCE_ID_NAME_19_0_0;
String expectedKey19_1_0 = RESOURCE_ID_NAME_19_1_0;
String expectedKeys = "[\"" + expectedKey3_0_9 + "\", \"" + expectedKey3_0_14 + "\", \"" + expectedKey19_0_0 + "\", \"" + expectedKey19_1_0 + "\"]";
String expectedIdVer19_1_0 = objectIdVer_19 + "/" + OBJECT_INSTANCE_ID_1 + "/" + RESOURCE_ID_0;
String actualResult = sendCompositeRPCByKeys("ObserveComposite", expectedKeys);
ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class);
assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText());
String actual = rpcActualResult.get("value").asText();
assertTrue(actual.contains(fromVersionedIdToObjectId(expectedIdVer19_1_0) + "=LwM2mMultipleResource"));
}
/**
* ObserveReadAll
* {"result":"CONTENT","value":"[\"CompositeObservation: [/19/1/0\",\"/19/0/0\",\"/3/0/14\",\"/3/0/9]\"]"} - Ok
* @throws Exception
*/
@Test
public void testObserveReadAll_AfterCompositeObservation_Result_CONTENT_Value_SingleObservation_Only() throws Exception {
sendCancelObserveAllWithAwait(deviceId);
String expectedKey3_0_9 = RESOURCE_ID_NAME_3_9;
String expectedKey3_0_14 = RESOURCE_ID_NAME_3_14;
String expectedKey19_0_0 = RESOURCE_ID_NAME_19_0_0;
String expectedKey19_1_0 = RESOURCE_ID_NAME_19_1_0;
String expectedKeys = "[\"" + expectedKey3_0_9 + "\", \"" + expectedKey3_0_14 + "\", \"" + expectedKey19_0_0 + "\", \"" + expectedKey19_1_0 + "\"]";
String actualResult = sendCompositeRPCByKeys("ObserveComposite", expectedKeys);
ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class);
assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText());
String actualResultReadAll = sendCompositeRPCByKeys("ObserveReadAll", null);
ObjectNode rpcActualResultReadAll = JacksonUtil.fromString(actualResultReadAll, ObjectNode.class);
assertEquals(ResponseCode.CONTENT.getName(), rpcActualResultReadAll.get("result").asText());
String actualValues = rpcActualResultReadAll.get("value").asText();
String expectedIdVer3_0_14 = objectInstanceIdVer_3 + "/" + RESOURCE_ID_14;
String expectedIdVer19_1_0 = objectIdVer_19 + "/" + OBJECT_INSTANCE_ID_1 + "/" + RESOURCE_ID_0;
assertTrue(actualValues.contains("SingleObservation:" + fromVersionedIdToObjectId(idVer_3_0_9)));
assertTrue(actualValues.contains("SingleObservation:" + fromVersionedIdToObjectId(expectedIdVer3_0_14)));
assertTrue(actualValues.contains("SingleObservation:" + fromVersionedIdToObjectId(expectedIdVer19_1_0)));
assertTrue(actualValues.contains("SingleObservation:" + fromVersionedIdToObjectId(idVer_19_0_0)));
}
/**
* ObserveReadAll
* {"result":"CONTENT","value":"{"result":"CONTENT","value":"["SingleObservation:/3/0/9","SingleObservation:/3/0/14","SingleObservation:/19/1/0/0","SingleObservation:/19/0/0"]"} - Ok
* @throws Exception
*/
@Test
public void testObserveReadAll_Result_CONTENT_Value_SingleObservation_Only() throws Exception {
sendCancelObserveAllWithAwait(deviceId);
String expectedIdVer3_0_14 = objectInstanceIdVer_3 + "/" + RESOURCE_ID_14;
String expectedIdVer19_1_0_0 = objectIdVer_19 + "/" + OBJECT_INSTANCE_ID_1 + "/" + RESOURCE_ID_0 + "/" + RESOURCE_INSTANCE_ID_0;
String actualResult3_0_9 = sendObserve("Observe", idVer_3_0_9);
ObjectNode rpcActualResult3_0_9 = JacksonUtil.fromString(actualResult3_0_9, ObjectNode.class);
assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult3_0_9.get("result").asText());
String actualResult3_0_14 = sendObserve("Observe", expectedIdVer3_0_14);
ObjectNode rpcActualResult3_0_14 = JacksonUtil.fromString(actualResult3_0_14, ObjectNode.class);
assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult3_0_14.get("result").asText());
String actualResult19_1_0_0 = sendObserve("Observe", expectedIdVer19_1_0_0);
ObjectNode rpcActualResult19_1_0_0 = JacksonUtil.fromString(actualResult19_1_0_0, ObjectNode.class);
assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult19_1_0_0.get("result").asText());
String actualResult19_0_0 = sendObserve("Observe", idVer_19_0_0);
ObjectNode rpcActualResult19_0_0 = JacksonUtil.fromString(actualResult19_0_0, ObjectNode.class);
assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult19_0_0.get("result").asText());
String actualResultReadAll = sendCompositeRPCByKeys("ObserveReadAll", null);
ObjectNode rpcActualResultReadAll = JacksonUtil.fromString(actualResultReadAll, ObjectNode.class);
assertEquals(ResponseCode.CONTENT.getName(), rpcActualResultReadAll.get("result").asText());
String actualValues = rpcActualResultReadAll.get("value").asText();
assertTrue(actualValues.contains("SingleObservation:" + fromVersionedIdToObjectId(idVer_3_0_9)));
assertTrue(actualValues.contains("SingleObservation:" + fromVersionedIdToObjectId(expectedIdVer3_0_14)));
assertTrue(actualValues.contains("SingleObservation:" + fromVersionedIdToObjectId(expectedIdVer19_1_0_0)));
assertTrue(actualValues.contains("SingleObservation:" + fromVersionedIdToObjectId(idVer_19_0_0)));
}
/**
* ObserveReadAll
* {"result":"CONTENT","value":"[\"CompositeObservation: [/19/1/0\",\"/19/0/0\",\"/3/0/14\",\"/3/0/9]\"]"} - Ok
* @throws Exception
*/
@Test
public void testObserveReadAll_AfterCompositeObservation_WithResourceNotReadable_Result_CONTENT_Value_SingleObservation_Only() throws Exception {
sendCancelObserveAllWithAwait(deviceId);
String expectedIdVer5_0_7 = objectInstanceIdVer_5 + "/" + RESOURCE_ID_7;
String expectedIdVer5_0_2= objectInstanceIdVer_5 + "/" + RESOURCE_ID_2;
String expectedIdVer5_0_3 = objectInstanceIdVer_5 + "/" + RESOURCE_ID_3;
String expectedIdVer19_1_0 = objectIdVer_19 + "/" + OBJECT_INSTANCE_ID_1 + "/" + RESOURCE_ID_0;
String expectedIds = "[\"" + expectedIdVer5_0_7 + "\", \"" + expectedIdVer5_0_2 + "\", \"" + expectedIdVer5_0_3 + "\", \"" + idVer_3_0_9 + "\", \"" + expectedIdVer19_1_0 + "\"]";
String actualResult = sendCompositeRPCByIds("ObserveComposite", expectedIds);
ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class);
assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText());
String actualValues = rpcActualResult.get("value").asText();
assertTrue(actualValues.contains(fromVersionedIdToObjectId(expectedIdVer5_0_2) + "=null"));
String actualResultReadAll = sendCompositeRPCByKeys("ObserveReadAll", null);
ObjectNode rpcActualResultReadAll = JacksonUtil.fromString(actualResultReadAll, ObjectNode.class);
assertEquals(ResponseCode.CONTENT.getName(), rpcActualResultReadAll.get("result").asText());
actualValues = rpcActualResultReadAll.get("value").asText();
assertFalse(actualValues.contains(fromVersionedIdToObjectId(expectedIdVer5_0_2)));
}
/**
* ObserveComposite {"ids":["/5/0/7", "/5/0/5", "/5/0/3", "/3/0/9", "/19/1/0/0"]} - Ok
* ObserveCompositeCancel {"ids":["/5/0/7", "/5/0/5", "/5/0/3", "/3/0/9", "/19/1/0/0"]} - Ok
* @throws Exception
*/
@Test
public void testObserveCompositeAnyResources_Result_CONTENT_CancelObserveComposite_This_Result_Content_Count_5() throws Exception {
sendCancelObserveAllWithAwait(deviceId);
// ObserveComposite
String expectedIdVer5_0_7 = objectInstanceIdVer_5 + "/" + RESOURCE_ID_7;
String expectedIdVer5_0_5= objectInstanceIdVer_5 + "/" + RESOURCE_ID_5;
String expectedIdVer5_0_3 = objectInstanceIdVer_5 + "/" + RESOURCE_ID_3;
String expectedIdVer19_1_0_0 = objectIdVer_19 + "/" + OBJECT_INSTANCE_ID_1 + "/" + RESOURCE_ID_0 + "/" + RESOURCE_INSTANCE_ID_0;
String expectedIds = "[\"" + expectedIdVer5_0_7 + "\", \"" + expectedIdVer5_0_5 + "\", \"" + expectedIdVer5_0_3 + "\", \"" + idVer_3_0_9 + "\", \"" + expectedIdVer19_1_0_0 + "\"]";
String actualResult = sendCompositeRPCByIds("ObserveComposite", expectedIds);
ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class);
assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText());
// ObserveCompositeCancel
actualResult = sendCompositeRPCByIds("ObserveCompositeCancel", expectedIds);
rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class);
assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText());
assertEquals("5", rpcActualResult.get("value").asText());
assertEquals(0, (Object) getCntObserveAll(deviceId));
}
/**
* ObserveComposite {"ids":["/3", "/5/0/3", "/19/1/0/0"]} - Ok
* ObserveCompositeCancel {"ids":["/3", "/5/0/3", "/19/1/0/0"]} - Ok
* @throws Exception
*/
@Test
public void testObserveCompositeOneObjectAnyResources_Result_CONTENT_CancelObserveComposite_This_Result_Content_Count_3() throws Exception {
sendCancelObserveAllWithAwait(deviceId);
// ObserveComposite
String expectedIdVer5_0_3 = objectInstanceIdVer_5 + "/" + RESOURCE_ID_3;
String expectedIdVer19_1_0_0 = objectIdVer_19 + "/" + OBJECT_INSTANCE_ID_1 + "/" + RESOURCE_ID_0 + "/" + RESOURCE_INSTANCE_ID_0;
String expectedIds = "[\"" + idVer_3_0_9 + "\", \"" + expectedIdVer5_0_3 + "\", \"" + expectedIdVer19_1_0_0 + "\"]";
String actualResult = sendCompositeRPCByIds("ObserveComposite", expectedIds);
ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class);
assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText());
// ObserveCompositeCancel
actualResult = sendCompositeRPCByIds("ObserveCompositeCancel", expectedIds);
rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class);
assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText());
assertEquals("3", rpcActualResult.get("value").asText());
assertEquals(0, (Object) getCntObserveAll(deviceId));
}
/**
* ObserveComposite {"ids":["/3/0/9", "/3/0/14", "/5/0/3", "/19/1/0/0"]} - Ok
* ObserveCompositeCancel {"ids":["/3", "/19/1/0/0"]} - Ok
* last Observation
* @throws Exception
*/
@Test
public void testObserveCompositeAnyResources_Result_CONTENT_CancelObserveComposite_OneObjectAnyResource_Result_Content_Count_4() throws Exception {
sendCancelObserveAllWithAwait(deviceId);
// ObserveComposite
String expectedIdVer5_0_7 = objectInstanceIdVer_5 + "/" + RESOURCE_ID_7;
String expectedIdVer5_0_5 = objectInstanceIdVer_5 + "/" + RESOURCE_ID_5;
String expectedIdVer5_0_3 = objectInstanceIdVer_5 + "/" + RESOURCE_ID_3;
String expectedIdVer3_0_9 = objectInstanceIdVer_3 + "/" + RESOURCE_ID_9;
String expectedIdVer19_1_0_0 = objectIdVer_19 + "/" + OBJECT_INSTANCE_ID_1 + "/" + RESOURCE_ID_0 + "/" + RESOURCE_INSTANCE_ID_0;
String expectedIds = "[\"" + expectedIdVer5_0_7 + "\", \"" + expectedIdVer5_0_5 + "\", \"" + expectedIdVer5_0_3 + "\", \"" + expectedIdVer3_0_9 + "\", \"" + expectedIdVer19_1_0_0 + "\"]";
String actualResult = sendCompositeRPCByIds("ObserveComposite", expectedIds);
ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class);
assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText());
awaitObserveReadAll(5, deviceId);
// ObserveCompositeCancel
expectedIds = "[\"" + objectInstanceIdVer_5 + "\", \"" + expectedIdVer19_1_0_0 + "\"]";
actualResult = sendCompositeRPCByIds("ObserveCompositeCancel", expectedIds);
rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class);
assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText());
assertEquals("4", rpcActualResult.get("value").asText());
String actualResultReadAll = sendCompositeRPCByKeys("ObserveReadAll", null);
ObjectNode rpcActualResultReadAll = JacksonUtil.fromString(actualResultReadAll, ObjectNode.class);
assertEquals(ResponseCode.CONTENT.getName(), rpcActualResultReadAll.get("result").asText());
String actualValues = rpcActualResultReadAll.get("value").asText();
assertEquals("[\"SingleObservation:" + fromVersionedIdToObjectId(expectedIdVer3_0_9) + "\"]", actualValues);
}
/**
* ObserveComposite {"ids":["/3", "/5/0/3", "/19/1/0/0"]} - Ok
* ObserveCompositeCancel {"ids":["/3/0/9", "/5/0/3", "/19/1/0/0"} -> BAD_REQUEST
* ObserveCompositeCancel {"ids":["/3"} -> CONTENT
*/
@Test
public void testObserveOneObjectAnyResources_Result_CONTENT_Cancel_OneResourceFromObjectAnyResource_Result_BAD_REQUEST_Cancel_OneObject_Result_CONTENT() throws Exception {
sendCancelObserveAllWithAwait(deviceId);
// ObserveComposite
sendCancelObserveAllWithAwait(deviceId);
String expectedIdVer5_0_3 = objectInstanceIdVer_5 + "/" + RESOURCE_ID_3;
String expectedIdVer19_1_0_0 = objectIdVer_19 + "/" + OBJECT_INSTANCE_ID_1 + "/" + RESOURCE_ID_0 + "/" + RESOURCE_INSTANCE_ID_0;
String expectedIds = "[\"" + objectIdVer_3 + "\", \"" + expectedIdVer5_0_3 + "\", \"" + expectedIdVer19_1_0_0 + "\"]";
String actualResult = sendCompositeRPCByIds("ObserveComposite", expectedIds);
ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class);
assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText());
// ObserveCompositeCancel
expectedIds = "[\"" + expectedIdVer19_1_0_0 + "\", \"" + idVer_3_0_9 + "\"]";
actualResult = sendCompositeRPCByIds("ObserveCompositeCancel", expectedIds);
rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class);
assertEquals(ResponseCode.BAD_REQUEST.getName(), rpcActualResult.get("result").asText());
String expectedValue = "for observation path " + fromVersionedIdToObjectId(idVer_3_0_9) + ", that includes this observation path " + fromVersionedIdToObjectId(objectIdVer_3);
assertTrue(rpcActualResult.get("error").asText().contains(expectedValue));
// ObserveCompositeCancel
expectedIds = "[\"" + objectIdVer_3 + "\", \"" + expectedIdVer19_1_0_0 + "\"]";
actualResult = sendCompositeRPCByIds("ObserveCompositeCancel", expectedIds);
rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class);
assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText());
assertEquals("2", rpcActualResult.get("value").asText());
String actualResultReadAll = sendCompositeRPCByKeys("ObserveReadAll", null);
ObjectNode rpcActualResultReadAll = JacksonUtil.fromString(actualResultReadAll, ObjectNode.class);
assertEquals(ResponseCode.CONTENT.getName(), rpcActualResultReadAll.get("result").asText());
String actualValues = rpcActualResultReadAll.get("value").asText();
assertEquals("[\"SingleObservation:" + fromVersionedIdToObjectId(expectedIdVer5_0_3) + "\"]", actualValues);
}
/**
* ObserveComposite {"ids":["/3/0/9", "/3/0/14", "/5/0/3", "/3/0/15", "/19/1/0/0"]} - Ok
* ObserveCancel {"id":"/3/0/9"} -> INTERNAL_SERVER_ERROR
* ObserveCompositeCancel {"ids":["/3/0/9", "/19/1/0/0", "/3]} - Ok
* last Observation
* @throws Exception
*/
@Test
public void testObserveCompositeAnyResources_Result_CONTENT_CancelObserveComposite_OneResource_OneObjectAnyResource_Result_Content_Count_4() throws Exception {
sendCancelObserveAllWithAwait(deviceId);
// ObserveComposite
sendCancelObserveAllWithAwait(deviceId);
String expectedIdVer3_0_14 = objectIdVer_3 + "/" + OBJECT_INSTANCE_ID_0 + "/" + RESOURCE_ID_14;
String expectedIdVer3_0_15= objectIdVer_3 + "/" + OBJECT_INSTANCE_ID_0 + "/" + RESOURCE_ID_15;
String expectedIdVer5_0_3 = objectInstanceIdVer_5 + "/" + RESOURCE_ID_3;
String expectedIdVer19_1_0_0 = objectIdVer_19 + "/" + OBJECT_INSTANCE_ID_1 + "/" + RESOURCE_ID_0 + "/" + RESOURCE_INSTANCE_ID_0;
String expectedIds = "[\"" + idVer_3_0_9 + "\", \"" + expectedIdVer3_0_14 + "\", \"" + expectedIdVer5_0_3 + "\", \"" + expectedIdVer3_0_15 + "\", \"" + expectedIdVer19_1_0_0 + "\"]";
String actualResult = sendCompositeRPCByIds("ObserveComposite", expectedIds);
ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class);
assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText());
// ObserveCompositeCancel
expectedIds = "[\"" + idVer_3_0_9 + "\", \"" + expectedIdVer19_1_0_0 + "\", \"" + objectIdVer_3 + "\"]";
actualResult = sendCompositeRPCByIds("ObserveCompositeCancel", expectedIds);
rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class);
assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText());
assertEquals("4", rpcActualResult.get("value").asText());
String actualResultReadAll = sendCompositeRPCByKeys("ObserveReadAll", null);
ObjectNode rpcActualResultReadAll = JacksonUtil.fromString(actualResultReadAll, ObjectNode.class);
assertEquals(ResponseCode.CONTENT.getName(), rpcActualResultReadAll.get("result").asText());
String actualValues = rpcActualResultReadAll.get("value").asText();
assertEquals("[\"SingleObservation:" + fromVersionedIdToObjectId(expectedIdVer5_0_3) + "\"]", actualValues);
}
private String sendObserve(String method, String params) throws Exception {
String sendRpcRequest;
if (params == null) {
sendRpcRequest = "{\"method\": \"" + method + "\"}";
}
else {
sendRpcRequest = "{\"method\": \"" + method + "\", \"params\": {\"id\": \"" + params + "\"}}";
}
return doPostAsync("/api/plugins/rpc/twoway/" + deviceId, sendRpcRequest, String.class, status().isOk());
}
private String sendCompositeRPCByIds(String method, String paths) throws Exception {
String setRpcRequest = "{\"method\": \"" + method + "\", \"params\": {\"ids\":" + paths + "}}";
return doPostAsync("/api/plugins/rpc/twoway/" + deviceId, setRpcRequest, String.class, status().isOk());
}
private String sendCompositeRPCByKeys(String method, String keys) throws Exception {
String setRpcRequest = "{\"method\": \"" + method + "\", \"params\": {\"keys\":" + keys + "}}";
return doPostAsync("/api/plugins/rpc/twoway/" + deviceId, setRpcRequest, String.class, status().isOk());
}
}

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

@ -99,7 +99,7 @@ public class RpcLwm2mIntegrationCreateTest extends AbstractRpcLwM2MIntegrationTe
String actualResult = sendRPCreateById(expectedPath, expectedValue);
ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class);
assertEquals(ResponseCode.BAD_REQUEST.getName(), rpcActualResult.get("result").asText());
String expectedObjectId = pathIdVerToObjectId((String) expectedPath);
String expectedObjectId = pathIdVerToObjectId(expectedPath);
LwM2mPath expectedPathId = new LwM2mPath(expectedObjectId);
String expected = "Specified object id " + expectedPathId.getObjectId() + " absent in the list supported objects of the client or is security object!";
String actual = rpcActualResult.get("error").asText();
@ -118,7 +118,7 @@ public class RpcLwm2mIntegrationCreateTest extends AbstractRpcLwM2MIntegrationTe
String actualResult = sendRPCreateById(expectedPath, expectedValue);
ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class);
assertEquals(ResponseCode.BAD_REQUEST.getName(), rpcActualResult.get("result").asText());
String expectedObjectId = pathIdVerToObjectId((String) expectedPath);
String expectedObjectId = pathIdVerToObjectId(expectedPath);
LwM2mPath expectedPathId = new LwM2mPath(expectedObjectId);
String expected = "Specified object id " + expectedPathId.getObjectId() + " absent in the list supported objects of the client or is security object!";
String actual = rpcActualResult.get("error").asText();

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

@ -18,6 +18,8 @@ package org.thingsboard.server.transport.lwm2m.rpc.sql;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.eclipse.leshan.core.ResponseCode;
import org.eclipse.leshan.core.link.Link;
import org.eclipse.leshan.core.link.LinkParseException;
import org.eclipse.leshan.core.node.LwM2mPath;
import org.junit.Test;
import org.thingsboard.common.util.JacksonUtil;
@ -33,6 +35,7 @@ import static org.junit.Assert.assertTrue;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.OBJECT_INSTANCE_ID_0;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_2;
import static org.thingsboard.server.transport.lwm2m.utils.LwM2MTransportUtil.convertObjectIdToVerId;
public class RpcLwm2mIntegrationDiscoverTest extends AbstractRpcLwM2MIntegrationTest {
@ -52,12 +55,17 @@ public class RpcLwm2mIntegrationDiscoverTest extends AbstractRpcLwM2MIntegration
Set actualObjects = ConcurrentHashMap.newKeySet();
Set actualInstances = ConcurrentHashMap.newKeySet();
rpcActualValue.forEach(node -> {
if (!node.get("uriReference").asText().equals("/")) {
LwM2mPath path = new LwM2mPath(node.get("uriReference").asText());
actualObjects.add("/" + path.getObjectId());
if (path.isObjectInstance()) {
actualInstances.add("/" + path.getObjectId() + "/" + path.getObjectInstanceId());
try {
Link[] parsedLink = linkParser.parseCoreLinkFormat(node.asText().getBytes());
if (!parsedLink[0].getUriReference().equals("/")) {
LwM2mPath path = new LwM2mPath(parsedLink[0].getUriReference());
actualObjects.add("/" + path.getObjectId());
if (path.isObjectInstance()) {
actualInstances.add("/" + path.getObjectId() + "/" + path.getObjectInstanceId());
}
}
} catch (LinkParseException e) {
throw new RuntimeException(e);
}
});
assertEquals(expectedInstances, actualInstances);
@ -92,8 +100,8 @@ public class RpcLwm2mIntegrationDiscoverTest extends AbstractRpcLwM2MIntegration
* {"result":"CONTENT","value":"</3/0>,</3/0/0>,</3/0/1>,</3/0/2>,</3/0/3>,</3/0/4>,</3/0/5>,</3/0/6>,</3/0/7>,</3/0/8>,</3/0/9>,</3/0/10>,</3/0/11>,</3/0/12>,</3/0/13>,</3/0/14>,</3/0/15>,</3/0/16>,</3/0/1
* 7>,</3/0/18>,</3/0/19>,</3/0/20>,</3/0/21>,</3/0/22>"}
* If WriteAttributes implemented and WriteAttributes saved
* Discover {"id":"19/0"}
* {"result":"CONTENT","value":"[</19/0>;dim=2;pmin=10;pmax=60;gt=50;lt=42.2,</19/0/0>;pmax=120, </19/0/1>, </19/0/2>, </19/0/3>, </19/0/4>, </19/0/5>;lt=45]"}
* Discover {"id":"19"}
* {"result":"CONTENT","value":"[</19>;ver=1.1,</19/0>;dim=2;pmin=10;pmax=60;gt=50;lt=42.2,</19/0/0>;pmax=120, </19/0/1>, </19/0/2>, </19/0/3>, </19/0/4>, </19/0/5>;lt=45]"}
*/
@Test
public void testDiscoverInstance_Return_CONTENT_LinksResourcesOnLyExpectedInstance() throws Exception {
@ -110,12 +118,12 @@ public class RpcLwm2mIntegrationDiscoverTest extends AbstractRpcLwM2MIntegration
/**
* Discover {"id":"3/0/14"}
* If WriteAttributes implemented:
* {"result":"CONTENT","value":"</3/0/14>;pmax=100, "pmin":10, "ver"=1.0"}
* {"result":"CONTENT","value":"</3/0/14>;pmax=100, "pmin":10}
* If WriteAttributes not implemented:
* {"result":"CONTENT","value":"</3/0/14>"}
* Discover {"id":"19_1.1/0/0"}
* If WriteAttributes implemented:
* {"result":"CONTENT","value":"</19/0/0>;pmax=100, "pmin":10, "ver"=1.1"}
* {"result":"CONTENT","value":"</19/0/0>;pmax=100, "pmin":10}
* If WriteAttributes not implemented:
* {"result":"CONTENT","value":"</19/0/0>"}
*/
@ -125,8 +133,10 @@ public class RpcLwm2mIntegrationDiscoverTest extends AbstractRpcLwM2MIntegration
String expectedObjectInstanceId = pathIdVerToObjectId(expectedInstance);
LwM2mPath expectedPath = new LwM2mPath(expectedObjectInstanceId);
int expectedResource = lwM2MTestClient.getLeshanClient().getObjectTree().getObjectEnablers().get(expectedPath.getObjectId()).getObjectModel().resources.entrySet().stream().findAny().get().getKey();
String ver = lwM2MTestClient.getLeshanClient().getObjectTree().getObjectEnablers().get(expectedPath.getObjectId()).getObjectModel().version;
String expected = expectedInstance + "/" + expectedResource;
String actualResult = sendDiscover(expected);
String expectedVerId = convertObjectIdToVerId(expected, ver);
String actualResult = sendDiscover(expectedVerId);
ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class);
assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText());
String expectedResourceId = "<" + expectedObjectInstanceId + "/" + expectedResource + ">";

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

@ -0,0 +1,249 @@
/**
* Copyright © 2016-2024 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.transport.lwm2m.rpc.sql;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.eclipse.leshan.core.ResponseCode;
import org.junit.Test;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.transport.lwm2m.rpc.AbstractRpcLwM2MIntegrationTest;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_14;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_6;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_7;
public class RpcLwm2mIntegrationDiscoverWriteAttributesTest extends AbstractRpcLwM2MIntegrationTest {
/**
* WriteAttributes {"id":"/3_1.2/0/6","attributes":{"pmax":100, "pmin":10}}
* if not implemented:
* {"result":"INTERNAL_SERVER_ERROR","error":"not implemented"}
* if implemented:
* {"result":"BAD_REQUEST","error":"Attribute pmax can be used for only Resource/Object Instance/Object."}
*/
@Test
public void testWriteAttributesResourceWithParametersByResourceInstanceId_Result_BAD_REQUEST() throws Exception {
String expectedPath = objectInstanceIdVer_3 + "/" + RESOURCE_ID_6 + "/1";
String expectedValue = "{\"pmax\":100, \"pmin\":10}";
String actualResult = sendRPCExecuteWithValueById(expectedPath, expectedValue);
ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class);
assertEquals(ResponseCode.BAD_REQUEST.getName(), rpcActualResult.get("result").asText());
String expected = "Attribute pmax can be used for only Resource/Object Instance/Object.";
String actual = rpcActualResult.get("error").asText();
assertTrue(actual.equals(expected));
}
/**
* WriteAttributes {"id":"/3_1.2/0/6","attributes":{"pmax":100, "pmin":10}}
* if not implemented:
* {"result":"INTERNAL_SERVER_ERROR","error":"not implemented"}
* if implemented:
* {"result":"BAD_REQUEST","error":"Attribute pmax can be used for only Resource/Object Instance/Object."}
*/
@Test
public void testWriteAttributeResourceDimWithParametersByResourceId_Result_BAD_REQUEST() throws Exception {
String expectedPath = objectInstanceIdVer_3 + "/" + RESOURCE_ID_6;
String expectedValue = "{\"dim\":3}";
String actualResult = sendRPCExecuteWithValueById(expectedPath, expectedValue);
ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class);
assertEquals(ResponseCode.BAD_REQUEST.getName(), rpcActualResult.get("result").asText());
String expected = "Attribute dim is of class PROPERTIES but only NOTIFICATION attribute can be used in WRITE ATTRIBUTE request.";
String actual = rpcActualResult.get("error").asText();
assertTrue(actual.equals(expected));
}
@Test
public void testWriteAttributesResourceVerWithParametersById_Result_BAD_REQUEST() throws Exception {
String expectedPath = objectIdVer_3;
String expectedValue = "{\"ver\":1.3}";
String actualResult = sendRPCExecuteWithValueById(expectedPath, expectedValue);
ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class);
assertEquals(ResponseCode.BAD_REQUEST.getName(), rpcActualResult.get("result").asText());
String expected = "Attribute ver is of class PROPERTIES but only NOTIFICATION attribute can be used in WRITE ATTRIBUTE request.";
String actual = rpcActualResult.get("error").asText();
assertTrue(actual.equals(expected));
}
@Test
public void testWriteAttributesResourceServerUriWithParametersById_Result_BAD_REQUEST() throws Exception {
String expectedPath = objectInstanceIdVer_1;
String actualResult = sendRPCReadById(expectedPath);
String expectedValue = "{\"uri\":\"coaps://localhost:5690\"}";
actualResult = sendRPCExecuteWithValueById(expectedPath, expectedValue);
ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class);
assertEquals(ResponseCode.BAD_REQUEST.getName(), rpcActualResult.get("result").asText());
String expected = "Attribute uri is of class PROPERTIES but only NOTIFICATION attribute can be used in WRITE ATTRIBUTE request.";
String actual = rpcActualResult.get("error").asText();
assertTrue(actual.equals(expected));
}
/**
* <PROPERTIES> Class Attributes
* Dimension dim Integer [0:255]
* Number of instances existing for a Multiple-Instance Resource
* <ObjectID>3</ObjectID>
* <Item ID="6">
* <Name>Available Power Sources</Name>
* <Operations>R</Operations>
* <MultipleInstances>Multiple</MultipleInstances>
* <Type>Integer</Type>
* <RangeEnumeration>0..7</RangeEnumeration>
* WriteAttributes implemented: Discover {"id":"3/0/6"} -> 'dim' = 3
* "ver" only for objectId
*/
@Test
public void testReadDIM_3_0_6_Only_R () throws Exception {
String path = objectInstanceIdVer_3 + "/" + RESOURCE_ID_6;
String actualResult = sendDiscover(path);
ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class);
assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText());
String expected = "</3/0/6>;dim=3";
assertTrue(rpcActualResult.get("value").asText().equals(expected));
}
/**
* <PROPERTIES> Class Attributes
* Object Version ver Object
* Provide the version of the associated Object.
* "ver" only for objectId
*/
@Test
public void testReadVer () throws Exception {
String path = objectIdVer_3;
String actualResult = sendDiscover(path);
ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class);
assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText());
String expected = "</3>;ver=1.2";
assertTrue(rpcActualResult.get("value").asText().contains(expected));
}
/**
* WriteAttributes {"id":"/3/0/14","attributes":{"pmax":100, "pmin":10}}
* if not implemented:
* {"result":"INTERNAL_SERVER_ERROR","error":"not implemented"}
* if implemented:
* {"result":"CHANGED"}
*/
@Test
public void testWriteAttributesResourceWithParametersById_Result_CHANGED() throws Exception {
String expectedPath = objectInstanceIdVer_3 + "/" + RESOURCE_ID_14;
String expectedValue = "{\"pmax\":100, \"pmin\":10}";
String actualResult = sendRPCExecuteWithValueById(expectedPath, expectedValue);
ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class);
assertEquals(ResponseCode.CHANGED.getName(), rpcActualResult.get("result").asText());
// result changed
actualResult = sendDiscover(expectedPath);
rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class);
assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText());
String expected = "</3/0/14>;pmax=100;pmin=10";
assertTrue(rpcActualResult.get("value").asText().contains(expected));
}
/**
* <NOTIFICATION> Class Attributes
* Minimum/Maximum Period pmin/pmax
* Notes: The Minimum Period Attribute:
* -- indicates the minimum time in seconds the LwM2M Client MUST wait between two notifications. If a notification of an observed Resource is supposed to be generated but it is before pmin expiry, notification MUST be sent as soon as pmin expires. In the absence of this parameter, the Minimum Period is defined by the Default Minimum Period set in the LwM2M Server Account.
* Notes: The Maximum Period Attribute:
* -- indicates the maximum time in seconds the LwM2M Client MAY wait between two notifications. When this "Maximum Period" expires after the last notification, a new notification MUST be sent. In the absence of this parameter, the "Maximum Period" is defined by the Default Maximum Period when set in the LwM2M Server Account or considered as 0 otherwise. The value of 0, means pmax MUST be ignored. The maximum period parameter MUST be greater than the minimum period parameter otherwise pmax will be ignored for the Resource to which such inconsistent timing conditions are applied.
* Greater Than gt Resource
* Less Than lt Resource
* Step st Resource
*
* Object Id = 1
* Default Minimum Period Id = 2 300 or 0
* Default Maximum Period Id = 3 6000 or "-"
* </3/0>;pmax=65, </3/0/1>, <3/0/2>, </3/0/3>, </3/0/4>,
* <3/0/6>;dim=8,<3/0/7>;gt=50;lt=42.2;st=0.5,<3/0/8>;...
*/
@Test
public void testWriteAttributesPeriodLtGt () throws Exception {
String expectedPath = objectInstanceIdVer_3;
String expectedValue = "{\"pmax\":60}";
String actualResult = sendRPCExecuteWithValueById(expectedPath, expectedValue);
ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class);
assertEquals(ResponseCode.CHANGED.getName(), rpcActualResult.get("result").asText());
expectedPath = objectInstanceIdVer_3;
expectedValue = "{\"pmax\":65}";
actualResult = sendRPCExecuteWithValueById(expectedPath, expectedValue);
rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class);
assertEquals(ResponseCode.CHANGED.getName(), rpcActualResult.get("result").asText());
expectedPath = objectInstanceIdVer_3 + "/" + RESOURCE_ID_7;
expectedValue ="{\"gt\":50, \"lt\":42.2, \"st\":0.5}";
actualResult = sendRPCExecuteWithValueById(expectedPath, expectedValue);
rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class);
assertEquals(ResponseCode.CHANGED.getName(), rpcActualResult.get("result").asText());
// ObjectId
expectedPath = objectIdVer_3;
actualResult = sendDiscover(expectedPath);
rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class);
assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText());
// String expected = "</3>;ver=1.2,</3/0>;pmax=60,</3/0/0>,</3/0/1>,</3/0/2>,</3/0/3>,</3/0/6>;dim=3,</3/0/7>;st=0.5;lt=42.2;gt=50.0,</3/0/8>,</3/0/9>,</3/0/10>,</3/0/11>;dim=1,</3/0/13>,</3/0/14>,</3/0/15>,</3/0/16>,</3/0/17>,</3/0/18>,</3/0/19>,</3/0/20>,</3/0/21>";
String expected = "</3>;ver=1.2,</3/0>;pmax=65";
assertTrue(rpcActualResult.get("value").asText().contains(expected));
expected = "</3/0/6>;dim=3,</3/0/7>;st=0.5;lt=42.2;gt=50.0";
assertTrue(rpcActualResult.get("value").asText().contains(expected));
// ObjectInstanceId
expectedPath = objectInstanceIdVer_3;
actualResult = sendDiscover(expectedPath);
rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class);
assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText());
expected = "</3/0>;pmax=65";
assertTrue(rpcActualResult.get("value").asText().contains(expected));
expected = "</3/0/6>;dim=3,</3/0/7>;st=0.5;lt=42.2;gt=50.0";
assertTrue(rpcActualResult.get("value").asText().contains(expected));
// ResourceId
expectedPath = objectInstanceIdVer_3 + "/" + RESOURCE_ID_6;
actualResult = sendDiscover(expectedPath);
rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class);
assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText());
expected = "</3/0/6>;dim=3";
assertTrue(rpcActualResult.get("value").asText().contains(expected));
expectedPath = objectInstanceIdVer_3 + "/" + RESOURCE_ID_7;
actualResult = sendDiscover(expectedPath);
rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class);
assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText());
expected = "</3/0/7>;st=0.5;lt=42.2;gt=50.0";
assertTrue(rpcActualResult.get("value").asText().contains(expected));
// ResourceInstanceId
expectedPath = objectInstanceIdVer_3 + "/" + RESOURCE_ID_6+ "/1";
actualResult = sendDiscover(expectedPath);
rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class);
assertEquals(ResponseCode.INTERNAL_SERVER_ERROR.getName(), rpcActualResult.get("result").asText());
expected = "InvalidRequestException: Discover request cannot target resource instance path: /3/0/6/1";
assertTrue(rpcActualResult.get("error").asText().contains(expected));
}
private String sendRPCExecuteWithValueById(String path, String value) throws Exception {
String setRpcRequest = "{\"method\": \"WriteAttributes\", \"params\": {\"id\": \"" + path + "\", \"attributes\": " + value + " }}";
return doPostAsync("/api/plugins/rpc/twoway/" + deviceId, setRpcRequest, String.class, status().isOk());
}
private String sendRPCReadById(String path) throws Exception {
String setRpcRequest = "{\"method\": \"Read\", \"params\": {\"id\": \"" + path + "\"}}";
return doPostAsync("/api/plugins/rpc/twoway/" + deviceId, setRpcRequest, String.class, status().isOk());
}
private String sendDiscover(String path) throws Exception {
String setRpcRequest = "{\"method\": \"Discover\", \"params\": {\"id\": \"" + path + "\"}}";
return doPostAsync("/api/plugins/rpc/twoway/" + deviceId, setRpcRequest, String.class, status().isOk());
}
}

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

@ -17,88 +17,141 @@ package org.thingsboard.server.transport.lwm2m.rpc.sql;
import com.fasterxml.jackson.databind.node.ObjectNode;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.leshan.core.LwM2m.Version;
import org.eclipse.leshan.core.ResponseCode;
import org.eclipse.leshan.core.node.LwM2mPath;
import org.eclipse.leshan.core.response.ReadResponse;
import org.eclipse.leshan.server.registration.Registration;
import org.junit.Test;
import org.mockito.Mockito;
import org.springframework.boot.test.mock.mockito.SpyBean;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.transport.lwm2m.rpc.AbstractRpcLwM2MIntegrationTest;
import org.thingsboard.server.transport.lwm2m.rpc.AbstractRpcLwM2MIntegrationObserveTest;
import org.thingsboard.server.transport.lwm2m.server.uplink.DefaultLwM2mUplinkMsgHandler;
import java.util.Optional;
import static org.eclipse.leshan.core.LwM2mId.ACCESS_CONTROL;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.timeout;
import static org.mockito.Mockito.verify;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.OBJECT_INSTANCE_ID_0;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_0;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_14;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_2;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_3;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_9;
import static org.thingsboard.server.transport.lwm2m.utils.LwM2MTransportUtil.fromVersionedIdToObjectId;
@Slf4j
public class RpcLwm2mIntegrationObserveTest extends AbstractRpcLwM2MIntegrationTest {
public class RpcLwm2mIntegrationObserveTest extends AbstractRpcLwM2MIntegrationObserveTest {
@SpyBean
DefaultLwM2mUplinkMsgHandler defaultUplinkMsgHandlerTest;
@Test
public void testObserveReadAll_Count_2_CancelAll_Count_0_Ok() throws Exception {
String actualResultReadAll = sendRpcObserve("ObserveReadAll", null);
ObjectNode rpcActualResult = JacksonUtil.fromString(actualResultReadAll, ObjectNode.class);
assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText());
String actualValuesReadAll = rpcActualResult.get("value").asText();
assertEquals(2, actualValuesReadAll.split(",").length);
String expected = "\"SingleObservation:/19/0/0\"";
assertTrue(actualValuesReadAll.contains(expected));
expected = "\"SingleObservation:/3/0/9\"";
assertTrue(actualValuesReadAll.contains(expected));
}
/**
* ObserveReadAll&ObserveReadAll
* Observe "3_1.2/0/9"
* @throws Exception
*/
@Test
public void testObserveReadAllNothingObservation_Result_CONTENT_Value_Count_0() throws Exception {
String idVer_3_0_0 = objectInstanceIdVer_3 + "/" + RESOURCE_ID_0;
sendRpcObserve("Observe", fromVersionedIdToObjectId(idVer_3_0_0));
String actualResultBefore = sendRpcObserve("ObserveReadAll", null);
ObjectNode rpcActualResultBefore = JacksonUtil.fromString(actualResultBefore, ObjectNode.class);
assertEquals(ResponseCode.CONTENT.getName(), rpcActualResultBefore.get("result").asText());
int cntObserveBefore = rpcActualResultBefore.get("value").asText().split(",").length;
assertTrue(cntObserveBefore > 0);
String actualResult = sendRpcObserve("ObserveCancelAll", null);
public void testObserveOneResource_Result_CONTENT_Value_Count_3_After_Cancel_Count_2() throws Exception {
sendCancelObserveAllWithAwait(deviceId);
String idVer_3_0_9 = objectInstanceIdVer_3 + "/" + RESOURCE_ID_9;
String actualResult = sendRpcObserve("Observe", idVer_3_0_9);
ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class);
assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText());
int cntObserveCancelAll = Integer.parseInt(rpcActualResult.get("value").asText());
assertTrue(cntObserveCancelAll > 0);
String actualResultAfter = sendRpcObserve("ObserveReadAll", null);
ObjectNode rpcActualResultAfter = JacksonUtil.fromString(actualResultAfter, ObjectNode.class);
assertEquals(ResponseCode.CONTENT.getName(), rpcActualResultAfter.get("result").asText());
String expectResultAfter = "[]";
assertEquals( expectResultAfter, rpcActualResultAfter.get("value").asText());
assertTrue(rpcActualResult.get("value").asText().contains("LwM2mSingleResource"));
assertEquals(Optional.of(1).get(), Optional.ofNullable(getCntObserveAll(deviceId)).get());
int cntUpdate = 3;
verify(defaultUplinkMsgHandlerTest, timeout(10000).times(cntUpdate))
.onUpdateValueAfterReadResponse(Mockito.any(Registration.class), eq(idVer_3_0_9), Mockito.any(ReadResponse.class));
}
/**
* Observe {"id":"/3/0/0"}
* Observe "3_1.2/0"
* @throws Exception
*/
@Test
public void testObserveSingleResourceWithout_IdVer_1_0_Result_CONTENT_Value_SingleResource() throws Exception {
String expectedId = objectInstanceIdVer_3 + "/" + RESOURCE_ID_0;
String actualResult = sendRpcObserve("Observe", fromVersionedIdToObjectId(expectedId));
public void testObserveOneObjectInstance_Result_CONTENT_Value_Count_3_After_Cancel_Count_2() throws Exception {
sendCancelObserveAllWithAwait(deviceId);
String idVer_3_0 = objectInstanceIdVer_3;
String actualResult = sendRpcObserve("Observe", idVer_3_0);
ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class);
assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText());
assertTrue(rpcActualResult.get("value").asText().contains("LwM2mSingleResource"));
assertEquals(Optional.of(1).get(), Optional.ofNullable(getCntObserveAll(deviceId)).get());
int cntUpdate = 3;
verify(defaultUplinkMsgHandlerTest, timeout(10000).times(cntUpdate))
.updateAttrTelemetry(Mockito.any(Registration.class), eq(idVer_3_0_9));
}
/**
* Observe {"id":"/3_1.0/0/14"}
* Observe "3_1.2"
* @throws Exception
*/
@Test
public void testObserveSingleResourceWith_IdVer_1_0_Result_CONTENT_Value_SingleResource() throws Exception {
String expectedId = objectInstanceIdVer_3 + "/" + RESOURCE_ID_14;
String actualResult = sendRpcObserve("Observe", expectedId);
public void testObserveOneObject_Result_CONTENT_Value_Count_3_After_Cancel_Count_2() throws Exception {
sendCancelObserveAllWithAwait(deviceId);
String idVer_3_0 = objectInstanceIdVer_3;
String actualResult = sendRpcObserve("Observe", idVer_3_0);
ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class);
assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText());
assertTrue(rpcActualResult.get("value").asText().contains("LwM2mSingleResource"));
assertEquals(Optional.of(1).get(), Optional.ofNullable(getCntObserveAll(deviceId)).get());
int cntUpdate = 3;
verify(defaultUplinkMsgHandlerTest, timeout(10000).times(cntUpdate))
.updateAttrTelemetry(Mockito.any(Registration.class), eq(idVer_3_0_9));
}
/**
* Repeated request on Observe
* Observe {"id":"/3_1.2/0/0"}
* @throws Exception
*/
@Test
public void testObserveRepeated_Result_CONTENT_AddIfAbsent() throws Exception {
String idVer_3_0_0 = objectInstanceIdVer_3 + "/" + RESOURCE_ID_0;
String actualResult = sendRpcObserve("Observe", idVer_3_0_0);
ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class);
assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText());
actualResult = sendRpcObserve("Observe", idVer_3_0_0);
rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class);
assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText());
String expected = "LwM2mSingleResource [id=0";
assertTrue(rpcActualResult.get("value").asText().contains(expected));
}
/**
* Observe {"id":"/3_1.1/0/13"}
* Observe {"id":"/3_1.?/0/13"}
* @throws Exception
*/
@Test
public void testObserveWithBadVersion_Result_BadRequest_ErrorMsg_BadVersionMustBe1_0() throws Exception {
public void testObserveWithBadVersion_Result_BadRequest_ErrorMsg_BadVersionMustBe_Ver() throws Exception {
String expectedInstance = (String) expectedInstances.stream().filter(path -> !((String)path).contains("_")).findFirst().get();
LwM2mPath expectedPath = new LwM2mPath(expectedInstance);
int expectedResource = lwM2MTestClient.getLeshanClient().getObjectTree().getObjectEnablers().get(expectedPath.getObjectId()).getObjectModel().resources.entrySet().stream().findAny().get().getKey();
String expectedId = "/" + expectedPath.getObjectId() + "_1.2" + "/" + expectedPath.getObjectInstanceId() + "/" + expectedResource;
String ver = lwM2MTestClient.getLeshanClient().getObjectTree().getObjectEnablers().get(expectedPath.getObjectId()).getObjectModel().version;
String expectedId = "/" + expectedPath.getObjectId() + "_" + Version.MAX + "/" + expectedPath.getObjectInstanceId() + "/" + expectedResource;
String actualResult = sendRpcObserve("Observe", expectedId);
ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class);
assertEquals(ResponseCode.BAD_REQUEST.getName(), rpcActualResult.get("result").asText());
String expected = "Specified resource id " + expectedId +" is not valid version! Must be version: 1.0";
String expected = "Specified resource id " + expectedId +" is not valid version! Must be version: " + ver;
assertEquals(expected, rpcActualResult.get("error").asText());
}
@ -132,12 +185,11 @@ public class RpcLwm2mIntegrationObserveTest extends AbstractRpcLwM2MIntegrationT
}
/**
* Repeated request on Observe
* Observe {"id":"/5/0/0"}
* Observe resource Write -> "/5/0/02"
* @throws Exception
*/
@Test
public void testObserveRSourceNotRead_Result_METHOD_NOT_ALLOWED() throws Exception {
public void testObserveResourceNotRead_Result_METHOD_NOT_ALLOWED() throws Exception {
String expectedId = objectInstanceIdVer_5 + "/" + RESOURCE_ID_0;
sendRpcObserve("Observe", expectedId);
String actualResult = sendRpcObserve("Observe", expectedId);
@ -145,51 +197,176 @@ public class RpcLwm2mIntegrationObserveTest extends AbstractRpcLwM2MIntegrationT
assertEquals(ResponseCode.METHOD_NOT_ALLOWED.getName(), rpcActualResult.get("result").asText());
}
/**
* Observe resource Execute -> "/5/0/2"
* @throws Exception
*/
@Test
public void testObserveExecuteResource_Result_METHOD_NOT_ALLOWED() throws Exception {
String expectedId = objectInstanceIdVer_5 + "/" + RESOURCE_ID_2;
sendRpcObserve("Observe", expectedId);
String actual = sendRpcObserve("Observe", expectedId);
ObjectNode rpcActual = JacksonUtil.fromString(actual, ObjectNode.class);
assertEquals(ResponseCode.METHOD_NOT_ALLOWED.getName(), rpcActual.get("result").asText());
}
/**
* Repeated request on Observe
* Observe {"id":"/3/0/9"}
* Observe {"id":"/3_1.2/0/0"}
* @throws Exception
*/
@Test
public void testObserveRepeatedRequestObserveOnDevice_Result_BAD_REQUEST_ErrorMsg_AlreadyRegistered() throws Exception {
public void testObserveRepeatedRequestObserveOnDevice_Result_CONTENT_PutIfAbsent() throws Exception {
String idVer_3_0_0 = objectInstanceIdVer_3 + "/" + RESOURCE_ID_0;
sendRpcObserve("Observe", fromVersionedIdToObjectId(idVer_3_0_0));
sendRpcObserve("ObserveReadAll", null);
String actualResult = sendRpcObserve("Observe", idVer_3_0_0);
ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class);
assertEquals(ResponseCode.BAD_REQUEST.getName(), rpcActualResult.get("result").asText());
String expected = "Observation is already registered!";
assertEquals(expected, rpcActualResult.get("error").asText());
assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText());
actualResult = sendRpcObserve("Observe", idVer_3_0_0);
rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class);
assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText());
String expected = "LwM2mSingleResource [id=0";
assertTrue(rpcActualResult.get("value").asText().contains(expected));
}
/**
* ObserveReadAll
* Observe {"id":["3"]} - Ok
* PreviousObservation contains "3/0/9"
* @throws Exception
*/
@Test
public void testObserveReadAll_Result_CONTENT_Value_Contains_Paths_Count_ObserveReadAll() throws Exception {
public void testObserve_Result_CONTENT_ONE_PATH_PreviousObservation_CONTAINCE_OTHER_CurrentObservation() throws Exception {
sendCancelObserveAllWithAwait(deviceId);
// "3/0/9"
String idVer_3_0_9 = objectInstanceIdVer_3 + "/" + RESOURCE_ID_9;
String actualResult3_0_9 = sendRpcObserve("Observe", idVer_3_0_9);
ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult3_0_9, ObjectNode.class);
assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText());
// "3"
String actualResult3 = sendRpcObserve("Observe", objectIdVer_3);
rpcActualResult = JacksonUtil.fromString(actualResult3, ObjectNode.class);
assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText());
// PreviousObservation "3/0/9" change to CurrentObservation "3"
String actualResultReadAll = sendRpcObserve("ObserveReadAll", null);
ObjectNode rpcActualResultReadAll = JacksonUtil.fromString(actualResultReadAll, ObjectNode.class);
assertEquals(ResponseCode.CONTENT.getName(), rpcActualResultReadAll.get("result").asText());
String actualValuesReadAll = rpcActualResultReadAll.get("value").asText();
log.warn("ObserveReadAll: [{}]", actualValuesReadAll);
assertEquals(2, actualValuesReadAll.split(",").length);
rpcActualResult = JacksonUtil.fromString(actualResultReadAll, ObjectNode.class);
assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText());
String actualValuesReadAll = rpcActualResult.get("value").asText();
assertEquals(1, actualValuesReadAll.split(",").length);
String expected = "\"SingleObservation:/3\"";
assertTrue(actualValuesReadAll.contains(expected));
}
/**
* Observe {"id":["3/0/9"]} - Ok
* PreviousObservation contains "3"
* @throws Exception
*/
@Test
public void testObserve_Result_CONTENT_ONE_PATH_CurrentObservation_CONTAINCE_OTHER_PreviousObservation() throws Exception {
sendCancelObserveAllWithAwait(deviceId);
// "3"
String actualResult3 = sendRpcObserve("Observe", objectIdVer_3);
ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult3, ObjectNode.class);
assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText());
// "3/0/0"; WARN: - Token collision ? existing observation [/3] includes input observation [/3/0/0]
String idVer_3_0_0 = objectInstanceIdVer_3 + "/" + RESOURCE_ID_0;
String actualResult3_0_0 = sendRpcObserve("Observe", idVer_3_0_0);
rpcActualResult = JacksonUtil.fromString(actualResult3_0_0, ObjectNode.class);
assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText());
String actualResultReadAll = sendRpcObserve("ObserveReadAll", null);
rpcActualResult = JacksonUtil.fromString(actualResultReadAll, ObjectNode.class);
assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText());
String actualValuesReadAll = rpcActualResult.get("value").asText();
assertEquals(1, actualValuesReadAll.split(",").length);
String expected = "\"SingleObservation:/3\"";
assertTrue(actualValuesReadAll.contains(expected));
}
/**
* ObserveCancel {"id":"/3/0/3"}
* ObserveCancel {"id":"/5/0/3"}
* Observe {"id":"/3/0/9"}
* ObserveCancel {"id":"/3/0/9"}
*/
@Test
public void testObserveCancelOneResource_Result_CONTENT_Value_Count_1() throws Exception {
sendRpcObserve("ObserveCancelAll", null);
String expectedId_3_0_3 = objectInstanceIdVer_3 + "/" + RESOURCE_ID_3;
String expectedId_5_0_3 = objectInstanceIdVer_5 + "/" + RESOURCE_ID_3;
sendRpcObserve("Observe", expectedId_3_0_3);
sendRpcObserve("Observe", expectedId_5_0_3);
String actualResult = sendRpcObserve("ObserveCancel", expectedId_3_0_3);
ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class);
public void testObserveResource_ObserveCancelResource_Result_CONTENT_Count_1() throws Exception {
sendCancelObserveAllWithAwait(deviceId);
String expectedId_3_0_9 = objectInstanceIdVer_3 + "/" + RESOURCE_ID_9;
sendRpcObserve("Observe", expectedId_3_0_9);
String actualResultReadAll = sendRpcObserve("ObserveReadAll", null);
ObjectNode rpcActualResult = JacksonUtil.fromString(actualResultReadAll, ObjectNode.class);
assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText());
String actualValuesReadAll = rpcActualResult.get("value").asText();
assertEquals(1, actualValuesReadAll.split(",").length);
String expected = "\"SingleObservation:" + fromVersionedIdToObjectId(expectedId_3_0_9) + "\"";
assertTrue(actualValuesReadAll.contains(expected));
// cancel observe "/3_1.2/0/9"
String actualResult = sendRpcObserve("ObserveCancel", expectedId_3_0_9);
rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class);
assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText());
assertEquals("1", rpcActualResult.get("value").asText());
}
/**
* Observe {"id":"/3"}
* ObserveCancel {"id":"/3/0/9"} -> INTERNAL_SERVER_ERROR
* ObserveCancel {"id":"/3"} -> CONTENT
*/
@Test
public void testObserveObject_ObserveCancelOneResource_Result_INTERNAL_SERVER_ERROR_Than_Cancel_ObserveObject_Result_CONTENT_Count_1() throws Exception {
sendCancelObserveAllWithAwait(deviceId);
String expectedId_3 = objectIdVer_3;
sendRpcObserve("Observe", expectedId_3);
String actualResultReadAll = sendRpcObserve("ObserveReadAll", null);
ObjectNode rpcActualResult = JacksonUtil.fromString(actualResultReadAll, ObjectNode.class);
assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText());
String actualValuesReadAll = rpcActualResult.get("value").asText();
assertEquals(1, actualValuesReadAll.split(",").length);
String expected = "\"SingleObservation:" + fromVersionedIdToObjectId(expectedId_3) + "\"";
assertTrue(actualValuesReadAll.contains(expected));
// cancel observe "/3_1.2/0/9"
String expectedId_3_0_9 = objectInstanceIdVer_3 + "/" + RESOURCE_ID_9;
String actualResult = sendRpcObserve("ObserveCancel", expectedId_3_0_9);
String expectedValue = "existing observation [" + fromVersionedIdToObjectId(objectIdVer_3) + "] includes input observation [" + fromVersionedIdToObjectId(expectedId_3_0_9);
rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class);
assertEquals(ResponseCode.INTERNAL_SERVER_ERROR.getName(), rpcActualResult.get("result").asText());
assertTrue(rpcActualResult.get("error").asText().contains(expectedValue));
// cancel observe "/3_1.2"
actualResult = sendRpcObserve("ObserveCancel", expectedId_3);
rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class);
assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText());
assertEquals("1", rpcActualResult.get("value").asText());
}
/**
* Observe {"id":"/3/0/9"}
* ObserveCancel {"id":"/3"}
*/
@Test
public void testObserveResource_ObserveCancelObject_Result_CONTENT_Count_1() throws Exception {
sendCancelObserveAllWithAwait(deviceId);
String expectedId_3_0_9 = objectInstanceIdVer_3 + "/" + RESOURCE_ID_9;
sendRpcObserve("Observe", expectedId_3_0_9);
String actualResultReadAll = sendRpcObserve("ObserveReadAll", null);
ObjectNode rpcActualResult = JacksonUtil.fromString(actualResultReadAll, ObjectNode.class);
assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText());
String actualValuesReadAll = rpcActualResult.get("value").asText();
assertEquals(1, actualValuesReadAll.split(",").length);
String expected = "\"SingleObservation:" + fromVersionedIdToObjectId(expectedId_3_0_9) + "\"";
assertTrue(actualValuesReadAll.contains(expected));
// cancel observe "/3_1.2"
String expectedId_3 = objectIdVer_3;
String actualResult = sendRpcObserve("ObserveCancel", expectedId_3);
rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class);
assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText());
assertEquals("1", rpcActualResult.get("value").asText());
}

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

@ -50,22 +50,28 @@ public class RpcLwm2mIntegrationReadTest extends AbstractRpcLwM2MIntegrationTest
*/
@Test
public void testReadAllObjectsInClientById_Result_CONTENT_Value_IsLwM2mObject_IsInstances() throws Exception {
expectedObjectIdVers.forEach(expected -> {
try {
String actualResult = sendRPCById((String) expected);
String expectedObjectId = pathIdVerToObjectId((String) expected);
LwM2mPath expectedPath = new LwM2mPath(expectedObjectId);
ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class);
assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText());
String expectedObjectInstances = "LwM2mObject [id=" + expectedPath.getObjectId() + ", instances={0=LwM2mObjectInstance [id=0, resources=";
if (expectedPath.getObjectId() == 2) {
expectedObjectInstances = "LwM2mObject [id=2, instances={}]";
try {
expectedObjectIdVers.forEach(expected -> {
try {
String actualResult = sendRPCById((String) expected);
String expectedObjectId = pathIdVerToObjectId((String) expected);
LwM2mPath expectedPath = new LwM2mPath(expectedObjectId);
ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class);
assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText());
String expectedObjectInstances = "LwM2mObject [id=" + expectedPath.getObjectId() + ", instances={0=LwM2mObjectInstance [id=0, resources=";
if (expectedPath.getObjectId() == 1) {
expectedObjectInstances = "LwM2mObject [id=1, instances={1=";
} else if (expectedPath.getObjectId() == 2) {
expectedObjectInstances = "LwM2mObject [id=2, instances={}]";
}
assertTrue(rpcActualResult.get("value").asText().contains(expectedObjectInstances));
} catch (Exception e) {
e.printStackTrace();
}
assertTrue(rpcActualResult.get("value").asText().contains(expectedObjectInstances));
} catch (Exception e) {
e.printStackTrace();
}
});
});
} catch (Exception e2){
e2.printStackTrace();
}
}
/**

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

@ -1,56 +0,0 @@
/**
* Copyright © 2016-2024 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.transport.lwm2m.rpc.sql;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.eclipse.leshan.core.ResponseCode;
import org.junit.Test;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.transport.lwm2m.rpc.AbstractRpcLwM2MIntegrationTest;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_14;
public class RpcLwm2mIntegrationWriteAttributesTest extends AbstractRpcLwM2MIntegrationTest {
/**
* WriteAttributes {"id":"/3/0/14","attributes":{"pmax":100, "pmin":10}}
* if not implemented:
* {"result":"INTERNAL_SERVER_ERROR","error":"not implemented"}
* if implemented:
* {"result":"CHANGED"}
*/
@Test
public void testWriteAttributesResourceWithParametersById_Result_INTERNAL_SERVER_ERROR() throws Exception {
String expectedPath = objectInstanceIdVer_3 + "/" + RESOURCE_ID_14;
String expectedValue = "{\"pmax\":100, \"pmin\":10}";
String actualResult = sendRPCExecuteWithValueById(expectedPath, expectedValue);
ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class);
assertEquals(ResponseCode.INTERNAL_SERVER_ERROR.getName(), rpcActualResult.get("result").asText());
String expected = "not implemented";
String actual = rpcActualResult.get("error").asText();
assertTrue(actual.equals(expected));
}
private String sendRPCExecuteWithValueById(String path, String value) throws Exception {
String setRpcRequest = "{\"method\": \"WriteAttributes\", \"params\": {\"id\": \"" + path + "\", \"attributes\": " + value + " }}";
return doPostAsync("/api/plugins/rpc/twoway/" + deviceId, setRpcRequest, String.class, status().isOk());
}
}

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

@ -161,8 +161,8 @@ public class RpcLwm2mIntegrationWriteTest extends AbstractRpcLwM2MIntegrationTes
int resourceInstanceId25 = 25;
String expectedValue0 = "00ad45675600";
String expectedValue25 = "25ad45675600cdef";
String expectedValue = "{\"" + RESOURCE_ID_0 + "\":{\"" + resourceInstanceId0 + "\":\"" + expectedValue0 + "\", \"" + resourceInstanceId25 + "\":\"" + expectedValue25 + "\"}}";
String actualResult = sendRPCWriteObjectById("WriteUpdate", expectedPath, expectedValue);
String expectedValueResourcesObject19 = "{\"" + RESOURCE_ID_0 + "\":{\"" + resourceInstanceId0 + "\":\"" + expectedValue0 + "\", \"" + resourceInstanceId25 + "\":\"" + expectedValue25 + "\"}}";
String actualResult = sendRPCWriteObjectById("WriteUpdate", expectedPath, expectedValueResourcesObject19);
ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class);
assertEquals(ResponseCode.CHANGED.getName(), rpcActualResult.get("result").asText());
String expectedPath0 = expectedPath + "/" + RESOURCE_ID_0 + "/" + resourceInstanceId0;
@ -181,29 +181,37 @@ public class RpcLwm2mIntegrationWriteTest extends AbstractRpcLwM2MIntegrationTes
/**
* ResourceInstance + KeySingleResource + IdSingleResource
* WriteComposite {"nodes":{"/19/1/0/2":"00001234", "UtfOffset":"+04", "/3/0/15":"Kiyv/Europe"}}
* WriteComposite {"/19_1.1/0":{"0":{"0":"00ad45675600", "25":"25ad45675600cdef"}}, "UtfOffset":"+04", "/3_1.0/0/15":"Kiyv/Europe"}
* {"result":"CHANGED"}
*/
@Test
public void testWriteCompositeValueSingleResourceResourceInstanceByIdKey_Result_CHANGED() throws Exception {
int resourceInstanceId2 = 2;
String expectedPath19_1_0_2 = objectIdVer_19 + "/" + OBJECT_INSTANCE_ID_1 + "/" + RESOURCE_ID_0 + "/" + resourceInstanceId2;
String expectedValue19_1_0_2 = "00001234";
public void testWriteCompositeValueSingleResourceWithMultiResourceInstanceByIdKey_Result_CHANGED() throws Exception {
String expectedPath_19_0 = objectIdVer_19 + "/" + OBJECT_INSTANCE_ID_0;
int resourceInstanceId0 = 0;
int resourceInstanceId25 = 25;
String expectedValue0 = "00ad45675600";
String expectedValue25 = "25ad45675600cdef";
String expectedValue_19_Resources = "{\"" + RESOURCE_ID_0 + "\":{\"" + resourceInstanceId0 + "\":\"" + expectedValue0 + "\", \"" + resourceInstanceId25 + "\":\"" + expectedValue25 + "\"}}";
String expectedKey3_0_14 = RESOURCE_ID_NAME_3_14;
String expectedValue3_0_14 = "+04";
String expectedPath3_0_15 = objectInstanceIdVer_3 + "/" + RESOURCE_ID_15;
String expectedValue3_0_15 = "Kiyv/Europe";
String nodes = "{\"" + expectedPath19_1_0_2 + "\":\"" + expectedValue19_1_0_2 + "\", \"" + expectedKey3_0_14 +
String nodes = "{\"" + expectedPath_19_0 + "\":" + expectedValue_19_Resources + ", \"" + expectedKey3_0_14 +
"\":\"" + expectedValue3_0_14 + "\", \"" + expectedPath3_0_15 + "\":\"" + expectedValue3_0_15 + "\"}";
String actualResult = sendCompositeRPC(nodes);
ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class);
assertEquals(ResponseCode.CHANGED.getName(), rpcActualResult.get("result").asText());
actualResult = sendRPCReadById(expectedPath19_1_0_2);
actualResult = sendRPCReadById(expectedPath_19_0 + "/" + RESOURCE_ID_0 + "/" + resourceInstanceId0);
rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class);
String actualValues = rpcActualResult.get("value").asText();
String expected = "LwM2mResourceInstance [id=" + resourceInstanceId2 + ", value=" + expectedValue19_1_0_2.length()/2 + "Bytes, type=OPAQUE]";
String expected = "LwM2mResourceInstance [id=" + resourceInstanceId0 + ", value=" + expectedValue0.length()/2 + "Bytes, type=OPAQUE]";
assertTrue(actualValues.contains(expected));
actualResult = sendRPCReadByKey(expectedKey3_0_14);
actualResult = sendRPCReadById(expectedPath_19_0 + "/" + RESOURCE_ID_0 + "/" + resourceInstanceId25);
rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class);
actualValues = rpcActualResult.get("value").asText();
expected = "LwM2mResourceInstance [id=" + resourceInstanceId25 + ", value=" + expectedValue25.length()/2 + "Bytes, type=OPAQUE]";
assertTrue(actualValues.contains(expected)); actualResult = sendRPCReadByKey(expectedKey3_0_14);
rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class);
actualValues = rpcActualResult.get("value").asText();
expected = "LwM2mSingleResource [id=" + RESOURCE_ID_14 + ", value=" + expectedValue3_0_14 + ", type=STRING]";
@ -217,29 +225,36 @@ public class RpcLwm2mIntegrationWriteTest extends AbstractRpcLwM2MIntegrationTes
/**
* multipleResource == error
* bad - cannot be used for value Json, only primitive: SingleResource, ResourceInstance (for Json: WriteUpdate, WriteReplace)
* WriteComposite {"nodes":{"/19/0/0":{"0":"abcd5678", "10":"abcd5678"}}}
* WriteComposite {"/19_1.1/0/0":{"0":"00ad45675600", "25":"25ad45675600cdef"}}
* {"result":"CHANGED"}
*/
@Test
public void testWriteCompositeValueSingleMultipleResourceByIdKey_Result_BAD_REQUEST_WriteComposite_operation_for_SingleResources_or_and_ResourceInstance() throws Exception {
String nodes = "{\"/19/0/0\":{\"0\":\"abcd5678\", \"10\":\"abcd5678\"}}";
public void testWriteCompositeValueSingleMultipleResourceOpaqueValueInputHexStringByIdKey_Result_CHANGED() throws Exception {
String expectedPath_19_0 = objectIdVer_19 + "/" + OBJECT_INSTANCE_ID_0;
int resourceInstanceId0 = 0;
int resourceInstanceId25 = 25;
String expectedValue0 = "00ad45675600";
String expectedValue25 = "25ad45675600cdef";
String nodes = "{\"" + expectedPath_19_0 + "/" + RESOURCE_ID_0 + "\":{\"" + resourceInstanceId0 + "\":\"" + expectedValue0 + "\", \"" + resourceInstanceId25 + "\":\"" + expectedValue25 + "\"}}";
String actualResult = sendCompositeRPC(nodes);
ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class);
assertEquals(ResponseCode.BAD_REQUEST.getName(), rpcActualResult.get("result").asText());
String actualValues = rpcActualResult.get("error").asText();
String expectedNodes = nodes.replaceAll("\"", "").replaceAll(":", "=");
String expected = String.format("nodes: %s is not validate value. " +
"The WriteComposite operation is only used for SingleResources or/and ResourceInstance.", expectedNodes);
assertEquals(expected, actualValues);
assertEquals(ResponseCode.CHANGED.getName(), rpcActualResult.get("result").asText());
actualResult = sendRPCReadById(expectedPath_19_0 + "/" + RESOURCE_ID_0 + "/" + resourceInstanceId0);
rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class);
String actualValues = rpcActualResult.get("value").asText();
String expected = "LwM2mResourceInstance [id=" + resourceInstanceId0 + ", value=" + expectedValue0.length()/2 + "Bytes, type=OPAQUE]";
assertTrue(actualValues.contains(expected));
actualResult = sendRPCReadById(expectedPath_19_0 + "/" + RESOURCE_ID_0 + "/" + resourceInstanceId25);
rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class);
actualValues = rpcActualResult.get("value").asText();
expected = "LwM2mResourceInstance [id=" + resourceInstanceId25 + ", value=" + expectedValue25.length()/2 + "Bytes, type=OPAQUE]";
assertTrue(actualValues.contains(expected));
}
/**
* update_resourceInstances&update_singleResource
* new ResourceInstance if Resource is Multiple & Resource Single
* - WriteReplace {"id":"/19_1.2/1/0","value":{"2":ddff12"}}
* - WriteReplace {"key":"UtfOffset","value":"+04"}
* - WriteReplace {"id":"/3/0/15","value":"Kiyv/Europe"}
* WriteComposite {"nodes":{"/19_1.1/1/0/2":"00001234", "UtfOffset":"+04", "/3/0/15":"Kiyv/Europe"}}}
* {"result":"CHANGED"}
*/
@ -331,4 +346,4 @@ public class RpcLwm2mIntegrationWriteTest extends AbstractRpcLwM2MIntegrationTes
String setRpcRequest = "{\"method\": \"WriteComposite\", \"params\": {\"nodes\":" + nodes + "}}";
return doPostAsync("/api/plugins/rpc/twoway/" + deviceId, setRpcRequest, String.class, status().isOk());
}
}
}

36
application/src/test/java/org/thingsboard/server/transport/lwm2m/security/AbstractSecurityLwM2MIntegrationTest.java

@ -63,7 +63,7 @@ import java.util.Set;
import java.util.concurrent.TimeUnit;
import static org.awaitility.Awaitility.await;
import static org.eclipse.leshan.client.object.Security.noSecBootstap;
import static org.eclipse.leshan.client.object.Security.noSecBootstrap;
import static org.junit.Assert.assertEquals;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.LwM2MClientState.ON_DEREGISTRATION_STARTED;
@ -89,6 +89,7 @@ public abstract class AbstractSecurityLwM2MIntegrationTest extends AbstractLwM2M
protected static final String SERVER_STORE_PWD = "server_ks_password";
protected static final String SERVER_CERT_ALIAS = "server";
protected static final String SERVER_CERT_ALIAS_BS = "bootstrap";
protected static final Security SECURITY_NO_SEC_BS = noSecBootstrap(URI_BS);;
protected final X509Certificate serverX509Cert; // server certificate signed by rootCA
protected final X509Certificate serverX509CertBs; // serverBs certificate signed by rootCA
protected final PublicKey serverPublicKeyFromCert; // server public key used for RPK
@ -171,7 +172,7 @@ public abstract class AbstractSecurityLwM2MIntegrationTest extends AbstractLwM2M
LwM2MClientState finishState) throws Exception {
Lwm2mDeviceProfileTransportConfiguration transportConfiguration = getTransportConfiguration(OBSERVE_ATTRIBUTES_WITHOUT_PARAMS, getBootstrapServerCredentialsNoSec(type));
LwM2MDeviceCredentials deviceCredentials = getDeviceCredentialsNoSec(createNoSecClientCredentials(clientEndpoint));
this.basicTestConnection(noSecBootstap(URI_BS),
this.basicTestConnection(null , SECURITY_NO_SEC_BS,
deviceCredentials,
COAP_CONFIG_BS,
clientEndpoint,
@ -183,22 +184,23 @@ public abstract class AbstractSecurityLwM2MIntegrationTest extends AbstractLwM2M
false);
}
protected void basicTestConnection(Security security,
protected void basicTestConnection(Security security, Security securityBs,
LwM2MDeviceCredentials deviceCredentials,
Configuration coapConfig,
String endpoint,
Lwm2mDeviceProfileTransportConfiguration transportConfiguration,
String awaitAlias,
Set<LwM2MClientState> expectedStatuses,
boolean isBootstrap,
boolean isAwaitObserveReadAll,
LwM2MClientState finishState,
boolean isStartLw) throws Exception {
createDeviceProfile(transportConfiguration);
final Device device = createDevice(deviceCredentials, endpoint);
device.getId().getId().toString();
createNewClient(security, coapConfig, true, endpoint, isBootstrap, null);
createNewClient(security, securityBs, coapConfig, true, endpoint);
lwM2MTestClient.start(isStartLw);
awaitObserveReadAll(0, isBootstrap, device.getId().getId().toString());
if (isAwaitObserveReadAll) {
awaitObserveReadAll(0, device.getId().getId().toString());
}
await(awaitAlias)
.atMost(40, TimeUnit.SECONDS)
.until(() -> {
@ -220,34 +222,31 @@ public abstract class AbstractSecurityLwM2MIntegrationTest extends AbstractLwM2M
LwM2MDeviceCredentials deviceCredentials = getDeviceCredentialsNoSec(createNoSecClientCredentials(clientEndpoint));
this.basicTestConnectionBootstrapRequestTrigger(
SECURITY_NO_SEC,
SECURITY_NO_SEC_BS,
deviceCredentials,
COAP_CONFIG,
clientEndpoint,
transportConfiguration,
awaitAlias,
expectedStatusesRegistrationLwm2mSuccess,
expectedStatusesRegistrationBsSuccess,
false,
SECURITY_NO_SEC_BS);
expectedStatusesRegistrationBsSuccess);
}
private void basicTestConnectionBootstrapRequestTrigger(Security security,
private void basicTestConnectionBootstrapRequestTrigger(Security security, Security securityBs,
LwM2MDeviceCredentials deviceCredentials,
Configuration coapConfig,
String endpoint,
Lwm2mDeviceProfileTransportConfiguration transportConfiguration,
String awaitAlias,
Set<LwM2MClientState> expectedStatusesLwm2m,
Set<LwM2MClientState> expectedStatusesBs,
boolean isBootstrap,
Security securityBs) throws Exception {
Set<LwM2MClientState> expectedStatusesBs) throws Exception {
createDeviceProfile(transportConfiguration);
final Device device = createDevice(deviceCredentials, endpoint);
String deviceIdStr = device.getId().getId().toString();
createNewClient(security, coapConfig, true, endpoint, isBootstrap, securityBs);
createNewClient(security, securityBs, coapConfig, true, endpoint);
lwM2MTestClient.start(true);
awaitObserveReadAll(0, isBootstrap, deviceIdStr);
awaitObserveReadAll(0, deviceIdStr);
await(awaitAlias)
.atMost(40, TimeUnit.SECONDS)
.until(() -> {
@ -263,7 +262,7 @@ public abstract class AbstractSecurityLwM2MIntegrationTest extends AbstractLwM2M
Assert.assertTrue(lwM2MTestClient.getClientStates().containsAll(expectedStatusesLwm2m));
String executedPath = "/" + OBJECT_ID_1 + "_" + lwM2MTestClient.getLeshanClient().getObjectTree().getModel().getObjectModel(OBJECT_ID_1).version
+ "/0/" + RESOURCE_ID_9;
+ "/" +serverId + "/" + RESOURCE_ID_9;
lwM2MTestClient.setClientStates(new HashSet<>());
String actualResult = sendRPCSecurityExecuteById(executedPath, deviceIdStr, endpoint);
ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class);
@ -337,7 +336,7 @@ public abstract class AbstractSecurityLwM2MIntegrationTest extends AbstractLwM2M
default:
throw new IllegalStateException("Unexpected value: " + mode);
}
bootstrapServerCredential.setShortServerId(isBootstrap ? shortServerIdBs : shortServerId);
bootstrapServerCredential.setShortServerId(isBootstrap ? shortServerIdBs0 : shortServerId);
bootstrapServerCredential.setBootstrapServerIs(isBootstrap);
bootstrapServerCredential.setHost(isBootstrap ? hostBs : host);
bootstrapServerCredential.setPort(isBootstrap ? securityPortBs : securityPort);
@ -443,3 +442,4 @@ public abstract class AbstractSecurityLwM2MIntegrationTest extends AbstractLwM2M
return doPostAsync("/api/plugins/rpc/twoway/" + deviceId, setRpcRequest, String.class, status().isOk());
}
}

18
application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/NoSecLwM2MIntegrationTest.java

@ -19,7 +19,6 @@ import org.junit.Test;
import org.thingsboard.server.common.data.device.credentials.lwm2m.LwM2MDeviceCredentials;
import org.thingsboard.server.transport.lwm2m.security.AbstractSecurityLwM2MIntegrationTest;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.LwM2MClientState.ON_BOOTSTRAP_SUCCESS;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.LwM2MClientState.ON_REGISTRATION_SUCCESS;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.LwM2MProfileBootstrapConfigType.BOOTSTRAP_ONLY;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.LwM2MProfileBootstrapConfigType.BOTH;
@ -39,7 +38,7 @@ public class NoSecLwM2MIntegrationTest extends AbstractSecurityLwM2MIntegrationT
// Bootstrap + Lwm2m
@Test
public void testWithNoSecConnectBsSuccess_UpdateTwoSectionsBootstrapAndLm2m_ConnectLwm2mSuccess() throws Exception {
String clientEndpoint = CLIENT_ENDPOINT_NO_SEC_BS;
String clientEndpoint = CLIENT_ENDPOINT_NO_SEC_BS + BOTH.name();
String awaitAlias = "await on client state (NoSecBS two section)";
basicTestConnectionBefore(clientEndpoint, awaitAlias, BOTH, expectedStatusesRegistrationBsSuccess, ON_REGISTRATION_SUCCESS);
}
@ -51,20 +50,7 @@ public class NoSecLwM2MIntegrationTest extends AbstractSecurityLwM2MIntegrationT
basicTestConnectionBefore(clientEndpoint, awaitAlias, LWM2M_ONLY, expectedStatusesRegistrationBsSuccess, ON_REGISTRATION_SUCCESS);
}
@Test
public void testWithNoSecConnectBsSuccess_UpdateBootstrapSectionAndLm2m_ConnectLwm2mSuccess() throws Exception {
String clientEndpoint = CLIENT_ENDPOINT_NO_SEC + BOOTSTRAP_ONLY.name();
String awaitAlias = "await on client state (NoSecBS Bootstrap section)";
basicTestConnectionBefore(clientEndpoint, awaitAlias, BOOTSTRAP_ONLY, expectedStatusesBsSuccess, ON_BOOTSTRAP_SUCCESS);
}
@Test
public void testWithNoSecConnectBsSuccess_UpdateNoneSectionAndLm2m_ConnectLwm2mSuccess() throws Exception {
String clientEndpoint = CLIENT_ENDPOINT_NO_SEC + NONE.name();
String awaitAlias = "await on client state (NoSecBS None section)";
basicTestConnectionBefore(clientEndpoint, awaitAlias, NONE, expectedStatusesBsSuccess, ON_BOOTSTRAP_SUCCESS);
}
// Bs trigger
@Test
public void testWithNoSecConnectLwm2mSuccessBootstrapRequestTriggerConnectBsSuccess_UpdateTwoSectionAndLm2m_ConnectLwm2mSuccess() throws Exception {
String clientEndpoint = CLIENT_ENDPOINT_NO_SEC_BS + "Trigger" + BOTH.name();

9
application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/PskLwm2mIntegrationTest.java

@ -55,13 +55,14 @@ public class PskLwm2mIntegrationTest extends AbstractSecurityLwM2MIntegrationTes
Lwm2mDeviceProfileTransportConfiguration transportConfiguration = getTransportConfiguration(OBSERVE_ATTRIBUTES_WITHOUT_PARAMS, getBootstrapServerCredentialsSecure(PSK, NONE));
LwM2MDeviceCredentials deviceCredentials = getDeviceCredentialsSecure(clientCredentials, null, null, PSK, false);
this.basicTestConnection(security,
null,
deviceCredentials,
COAP_CONFIG,
clientEndpoint,
transportConfiguration,
"await on client state (Psk_Lwm2m)",
expectedStatusesRegistrationLwm2mSuccess,
false,
true,
ON_REGISTRATION_SUCCESS,
true);
}
@ -100,15 +101,15 @@ public class PskLwm2mIntegrationTest extends AbstractSecurityLwM2MIntegrationTes
Hex.decodeHex(keyPsk.toCharArray()));
Lwm2mDeviceProfileTransportConfiguration transportConfiguration = getTransportConfiguration(OBSERVE_ATTRIBUTES_WITHOUT_PARAMS, getBootstrapServerCredentialsSecure(PSK, BOTH));
LwM2MDeviceCredentials deviceCredentials = getDeviceCredentialsSecure(clientCredentials, null, null, PSK, false);
this.basicTestConnection(securityBs,
this.basicTestConnection(null, securityBs,
deviceCredentials,
COAP_CONFIG_BS,
clientEndpoint,
transportConfiguration,
"await on client state (PskBS two section)",
expectedStatusesRegistrationBsSuccess,
true,
false,
ON_REGISTRATION_SUCCESS,
true);
false);
}
}

10
application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/RpkLwM2MIntegrationTest.java

@ -49,21 +49,21 @@ public class RpkLwM2MIntegrationTest extends AbstractSecurityLwM2MIntegrationTes
RPKClientCredential clientCredentials = new RPKClientCredential();
clientCredentials.setEndpoint(clientEndpoint);
clientCredentials.setKey(Base64.encodeBase64String(certificate.getPublicKey().getEncoded()));
Security securityBs = rpk(SECURE_URI,
Security security = rpk(SECURE_URI,
shortServerId,
certificate.getPublicKey().getEncoded(),
privateKey.getEncoded(),
serverX509Cert.getPublicKey().getEncoded());
Lwm2mDeviceProfileTransportConfiguration transportConfiguration = getTransportConfiguration(OBSERVE_ATTRIBUTES_WITHOUT_PARAMS, getBootstrapServerCredentialsSecure(RPK, NONE));
LwM2MDeviceCredentials deviceCredentials = getDeviceCredentialsSecure(clientCredentials, privateKey, certificate, RPK, false);
this.basicTestConnection(securityBs,
this.basicTestConnection(security, null,
deviceCredentials,
COAP_CONFIG,
clientEndpoint,
transportConfiguration,
"await on client state (Rpk_Lwm2m)",
expectedStatusesRegistrationLwm2mSuccess,
false,
true,
ON_REGISTRATION_SUCCESS,
true);
}
@ -117,14 +117,14 @@ public class RpkLwM2MIntegrationTest extends AbstractSecurityLwM2MIntegrationTes
serverX509CertBs.getPublicKey().getEncoded());
Lwm2mDeviceProfileTransportConfiguration transportConfiguration = getTransportConfiguration(OBSERVE_ATTRIBUTES_WITHOUT_PARAMS, getBootstrapServerCredentialsSecure(RPK, BOTH));
LwM2MDeviceCredentials deviceCredentials = getDeviceCredentialsSecure(clientCredentials, clientPrivateKeyFromCertTrust, certificate, RPK, false);
this.basicTestConnection(securityBs,
this.basicTestConnection(null, securityBs,
deviceCredentials,
COAP_CONFIG_BS,
clientEndpoint,
transportConfiguration,
"await on client state (RpkBS two section)",
expectedStatusesRegistrationBsSuccess,
true,
false,
ON_REGISTRATION_SUCCESS,
true);
}

6
application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/X509_NoTrustLwM2MIntegrationTest.java

@ -57,13 +57,14 @@ public class X509_NoTrustLwM2MIntegrationTest extends AbstractSecurityLwM2MInteg
Lwm2mDeviceProfileTransportConfiguration transportConfiguration = getTransportConfiguration(OBSERVE_ATTRIBUTES_WITHOUT_PARAMS, getBootstrapServerCredentialsSecure(X509, NONE));
LwM2MDeviceCredentials deviceCredentials = getDeviceCredentialsSecure(clientCredentials, privateKey, certificate, X509, false);
this.basicTestConnection(security,
null,
deviceCredentials,
COAP_CONFIG,
clientEndpoint,
transportConfiguration,
"await on client state (X509_Trust_Lwm2m)",
expectedStatusesRegistrationLwm2mSuccess,
false,
true,
ON_REGISTRATION_SUCCESS,
true);
}
@ -118,13 +119,14 @@ public class X509_NoTrustLwM2MIntegrationTest extends AbstractSecurityLwM2MInteg
Lwm2mDeviceProfileTransportConfiguration transportConfiguration = getTransportConfiguration(OBSERVE_ATTRIBUTES_WITHOUT_PARAMS, getBootstrapServerCredentialsSecure(X509, BOTH));
LwM2MDeviceCredentials deviceCredentials = getDeviceCredentialsSecure(clientCredentials, privateKey, certificate, X509, false);
this.basicTestConnection(security,
null,
deviceCredentials,
COAP_CONFIG_BS,
clientEndpoint,
transportConfiguration,
"await on client state (X509NoTrust two section)",
expectedStatusesRegistrationBsSuccess,
true,
false,
ON_REGISTRATION_SUCCESS,
true);
}

6
application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/X509_TrustLwM2MIntegrationTest.java

@ -51,13 +51,14 @@ public class X509_TrustLwM2MIntegrationTest extends AbstractSecurityLwM2MIntegra
Lwm2mDeviceProfileTransportConfiguration transportConfiguration = getTransportConfiguration(OBSERVE_ATTRIBUTES_WITHOUT_PARAMS, getBootstrapServerCredentialsSecure(X509, NONE));
LwM2MDeviceCredentials deviceCredentials = getDeviceCredentialsSecure(clientCredentials, privateKey, certificate, X509, false);
this.basicTestConnection(security,
null,
deviceCredentials,
COAP_CONFIG,
clientEndpoint,
transportConfiguration,
"await on client state (X509_Trust_Lwm2m)",
expectedStatusesRegistrationLwm2mSuccess,
false,
true,
ON_REGISTRATION_SUCCESS,
true);
}
@ -78,13 +79,14 @@ public class X509_TrustLwM2MIntegrationTest extends AbstractSecurityLwM2MIntegra
Lwm2mDeviceProfileTransportConfiguration transportConfiguration = getTransportConfiguration(OBSERVE_ATTRIBUTES_WITHOUT_PARAMS, getBootstrapServerCredentialsSecure(X509, BOTH));
LwM2MDeviceCredentials deviceCredentials = getDeviceCredentialsSecure(clientCredentials, privateKey, certificate, X509, false);
this.basicTestConnection(security,
null,
deviceCredentials,
COAP_CONFIG_BS,
clientEndpoint,
transportConfiguration,
"await on client state (X509Trust two section)",
expectedStatusesRegistrationBsSuccess,
true,
false,
ON_REGISTRATION_SUCCESS,
true);
}

2
application/src/test/resources/lwm2m/3.xml

@ -64,7 +64,7 @@ LEGAL DISCLAIMER
<ObjectID>3</ObjectID>
<ObjectURN>urn:oma:lwm2m:oma:3:1.0</ObjectURN>
<LWM2MVersion>1.1</LWM2MVersion>
<ObjectVersion>1.0</ObjectVersion>
<ObjectVersion>1.2</ObjectVersion>
<MultipleInstances>Single</MultipleInstances>
<Mandatory>Mandatory</Mandatory>
<Resources>

2
application/src/test/resources/lwm2m/5.xml

@ -70,7 +70,7 @@ A LwM2M Server MUST support block-wise transfer. Other protocols, such as HTTP/H
<ObjectID>5</ObjectID>
<ObjectURN>urn:oma:lwm2m:oma:5</ObjectURN>
<LWM2MVersion>1.0</LWM2MVersion>
<ObjectVersion>1.0</ObjectVersion>
<ObjectVersion>1.2</ObjectVersion>
<MultipleInstances>Single</MultipleInstances>
<Mandatory>Optional</Mandatory>
<Resources>

2
application/src/test/resources/lwm2m/9.xml

@ -64,7 +64,7 @@ LEGAL DISCLAIMER
<ObjectID>9</ObjectID>
<ObjectURN>urn:oma:lwm2m:oma:9</ObjectURN>
<LWM2MVersion>1.0</LWM2MVersion>
<ObjectVersion>1.0</ObjectVersion>
<ObjectVersion>1.1</ObjectVersion>
<MultipleInstances>Multiple</MultipleInstances>
<Mandatory>Optional</Mandatory>
<Resources>

15
common/data/src/main/java/org/thingsboard/server/common/data/device/profile/lwm2m/ObjectAttributes.java

@ -17,6 +17,7 @@ package org.thingsboard.server.common.data.device.profile.lwm2m;
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.Data;
import org.eclipse.leshan.core.LwM2m;
import java.io.Serializable;
@ -25,13 +26,25 @@ import java.io.Serializable;
public class ObjectAttributes implements Serializable {
private static final long serialVersionUID = 4765123984733721312L;
private Long dim;
private Long ssid;
private String uri;
private String ver;
private String lwm2m;
private Long pmin;
private Long pmax;
private Double gt;
private Double lt;
private Double st;
private Long epmin;
private Long epmax;
public LwM2m.Version getVer(){
return ver != null ? new LwM2m.Version(ver) : null;
}
public LwM2m.LwM2mVersion getLwm2m(){
return lwm2m != null ? LwM2m.LwM2mVersion.get(lwm2m) : null;
}
}

2
common/data/src/main/java/org/thingsboard/server/common/data/device/profile/lwm2m/bootstrap/LwM2MServerSecurityConfig.java

@ -26,7 +26,7 @@ public class LwM2MServerSecurityConfig implements Serializable {
@Schema(description = "Server short Id. Used as link to associate server Object Instance. This identifier uniquely identifies each LwM2M Server configured for the LwM2M Client. " +
"This Resource MUST be set when the Bootstrap-Server Resource has a value of 'false'. " +
"The values ID:0 and ID:65535 values MUST NOT be used for identifying the LwM2M Server.", example = "123", accessMode = Schema.AccessMode.READ_ONLY)
"The values ID:1 and ID:65534 values MUST NOT be used for identifying the LwM2M Server.", example = "123", accessMode = Schema.AccessMode.READ_ONLY)
protected Integer shortServerId = 123;
/** Security -> ObjectId = 0 'LWM2M Security' */
@Schema(description = "Is Bootstrap Server or Lwm2m Server. " +

6
common/data/src/main/java/org/thingsboard/server/common/data/util/TbDDFFileParser.java

@ -17,8 +17,6 @@ package org.thingsboard.server.common.data.util;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.leshan.core.LwM2m;
import org.eclipse.leshan.core.model.DDFFileValidator;
import org.eclipse.leshan.core.model.DefaultDDFFileValidator;
import org.eclipse.leshan.core.model.InvalidDDFFileException;
import org.eclipse.leshan.core.model.ObjectModel;
import org.eclipse.leshan.core.model.ResourceModel;
@ -41,7 +39,7 @@ import java.util.Map;
@Slf4j
public class TbDDFFileParser {
private static final DDFFileValidator ddfFileValidator = new DefaultDDFFileValidator();
private static final TbDefaultDDFFileValidator ddfFileValidator = new TbDefaultDDFFileValidator();
public List<ObjectModel> parse(InputStream inputStream, String streamName)
throws InvalidDDFFileException, IOException {
@ -269,4 +267,4 @@ public class TbDDFFileParser {
}
return new ResourceModel(id, name, operations, multiple, mandatory, type, rangeEnumeration, units, description);
}
}
}

129
common/data/src/main/java/org/thingsboard/server/common/data/util/TbDefaultDDFFileValidator.java

@ -0,0 +1,129 @@
/**
* Copyright © 2016-2024 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.common.data.util;
import org.eclipse.leshan.core.LwM2m.LwM2mVersion;
import org.eclipse.leshan.core.model.DDFFileValidator;
import org.eclipse.leshan.core.model.InvalidDDFFileException;
import org.eclipse.leshan.core.util.Validate;
import org.w3c.dom.Node;
import org.xml.sax.SAXException;
import org.xml.sax.SAXNotRecognizedException;
import org.xml.sax.SAXNotSupportedException;
import javax.xml.XMLConstants;
import javax.xml.transform.Source;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamSource;
import javax.xml.validation.Schema;
import javax.xml.validation.SchemaFactory;
import javax.xml.validation.Validator;
import java.io.IOException;
import java.io.InputStream;
/**
* A DDF File Validator.
* <p>
* Validate a DDF File against the embedded LWM2M schema.
* <p>
* Support LWM2M version 1.0 and 1.1.
*/
public class TbDefaultDDFFileValidator implements DDFFileValidator {
private static String LWM2M_V1_0_SCHEMA_PATH = "/schemas/LWM2M.xsd";
private static String LWM2M_V1_1_SCHEMA_PATH = "/schemas/LWM2M-v1_1.xsd";
private final String schema;
/**
* Create a {@link DDFFileValidator} using the LWM2M v1.1 schema.
*/
public TbDefaultDDFFileValidator() {
this(LwM2mVersion.V1_1);
}
/**
* Create a {@link DDFFileValidator} using schema corresponding to LWM2M {@link LwM2mVersion}.
*/
public TbDefaultDDFFileValidator(LwM2mVersion version) {
Validate.notNull(version, "version must not be null");
if (LwM2mVersion.V1_0.equals(version)) {
schema = LWM2M_V1_0_SCHEMA_PATH;
} else if (LwM2mVersion.V1_1.equals(version)) {
schema = LWM2M_V1_1_SCHEMA_PATH;
} else {
throw new IllegalStateException(String.format("Unsupported version %s", version));
}
}
@Override
public void validate(Node xmlToValidate) throws InvalidDDFFileException {
try {
validate(new DOMSource(xmlToValidate));
} catch (SAXException | IOException e) {
throw new InvalidDDFFileException(e);
}
}
/**
* Validate a XML {@link Source} against the embedded LWM2M Schema.
*
* @param xmlToValidate an XML source to validate
* @throws SAXException see {@link Validator#validate(Source)}
* @throws IOException see {@link Validator#validate(Source)}
*/
public void validate(Source xmlToValidate) throws SAXException, IOException {
Validator validator = getEmbeddedLwM2mSchema().newValidator();
validator.validate(xmlToValidate);
}
/**
* Get the Embedded the LWM2M.xsd Schema.
*
* @throws SAXException see {@link SchemaFactory#newSchema(Source)}
*/
protected Schema getEmbeddedLwM2mSchema() throws SAXException {
InputStream inputStream = DDFFileValidator.class.getResourceAsStream(schema);
Source source = new StreamSource(inputStream);
SchemaFactory schemaFactory = createSchemaFactory();
return schemaFactory.newSchema(source);
}
protected SchemaFactory createSchemaFactory() {
SchemaFactory factory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
// SchemaFactory factory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_INSTANCE_NS_URI);
try {
// Create Safe SchemaFactory (not vulnerable to XXE Attacks)
// --------------------------------------------------------
// There is several recommendation from different source we try to apply all, even if some are maybe
// redundant.
// from :
// https://semgrep.dev/docs/cheat-sheets/java-xxe/
factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
// from :
// https://cheatsheetseries.owasp.org/cheatsheets/XML_External_Entity_Prevention_Cheat_Sheet.html#schemafactory
// factory.setProperty(XMLConstants.ACCESS_EXTERNAL_DTD, "");
// factory.setProperty(XMLConstants.ACCESS_EXTERNAL_SCHEMA, "");
} catch (SAXNotRecognizedException | SAXNotSupportedException e) {
throw new IllegalStateException("Unable to create SchemaFactory", e);
}
return factory;
}
}

40
common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportResource.java

@ -19,7 +19,6 @@ import com.google.gson.JsonParseException;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.californium.core.coap.CoAP;
import org.eclipse.californium.core.coap.Request;
import org.eclipse.californium.core.coap.Response;
import org.eclipse.californium.core.network.Exchange;
import org.eclipse.californium.core.observe.ObserveRelation;
import org.eclipse.californium.core.server.resources.CoapExchange;
@ -27,6 +26,8 @@ import org.eclipse.californium.core.server.resources.Resource;
import org.eclipse.californium.core.server.resources.ResourceObserver;
import org.thingsboard.server.coapserver.CoapServerService;
import org.thingsboard.server.coapserver.TbCoapDtlsSessionInfo;
import org.thingsboard.server.common.adaptor.AdaptorException;
import org.thingsboard.server.common.adaptor.JsonConverter;
import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.DeviceProfile;
import org.thingsboard.server.common.data.DeviceTransportType;
@ -35,13 +36,11 @@ import org.thingsboard.server.common.data.TransportPayloadType;
import org.thingsboard.server.common.data.security.DeviceTokenCredentials;
import org.thingsboard.server.common.msg.session.FeatureType;
import org.thingsboard.server.common.transport.TransportServiceCallback;
import org.thingsboard.server.common.adaptor.AdaptorException;
import org.thingsboard.server.common.adaptor.JsonConverter;
import org.thingsboard.server.common.transport.auth.ValidateDeviceCredentialsResponse;
import org.thingsboard.server.gen.transport.TransportProtos;
import org.thingsboard.server.transport.coap.callback.CoapDeviceAuthCallback;
import org.thingsboard.server.transport.coap.callback.CoapNoOpCallback;
import org.thingsboard.server.transport.coap.callback.CoapOkCallback;
import org.thingsboard.server.transport.coap.callback.CoapResponseCodeCallback;
import org.thingsboard.server.transport.coap.callback.GetAttributesSyncSessionCallback;
import org.thingsboard.server.transport.coap.callback.ToServerRpcSyncSessionCallback;
import org.thingsboard.server.transport.coap.client.CoapClientContext;
@ -54,7 +53,6 @@ import java.util.Random;
import java.util.UUID;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import static org.eclipse.californium.elements.DtlsEndpointContext.KEY_SESSION_ID;
@ -84,30 +82,6 @@ public class CoapTransportResource extends AbstractCoapTransportResource {
ctx.getScheduler().scheduleAtFixedRate(clients::reportActivity, new Random().nextInt((int) sessionReportTimeout), sessionReportTimeout, TimeUnit.MILLISECONDS);
}
/*
* Overwritten method from CoapResource to be able to manage our own observe notification counters.
*/
@Override
public void checkObserveRelation(Exchange exchange, Response response) {
String token = getTokenFromRequest(exchange.getRequest());
final ObserveRelation relation = exchange.getRelation();
if (relation == null || relation.isCanceled()) {
return; // because request did not try to establish a relation
}
if (response.getCode().isSuccess()) {
if (!relation.isEstablished()) {
relation.setEstablished();
addObserveRelation(relation);
}
AtomicInteger state = clients.getNotificationCounterByToken(token);
if (state != null) {
response.getOptions().setObserve(state.getAndIncrement());
} else {
response.getOptions().removeObserve();
}
} // ObserveLayer takes care of the else case
}
@Override
protected void processHandleGet(CoapExchange exchange) {
Optional<FeatureType> featureType = getFeatureType(exchange.advanced().getRequest());
@ -278,7 +252,7 @@ public class CoapTransportResource extends AbstractCoapTransportResource {
UUID sessionId = toSessionId(sessionInfo);
transportService.process(sessionInfo, clientState.getAdaptor().convertToPostAttributes(sessionId, request,
clientState.getConfiguration().getAttributesMsgDescriptor()),
new CoapOkCallback(exchange, CoAP.ResponseCode.CREATED, CoAP.ResponseCode.INTERNAL_SERVER_ERROR));
new CoapResponseCodeCallback(exchange, CoAP.ResponseCode.CREATED, CoAP.ResponseCode.INTERNAL_SERVER_ERROR));
}
private void handlePostTelemetryRequest(TbCoapClientState clientState, CoapExchange exchange, Request request) throws AdaptorException {
@ -286,7 +260,7 @@ public class CoapTransportResource extends AbstractCoapTransportResource {
UUID sessionId = toSessionId(sessionInfo);
transportService.process(sessionInfo, clientState.getAdaptor().convertToPostTelemetry(sessionId, request,
clientState.getConfiguration().getTelemetryMsgDescriptor()),
new CoapOkCallback(exchange, CoAP.ResponseCode.CREATED, CoAP.ResponseCode.INTERNAL_SERVER_ERROR));
new CoapResponseCodeCallback(exchange, CoAP.ResponseCode.CREATED, CoAP.ResponseCode.INTERNAL_SERVER_ERROR));
}
private void handleClaimRequest(TbCoapClientState clientState, CoapExchange exchange, Request request) throws AdaptorException {
@ -294,7 +268,7 @@ public class CoapTransportResource extends AbstractCoapTransportResource {
UUID sessionId = toSessionId(sessionInfo);
transportService.process(sessionInfo,
clientState.getAdaptor().convertToClaimDevice(sessionId, request, sessionInfo),
new CoapOkCallback(exchange, CoAP.ResponseCode.CREATED, CoAP.ResponseCode.INTERNAL_SERVER_ERROR));
new CoapResponseCodeCallback(exchange, CoAP.ResponseCode.CREATED, CoAP.ResponseCode.INTERNAL_SERVER_ERROR));
}
private void handleAttributeSubscribeRequest(TbCoapClientState clientState, CoapExchange exchange, Request request) {
@ -320,7 +294,7 @@ public class CoapTransportResource extends AbstractCoapTransportResource {
UUID sessionId = toSessionId(session);
transportService.process(session,
clientState.getAdaptor().convertToDeviceRpcResponse(sessionId, request, clientState.getConfiguration().getRpcResponseMsgDescriptor()),
new CoapOkCallback(exchange, CoAP.ResponseCode.CREATED, CoAP.ResponseCode.INTERNAL_SERVER_ERROR));
new CoapResponseCodeCallback(exchange, CoAP.ResponseCode.CREATED, CoAP.ResponseCode.INTERNAL_SERVER_ERROR));
}
private void handleRpcSubscribeRequest(TbCoapClientState clientState, CoapExchange exchange, Request request) {

54
common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/callback/CoapResponseCallback.java

@ -0,0 +1,54 @@
/**
* Copyright © 2016-2024 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.transport.coap.callback;
import org.eclipse.californium.core.coap.Response;
import org.eclipse.californium.core.server.resources.CoapExchange;
import org.thingsboard.server.common.transport.TransportServiceCallback;
public class CoapResponseCallback implements TransportServiceCallback<Void> {
protected final CoapExchange exchange;
protected final Response onSuccessResponse;
protected final Response onFailureResponse;
public CoapResponseCallback(CoapExchange exchange, Response onSuccessResponse, Response onFailureResponse) {
this.exchange = exchange;
this.onSuccessResponse = onSuccessResponse;
this.onFailureResponse = onFailureResponse;
}
/**
* @param msg
*/
@Override
public void onSuccess(Void msg) {
this.onSuccessResponse.setConfirmable(isConRequest());
exchange.respond(this.onSuccessResponse);
}
/**
* @param e
*/
@Override
public void onError(Throwable e) {
exchange.respond(onFailureResponse);
}
protected boolean isConRequest() {
return exchange.advanced().getRequest().isConfirmable();
}
}

4
common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/callback/CoapOkCallback.java → common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/callback/CoapResponseCodeCallback.java

@ -20,13 +20,13 @@ import org.eclipse.californium.core.coap.Response;
import org.eclipse.californium.core.server.resources.CoapExchange;
import org.thingsboard.server.common.transport.TransportServiceCallback;
public class CoapOkCallback implements TransportServiceCallback<Void> {
public class CoapResponseCodeCallback implements TransportServiceCallback<Void> {
protected final CoapExchange exchange;
protected final CoAP.ResponseCode onSuccessResponse;
protected final CoAP.ResponseCode onFailureResponse;
public CoapOkCallback(CoapExchange exchange, CoAP.ResponseCode onSuccessResponse, CoAP.ResponseCode onFailureResponse) {
public CoapResponseCodeCallback(CoapExchange exchange, CoAP.ResponseCode onSuccessResponse, CoAP.ResponseCode onFailureResponse) {
this.exchange = exchange;
this.onSuccessResponse = onSuccessResponse;
this.onFailureResponse = onFailureResponse;

22
common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/client/DefaultCoapClientContext.java

@ -18,6 +18,7 @@ package org.thingsboard.server.transport.coap.client;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.californium.core.coap.CoAP;
import org.eclipse.californium.core.coap.CoAP.ResponseCode;
import org.eclipse.californium.core.coap.Response;
import org.eclipse.californium.core.observe.ObserveRelation;
import org.eclipse.californium.core.server.resources.CoapExchange;
@ -26,6 +27,7 @@ import org.springframework.context.annotation.Lazy;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Service;
import org.thingsboard.server.coapserver.CoapServerContext;
import org.thingsboard.server.common.adaptor.AdaptorException;
import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.DeviceProfile;
@ -45,7 +47,6 @@ import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.DeviceProfileId;
import org.thingsboard.server.common.data.rpc.RpcStatus;
import org.thingsboard.server.common.msg.session.FeatureType;
import org.thingsboard.server.transport.coap.CoapSessionMsgType;
import org.thingsboard.server.common.transport.DeviceDeletedEvent;
import org.thingsboard.server.common.transport.DeviceProfileUpdatedEvent;
import org.thingsboard.server.common.transport.DeviceUpdatedEvent;
@ -53,18 +54,19 @@ import org.thingsboard.server.common.transport.SessionMsgListener;
import org.thingsboard.server.common.transport.TransportDeviceProfileCache;
import org.thingsboard.server.common.transport.TransportService;
import org.thingsboard.server.common.transport.TransportServiceCallback;
import org.thingsboard.server.common.adaptor.AdaptorException;
import org.thingsboard.server.common.transport.auth.SessionInfoCreator;
import org.thingsboard.server.common.transport.auth.ValidateDeviceCredentialsResponse;
import org.thingsboard.server.gen.transport.TransportProtos;
import org.thingsboard.server.queue.discovery.PartitionService;
import org.thingsboard.server.transport.coap.CoapSessionMsgType;
import org.thingsboard.server.transport.coap.CoapTransportContext;
import org.thingsboard.server.transport.coap.TbCoapMessageObserver;
import org.thingsboard.server.transport.coap.TransportConfigurationContainer;
import org.thingsboard.server.transport.coap.adaptors.CoapTransportAdaptor;
import org.thingsboard.server.transport.coap.callback.AbstractSyncSessionCallback;
import org.thingsboard.server.transport.coap.callback.CoapNoOpCallback;
import org.thingsboard.server.transport.coap.callback.CoapOkCallback;
import org.thingsboard.server.transport.coap.callback.CoapResponseCallback;
import org.thingsboard.server.transport.coap.callback.CoapResponseCodeCallback;
import java.util.Optional;
import java.util.UUID;
@ -329,9 +331,14 @@ public class DefaultCoapClientContext implements CoapClientContext {
TransportProtos.GetAttributeRequestMsg.newBuilder().setOnlyShared(true).build(),
new CoapNoOpCallback(exchange));
} else {
Response response = new Response(CoAP.ResponseCode.VALID);
if (state.getRpc() == null) {
state.setRpc(new TbCoapObservationState(exchange, token));
}
response.getOptions().setObserve(state.getRpc().getObserveCounter().getAndIncrement());
transportService.process(state.getSession(),
TransportProtos.SubscribeToRPCMsg.getDefaultInstance(),
new CoapOkCallback(exchange, CoAP.ResponseCode.VALID, CoAP.ResponseCode.INTERNAL_SERVER_ERROR)
new CoapResponseCallback(exchange, response, new Response(CoAP.ResponseCode.INTERNAL_SERVER_ERROR))
);
}
}
@ -479,6 +486,7 @@ public class DefaultCoapClientContext implements CoapClientContext {
if (attrs != null) {
try {
Response response = state.getAdaptor().convertToPublish(msg);
response.getOptions().setObserve(attrs.getObserveCounter().getAndIncrement());
respond(attrs.getExchange(), response, state.getContentFormat());
} catch (AdaptorException e) {
log.trace("Failed to reply due to error", e);
@ -509,6 +517,7 @@ public class DefaultCoapClientContext implements CoapClientContext {
boolean conRequest = AbstractSyncSessionCallback.isConRequest(state.getAttrs());
int requestId = getNextMsgId();
Response response = state.getAdaptor().convertToPublish(msg);
response.getOptions().setObserve(attrs.getObserveCounter().getAndIncrement());
response.setConfirmable(conRequest);
response.setMID(requestId);
if (conRequest) {
@ -573,6 +582,7 @@ public class DefaultCoapClientContext implements CoapClientContext {
int requestId = getNextMsgId();
try {
Response response = state.getAdaptor().convertToPublish(msg, state.getConfiguration().getRpcRequestDynamicMessageBuilder());
response.getOptions().setObserve(state.getRpc().getObserveCounter().getAndIncrement());
response.setConfirmable(conRequest);
response.setMID(requestId);
if (conRequest) {
@ -808,7 +818,7 @@ public class DefaultCoapClientContext implements CoapClientContext {
state.setRpc(null);
transportService.process(state.getSession(),
TransportProtos.SubscribeToRPCMsg.newBuilder().setUnsubscribe(true).build(),
new CoapOkCallback(exchange, CoAP.ResponseCode.DELETED, CoAP.ResponseCode.INTERNAL_SERVER_ERROR));
new CoapResponseCodeCallback(exchange, CoAP.ResponseCode.DELETED, CoAP.ResponseCode.INTERNAL_SERVER_ERROR));
if (state.getAttrs() == null) {
closeAndCleanup(state);
}
@ -822,7 +832,7 @@ public class DefaultCoapClientContext implements CoapClientContext {
state.setAttrs(null);
transportService.process(state.getSession(),
TransportProtos.SubscribeToAttributeUpdatesMsg.newBuilder().setUnsubscribe(true).build(),
new CoapOkCallback(exchange, CoAP.ResponseCode.DELETED, CoAP.ResponseCode.INTERNAL_SERVER_ERROR));
new CoapResponseCodeCallback(exchange, CoAP.ResponseCode.DELETED, CoAP.ResponseCode.INTERNAL_SERVER_ERROR));
if (state.getRpc() == null) {
closeAndCleanup(state);
}

85
common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/LwM2MTransportBootstrapService.java

@ -15,13 +15,20 @@
*/
package org.thingsboard.server.transport.lwm2m.bootstrap;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.californium.elements.config.Configuration;
import org.eclipse.californium.scandium.config.DtlsConfig;
import org.eclipse.californium.scandium.config.DtlsConnectorConfig;
import org.eclipse.leshan.core.endpoint.Protocol;
import org.eclipse.leshan.server.bootstrap.BootstrapSessionManager;
import org.eclipse.leshan.server.californium.bootstrap.LeshanBootstrapServer;
import org.eclipse.leshan.server.californium.bootstrap.LeshanBootstrapServerBuilder;
import org.eclipse.leshan.server.bootstrap.LeshanBootstrapServer;
import org.eclipse.leshan.server.bootstrap.LeshanBootstrapServerBuilder;
import org.eclipse.leshan.server.californium.bootstrap.LwM2mBootstrapPskStore;
import org.eclipse.leshan.server.californium.bootstrap.endpoint.CaliforniumBootstrapServerEndpointsProvider;
import org.eclipse.leshan.server.californium.bootstrap.endpoint.coap.CoapBootstrapServerProtocolProvider;
import org.eclipse.leshan.server.californium.bootstrap.endpoint.coaps.CoapsBootstrapServerProtocolProvider;
import org.springframework.stereotype.Component;
import org.thingsboard.server.common.transport.TransportService;
import org.thingsboard.server.common.transport.config.ssl.SslCredentials;
@ -33,16 +40,12 @@ import org.thingsboard.server.transport.lwm2m.bootstrap.store.LwM2MInMemoryBoots
import org.thingsboard.server.transport.lwm2m.config.LwM2MTransportBootstrapConfig;
import org.thingsboard.server.transport.lwm2m.config.LwM2MTransportServerConfig;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import java.net.InetSocketAddress;
import java.security.cert.X509Certificate;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static org.eclipse.californium.scandium.config.DtlsConfig.DTLS_RECOMMENDED_CIPHER_SUITES_ONLY;
import static org.eclipse.californium.scandium.config.DtlsConfig.DTLS_RECOMMENDED_CURVES_ONLY;
import static org.eclipse.californium.scandium.config.DtlsConfig.DTLS_RETRANSMISSION_TIMEOUT;
import static org.eclipse.californium.scandium.config.DtlsConfig.DTLS_ROLE;
import static org.eclipse.californium.scandium.config.DtlsConfig.DtlsRole.SERVER_ONLY;
import static org.thingsboard.server.transport.lwm2m.server.DefaultLwM2mTransportService.PSK_CIPHER_SUITES;
import static org.thingsboard.server.transport.lwm2m.server.DefaultLwM2mTransportService.RPK_OR_X509_CIPHER_SUITES;
import static org.thingsboard.server.transport.lwm2m.server.LwM2MNetworkConfig.getCoapConfig;
@ -82,24 +85,57 @@ public class LwM2MTransportBootstrapService {
public LeshanBootstrapServer getLhBootstrapServer() {
LeshanBootstrapServerBuilder builder = new LeshanBootstrapServerBuilder();
builder.setLocalAddress(bootstrapConfig.getHost(), bootstrapConfig.getPort());
builder.setLocalSecureAddress(bootstrapConfig.getSecureHost(), bootstrapConfig.getSecurePort());
/* Create CoAP Config */
builder.setCoapConfig(getCoapConfig(bootstrapConfig.getPort(), bootstrapConfig.getSecurePort(), serverConfig));
// Create Californium Endpoints Provider:
// ------------------
// Create Server Endpoints Provider
CaliforniumBootstrapServerEndpointsProvider.Builder endpointsBuilder = new CaliforniumBootstrapServerEndpointsProvider.Builder(
// Add coap Protocol support
new CoapBootstrapServerProtocolProvider(),
// Add coaps/dtls protocol support
new CoapsBootstrapServerProtocolProvider(c -> {
if (this.bootstrapConfig.getSslCredentials() != null) {
c.setAdvancedCertificateVerifier(certificateVerifier);
c.setAsList(DtlsConfig.DTLS_CIPHER_SUITES, RPK_OR_X509_CIPHER_SUITES);
} else {
log.info("Unable to load X509 files for LWM2MServer");
LwM2mBootstrapPskStore lwM2mBsPskStore = new LwM2mBootstrapPskStore(lwM2MBootstrapSecurityStore);
c.setAdvancedPskStore(lwM2mBsPskStore);
c.setAsList(DtlsConfig.DTLS_CIPHER_SUITES, PSK_CIPHER_SUITES);
}
}));
// Create Californium Configuration
Configuration serverCoapConfig = endpointsBuilder.createDefaultConfiguration();
getCoapConfig(serverCoapConfig, bootstrapConfig.getPort(), bootstrapConfig.getSecurePort(),serverConfig);
serverCoapConfig.setTransient(DtlsConfig.DTLS_RECOMMENDED_CIPHER_SUITES_ONLY);
serverCoapConfig.set(DtlsConfig.DTLS_RECOMMENDED_CIPHER_SUITES_ONLY, serverConfig.isRecommendedCiphers());
serverCoapConfig.setTransient(DtlsConfig.DTLS_CONNECTION_ID_LENGTH);
serverCoapConfig.set(DtlsConfig.DTLS_CONNECTION_ID_LENGTH, 6);
serverCoapConfig.set(DTLS_RECOMMENDED_CURVES_ONLY, serverConfig.isRecommendedSupportedGroups());
serverCoapConfig.setTransient(DTLS_RETRANSMISSION_TIMEOUT);
serverCoapConfig.set(DTLS_RETRANSMISSION_TIMEOUT, serverConfig.getDtlsRetransmissionTimeout(), MILLISECONDS);
/* Create DTLS Config */
this.setServerWithCredentials(builder);
// Set Californium Configuration
endpointsBuilder.setConfiguration(serverCoapConfig);
serverConfig.setCoapConfig(serverCoapConfig);
/* Create and Set DTLS Config */
DtlsConnectorConfig.Builder dtlsConfig = new DtlsConnectorConfig.Builder(getCoapConfig(bootstrapConfig.getPort(), bootstrapConfig.getSecurePort(), serverConfig));
dtlsConfig.set(DTLS_RECOMMENDED_CURVES_ONLY, serverConfig.isRecommendedSupportedGroups());
dtlsConfig.set(DTLS_RECOMMENDED_CIPHER_SUITES_ONLY, serverConfig.isRecommendedCiphers());
dtlsConfig.set(DTLS_RETRANSMISSION_TIMEOUT, serverConfig.getDtlsRetransmissionTimeout(), MILLISECONDS);
dtlsConfig.set(DTLS_ROLE, SERVER_ONLY);
setServerWithCredentials(builder, dtlsConfig);
// Create CoAP endpoint
InetSocketAddress coapAddr = new InetSocketAddress(bootstrapConfig.getHost(), bootstrapConfig.getPort());
endpointsBuilder.addEndpoint(coapAddr, Protocol.COAP);
/* Set DTLS Config */
builder.setDtlsConfig(dtlsConfig);
// Create CoAP over DTLS endpoint
InetSocketAddress coapsAddr = new InetSocketAddress(bootstrapConfig.getSecureHost(), bootstrapConfig.getSecurePort());
endpointsBuilder.addEndpoint(coapsAddr, Protocol.COAPS);
/* Set securityStore with new ConfigStore */
builder.setConfigStore(lwM2MInMemoryBootstrapConfigStore);
@ -112,22 +148,19 @@ public class LwM2MTransportBootstrapService {
builder.setSessionManager(sessionManager);
/* Create BootstrapServer */
builder.setEndpointsProviders(endpointsBuilder.build());
return builder.build();
}
private void setServerWithCredentials(LeshanBootstrapServerBuilder builder, DtlsConnectorConfig.Builder dtlsConfig) {
private void setServerWithCredentials(LeshanBootstrapServerBuilder builder) {
if (this.bootstrapConfig.getSslCredentials() != null) {
SslCredentials sslCredentials = this.bootstrapConfig.getSslCredentials();
builder.setPublicKey(sslCredentials.getPublicKey());
builder.setPrivateKey(sslCredentials.getPrivateKey());
builder.setCertificateChain(sslCredentials.getCertificateChain());
dtlsConfig.setAdvancedCertificateVerifier(certificateVerifier);
dtlsConfig.setAsList(DtlsConfig.DTLS_CIPHER_SUITES, RPK_OR_X509_CIPHER_SUITES);
} else {
/* by default trust all */
builder.setTrustedCertificates(new X509Certificate[0]);
log.info("Unable to load X509 files for BootStrap Server");
dtlsConfig.setAsList(DtlsConfig.DTLS_CIPHER_SUITES, PSK_CIPHER_SUITES);
}
}
}

35
common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/secure/LwM2mDefaultBootstrapSessionManager.java

@ -16,10 +16,13 @@
package org.thingsboard.server.transport.lwm2m.bootstrap.secure;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.leshan.core.peer.IpPeer;
import org.eclipse.leshan.core.peer.LwM2mPeer;
import org.eclipse.leshan.core.peer.PskIdentity;
import org.eclipse.leshan.core.peer.X509Identity;
import org.eclipse.leshan.core.request.BootstrapDownlinkRequest;
import org.eclipse.leshan.core.request.BootstrapFinishRequest;
import org.eclipse.leshan.core.request.BootstrapRequest;
import org.eclipse.leshan.core.request.Identity;
import org.eclipse.leshan.core.response.LwM2mResponse;
import org.eclipse.leshan.server.bootstrap.BootstrapConfigStore;
import org.eclipse.leshan.server.bootstrap.BootstrapFailureCause;
@ -39,6 +42,7 @@ import org.thingsboard.server.transport.lwm2m.bootstrap.store.LwM2MBootstrapSecu
import org.thingsboard.server.transport.lwm2m.bootstrap.store.LwM2MBootstrapTaskProvider;
import org.thingsboard.server.transport.lwm2m.server.client.LwM2MAuthException;
import java.net.URI;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
@ -63,7 +67,7 @@ public class LwM2mDefaultBootstrapSessionManager extends DefaultBootstrapSession
* @param bsSecurityStore the {@link BootstrapSecurityStore} used by default {@link SecurityChecker}.
*/
public LwM2mDefaultBootstrapSessionManager(BootstrapSecurityStore bsSecurityStore, BootstrapConfigStore configStore, TransportService transportService) {
this(bsSecurityStore, new SecurityChecker(), new LwM2MBootstrapConfigStoreTaskProvider(configStore),
this(bsSecurityStore, configStore, new SecurityChecker(), new LwM2MBootstrapConfigStoreTaskProvider(configStore),
new StandardBootstrapModelProvider());
this.transportService = transportService;
}
@ -74,9 +78,9 @@ public class LwM2mDefaultBootstrapSessionManager extends DefaultBootstrapSession
* @param bsSecurityStore the {@link BootstrapSecurityStore} used by {@link SecurityChecker}.
* @param securityChecker used to accept or refuse new {@link BootstrapSession}.
*/
public LwM2mDefaultBootstrapSessionManager(BootstrapSecurityStore bsSecurityStore, SecurityChecker securityChecker,
public LwM2mDefaultBootstrapSessionManager(BootstrapSecurityStore bsSecurityStore, BootstrapConfigStore configStore, SecurityChecker securityChecker,
LwM2MBootstrapTaskProvider tasksProvider, LwM2mBootstrapModelProvider modelProvider) {
super(bsSecurityStore, securityChecker, tasksProvider, modelProvider);
super(bsSecurityStore, configStore);
this.bsSecurityStore = bsSecurityStore;
this.securityChecker = securityChecker;
this.tasksProvider = tasksProvider;
@ -84,23 +88,24 @@ public class LwM2mDefaultBootstrapSessionManager extends DefaultBootstrapSession
}
@Override
public BootstrapSession begin(BootstrapRequest request, Identity clientIdentity) {
boolean authorized = true;
public BootstrapSession begin(BootstrapRequest request, LwM2mPeer sender, URI endpointUsed) {
boolean authorized = sender.getIdentity().isSecure();
Iterator<SecurityInfo> securityInfos = null;
try {
try {
if (bsSecurityStore != null && securityChecker != null) {
if (clientIdentity.isPSK()) {
SecurityInfo securityInfo = bsSecurityStore.getByIdentity(clientIdentity.getPskIdentity());
if (((IpPeer) sender).isPSK()) {
SecurityInfo securityInfo = bsSecurityStore.getByIdentity(((PskIdentity) sender.getIdentity()).getPskIdentity());
securityInfos = Collections.singletonList(securityInfo).iterator();
} else if (!clientIdentity.isX509()) {
}
else if (!((IpPeer) sender).isX509()) {
securityInfos = bsSecurityStore.getAllByEndpoint(request.getEndpointName());
}
authorized = this.checkSecurityInfo(request.getEndpointName(), clientIdentity, securityInfos);
authorized = this.checkSecurityInfo(request.getEndpointName(), sender, securityInfos);
}
} catch (LwM2MAuthException e) {
authorized = false;
}
DefaultBootstrapSession session = new DefaultBootstrapSession(request, clientIdentity, authorized);
DefaultBootstrapSession session = new DefaultBootstrapSession(request, sender, authorized, null, endpointUsed);
if (authorized) {
try {
this.tasksProvider.put(session.getEndpoint());
@ -239,9 +244,9 @@ public class LwM2mDefaultBootstrapSessionManager extends DefaultBootstrapSession
transportService.log(((LwM2MBootstrapSecurityStore) bsSecurityStore).getSessionByEndpoint(endpointName), logMsg);
}
private boolean checkSecurityInfo(String endpoint, Identity clientIdentity, Iterator<SecurityInfo> securityInfos) {
if (clientIdentity.isX509()) {
return clientIdentity.getX509CommonName().equals(endpoint)
private boolean checkSecurityInfo(String endpoint, LwM2mPeer clientIdentity, Iterator<SecurityInfo> securityInfos) {
if (((IpPeer) clientIdentity).isX509()) {
return ((X509Identity)clientIdentity.getIdentity()).getX509CommonName().equals(endpoint)
& ((LwM2MBootstrapSecurityStore) bsSecurityStore).getBootstrapConfigByEndpoint(endpoint) != null;
} else {
return securityChecker.checkSecurityInfos(endpoint, clientIdentity, securityInfos);

21
common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/store/LwM2MBootstrapConfigStoreTaskProvider.java

@ -50,7 +50,7 @@ import java.util.stream.Collectors;
import static org.eclipse.leshan.core.model.ResourceModel.Type.OPAQUE;
import static org.eclipse.leshan.server.bootstrap.BootstrapUtil.toWriteRequest;
import static org.thingsboard.server.transport.lwm2m.utils.LwM2MTransportUtil.BOOTSTRAP_DEFAULT_SHORT_ID;
import static org.thingsboard.server.transport.lwm2m.utils.LwM2MTransportUtil.BOOTSTRAP_DEFAULT_SHORT_ID_0;
@Slf4j
public class LwM2MBootstrapConfigStoreTaskProvider implements LwM2MBootstrapTaskProvider {
@ -76,7 +76,8 @@ public class LwM2MBootstrapConfigStoreTaskProvider implements LwM2MBootstrapTask
@Override
public Tasks getTasks(BootstrapSession session, List<LwM2mResponse> previousResponse) {
BootstrapConfig config = store.get(session.getEndpoint(), session.getIdentity(), session);
// BootstrapConfig config = store.get(session.getEndpoint(), session.getClientTransportData().getIdentity(), session);
BootstrapConfig config = store.get(session);
if (config == null) {
return null;
}
@ -105,7 +106,7 @@ public class LwM2MBootstrapConfigStoreTaskProvider implements LwM2MBootstrapTask
"Bootstrap Discover return error {} : to continue bootstrap session without autoIdForSecurityObject mode. {}",
discoverResponse, session);
}
if (this.lwM2MBootstrapSessionClients.get(session.getEndpoint()).getSecurityInstances().get(BOOTSTRAP_DEFAULT_SHORT_ID) == null) {
if (this.lwM2MBootstrapSessionClients.get(session.getEndpoint()).getSecurityInstances().get(BOOTSTRAP_DEFAULT_SHORT_ID_0) == null) {
log.error(
"Unable to find bootstrap server instance in Security Object (0) in response {}: unable to continue bootstrap session with autoIdForSecurityObject mode. {}",
discoverResponse, session);
@ -150,7 +151,7 @@ public class LwM2MBootstrapConfigStoreTaskProvider implements LwM2MBootstrapTask
* The values ID:0 and ID:65535 values MUST NOT be used for identifying the LwM2M Server.
* "Short Server ID":
* - Link Instance (lwm2m Server) hase linkParams with key = "ssid" value = "shortId" (ver lvm2m = 1.1).
* - Link Instance (bootstrap Server) hase not linkParams with key = "ssid" (ver lvm2m = 1.1).
* - Link Instance (bootstrap Server) hase not linkParams with key = "ssid" (ver lvm2m = 1.0).
*/
protected void findSecurityInstanceId(Link[] objectLinks, String endpoint) {
log.info("Object after discover: [{}]", objectLinks);
@ -159,17 +160,17 @@ public class LwM2MBootstrapConfigStoreTaskProvider implements LwM2MBootstrapTask
try {
LwM2mPath path = new LwM2mPath(link.getUriReference());
if (path.isObjectInstance()) {
if (link.getLinkParams().containsKey("ssid")) {
int serverId = Integer.parseInt(link.getLinkParams().get("ssid").getUnquoted());
if (link.getAttributes().get("ssid") != null) {
int serverId = Integer.parseInt(link.getAttributes().get("ssid").getCoreLinkValue());
if (!lwM2MBootstrapSessionClients.get(endpoint).getSecurityInstances().containsKey(serverId)) {
lwM2MBootstrapSessionClients.get(endpoint).getSecurityInstances().put(serverId, path.getObjectInstanceId());
} else {
log.error("Invalid lwm2mSecurityInstance by [{}]", path.getObjectInstanceId());
}
lwM2MBootstrapSessionClients.get(endpoint).getSecurityInstances().put(Integer.valueOf(link.getLinkParams().get("ssid").getUnquoted()), path.getObjectInstanceId());
lwM2MBootstrapSessionClients.get(endpoint).getSecurityInstances().put(serverId, path.getObjectInstanceId());
} else {
if (!this.lwM2MBootstrapSessionClients.get(endpoint).getSecurityInstances().containsKey(0)) {
this.lwM2MBootstrapSessionClients.get(endpoint).getSecurityInstances().put(BOOTSTRAP_DEFAULT_SHORT_ID, path.getObjectInstanceId());
this.lwM2MBootstrapSessionClients.get(endpoint).getSecurityInstances().put(BOOTSTRAP_DEFAULT_SHORT_ID_0, path.getObjectInstanceId());
} else {
log.error("Invalid bootstrapSecurityInstance by [{}]", path.getObjectInstanceId());
}
@ -221,7 +222,7 @@ public class LwM2MBootstrapConfigStoreTaskProvider implements LwM2MBootstrapTask
LwM2mPath path = new LwM2mPath(link.getUriReference());
if (!path.isRoot() && path.getObjectId() < 3) {
if (path.isObject()) {
String ver = link.getLinkParams().get("ver") != null ? link.getLinkParams().get("ver").getUnquoted() : "1.0";
String ver = link.getAttributes().get("ver") != null ? link.getAttributes().get("ver").getCoreLinkValue() : "1.0";
this.supportedObjects.put(path.getObjectId(), ver);
}
}
@ -243,7 +244,7 @@ public class LwM2MBootstrapConfigStoreTaskProvider implements LwM2MBootstrapTask
Integer bootstrapServerIdNew = null;
// handle security
int lwm2mSecurityInstanceId = 0;
int bootstrapSecurityInstanceId = this.lwM2MBootstrapSessionClients.get(endpoint).getSecurityInstances().get(BOOTSTRAP_DEFAULT_SHORT_ID);
int bootstrapSecurityInstanceId = this.lwM2MBootstrapSessionClients.get(endpoint).getSecurityInstances().get(BOOTSTRAP_DEFAULT_SHORT_ID_0);
for (BootstrapConfig.ServerSecurity security : new TreeMap<>(bootstrapConfig.security).values()) {
if (security.bootstrapServer) {
requestsWrite.add(toWriteRequest(bootstrapSecurityInstanceId, security, contentFormat));

8
common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/store/LwM2MBootstrapSecurityStore.java

@ -16,6 +16,7 @@
package org.thingsboard.server.transport.lwm2m.bootstrap.store;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.leshan.core.peer.OscoreIdentity;
import org.eclipse.leshan.server.bootstrap.BootstrapConfig;
import org.eclipse.leshan.server.bootstrap.EditableBootstrapConfigStore;
import org.eclipse.leshan.server.bootstrap.InvalidConfigurationException;
@ -98,6 +99,11 @@ public class LwM2MBootstrapSecurityStore implements BootstrapSecurityStore {
}
}
@Override
public SecurityInfo getByOscoreIdentity(OscoreIdentity oscoreIdentity) {
return null;
}
public TbLwM2MSecurityInfo getX509ByEndpoint(String endPoint) {
TbLwM2MSecurityInfo store = lwM2MCredentialsSecurityInfoValidator.getEndpointSecurityInfoByCredentialsId(endPoint, BOOTSTRAP);
this.addValueToStore(store, store.getEndpoint());
@ -210,4 +216,4 @@ public class LwM2MBootstrapSecurityStore implements BootstrapSecurityStore {
}
return securityInfo;
}
}
}

22
common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/store/LwM2MConfigurationChecker.java

@ -58,17 +58,25 @@ public class LwM2MConfigurationChecker extends ConfigurationChecker {
for (Map.Entry<Integer, BootstrapConfig.ServerConfig> e : config.servers.entrySet()) {
BootstrapConfig.ServerConfig srvCfg = e.getValue();
// shortId checks
if (srvCfg.shortId == 0) {
throw new InvalidConfigurationException("short ID must not be 0");
}
// look for security entry
BootstrapConfig.ServerSecurity security = getSecurityEntry(config, srvCfg.shortId);
if (security == null) {
throw new InvalidConfigurationException("no security entry for server instance: " + e.getKey());
}
// BS Server
if (security.bootstrapServer && srvCfg.shortId != 0) {
throw new InvalidConfigurationException("short ID must be 0");
}
// LwM2M Server
/**
* This identifier uniquely identifies each LwM2M Server configured for the LwM2M Client.
* This Resource MUST be set when the Bootstrap-Server Resource has false value.
* Specific ID:0 and ID:65535 values MUST NOT be used for identifying the LwM2M Server (Section 6.3 of the LwM2M version 1.0 specification).
*/
if (!security.bootstrapServer && (srvCfg.shortId < 1 && srvCfg.shortId > 65534 )) {
throw new InvalidConfigurationException("Specific ID:0 and ID:65535 values MUST NOT be used for identifying the LwM2M Server");
}
}
}
@ -81,4 +89,4 @@ public class LwM2MConfigurationChecker extends ConfigurationChecker {
return null;
}
}
}

6
common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/store/LwM2MInMemoryBootstrapConfigStore.java

@ -16,9 +16,7 @@
package org.thingsboard.server.transport.lwm2m.bootstrap.store;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.leshan.core.request.Identity;
import org.eclipse.leshan.server.bootstrap.BootstrapConfig;
import org.eclipse.leshan.server.bootstrap.BootstrapSession;
import org.eclipse.leshan.server.bootstrap.ConfigurationChecker;
import org.eclipse.leshan.server.bootstrap.InMemoryBootstrapConfigStore;
import org.eclipse.leshan.server.bootstrap.InvalidConfigurationException;
@ -39,8 +37,8 @@ public class LwM2MInMemoryBootstrapConfigStore extends InMemoryBootstrapConfigSt
private final Lock writeLock = readWriteLock.writeLock();
protected final ConfigurationChecker configChecker = new LwM2MConfigurationChecker();
@Override
public BootstrapConfig get(String endpoint, Identity deviceIdentity, BootstrapSession session) {
public BootstrapConfig get(String endpoint) {
return bootstrapByEndpoint.get(endpoint);
}

5
common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/config/LwM2MTransportServerConfig.java

@ -18,6 +18,7 @@ package org.thingsboard.server.transport.lwm2m.config;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.californium.elements.config.Configuration;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
@ -105,6 +106,10 @@ public class LwM2MTransportServerConfig implements LwM2MSecureServerConfig {
@Setter
private List<TbProperty> networkConfig;
@Getter
@Setter
private Configuration coapConfig;
@Bean
@ConfigurationProperties(prefix = "transport.lwm2m.server.security.credentials")
public SslCredentialsConfig lwm2mServerCredentials() {

2
common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/secure/LwM2mCredentialsSecurityInfoValidator.java

@ -74,7 +74,7 @@ public class LwM2mCredentialsSecurityInfoValidator {
@Override
public void onError(Throwable e) {
log.trace("[{}] [{}] Failed to process credentials ", credentialsId, e);
log.info("[{}] [{}] Failed to process credentials ", credentialsId, e);
TbLwM2MSecurityInfo result = new TbLwM2MSecurityInfo();
result.setEndpoint(credentialsId);
resultSecurityStore[0] = result;

38
common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/secure/TbLwM2MAuthorizer.java

@ -18,9 +18,11 @@ package org.thingsboard.server.transport.lwm2m.secure;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.leshan.core.SecurityMode;
import org.eclipse.leshan.core.request.Identity;
import org.eclipse.leshan.core.peer.LwM2mPeer;
import org.eclipse.leshan.core.peer.X509Identity;
import org.eclipse.leshan.core.request.UplinkRequest;
import org.eclipse.leshan.server.registration.Registration;
import org.eclipse.leshan.server.security.Authorization;
import org.eclipse.leshan.server.security.Authorizer;
import org.eclipse.leshan.server.security.SecurityChecker;
import org.eclipse.leshan.server.security.SecurityInfo;
@ -45,38 +47,30 @@ public class TbLwM2MAuthorizer implements Authorizer {
private final LwM2mClientContext clientContext;
@Override
public Registration isAuthorized(UplinkRequest<?> request, Registration registration, Identity senderIdentity) {
if (senderIdentity.isX509()) {
TbX509DtlsSessionInfo sessionInfo = sessionStorage.get(registration.getEndpoint());
if (sessionInfo != null) {
if (sessionInfo.getX509CommonName().endsWith(senderIdentity.getX509CommonName())) {
clientContext.registerClient(registration, sessionInfo.getCredentials());
public Authorization isAuthorized(UplinkRequest<?> request, Registration registration, LwM2mPeer sender) {
SecurityInfo expectedSecurityInfo = null;
if (securityStore != null) expectedSecurityInfo = securityStore.getByEndpoint(registration.getEndpoint());
if (securityChecker.checkSecurityInfo(registration.getEndpoint(), sender, expectedSecurityInfo)) {
if (sender.getIdentity() instanceof X509Identity) {
TbX509DtlsSessionInfo sessionInfo = sessionStorage.get(registration.getEndpoint());
if (sessionInfo != null) {
// X509 certificate is valid and matches endpoint.
return registration;
} else {
// X509 certificate is not valid.
return null;
clientContext.registerClient(registration, sessionInfo.getCredentials());
}
}
// If session info is not found, this may be the trusted certificate, so we still need to check all other options below.
}
SecurityInfo expectedSecurityInfo;
try {
expectedSecurityInfo = securityStore.getByEndpoint(registration.getEndpoint());
if (expectedSecurityInfo != null && expectedSecurityInfo.usePSK() && expectedSecurityInfo.getEndpoint().equals(SecurityMode.NO_SEC.toString())
&& expectedSecurityInfo.getIdentity().equals(SecurityMode.NO_SEC.toString())
&& expectedSecurityInfo.getPskIdentity().equals(SecurityMode.NO_SEC.toString())
&& Arrays.equals(SecurityMode.NO_SEC.toString().getBytes(), expectedSecurityInfo.getPreSharedKey())) {
expectedSecurityInfo = null;
return Authorization.declined();
}
} catch (LwM2MAuthException e) {
log.info("Registration failed: FORBIDDEN, endpointId: [{}]", registration.getEndpoint());
return null;
return Authorization.declined();
}
if (securityChecker.checkSecurityInfo(registration.getEndpoint(), senderIdentity, expectedSecurityInfo)) {
return registration;
return Authorization.approved();
} else {
securityStore.remove(registration.getEndpoint(), registration.getId());
return null;
return Authorization.declined();
}
}
}

3
common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/AbstractLwM2mTransportResource.java

@ -18,12 +18,13 @@ package org.thingsboard.server.transport.lwm2m.server;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.californium.core.server.resources.CoapExchange;
import org.eclipse.leshan.core.californium.LwM2mCoapResource;
import org.eclipse.leshan.core.californium.identity.IdentityHandlerProvider;
@Slf4j
public abstract class AbstractLwM2mTransportResource extends LwM2mCoapResource {
public AbstractLwM2mTransportResource(String name) {
super(name);
super(name, new IdentityHandlerProvider());
}
@Override

116
common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/DefaultLwM2mTransportService.java

@ -15,16 +15,22 @@
*/
package org.thingsboard.server.transport.lwm2m.server;
import jakarta.annotation.PreDestroy;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.californium.elements.config.Configuration;
import org.eclipse.californium.scandium.config.DtlsConfig;
import org.eclipse.californium.scandium.config.DtlsConnectorConfig;
import org.eclipse.californium.scandium.dtls.cipher.CipherSuite;
import org.eclipse.leshan.core.endpoint.Protocol;
import org.eclipse.leshan.core.node.codec.DefaultLwM2mDecoder;
import org.eclipse.leshan.core.node.codec.DefaultLwM2mEncoder;
import org.eclipse.leshan.server.californium.LeshanServer;
import org.eclipse.leshan.server.californium.LeshanServerBuilder;
import org.eclipse.leshan.server.californium.registration.CaliforniumRegistrationStore;
import org.eclipse.leshan.server.LeshanServer;
import org.eclipse.leshan.server.LeshanServerBuilder;
import org.eclipse.leshan.server.californium.LwM2mPskStore;
import org.eclipse.leshan.server.californium.endpoint.CaliforniumServerEndpointsProvider;
import org.eclipse.leshan.server.californium.endpoint.coap.CoapServerProtocolProvider;
import org.eclipse.leshan.server.californium.endpoint.coaps.CoapsServerProtocolProvider;
import org.eclipse.leshan.server.registration.RegistrationStore;
import org.springframework.context.annotation.DependsOn;
import org.springframework.stereotype.Component;
import org.thingsboard.server.cache.ota.OtaPackageDataCache;
@ -37,13 +43,11 @@ import org.thingsboard.server.transport.lwm2m.secure.TbLwM2MAuthorizer;
import org.thingsboard.server.transport.lwm2m.secure.TbLwM2MDtlsCertificateVerifier;
import org.thingsboard.server.transport.lwm2m.server.store.TbSecurityStore;
import org.thingsboard.server.transport.lwm2m.server.uplink.LwM2mUplinkMsgHandler;
import org.thingsboard.server.transport.lwm2m.utils.LwM2mValueConverterImpl;
import jakarta.annotation.PreDestroy;
import java.net.InetSocketAddress;
import java.security.cert.X509Certificate;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static org.eclipse.californium.scandium.config.DtlsConfig.DTLS_RECOMMENDED_CIPHER_SUITES_ONLY;
import static org.eclipse.californium.scandium.config.DtlsConfig.DTLS_RECOMMENDED_CURVES_ONLY;
import static org.eclipse.californium.scandium.config.DtlsConfig.DTLS_RETRANSMISSION_TIMEOUT;
import static org.eclipse.californium.scandium.config.DtlsConfig.DTLS_ROLE;
@ -69,7 +73,7 @@ public class DefaultLwM2mTransportService implements LwM2MTransportService {
private final LwM2MTransportServerConfig config;
private final OtaPackageDataCache otaPackageDataCache;
private final LwM2mUplinkMsgHandler handler;
private final CaliforniumRegistrationStore registrationStore;
private final RegistrationStore registrationStore;
private final TbSecurityStore securityStore;
private final TbLwM2MDtlsCertificateVerifier certificateVerifier;
private final TbLwM2MAuthorizer authorizer;
@ -87,8 +91,7 @@ public class DefaultLwM2mTransportService implements LwM2MTransportService {
* nameFile = "BC68JAR01A09_TO_BC68JAR01A10.bin"
* "coap://host:port/{path}/{token}/{nameFile}"
*/
LwM2mTransportCoapResource otaCoapResource = new LwM2mTransportCoapResource(otaPackageDataCache, FIRMWARE_UPDATE_COAP_RESOURCE);
this.server.coap().getServer().add(otaCoapResource);
new LwM2mTransportCoapResource(otaPackageDataCache, FIRMWARE_UPDATE_COAP_RESOURCE);
this.context.setServer(server);
this.startLhServer();
}
@ -117,54 +120,101 @@ public class DefaultLwM2mTransportService implements LwM2MTransportService {
private LeshanServer getLhServer() {
LeshanServerBuilder builder = new LeshanServerBuilder();
builder.setLocalAddress(config.getHost(), config.getPort());
builder.setLocalSecureAddress(config.getSecureHost(), config.getSecurePort());
builder.setDecoder(new DefaultLwM2mDecoder());
/* Use a magic converter to support bad type send by the UI. */
builder.setEncoder(new DefaultLwM2mEncoder(LwM2mValueConverterImpl.getInstance()));
/* Create CoAP Config */
builder.setCoapConfig(getCoapConfig(config.getPort(), config.getSecurePort(), config));
/* Define model provider (Create Models )*/
builder.setObjectModelProvider(modelProvider);
/* Set securityStore with new registrationStore */
builder.setSecurityStore(securityStore);
builder.setRegistrationStore(registrationStore);
// Create Californium Endpoints Provider:
// ------------------
// Create Server Endpoints Provider
CaliforniumServerEndpointsProvider.Builder endpointsBuilder = new CaliforniumServerEndpointsProvider.Builder(
// Add coap Protocol support
new CoapServerProtocolProvider(),
// Add coaps/dtls protocol support
new CoapsServerProtocolProvider(c -> {
if (this.config.getSslCredentials() != null) {
c.setAdvancedCertificateVerifier(certificateVerifier);
c.setAsList(DtlsConfig.DTLS_CIPHER_SUITES, RPK_OR_X509_CIPHER_SUITES);
} else {
log.info("Unable to load X509 files for LWM2MServer");
LwM2mPskStore lwM2mPskStore = new LwM2mPskStore(securityStore, registrationStore);
c.setAdvancedPskStore(lwM2mPskStore);
c.setAsList(DtlsConfig.DTLS_CIPHER_SUITES, PSK_CIPHER_SUITES);
}
}));
// Create Californium Configuration
Configuration serverCoapConfig = endpointsBuilder.createDefaultConfiguration();
getCoapConfig(serverCoapConfig, config.getPort(), config.getSecurePort(), config);
// Set some DTLS stuff
serverCoapConfig.setTransient(DTLS_RECOMMENDED_CURVES_ONLY);
serverCoapConfig.set(DTLS_RECOMMENDED_CURVES_ONLY, config.isRecommendedSupportedGroups());
serverCoapConfig.setTransient(DtlsConfig.DTLS_RECOMMENDED_CIPHER_SUITES_ONLY);
serverCoapConfig.set(DtlsConfig.DTLS_RECOMMENDED_CIPHER_SUITES_ONLY, config.isRecommendedCiphers());
serverCoapConfig.set(DTLS_RETRANSMISSION_TIMEOUT, config.getDtlsRetransmissionTimeout(), MILLISECONDS);
serverCoapConfig.set(DTLS_ROLE, SERVER_ONLY);
serverCoapConfig.setTransient(DtlsConfig.DTLS_CONNECTION_ID_LENGTH);
/**
* "Control usage of DTLS connection ID.",
* If DTLS_CONNECTION_ID_LENGTH enables the use of a connection id, this node id could be used
* to configure the generation of connection ids specific for node in a multi-node deployment (cluster).
* The value is used as first byte in generated connection ids.
* DTLS connection ID length. <blank> disabled,
* 0 enables support without active use of CID.", defaultValue == null, minimumValue == 0);
* "- 'on' to activate Connection ID support ",
* " (same as -cid 6)",
* "- 'off' to deactivate it",
* "- Positive value define the size in byte of CID generated.",
* "- 0 value means we accept to use CID but will not generated one for foreign peer.",
*/
serverCoapConfig.set(DtlsConfig.DTLS_CONNECTION_ID_LENGTH, 6);
/* Create DTLS Config */
DtlsConnectorConfig.Builder dtlsConfig = new DtlsConnectorConfig.Builder(getCoapConfig(config.getPort(), config.getSecurePort(), config));
this.setServerWithCredentials(builder);
// Set Californium Configuration
endpointsBuilder.setConfiguration(serverCoapConfig);
// Create CoAP endpoint
InetSocketAddress coapAddr = new InetSocketAddress(config.getHost(), config.getPort());
endpointsBuilder.addEndpoint(coapAddr, Protocol.COAP);
dtlsConfig.set(DTLS_RECOMMENDED_CURVES_ONLY, config.isRecommendedSupportedGroups());
dtlsConfig.set(DTLS_RECOMMENDED_CIPHER_SUITES_ONLY, config.isRecommendedCiphers());
dtlsConfig.set(DTLS_RETRANSMISSION_TIMEOUT, config.getDtlsRetransmissionTimeout(), MILLISECONDS);
dtlsConfig.set(DTLS_ROLE, SERVER_ONLY);
// Create CoAP over DTLS endpoint
InetSocketAddress coapsAddr = new InetSocketAddress(config.getSecureHost(), config.getSecurePort());
endpointsBuilder.addEndpoint(coapsAddr, Protocol.COAPS);
/* Create credentials */
this.setServerWithCredentials(builder, dtlsConfig);
/* Set DTLS Config */
builder.setDtlsConfig(dtlsConfig);
builder.setDecoder(new DefaultLwM2mDecoder(true));
builder.setEncoder(new DefaultLwM2mEncoder(true));
/* Create LWM2M server */
// Create LWM2M server
builder.setEndpointsProviders(endpointsBuilder.build());
return builder.build();
}
private void setServerWithCredentials(LeshanServerBuilder builder, DtlsConnectorConfig.Builder dtlsConfig) {
private void setServerWithCredentials(LeshanServerBuilder builder) {
// private void setServerWithCredentials(LeshanServerBuilder builder) {
if (this.config.getSslCredentials() != null) {
SslCredentials sslCredentials = this.config.getSslCredentials();
builder.setPublicKey(sslCredentials.getPublicKey());
builder.setPrivateKey(sslCredentials.getPrivateKey());
builder.setCertificateChain(sslCredentials.getCertificateChain());
dtlsConfig.setAdvancedCertificateVerifier(certificateVerifier);
builder.setAuthorizer(authorizer);
dtlsConfig.setAsList(DtlsConfig.DTLS_CIPHER_SUITES, RPK_OR_X509_CIPHER_SUITES);
} else {
/* by default trust all */
builder.setTrustedCertificates(new X509Certificate[0]);
log.info("Unable to load X509 files for LWM2MServer");
dtlsConfig.setAsList(DtlsConfig.DTLS_CIPHER_SUITES, PSK_CIPHER_SUITES);
}
}

5
common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2MNetworkConfig.java

@ -27,8 +27,7 @@ import static org.eclipse.californium.core.config.CoapConfig.DEFAULT_BLOCKWISE_S
public class LwM2MNetworkConfig {
public static Configuration getCoapConfig(Integer serverPortNoSec, Integer serverSecurePort, LwM2MTransportServerConfig config) {
Configuration coapConfig = new Configuration();
public static Configuration getCoapConfig(Configuration coapConfig, Integer serverPortNoSec, Integer serverSecurePort, LwM2MTransportServerConfig config) {
coapConfig.set(CoapConfig.COAP_PORT, serverPortNoSec);
coapConfig.set(CoapConfig.COAP_SECURE_PORT, serverSecurePort);
/**
@ -107,4 +106,4 @@ public class LwM2MNetworkConfig {
return coapConfig;
}
}
}

33
common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mServerListener.java

@ -16,7 +16,7 @@
package org.thingsboard.server.transport.lwm2m.server;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.leshan.core.node.LwM2mNode;
import org.eclipse.leshan.core.node.TimestampedLwM2mNodes;
import org.eclipse.leshan.core.observation.CompositeObservation;
import org.eclipse.leshan.core.observation.Observation;
import org.eclipse.leshan.core.observation.SingleObservation;
@ -32,7 +32,6 @@ import org.eclipse.leshan.server.send.SendListener;
import org.thingsboard.server.transport.lwm2m.server.uplink.LwM2mUplinkMsgHandler;
import java.util.Collection;
import java.util.Map;
import static org.thingsboard.server.transport.lwm2m.utils.LwM2MTransportUtil.convertObjectIdToVersionedId;
@ -94,9 +93,10 @@ public class LwM2mServerListener {
@Override
public void cancelled(Observation observation) {
//TODO: should be able to use CompositeObservation
log.trace("Canceled Observation {}.", ((SingleObservation)observation).getPath());
}
log.trace("Canceled Observation [RegistrationId:{}: {}].", observation.getRegistrationId(), observation instanceof SingleObservation ?
"SingleObservation: " + ((SingleObservation) observation).getPath() :
"CompositeObservation: " + ((CompositeObservation) observation).getPaths());
}
@Override
public void onResponse(SingleObservation observation, Registration registration, ObserveResponse response) {
@ -107,31 +107,40 @@ public class LwM2mServerListener {
@Override
public void onResponse(CompositeObservation observation, Registration registration, ObserveCompositeResponse response) {
throw new RuntimeException("Not implemented yet!");
log.trace("Update Composite Observation [{}: {}].", observation.getRegistrationId(), observation.getPaths());
service.onUpdateValueAfterReadCompositeResponse(registration, response);
}
@Override
public void onError(Observation observation, Registration registration, Exception error) {
if (error != null) {
//TODO: should be able to use CompositeObservation
log.debug("Unable to handle notification of [{}:{}] [{}]", observation.getRegistrationId(), ((SingleObservation)observation).getPath(), error.getMessage());
var path = observation instanceof SingleObservation ? "Single Observation Cancel: " + ((SingleObservation) observation).getPath() : "Composite Observation Cancel: " + ((CompositeObservation) observation).getPaths();
var msgError = path + ": " + error.getMessage();
log.trace("Unable to handle notification [RegistrationId:{}]: [{}].", observation.getRegistrationId(), msgError);
service.onErrorObservation(registration, msgError);
}
}
@Override
public void newObservation(Observation observation, Registration registration) {
//TODO: should be able to use CompositeObservation
log.trace("Successful start newObservation {}.", ((SingleObservation)observation).getPath());
log.trace("Successful start newObservation [RegistrationId:{}: {}].", observation.getRegistrationId(), observation instanceof SingleObservation ?
"Single: " + ((SingleObservation) observation).getPath() :
"Composite: " + ((CompositeObservation) observation).getPaths());
}
};
public final SendListener sendListener = new SendListener() {
@Override
public void dataReceived(Registration registration, Map<String, LwM2mNode> map, SendRequest sendRequest) {
public void dataReceived(Registration registration, TimestampedLwM2mNodes data, SendRequest request) {
if (registration != null) {
service.onUpdateValueWithSendRequest(registration, sendRequest);
service.onUpdateValueWithSendRequest(registration, request);
}
}
@Override
public void onError(Registration registration, String errorMessage, Exception error) {
}
};
}

2
common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mTransportContext.java

@ -17,7 +17,7 @@ package org.thingsboard.server.transport.lwm2m.server;
import lombok.Getter;
import lombok.Setter;
import org.eclipse.leshan.server.californium.LeshanServer;
import org.eclipse.leshan.server.LeshanServer;
import org.springframework.stereotype.Component;
import org.thingsboard.server.common.transport.TransportContext;
import org.thingsboard.server.queue.util.TbLwM2mTransportComponent;

6
common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mTransportServerHelper.java

@ -16,10 +16,10 @@
package org.thingsboard.server.transport.lwm2m.server;
import com.google.gson.JsonParser;
import jakarta.annotation.Nonnull;
import jakarta.annotation.Nullable;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.leshan.core.model.DDFFileParser;
import org.eclipse.leshan.core.model.DefaultDDFFileValidator;
import org.eclipse.leshan.core.model.InvalidDDFFileException;
import org.eclipse.leshan.core.model.ObjectModel;
import org.eclipse.leshan.core.model.ResourceModel;
@ -34,8 +34,6 @@ import org.thingsboard.server.gen.transport.TransportProtos.PostTelemetryMsg;
import org.thingsboard.server.gen.transport.TransportProtos.SessionInfoProto;
import org.thingsboard.server.queue.util.TbLwM2mTransportComponent;
import jakarta.annotation.Nonnull;
import jakarta.annotation.Nullable;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.util.ArrayList;

10
common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mVersionedModelProvider.java

@ -16,7 +16,7 @@
package org.thingsboard.server.transport.lwm2m.server;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.leshan.core.model.DefaultDDFFileValidator;
import org.eclipse.leshan.core.LwM2m;
import org.eclipse.leshan.core.model.LwM2mModel;
import org.eclipse.leshan.core.model.ObjectModel;
import org.eclipse.leshan.core.model.ResourceModel;
@ -107,7 +107,7 @@ public class LwM2mVersionedModelProvider implements LwM2mModelProvider {
@Override
public ObjectModel getObjectModel(int objectId) {
String version = registration.getSupportedVersion(objectId);
String version = String.valueOf(registration.getSupportedVersion(objectId));
if (version != null) {
return this.getObjectModelDynamic(objectId, version);
}
@ -116,10 +116,10 @@ public class LwM2mVersionedModelProvider implements LwM2mModelProvider {
@Override
public Collection<ObjectModel> getObjectModels() {
Map<Integer, String> supportedObjects = this.registration.getSupportedObject();
Map<Integer, LwM2m.Version> supportedObjects = this.registration.getSupportedObject();
Collection<ObjectModel> result = new ArrayList<>(supportedObjects.size());
for (Map.Entry<Integer, String> supportedObject : supportedObjects.entrySet()) {
ObjectModel objectModel = this.getObjectModelDynamic(supportedObject.getKey(), supportedObject.getValue());
for (Map.Entry<Integer, LwM2m.Version> supportedObject : supportedObjects.entrySet()) {
ObjectModel objectModel = this.getObjectModelDynamic(supportedObject.getKey(), String.valueOf(supportedObject.getValue()));
if (objectModel != null) {
result.add(objectModel);
}

21
common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClient.java

@ -20,7 +20,8 @@ import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.leshan.core.link.LinkParamValue;
import org.eclipse.leshan.core.LwM2m;
import org.eclipse.leshan.core.link.attributes.Attribute;
import org.eclipse.leshan.core.model.ObjectModel;
import org.eclipse.leshan.core.model.ResourceModel;
import org.eclipse.leshan.core.node.LwM2mMultipleResource;
@ -271,7 +272,7 @@ public class LwM2mClient {
public ResourceModel getResourceModel(String pathIdVer, LwM2mModelProvider modelProvider) {
LwM2mPath pathIds = new LwM2mPath(fromVersionedIdToObjectId(pathIdVer));
String verSupportedObject = registration.getSupportedObject().get(pathIds.getObjectId());
String verSupportedObject = String.valueOf(registration.getSupportedObject().get(pathIds.getObjectId()));
String verRez = getVerFromPathIdVerOrId(pathIdVer);
return verRez != null && verRez.equals(verSupportedObject) ? modelProvider.getObjectModel(registration)
.getResourceModel(pathIds.getObjectId(), pathIds.getResourceId()) : null;
@ -289,7 +290,7 @@ public class LwM2mClient {
public ObjectModel getObjectModel(String pathIdVer, LwM2mModelProvider modelProvider) {
try {
LwM2mPath pathIds = new LwM2mPath(fromVersionedIdToObjectId(pathIdVer));
String verSupportedObject = registration.getSupportedObject().get(pathIds.getObjectId());
String verSupportedObject = String.valueOf(registration.getSupportedObject().get(pathIds.getObjectId()));
String verRez = getVerFromPathIdVerOrId(pathIdVer);
return verRez != null && verRez.equals(verSupportedObject) ? modelProvider.getObjectModel(registration)
.getObjectModel(pathIds.getObjectId()) : null;
@ -370,12 +371,12 @@ public class LwM2mClient {
public String isValidObjectVersion(String path) {
LwM2mPath pathIds = new LwM2mPath(fromVersionedIdToObjectId(path));
String verSupportedObject = registration.getSupportedObject().get(pathIds.getObjectId());
LwM2m.Version verSupportedObject = registration.getSupportedObject().get(pathIds.getObjectId());
if (verSupportedObject == null) {
return String.format("Specified object id %s absent in the list supported objects of the client or is security object!", pathIds.getObjectId());
} else {
String verRez = getVerFromPathIdVerOrId(path);
if (verRez == null || !verRez.equals(verSupportedObject)) {
if (verRez == null || !verRez.equals(verSupportedObject.toString())) {
return String.format("Specified resource id %s is not valid version! Must be version: %s", path, verSupportedObject);
}
}
@ -432,16 +433,12 @@ public class LwM2mClient {
private static Set<ContentFormat> clientSupportContentFormat(Registration registration) {
Set<ContentFormat> contentFormats = new HashSet<>();
contentFormats.add(ContentFormat.DEFAULT);
LinkParamValue ct = Arrays.stream(registration.getObjectLinks())
Attribute ct = Arrays.stream(registration.getObjectLinks())
.filter(link -> link.getUriReference().equals("/"))
.findFirst()
.map(link -> link.getLinkParams().get("ct")).orElse(null);
.map(link -> link.getAttributes().get("ct")).orElse(null);
if (ct != null) {
Set<ContentFormat> codes = Stream.of(ct.getUnquoted().replaceAll("\"", "").split(" ", -1))
.map(String::trim)
.map(Integer::parseInt)
.map(ContentFormat::fromCode)
.collect(Collectors.toSet());
Set codes = Stream.of(ct.getValue()).collect(Collectors.toSet());
contentFormats.addAll(codes);
}
return contentFormats;

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

@ -15,12 +15,15 @@
*/
package org.thingsboard.server.transport.lwm2m.server.downlink;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.leshan.core.LwM2m;
import org.eclipse.leshan.core.attributes.Attribute;
import org.eclipse.leshan.core.attributes.AttributeSet;
import org.eclipse.leshan.core.link.Link;
import org.eclipse.leshan.core.link.lwm2m.attributes.LwM2mAttribute;
import org.eclipse.leshan.core.link.lwm2m.attributes.LwM2mAttributeModel;
import org.eclipse.leshan.core.link.lwm2m.attributes.LwM2mAttributeSet;
import org.eclipse.leshan.core.model.LwM2mModel;
import org.eclipse.leshan.core.model.ObjectModel;
import org.eclipse.leshan.core.model.ResourceModel;
@ -28,6 +31,7 @@ import org.eclipse.leshan.core.node.LwM2mObjectInstance;
import org.eclipse.leshan.core.node.LwM2mPath;
import org.eclipse.leshan.core.node.LwM2mResource;
import org.eclipse.leshan.core.node.ObjectLink;
import org.eclipse.leshan.core.observation.CompositeObservation;
import org.eclipse.leshan.core.observation.Observation;
import org.eclipse.leshan.core.observation.SingleObservation;
import org.eclipse.leshan.core.request.CompositeDownlinkRequest;
@ -37,6 +41,7 @@ import org.eclipse.leshan.core.request.DeleteRequest;
import org.eclipse.leshan.core.request.DiscoverRequest;
import org.eclipse.leshan.core.request.DownlinkRequest;
import org.eclipse.leshan.core.request.ExecuteRequest;
import org.eclipse.leshan.core.request.ObserveCompositeRequest;
import org.eclipse.leshan.core.request.ObserveRequest;
import org.eclipse.leshan.core.request.ReadCompositeRequest;
import org.eclipse.leshan.core.request.ReadRequest;
@ -52,6 +57,7 @@ import org.eclipse.leshan.core.response.DeleteResponse;
import org.eclipse.leshan.core.response.DiscoverResponse;
import org.eclipse.leshan.core.response.ExecuteResponse;
import org.eclipse.leshan.core.response.LwM2mResponse;
import org.eclipse.leshan.core.response.ObserveCompositeResponse;
import org.eclipse.leshan.core.response.ObserveResponse;
import org.eclipse.leshan.core.response.ReadCompositeResponse;
import org.eclipse.leshan.core.response.ReadResponse;
@ -63,6 +69,8 @@ import org.eclipse.leshan.server.model.LwM2mModelProvider;
import org.eclipse.leshan.server.registration.Registration;
import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.device.profile.lwm2m.ObjectAttributes;
import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.queue.util.TbLwM2mTransportComponent;
import org.thingsboard.server.transport.lwm2m.config.LwM2MTransportServerConfig;
import org.thingsboard.server.transport.lwm2m.server.LwM2mTransportContext;
@ -70,31 +78,43 @@ import org.thingsboard.server.transport.lwm2m.server.LwM2mVersionedModelProvider
import org.thingsboard.server.transport.lwm2m.server.client.LwM2mClient;
import org.thingsboard.server.transport.lwm2m.server.client.LwM2mClientContext;
import org.thingsboard.server.transport.lwm2m.server.common.LwM2MExecutorAwareService;
import org.thingsboard.server.transport.lwm2m.server.downlink.composite.TbLwM2MCancelObserveCompositeRequest;
import org.thingsboard.server.transport.lwm2m.server.downlink.composite.TbLwM2MObserveCompositeRequest;
import org.thingsboard.server.transport.lwm2m.server.downlink.composite.TbLwM2MReadCompositeRequest;
import org.thingsboard.server.transport.lwm2m.server.log.LwM2MTelemetryLogService;
import org.thingsboard.server.transport.lwm2m.server.rpc.composite.RpcWriteCompositeRequest;
import org.thingsboard.server.transport.lwm2m.utils.LwM2mValueConverterImpl;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import java.util.Arrays;
import java.util.Collection;
import java.util.Date;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.TreeSet;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import static org.eclipse.leshan.core.attributes.Attribute.GREATER_THAN;
import static org.eclipse.leshan.core.attributes.Attribute.LESSER_THAN;
import static org.eclipse.leshan.core.attributes.Attribute.MAXIMUM_PERIOD;
import static org.eclipse.leshan.core.attributes.Attribute.MINIMUM_PERIOD;
import static org.eclipse.leshan.core.attributes.Attribute.STEP;
import static org.eclipse.leshan.core.link.lwm2m.attributes.LwM2mAttributes.DIMENSION;
import static org.eclipse.leshan.core.link.lwm2m.attributes.LwM2mAttributes.ENABLER_VERSION;
import static org.eclipse.leshan.core.link.lwm2m.attributes.LwM2mAttributes.EVALUATE_MAXIMUM_PERIOD;
import static org.eclipse.leshan.core.link.lwm2m.attributes.LwM2mAttributes.EVALUATE_MINIMUM_PERIOD;
import static org.eclipse.leshan.core.link.lwm2m.attributes.LwM2mAttributes.GREATER_THAN;
import static org.eclipse.leshan.core.link.lwm2m.attributes.LwM2mAttributes.LESSER_THAN;
import static org.eclipse.leshan.core.link.lwm2m.attributes.LwM2mAttributes.MAXIMUM_PERIOD;
import static org.eclipse.leshan.core.link.lwm2m.attributes.LwM2mAttributes.MINIMUM_PERIOD;
import static org.eclipse.leshan.core.link.lwm2m.attributes.LwM2mAttributes.OBJECT_VERSION;
import static org.eclipse.leshan.core.link.lwm2m.attributes.LwM2mAttributes.SERVER_URI;
import static org.eclipse.leshan.core.link.lwm2m.attributes.LwM2mAttributes.SHORT_SERVER_ID;
import static org.eclipse.leshan.core.link.lwm2m.attributes.LwM2mAttributes.STEP;
import static org.eclipse.leshan.core.model.ResourceModel.Type.OBJLNK;
import static org.eclipse.leshan.core.model.ResourceModel.Type.OPAQUE;
import static org.thingsboard.server.transport.lwm2m.utils.LwM2MTransportUtil.convertMultiResourceValuesFromRpcBody;
@ -148,8 +168,9 @@ public class DefaultLwM2mDownlinkMsgHandler extends LwM2MExecutorAwareService im
@Override
public void sendReadCompositeRequest(LwM2mClient client, TbLwM2MReadCompositeRequest request,
DownlinkRequestCallback<ReadCompositeRequest, ReadCompositeResponse> callback, ContentFormat compositeContentFormat) {
DownlinkRequestCallback<ReadCompositeRequest, ReadCompositeResponse> callback) {
try {
ContentFormat compositeContentFormat = this.findFirstContentFormatForComposite(client.getClientSupportContentFormats());
ReadCompositeRequest downlink = new ReadCompositeRequest(compositeContentFormat, compositeContentFormat, request.getObjectIds());
sendCompositeRequest(client, downlink, this.config.getTimeout(), callback);
} catch (InvalidRequestException e) {
@ -162,23 +183,19 @@ public class DefaultLwM2mDownlinkMsgHandler extends LwM2MExecutorAwareService im
try {
validateVersionedId(client, request);
LwM2mPath resultIds = new LwM2mPath(request.getObjectId());
Set<Observation> observations = context.getServer().getObservationService().getObservations(client.getRegistration());
//TODO: should be able to use CompositeObservation
if (observations.stream().noneMatch(observation -> ((SingleObservation)observation).getPath().equals(resultIds))) {
ObserveRequest downlink;
ContentFormat contentFormat = getReadRequestContentFormat(client, request, modelProvider);
if (resultIds.isResource()) {
downlink = new ObserveRequest(contentFormat, resultIds.getObjectId(), resultIds.getObjectInstanceId(), resultIds.getResourceId());
} else if (resultIds.isObjectInstance()) {
downlink = new ObserveRequest(contentFormat, resultIds.getObjectId(), resultIds.getObjectInstanceId());
} else {
downlink = new ObserveRequest(contentFormat, resultIds.getObjectId());
}
log.info("[{}] Send observation: {}.", client.getEndpoint(), request.getVersionedId());
sendSimpleRequest(client, downlink, request.getTimeout(), callback);
ObserveRequest downlink;
ContentFormat contentFormat = getReadRequestContentFormat(client, request, modelProvider);
if (resultIds.isResourceInstance()) {
downlink = new ObserveRequest(contentFormat, resultIds.getObjectId(), resultIds.getObjectInstanceId(), resultIds.getResourceId(), resultIds.getResourceInstanceId());
} else if (resultIds.isResource()) {
downlink = new ObserveRequest(contentFormat, resultIds.getObjectId(), resultIds.getObjectInstanceId(), resultIds.getResourceId());
} else if (resultIds.isObjectInstance()) {
downlink = new ObserveRequest(contentFormat, resultIds.getObjectId(), resultIds.getObjectInstanceId());
} else {
callback.onValidationError(resultIds.toString(), "Observation is already registered!");
downlink = new ObserveRequest(contentFormat, resultIds.getObjectId());
}
log.info("[{}] Send observation: {}.", client.getEndpoint(), request.getVersionedId());
sendSimpleRequest(client, downlink, request.getTimeout(), callback);
} catch (InvalidRequestException e) {
callback.onValidationError(request.toString(), e.getMessage());
}
@ -187,14 +204,90 @@ public class DefaultLwM2mDownlinkMsgHandler extends LwM2MExecutorAwareService im
@Override
public void sendObserveAllRequest(LwM2mClient client, TbLwM2MObserveAllRequest request, DownlinkRequestCallback<TbLwM2MObserveAllRequest, Set<String>> callback) {
Set<Observation> observations = context.getServer().getObservationService().getObservations(client.getRegistration());
//TODO: should be able to use CompositeObservation
Set<String> paths = observations.stream().map(observation -> ((SingleObservation)observation).getPath().toString()).collect(Collectors.toUnmodifiableSet());
Set<String> paths = new LinkedHashSet<>();
observations.stream().forEach(observation -> {
if (observation instanceof SingleObservation) {
paths.add("SingleObservation:" + ((SingleObservation) observation).getPath().toString());
} else {
List<LwM2mPath> listPath = ((CompositeObservation) observation).getPaths();
List<String> pathsComposite = listPath.stream().map(lwM2mPath -> (lwM2mPath.toString())).collect(Collectors.toList());
Set <String> pathsCompositeSort = new TreeSet<>();
if (pathsComposite.size() == 1) {
pathsCompositeSort.add("CompositeObservation: [" + pathsComposite.get(0) + "]");
} else if (pathsComposite.size() > 1) {
List <String> sort = new LinkedList<>();
sort.addAll(pathsComposite);
sort.set(0, "CompositeObservation: [" + sort.get(0));
sort.set(pathsComposite.size()-1, sort.get(pathsComposite.size()-1) + "]");
pathsCompositeSort = new LinkedHashSet<>(sort);
}
paths.addAll(pathsCompositeSort);
}
});
callback.onSuccess(request, paths);
}
/**
* if resource (SingleObservation or in CompositeObservation) is already registered - return BAD REQUEST
*/
@Override
public void sendObserveCompositeRequest(LwM2mClient client, TbLwM2MObserveCompositeRequest request, DownlinkRequestCallback<ObserveCompositeRequest,
ObserveCompositeResponse> callback) {
try {
log.trace("[{}] Send Composite observation: [{}].", client.getEndpoint(), request.getObjectIds());
ContentFormat compositeContentFormat = this.findFirstContentFormatForComposite(client.getClientSupportContentFormats());
ObserveCompositeRequest downlink = new ObserveCompositeRequest(compositeContentFormat, compositeContentFormat, request.getObjectIds());
sendCompositeRequest(client, downlink, this.config.getTimeout(), callback);
} catch (InvalidRequestException e) {
callback.onValidationError(request.toString(), e.getMessage());
}
}
@Override
public void sendCancelObserveCompositeRequest(LwM2mClient client, TbLwM2MCancelObserveCompositeRequest request, DownlinkRequestCallback<TbLwM2MCancelObserveCompositeRequest, Integer> callback) {
try {
Set<Observation> observations = context.getServer().getObservationService().getObservations(client.getRegistration());
List<LwM2mPath> listPath = LwM2mPath.getLwM2mPathList(Arrays.asList(request.getObjectIds()));
Optional<Observation> observationOpt = Optional.ofNullable(observations.stream().filter(observation -> observation instanceof CompositeObservation && ((CompositeObservation) observation).getPaths().equals(listPath)).findFirst().orElse(null));
int cnt = 0;
if (observationOpt.isPresent()) {
cnt = context.getServer().getObservationService().cancelCompositeObservations(client.getRegistration(), request.getObjectIds());
callback.onSuccess(request, cnt);
} else {
Set<String> lwPaths = new HashSet<>();
for (Observation obs : observations) {
LwM2mPath lwPathObs = ((SingleObservation) obs).getPath();
for (LwM2mPath nodePath : listPath) {
String validNodePath = validatePathCompositeCancel(nodePath, lwPathObs, client);
if (validNodePath != null) lwPaths.add(validNodePath);
}
};
for (String nodePath : lwPaths) {
cnt += context.getServer().getObservationService().cancelObservations(client.getRegistration(), nodePath);
}
}
callback.onSuccess(request, cnt);
} catch (ThingsboardException e){
callback.onValidationError(request.toString(), e.getMessage());
}
}
private String validatePathCompositeCancel(LwM2mPath nodePath, LwM2mPath lwPathObs, LwM2mClient client) throws ThingsboardException {
if (nodePath.equals(lwPathObs) || lwPathObs.startWith(nodePath)) { // nodePath = "3", lwPathObs = "3/0/9": cancel for tne all lwPathObs
return nodePath.toString();
} else if (!nodePath.equals(lwPathObs) && nodePath.startWith(lwPathObs)) {
String errorMsg = String.format(
"Unexpected error: There is registration with Endpoint %s for observation path %s, that includes this observation path %s",
client.getRegistration().getEndpoint(), nodePath, lwPathObs);
throw new ThingsboardException(errorMsg, ThingsboardErrorCode.BAD_REQUEST_PARAMS);
}
return null;
}
@Override
public void sendDiscoverAllRequest(LwM2mClient client, TbLwM2MDiscoverAllRequest request, DownlinkRequestCallback<TbLwM2MDiscoverAllRequest, List<Link>> callback) {
callback.onSuccess(request, Arrays.asList(client.getRegistration().getSortedObjectLinks()));
public void sendDiscoverAllRequest(LwM2mClient client, TbLwM2MDiscoverAllRequest request, DownlinkRequestCallback<TbLwM2MDiscoverAllRequest, List<String>> callback) {
callback.onSuccess(request, Arrays.stream(client.getRegistration().getSortedObjectLinks()).map(Link::toCoreLinkFormat).collect(Collectors.toList()));
}
@Override
@ -248,7 +341,7 @@ public class DefaultLwM2mDownlinkMsgHandler extends LwM2MExecutorAwareService im
}
@Override
public void sendCancelAllRequest(LwM2mClient client, TbLwM2MCancelAllRequest request, DownlinkRequestCallback<TbLwM2MCancelAllRequest, Integer> callback) {
public void sendCancelObserveAllRequest(LwM2mClient client, TbLwM2MCancelAllRequest request, DownlinkRequestCallback<TbLwM2MCancelAllRequest, Integer> callback) {
int observeCancelCnt = context.getServer().getObservationService().cancelObservations(client.getRegistration());
callback.onSuccess(request, observeCancelCnt);
}
@ -278,20 +371,40 @@ public class DefaultLwM2mDownlinkMsgHandler extends LwM2MExecutorAwareService im
if (request.getAttributes() == null) {
throw new IllegalArgumentException("Attributes to write are not specified!");
}
ObjectAttributes params = request.getAttributes();
List<Attribute> attributes = new LinkedList<>();
addAttribute(attributes, MAXIMUM_PERIOD, params.getPmax());
addAttribute(attributes, MINIMUM_PERIOD, params.getPmin());
addAttribute(attributes, GREATER_THAN, params.getGt());
addAttribute(attributes, LESSER_THAN, params.getLt());
addAttribute(attributes, STEP, params.getSt());
AttributeSet attributeSet = new AttributeSet(attributes);
sendSimpleRequest(client, new WriteAttributesRequest(request.getObjectId(), attributeSet), request.getTimeout(), callback);
sendSimpleRequest(client, new WriteAttributesRequest(request.getObjectId(), getAttributesSet(request.getAttributes())), request.getTimeout(), callback);
} catch (InvalidRequestException e) {
callback.onValidationError(request.toString(), e.getMessage());
}
}
private LwM2mAttributeSet getAttributesSet(ObjectAttributes params) {
List<LwM2mAttribute<?>> attributes = new LinkedList<>();
/**
* Only: AttributeClass.NOTIFICATION -> RW
*/
addAttribute(attributes, MAXIMUM_PERIOD, params.getPmax());
addAttribute(attributes, MINIMUM_PERIOD, params.getPmin());
addAttribute(attributes, GREATER_THAN, params.getGt());
addAttribute(attributes, LESSER_THAN, params.getLt());
addAttribute(attributes, STEP, params.getSt());
addAttribute(attributes, EVALUATE_MAXIMUM_PERIOD, params.getEpmax());
addAttribute(attributes, EVALUATE_MINIMUM_PERIOD, params.getEpmin());
/**
* Only: AttributeClass.PROPERTIES -> R
*/
addAttribute(attributes, DIMENSION, params.getDim()); // Attachment.RESOURCE
addAttribute(attributes, SHORT_SERVER_ID, params.getSsid()); // Attachment.OBJECT_INSTANCE
addAttribute(attributes, SERVER_URI, params.getUri()); // Attachment.OBJECT_INSTANCE
if (params.getLwm2m() != null) {
addAttribute(attributes, ENABLER_VERSION, params.getLwm2m()); // attachment.ROOT
}
if (params.getVer() != null) {
addAttribute(attributes, OBJECT_VERSION, params.getVer()); // Attachment.OBJECT
}
return new LwM2mAttributeSet(attributes);
}
@Override
public void sendWriteReplaceRequest(LwM2mClient client, TbLwM2MWriteReplaceRequest request, DownlinkRequestCallback<WriteRequest, WriteResponse> callback) {
LwM2mPath resultIds = new LwM2mPath(request.getObjectId());
@ -338,9 +451,10 @@ public class DefaultLwM2mDownlinkMsgHandler extends LwM2MExecutorAwareService im
@Override
public void sendWriteCompositeRequest(LwM2mClient client, RpcWriteCompositeRequest rpcWriteCompositeRequest,
DownlinkRequestCallback<WriteCompositeRequest, WriteCompositeResponse> callback, ContentFormat contentFormatComposite) {
DownlinkRequestCallback<WriteCompositeRequest, WriteCompositeResponse> callback) {
try {
WriteCompositeRequest downlink = new WriteCompositeRequest(contentFormatComposite, rpcWriteCompositeRequest.getNodes());
ContentFormat compositeContentFormat = this.findFirstContentFormatForComposite(client.getClientSupportContentFormats());
WriteCompositeRequest downlink = new WriteCompositeRequest(compositeContentFormat, rpcWriteCompositeRequest.getNodes());
//TODO: replace config.getTimeout();
sendWriteCompositeRequest(client, downlink, this.config.getTimeout(), callback);
} catch (InvalidRequestException e) {
@ -570,17 +684,18 @@ public class DefaultLwM2mDownlinkMsgHandler extends LwM2MExecutorAwareService im
}
}
private static <T> void addAttribute(List<Attribute> attributes, String attributeName, T value) {
addAttribute(attributes, attributeName, value, null, null);
private static <T> void addAttribute(List<LwM2mAttribute<?>> attributes, LwM2mAttributeModel<T> attribute, T value) {
addAttribute(attributes, attribute, value, null, null);
}
private static <T> void addAttribute(List<Attribute> attributes, String attributeName, T value, Function<T, ?> converter) {
addAttribute(attributes, attributeName, value, null, converter);
private static <T> void addAttribute(List<LwM2mAttribute<?>> attributes, LwM2mAttributeModel<T> attribute, T value, Function<T, ?> converter) {
addAttribute(attributes, attribute, value, null, converter);
}
private static <T> void addAttribute(List<Attribute> attributes, String attributeName, T value, Predicate<T> filter, Function<T, ?> converter) {
private static <T> void addAttribute(List<LwM2mAttribute<?>> attributes, LwM2mAttributeModel<T> attributeName, T value, Predicate<T> filter, Function<T, ?> converter) {
if (value != null && ((filter == null) || filter.test(value))) {
attributes.add(new Attribute(attributeName, converter != null ? converter.apply(value) : value));
T valueConvert = (T) converter != null ? (T) converter.apply(value) : value;
attributes.add(new LwM2mAttribute<>(attributeName, valueConvert));
}
}
@ -620,7 +735,7 @@ public class DefaultLwM2mDownlinkMsgHandler extends LwM2MExecutorAwareService im
} else if (OPAQUE.equals(resourceModel.type)) {
return ContentFormat.OPAQUE;
} else {
return findFirst(client.getClientSupportContentFormats(), client.getDefaultContentFormat(), ContentFormat.CBOR, ContentFormat.SENML_CBOR, ContentFormat.SENML_JSON);
return findFirstContentFormatForComp(client.getClientSupportContentFormats(), client.getDefaultContentFormat(), ContentFormat.CBOR, ContentFormat.SENML_CBOR, ContentFormat.SENML_JSON);
}
} else {
return getContentFormatForComplex(client);
@ -634,7 +749,7 @@ public class DefaultLwM2mDownlinkMsgHandler extends LwM2MExecutorAwareService im
if (LwM2m.LwM2mVersion.V1_0.equals(client.getRegistration().getLwM2mVersion())) {
return ContentFormat.TLV;
} else if (LwM2m.LwM2mVersion.V1_1.equals(client.getRegistration().getLwM2mVersion())) {
ContentFormat result = findFirst(client.getClientSupportContentFormats(), null, ContentFormat.SENML_CBOR, ContentFormat.SENML_JSON, ContentFormat.TLV, ContentFormat.JSON);
ContentFormat result = findFirstContentFormatForComp(client.getClientSupportContentFormats(), null, ContentFormat.SENML_CBOR, ContentFormat.SENML_JSON, ContentFormat.TLV, ContentFormat.JSON);
if (result != null) {
return result;
} else {
@ -644,16 +759,6 @@ public class DefaultLwM2mDownlinkMsgHandler extends LwM2MExecutorAwareService im
throw new RuntimeException("The version " + client.getRegistration().getLwM2mVersion() + " is not supported!");
}
}
private static ContentFormat findFirst(Set<ContentFormat> supported, ContentFormat defaultValue, ContentFormat... desiredFormats) {
for (ContentFormat contentFormat : desiredFormats) {
if (supported.contains(contentFormat)) {
return contentFormat;
}
}
return defaultValue;
}
private <R> String toString(R request) {
try {
return request != null ? request.toString() : "";
@ -662,4 +767,38 @@ public class DefaultLwM2mDownlinkMsgHandler extends LwM2MExecutorAwareService im
return request.getClass().getSimpleName();
}
}
private ContentFormat findFirstContentFormatForComposite (Set<?> clientSupportContentFormats) {
ContentFormat contentFormat = findFirstContentFormatForComp(clientSupportContentFormats, null, ContentFormat.SENML_JSON, ContentFormat.SENML_CBOR);
if (contentFormat != null) {
return contentFormat;
} else {
throw new RuntimeException("This device does not support Composite Operation");
}
}
private static ContentFormat findFirstContentFormatForComp(Set<?> clientSupportContentFormats, ContentFormat defaultValue, ContentFormat... desiredFormats) {
AtomicReference<ContentFormat> compositeContentFormat = new AtomicReference<>();
clientSupportContentFormats.forEach(c -> {
if (c instanceof Collection) {
for (ContentFormat contentFormat : desiredFormats) {
if (((Collection<?>) c).contains(contentFormat)) {
compositeContentFormat.set(contentFormat);
break;
}
}
} else if (compositeContentFormat.get() == null && c instanceof ContentFormat) {
compositeContentFormat.set(Arrays.stream(desiredFormats).filter(f -> f.equals(c)).findFirst().get());
}
});
return compositeContentFormat.get() != null ? compositeContentFormat.get() : defaultValue;
}
private static <T> boolean containsObserveComposite(List<T> l1, List<T> l2) {
for (T elem : l1) {
if (l2.contains(elem)) {
return true;
}
}
return false;
}
}

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

@ -15,12 +15,11 @@
*/
package org.thingsboard.server.transport.lwm2m.server.downlink;
import org.eclipse.leshan.core.link.Link;
import org.eclipse.leshan.core.request.ContentFormat;
import org.eclipse.leshan.core.request.CreateRequest;
import org.eclipse.leshan.core.request.DeleteRequest;
import org.eclipse.leshan.core.request.DiscoverRequest;
import org.eclipse.leshan.core.request.ExecuteRequest;
import org.eclipse.leshan.core.request.ObserveCompositeRequest;
import org.eclipse.leshan.core.request.ObserveRequest;
import org.eclipse.leshan.core.request.ReadCompositeRequest;
import org.eclipse.leshan.core.request.ReadRequest;
@ -31,6 +30,7 @@ import org.eclipse.leshan.core.response.CreateResponse;
import org.eclipse.leshan.core.response.DeleteResponse;
import org.eclipse.leshan.core.response.DiscoverResponse;
import org.eclipse.leshan.core.response.ExecuteResponse;
import org.eclipse.leshan.core.response.ObserveCompositeResponse;
import org.eclipse.leshan.core.response.ObserveResponse;
import org.eclipse.leshan.core.response.ReadCompositeResponse;
import org.eclipse.leshan.core.response.ReadResponse;
@ -38,6 +38,8 @@ import org.eclipse.leshan.core.response.WriteAttributesResponse;
import org.eclipse.leshan.core.response.WriteCompositeResponse;
import org.eclipse.leshan.core.response.WriteResponse;
import org.thingsboard.server.transport.lwm2m.server.client.LwM2mClient;
import org.thingsboard.server.transport.lwm2m.server.downlink.composite.TbLwM2MCancelObserveCompositeRequest;
import org.thingsboard.server.transport.lwm2m.server.downlink.composite.TbLwM2MObserveCompositeRequest;
import org.thingsboard.server.transport.lwm2m.server.downlink.composite.TbLwM2MReadCompositeRequest;
import org.thingsboard.server.transport.lwm2m.server.rpc.composite.RpcWriteCompositeRequest;
@ -48,7 +50,7 @@ public interface LwM2mDownlinkMsgHandler {
void sendReadRequest(LwM2mClient client, TbLwM2MReadRequest request, DownlinkRequestCallback<ReadRequest, ReadResponse> callback);
void sendReadCompositeRequest(LwM2mClient client, TbLwM2MReadCompositeRequest request, DownlinkRequestCallback<ReadCompositeRequest, ReadCompositeResponse> callback, ContentFormat contentFormatComposite);
void sendReadCompositeRequest(LwM2mClient client, TbLwM2MReadCompositeRequest request, DownlinkRequestCallback<ReadCompositeRequest, ReadCompositeResponse> callback);
void sendObserveRequest(LwM2mClient client, TbLwM2MObserveRequest request, DownlinkRequestCallback<ObserveRequest, ObserveResponse> callback);
@ -60,17 +62,21 @@ public interface LwM2mDownlinkMsgHandler {
void sendCancelObserveRequest(LwM2mClient client, TbLwM2MCancelObserveRequest request, DownlinkRequestCallback<TbLwM2MCancelObserveRequest, Integer> callback);
void sendCancelAllRequest(LwM2mClient client, TbLwM2MCancelAllRequest request, DownlinkRequestCallback<TbLwM2MCancelAllRequest, Integer> callback);
void sendCancelObserveAllRequest(LwM2mClient client, TbLwM2MCancelAllRequest request, DownlinkRequestCallback<TbLwM2MCancelAllRequest, Integer> callback);
void sendObserveCompositeRequest(LwM2mClient client, TbLwM2MObserveCompositeRequest request, DownlinkRequestCallback<ObserveCompositeRequest, ObserveCompositeResponse> callback);
void sendCancelObserveCompositeRequest(LwM2mClient client, TbLwM2MCancelObserveCompositeRequest request, DownlinkRequestCallback<TbLwM2MCancelObserveCompositeRequest, Integer> callback);
void sendDiscoverRequest(LwM2mClient client, TbLwM2MDiscoverRequest request, DownlinkRequestCallback<DiscoverRequest, DiscoverResponse> callback);
void sendDiscoverAllRequest(LwM2mClient client, TbLwM2MDiscoverAllRequest request, DownlinkRequestCallback<TbLwM2MDiscoverAllRequest, List<Link>> callback);
void sendDiscoverAllRequest(LwM2mClient client, TbLwM2MDiscoverAllRequest request, DownlinkRequestCallback<TbLwM2MDiscoverAllRequest, List<String>> callback);
void sendWriteAttributesRequest(LwM2mClient client, TbLwM2MWriteAttributesRequest request, DownlinkRequestCallback<WriteAttributesRequest, WriteAttributesResponse> callback);
void sendWriteReplaceRequest(LwM2mClient client, TbLwM2MWriteReplaceRequest request, DownlinkRequestCallback<WriteRequest, WriteResponse> callback);
void sendWriteCompositeRequest(LwM2mClient client, RpcWriteCompositeRequest nodes, DownlinkRequestCallback<WriteCompositeRequest, WriteCompositeResponse> callback, ContentFormat contentFormatComposite);
void sendWriteCompositeRequest(LwM2mClient client, RpcWriteCompositeRequest nodes, DownlinkRequestCallback<WriteCompositeRequest, WriteCompositeResponse> callback);
void sendWriteUpdateRequest(LwM2mClient client, TbLwM2MWriteUpdateRequest request, DownlinkRequestCallback<WriteRequest, WriteResponse> callback);

40
common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/downlink/composite/TbLwM2MCancelObserveCompositeCallback.java

@ -0,0 +1,40 @@
/**
* Copyright © 2016-2024 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.transport.lwm2m.server.downlink.composite;
import lombok.extern.slf4j.Slf4j;
import org.thingsboard.server.transport.lwm2m.server.client.LwM2mClient;
import org.thingsboard.server.transport.lwm2m.server.downlink.AbstractTbLwM2MRequestCallback;
import org.thingsboard.server.transport.lwm2m.server.log.LwM2MTelemetryLogService;
import static org.thingsboard.server.transport.lwm2m.utils.LwM2MTransportUtil.LOG_LWM2M_INFO;
@Slf4j
public class TbLwM2MCancelObserveCompositeCallback extends AbstractTbLwM2MRequestCallback<TbLwM2MCancelObserveCompositeRequest, Integer> {
private final String [] versionedIds;
public TbLwM2MCancelObserveCompositeCallback(LwM2MTelemetryLogService logService, LwM2mClient client, String [] versionedIds) {
super(logService, client);
this.versionedIds = versionedIds;
}
@Override
public void onSuccess(TbLwM2MCancelObserveCompositeRequest request, Integer canceledSubscriptionsCount) {
log.trace("[{}] Cancel composite observation of [{}] successful: {}", client.getEndpoint(), this.versionedIds, canceledSubscriptionsCount);
logService.log(client, String.format("[%s]: Cancel Composite Observe for [%s] successful. Result: [%s]", LOG_LWM2M_INFO, this.versionedIds, canceledSubscriptionsCount));
}
}

32
common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/downlink/composite/TbLwM2MCancelObserveCompositeRequest.java

@ -0,0 +1,32 @@
/**
* Copyright © 2016-2024 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.transport.lwm2m.server.downlink.composite;
import lombok.Builder;
import org.thingsboard.server.transport.lwm2m.server.LwM2MOperationType;
public class TbLwM2MCancelObserveCompositeRequest extends AbstractTbLwM2MTargetedDownlinkCompositeRequest {
@Builder
private TbLwM2MCancelObserveCompositeRequest(String [] versionedIds, long timeout) {
super(versionedIds, timeout);
}
@Override
public LwM2MOperationType getType() {
return LwM2MOperationType.OBSERVE_COMPOSITE_CANCEL;
}
}

39
common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/downlink/composite/TbLwM2MObserveCompositeCallback.java

@ -0,0 +1,39 @@
/**
* Copyright © 2016-2024 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.transport.lwm2m.server.downlink.composite;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.leshan.core.request.ObserveCompositeRequest;
import org.eclipse.leshan.core.response.ObserveCompositeResponse;
import org.thingsboard.server.transport.lwm2m.server.client.LwM2mClient;
import org.thingsboard.server.transport.lwm2m.server.downlink.TbLwM2MUplinkTargetedCallback;
import org.thingsboard.server.transport.lwm2m.server.log.LwM2MTelemetryLogService;
import org.thingsboard.server.transport.lwm2m.server.uplink.LwM2mUplinkMsgHandler;
@Slf4j
public class TbLwM2MObserveCompositeCallback extends TbLwM2MUplinkTargetedCallback<ObserveCompositeRequest, ObserveCompositeResponse> {
public TbLwM2MObserveCompositeCallback(LwM2mUplinkMsgHandler handler, LwM2MTelemetryLogService logService, LwM2mClient client, String[] versionedIds) {
super(handler, logService, client, versionedIds);
}
@Override
public void onSuccess(ObserveCompositeRequest request, ObserveCompositeResponse response) {
super.onSuccess(request, response);
handler.onUpdateValueAfterReadCompositeResponse(client.getRegistration(), response);
}
}

51
common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/downlink/composite/TbLwM2MObserveCompositeRequest.java

@ -0,0 +1,51 @@
/**
* Copyright © 2016-2024 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.transport.lwm2m.server.downlink.composite;
import lombok.Builder;
import lombok.Getter;
import org.eclipse.leshan.core.request.ContentFormat;
import org.eclipse.leshan.core.response.ObserveCompositeResponse;
import org.thingsboard.server.transport.lwm2m.server.LwM2MOperationType;
import org.thingsboard.server.transport.lwm2m.server.downlink.HasContentFormat;
import java.util.Optional;
public class TbLwM2MObserveCompositeRequest extends AbstractTbLwM2MTargetedDownlinkCompositeRequest<ObserveCompositeResponse> implements HasContentFormat {
private final Optional<ContentFormat> requestContentFormatOpt;
@Getter
private final ContentFormat responseContentFormat;
@Builder
private TbLwM2MObserveCompositeRequest(String [] versionedIds, long timeout, ContentFormat requestContentFormat, ContentFormat responseContentFormat) {
super(versionedIds, timeout);
this.requestContentFormatOpt = Optional.ofNullable(requestContentFormat);
this.responseContentFormat = responseContentFormat;
}
@Override
public LwM2MOperationType getType() {
return LwM2MOperationType.OBSERVE_COMPOSITE;
}
@Override
public Optional<ContentFormat> getRequestContentFormat() {
return this.requestContentFormatOpt;
}
}

55
common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/ota/DefaultLwM2MOtaUpdateService.java

@ -233,19 +233,19 @@ public class DefaultLwM2MOtaUpdateService extends LwM2MExecutorAwareService impl
@Override
public void onCurrentFirmwareNameUpdate(LwM2mClient client, String name) {
log.debug("[{}] Current fw name: {}", client.getEndpoint(), name);
log.trace("[{}] Current fw name: {}", client.getEndpoint(), name);
getOrInitFwInfo(client).setCurrentName(name);
}
@Override
public void onCurrentSoftwareNameUpdate(LwM2mClient client, String name) {
log.debug("[{}] Current sw name: {}", client.getEndpoint(), name);
log.trace("[{}] Current sw name: {}", client.getEndpoint(), name);
getOrInitSwInfo(client).setCurrentName(name);
}
@Override
public void onFirmwareStrategyUpdate(LwM2mClient client, OtherConfiguration configuration) {
log.debug("[{}] Current fw strategy: {}", client.getEndpoint(), configuration.getFwUpdateStrategy());
log.trace("[{}] Current fw strategy: {}", client.getEndpoint(), configuration.getFwUpdateStrategy());
startFirmwareUpdateIfNeeded(client, initFwStrategy(client, configuration));
}
@ -258,7 +258,7 @@ public class DefaultLwM2MOtaUpdateService extends LwM2MExecutorAwareService impl
@Override
public void onCurrentSoftwareStrategyUpdate(LwM2mClient client, OtherConfiguration configuration) {
log.debug("[{}] Current sw strategy: {}", client.getEndpoint(), configuration.getSwUpdateStrategy());
log.trace("[{}] Current sw strategy: {}", client.getEndpoint(), configuration.getSwUpdateStrategy());
startSoftwareUpdateIfNeeded(client, initSwStrategy(client, configuration));
}
@ -271,21 +271,21 @@ public class DefaultLwM2MOtaUpdateService extends LwM2MExecutorAwareService impl
@Override
public void onCurrentFirmwareVersion3Update(LwM2mClient client, String version) {
log.debug("[{}] Current fw version(3): {}", client.getEndpoint(), version);
log.trace("[{}] Current fw version(3): {}", client.getEndpoint(), version);
LwM2MClientFwOtaInfo fwInfo = getOrInitFwInfo(client);
fwInfo.setCurrentVersion3(version);
}
@Override
public void onCurrentFirmwareVersionUpdate(LwM2mClient client, String version) {
log.debug("[{}] Current fw version(5): {}", client.getEndpoint(), version);
log.trace("[{}] Current fw version(5): {}", client.getEndpoint(), version);
LwM2MClientFwOtaInfo fwInfo = getOrInitFwInfo(client);
fwInfo.setCurrentVersion(version);
}
@Override
public void onCurrentFirmwareStateUpdate(LwM2mClient client, Long stateCode) {
log.debug("[{}] Current fw state: {}", client.getEndpoint(), stateCode);
log.trace("[{}] Current fw state: {}", client.getEndpoint(), stateCode);
LwM2MClientFwOtaInfo fwInfo = getOrInitFwInfo(client);
FirmwareUpdateState state = FirmwareUpdateState.fromStateFwByCode(stateCode.intValue());
if (FirmwareUpdateState.DOWNLOADED.equals(state)) {
@ -309,7 +309,7 @@ public class DefaultLwM2MOtaUpdateService extends LwM2MExecutorAwareService impl
@Override
public void onCurrentFirmwareResultUpdate(LwM2mClient client, Long code) {
log.debug("[{}] Current fw result: {}", client.getEndpoint(), code);
log.trace("[{}] Current fw result: {}", client.getEndpoint(), code);
LwM2MClientFwOtaInfo fwInfo = getOrInitFwInfo(client);
FirmwareUpdateResult result = FirmwareUpdateResult.fromUpdateResultFwByCode(code.intValue());
Optional<OtaPackageUpdateStatus> status = toOtaPackageUpdateStatus(result);
@ -338,26 +338,26 @@ public class DefaultLwM2MOtaUpdateService extends LwM2MExecutorAwareService impl
@Override
public void onCurrentFirmwareDeliveryMethodUpdate(LwM2mClient client, Long value) {
log.debug("[{}] Current fw delivery method: {}", client.getEndpoint(), value);
log.trace("[{}] Current fw delivery method: {}", client.getEndpoint(), value);
LwM2MClientFwOtaInfo fwInfo = getOrInitFwInfo(client);
fwInfo.setDeliveryMethod(value.intValue());
}
@Override
public void onCurrentSoftwareVersion3Update(LwM2mClient client, String version) {
log.debug("[{}] Current sw version(3): {}", client.getEndpoint(), version);
log.trace("[{}] Current sw version(3): {}", client.getEndpoint(), version);
getOrInitSwInfo(client).setCurrentVersion3(version);
}
@Override
public void onCurrentSoftwareVersionUpdate(LwM2mClient client, String version) {
log.debug("[{}] Current sw version(9): {}", client.getEndpoint(), version);
log.trace("[{}] Current sw version(9): {}", client.getEndpoint(), version);
getOrInitSwInfo(client).setCurrentVersion(version);
}
@Override
public void onCurrentSoftwareStateUpdate(LwM2mClient client, Long stateCode) {
log.debug("[{}] Current sw state: {}", client.getEndpoint(), stateCode);
log.trace("[{}] Current sw state: {}", client.getEndpoint(), stateCode);
LwM2MClientSwOtaInfo swInfo = getOrInitSwInfo(client);
SoftwareUpdateState state = SoftwareUpdateState.fromUpdateStateSwByCode(stateCode.intValue());
if (SoftwareUpdateState.INITIAL.equals(state)) {
@ -375,7 +375,7 @@ public class DefaultLwM2MOtaUpdateService extends LwM2MExecutorAwareService impl
@Override
public void onCurrentSoftwareResultUpdate(LwM2mClient client, Long code) {
log.debug("[{}] Current sw result: {}", client.getEndpoint(), code);
log.trace("[{}] Current sw result: {}", client.getEndpoint(), code);
LwM2MClientSwOtaInfo swInfo = getOrInitSwInfo(client);
SoftwareUpdateResult result = SoftwareUpdateResult.fromUpdateResultSwByCode(code.intValue());
Optional<OtaPackageUpdateStatus> status = toOtaPackageUpdateStatus(result);
@ -417,14 +417,14 @@ public class DefaultLwM2MOtaUpdateService extends LwM2MExecutorAwareService impl
private void startFirmwareUpdateIfNeeded(LwM2mClient client, LwM2MClientFwOtaInfo fwInfo) {
try {
if (!fwInfo.isSupported() && fwInfo.isAssigned()) {
log.debug("[{}] Fw update is not supported: {}", client.getEndpoint(), fwInfo);
log.trace("[{}] Fw update is not supported: {}", client.getEndpoint(), fwInfo);
sendStateUpdateToTelemetry(client, fwInfo, OtaPackageUpdateStatus.FAILED, "Client does not support firmware update or profile misconfiguration!");
} else if (fwInfo.isUpdateRequired()) {
if (StringUtils.isNotEmpty(fwInfo.getTargetUrl())) {
log.debug("[{}] Starting update to [{}{}][] using URL: {}", client.getEndpoint(), fwInfo.getTargetName(), fwInfo.getTargetVersion(), fwInfo.getTargetUrl());
log.trace("[{}] Starting update to [{}{}][] using URL: {}", client.getEndpoint(), fwInfo.getTargetName(), fwInfo.getTargetVersion(), fwInfo.getTargetUrl());
startUpdateUsingUrl(client, FW_URL_ID, fwInfo.getTargetUrl());
} else {
log.debug("[{}] Starting update to [{}{}] using binary", client.getEndpoint(), fwInfo.getTargetName(), fwInfo.getTargetVersion());
log.trace("[{}] Starting update to [{}{}] using binary", client.getEndpoint(), fwInfo.getTargetName(), fwInfo.getTargetVersion());
startUpdateUsingBinary(client, fwInfo);
}
} else if (fwInfo.getResult() != null && fwInfo.getResult().getCode() > UPDATE_SUCCESSFULLY.getCode()) {
@ -440,18 +440,18 @@ public class DefaultLwM2MOtaUpdateService extends LwM2MExecutorAwareService impl
private void startSoftwareUpdateIfNeeded(LwM2mClient client, LwM2MClientSwOtaInfo swInfo) {
try {
if (!swInfo.isSupported() && swInfo.isAssigned()) {
log.debug("[{}] Sw update is not supported: {}", client.getEndpoint(), swInfo);
log.trace("[{}] Sw update is not supported: {}", client.getEndpoint(), swInfo);
sendStateUpdateToTelemetry(client, swInfo, OtaPackageUpdateStatus.FAILED, "Client does not support software update or profile misconfiguration!");
} else if (swInfo.isUpdateRequired()) {
if (SoftwareUpdateState.INSTALLED.equals(swInfo.getUpdateState())) {
log.debug("[{}] Attempt to restore the update state: {}", client.getEndpoint(), swInfo.getUpdateState());
log.trace("[{}] Attempt to restore the update state: {}", client.getEndpoint(), swInfo.getUpdateState());
executeSwUninstallForUpdate(client);
} else {
if (StringUtils.isNotEmpty(swInfo.getTargetUrl())) {
log.debug("[{}] Starting update to [{}{}] using URL: {}", client.getEndpoint(), swInfo.getTargetName(), swInfo.getTargetVersion(), swInfo.getTargetUrl());
log.trace("[{}] Starting update to [{}{}] using URL: {}", client.getEndpoint(), swInfo.getTargetName(), swInfo.getTargetVersion(), swInfo.getTargetUrl());
startUpdateUsingUrl(client, SW_PACKAGE_URI_ID, swInfo.getTargetUrl());
} else {
log.debug("[{}] Starting update to [{}{}] using binary", client.getEndpoint(), swInfo.getTargetName(), swInfo.getTargetVersion());
log.trace("[{}] Starting update to [{}{}] using binary", client.getEndpoint(), swInfo.getTargetName(), swInfo.getTargetVersion());
startUpdateUsingBinary(client, swInfo);
}
}
@ -566,18 +566,21 @@ public class DefaultLwM2MOtaUpdateService extends LwM2MExecutorAwareService impl
}
private void executeFwUpdate(LwM2mClient client) {
TbLwM2MExecuteRequest request = TbLwM2MExecuteRequest.builder().versionedId(FW_EXECUTE_ID).timeout(clientContext.getRequestTimeout(client)).build();
downlinkHandler.sendExecuteRequest(client, request, new TbLwM2MExecuteCallback(logService, client, FW_EXECUTE_ID));
String fwExecuteVerId = convertObjectIdToVersionedId(FW_EXECUTE_ID, client.getRegistration());
TbLwM2MExecuteRequest request = TbLwM2MExecuteRequest.builder().versionedId(fwExecuteVerId).timeout(clientContext.getRequestTimeout(client)).build();
downlinkHandler.sendExecuteRequest(client, request, new TbLwM2MExecuteCallback(logService, client, fwExecuteVerId));
}
private void executeSwInstall(LwM2mClient client) {
TbLwM2MExecuteRequest request = TbLwM2MExecuteRequest.builder().versionedId(SW_INSTALL_ID).timeout(clientContext.getRequestTimeout(client)).build();
downlinkHandler.sendExecuteRequest(client, request, new TbLwM2MExecuteCallback(logService, client, SW_INSTALL_ID));
String swInstallVerId = convertObjectIdToVersionedId(SW_INSTALL_ID, client.getRegistration());
TbLwM2MExecuteRequest request = TbLwM2MExecuteRequest.builder().versionedId(swInstallVerId).timeout(clientContext.getRequestTimeout(client)).build();
downlinkHandler.sendExecuteRequest(client, request, new TbLwM2MExecuteCallback(logService, client, swInstallVerId));
}
private void executeSwUninstallForUpdate(LwM2mClient client) {
TbLwM2MExecuteRequest request = TbLwM2MExecuteRequest.builder().versionedId(SW_UN_INSTALL_ID).params("1").timeout(clientContext.getRequestTimeout(client)).build();
downlinkHandler.sendExecuteRequest(client, request, new TbLwM2MExecuteCallback(logService, client, SW_INSTALL_ID));
String swInInstallVerId = convertObjectIdToVersionedId(SW_UN_INSTALL_ID, client.getRegistration());
TbLwM2MExecuteRequest request = TbLwM2MExecuteRequest.builder().versionedId(swInInstallVerId).params("1").timeout(clientContext.getRequestTimeout(client)).build();
downlinkHandler.sendExecuteRequest(client, request, new TbLwM2MExecuteCallback(logService, client, swInInstallVerId));
}
private Optional<String> getAttributeValue(List<TransportProtos.TsKvProto> attrs, String keyName) {

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

@ -23,7 +23,6 @@ import org.apache.commons.lang3.exception.ExceptionUtils;
import org.eclipse.leshan.core.ResponseCode;
import org.eclipse.leshan.core.model.ResourceModel;
import org.eclipse.leshan.core.node.LwM2mPath;
import org.eclipse.leshan.core.request.ContentFormat;
import org.eclipse.leshan.server.model.LwM2mModelProvider;
import org.springframework.stereotype.Service;
import org.thingsboard.common.util.JacksonUtil;
@ -59,10 +58,16 @@ import org.thingsboard.server.transport.lwm2m.server.downlink.TbLwM2MWriteAttrib
import org.thingsboard.server.transport.lwm2m.server.downlink.TbLwM2MWriteReplaceRequest;
import org.thingsboard.server.transport.lwm2m.server.downlink.TbLwM2MWriteResponseCallback;
import org.thingsboard.server.transport.lwm2m.server.downlink.TbLwM2MWriteUpdateRequest;
import org.thingsboard.server.transport.lwm2m.server.downlink.composite.TbLwM2MObserveCompositeCallback;
import org.thingsboard.server.transport.lwm2m.server.downlink.composite.TbLwM2MCancelObserveCompositeCallback;
import org.thingsboard.server.transport.lwm2m.server.downlink.composite.TbLwM2MCancelObserveCompositeRequest;
import org.thingsboard.server.transport.lwm2m.server.downlink.composite.TbLwM2MObserveCompositeRequest;
import org.thingsboard.server.transport.lwm2m.server.downlink.composite.TbLwM2MReadCompositeCallback;
import org.thingsboard.server.transport.lwm2m.server.downlink.composite.TbLwM2MReadCompositeRequest;
import org.thingsboard.server.transport.lwm2m.server.downlink.composite.TbLwM2MWriteResponseCompositeCallback;
import org.thingsboard.server.transport.lwm2m.server.log.LwM2MTelemetryLogService;
import org.thingsboard.server.transport.lwm2m.server.rpc.composite.RpcCancelObserveCompositeCallback;
import org.thingsboard.server.transport.lwm2m.server.rpc.composite.RpcObserveResponseCompositeCallback;
import org.thingsboard.server.transport.lwm2m.server.rpc.composite.RpcReadCompositeRequest;
import org.thingsboard.server.transport.lwm2m.server.rpc.composite.RpcReadResponseCompositeCallback;
import org.thingsboard.server.transport.lwm2m.server.rpc.composite.RpcWriteCompositeRequest;
@ -75,6 +80,7 @@ import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import static org.thingsboard.server.transport.lwm2m.utils.LwM2MTransportUtil.convertMultiResourceValuesFromRpcBody;
import static org.thingsboard.server.transport.lwm2m.utils.LwM2MTransportUtil.convertValueByTypeResource;
import static org.thingsboard.server.transport.lwm2m.utils.LwM2MTransportUtil.fromVersionedIdToObjectId;
@Slf4j
@ -155,22 +161,22 @@ public class DefaultLwM2MRpcRequestHandler implements LwM2MRpcRequestHandler {
throw new IllegalArgumentException("Unsupported operation: " + operationType.name());
}
} else if (operationType.isComposite()) {
ContentFormat contentFormatComposite = this.getCompositeContentFormat(client);
if (contentFormatComposite != null) {
switch (operationType) {
case READ_COMPOSITE:
sendReadCompositeRequest(client, rpcRequest, contentFormatComposite);
sendReadCompositeRequest(client, rpcRequest);
break;
case WRITE_COMPOSITE:
sendWriteCompositeRequest(client, rpcRequest, contentFormatComposite);
sendWriteCompositeRequest(client, rpcRequest);
break;
case OBSERVE_COMPOSITE:
sendObserveCompositeRequest(client, rpcRequest);
break;
case OBSERVE_COMPOSITE_CANCEL:
sendCancelObserveCompositeRequest(client, rpcRequest);
break;
default:
throw new IllegalArgumentException("Unsupported operation: " + operationType.name());
}
} else {
this.sendErrorRpcResponse(sessionInfo, rpcRequest.getRequestId(),
ResponseCode.INTERNAL_SERVER_ERROR, "This device does not support Composite Operation");
}
} else {
switch (operationType) {
case OBSERVE_CANCEL_ALL:
@ -202,12 +208,12 @@ public class DefaultLwM2MRpcRequestHandler implements LwM2MRpcRequestHandler {
downlinkHandler.sendReadRequest(client, request, rpcCallback);
}
private void sendReadCompositeRequest(LwM2mClient client, TransportProtos.ToDeviceRpcRequestMsg requestMsg, ContentFormat contentFormatComposite) {
private void sendReadCompositeRequest(LwM2mClient client, TransportProtos.ToDeviceRpcRequestMsg requestMsg) {
String[] versionedIds = getIdsFromParameters(client, requestMsg);
TbLwM2MReadCompositeRequest request = TbLwM2MReadCompositeRequest.builder().versionedIds(versionedIds).timeout(clientContext.getRequestTimeout(client)).build();
var mainCallback = new TbLwM2MReadCompositeCallback(uplinkHandler, logService, client, versionedIds);
var rpcCallback = new RpcReadResponseCompositeCallback(transportService, client, requestMsg, mainCallback);
downlinkHandler.sendReadCompositeRequest(client, request, rpcCallback, contentFormatComposite);
downlinkHandler.sendReadCompositeRequest(client, request, rpcCallback);
}
private void sendObserveRequest(LwM2mClient client, TransportProtos.ToDeviceRpcRequestMsg requestMsg, String versionedId) {
@ -243,7 +249,7 @@ public class DefaultLwM2MRpcRequestHandler implements LwM2MRpcRequestHandler {
private void sendWriteAttributesRequest(LwM2mClient client, TransportProtos.ToDeviceRpcRequestMsg requestMsg, String versionedId) {
RpcWriteAttributesRequest requestBody = JacksonUtil.fromString(requestMsg.getParams(), RpcWriteAttributesRequest.class);
TbLwM2MWriteAttributesRequest request = TbLwM2MWriteAttributesRequest.builder().versionedId(versionedId)
TbLwM2MWriteAttributesRequest request = TbLwM2MWriteAttributesRequest.builder().versionedId(versionedId)
.attributes(requestBody.getAttributes())
.timeout(clientContext.getRequestTimeout(client)).build();
var mainCallback = new TbLwM2MWriteAttributesCallback(logService, client, versionedId);
@ -298,54 +304,89 @@ public class DefaultLwM2MRpcRequestHandler implements LwM2MRpcRequestHandler {
* nodes.put("/1/0/2", 100);
* nodes.put("/5/0/1", "coap://localhost:5685");
*/
private void sendWriteCompositeRequest(LwM2mClient client, TransportProtos.ToDeviceRpcRequestMsg requestMsg, ContentFormat contentFormatComposite) {
private void sendWriteCompositeRequest(LwM2mClient client, TransportProtos.ToDeviceRpcRequestMsg requestMsg) {
RpcWriteCompositeRequest rpcWriteCompositeRequest = JacksonUtil.fromString(requestMsg.getParams(), RpcWriteCompositeRequest.class);
Map<String, Object> validNodes = validateNodes(client, rpcWriteCompositeRequest.getNodes());
Map<String, Object> validNodes = validateCompositesNodes(client, rpcWriteCompositeRequest.getNodes());
if (validNodes.size() > 0) {
rpcWriteCompositeRequest.setNodes(validNodes);
var mainCallback = new TbLwM2MWriteResponseCompositeCallback(uplinkHandler, logService, client, null);
var rpcCallback = new RpcEmptyResponseCallback<>(transportService, client, requestMsg, mainCallback);
downlinkHandler.sendWriteCompositeRequest(client, rpcWriteCompositeRequest, rpcCallback, contentFormatComposite);
downlinkHandler.sendWriteCompositeRequest(client, rpcWriteCompositeRequest, rpcCallback);
} else {
throw new IllegalArgumentException(String.format("nodes: %s is not validate value", rpcWriteCompositeRequest.getNodes().toString()));
}
}
private Map<String, Object> validateNodes(LwM2mClient client, Map<String, Object> nodes) {
private Map<String, Object> validateCompositesNodes(LwM2mClient client, Map<String, Object> nodes) {
Map<String, Object> newNodes = new LinkedHashMap<>();
nodes.forEach((key, value) -> {
String versionedId;
String versionedId = null;
LwM2mPath path = null;
try {
LwM2mPath path = new LwM2mPath(fromVersionedIdToObjectId(key));
if (path.isResource() || path.isResourceInstance()) {
versionedId = key;
}
else {
throw new IllegalArgumentException(String.format("nodes: %s is not validate value. " +
"The WriteComposite operation is only used for SingleResources or/and ResourceInstance.", nodes.toString()));
}
path = new LwM2mPath(fromVersionedIdToObjectId(key));
} catch (Exception e) {
versionedId = clientContext.getObjectIdByKeyNameFromProfile(client, key);
}
// validate value. Must be only primitive, not JsonObject or JsonArray
try {
JsonElement element = JsonUtils.parse(value.toString());
if (!element.isJsonNull() && !element.isJsonPrimitive()) {
if (versionedId == null) {
if (path.isResourceInstance()) {
setValueToCompositeNodes(client, newNodes, nodes, key, value.toString());
} else if (path.isResource()) {
validateResource(client, newNodes, nodes, key , value);
} else if (path.isObjectInstance() && value instanceof Map<?, ?>) {
((Map) value).forEach((k, v) -> {
validateResource(client, newNodes, nodes, validateResourceId (key, k.toString(), nodes), v);
});
} else {
throw new IllegalArgumentException(String.format("nodes: %s is not validate value. " +
"The WriteComposite operation is only used for SingleResources or/and ResourceInstance.", nodes.toString()));
}
else if (versionedId != null) {
newNodes.put(fromVersionedIdToObjectId(versionedId), value);
}
} catch (JsonSyntaxException jse) {
if (versionedId != null) {
newNodes.put(fromVersionedIdToObjectId(versionedId), value);
"The WriteComposite operation is only used for SingleResources or/and ResourceInstance.", nodes));
}
} else {
setValueToCompositeNodes(client, newNodes, nodes, versionedId, value.toString());
}
});
return newNodes;
}
private void validateResource(LwM2mClient client, Map newNodes, Map nodes, String resourceId , Object value) {
if (value instanceof Map<?, ?>) {
((Map<?, ?>) value).forEach((k, v) -> {
setValueToCompositeNodes(client, newNodes, nodes, validateResourceId (resourceId, k.toString(), nodes), v.toString());
});
} else {
setValueToCompositeNodes(client, newNodes, nodes, resourceId, value.toString());
}
}
private String validateResourceId (String key, String id, Map nodes) {
try {
Integer.parseInt(id);
return key + "/" + id;
} catch (NumberFormatException e) {
throw new IllegalArgumentException(String.format("nodes: %s is not validate value. " +
"The WriteComposite operation is only used for SingleResources or/and ResourceInstance.", nodes.toString()));
}
}
private void setValueToCompositeNodes (LwM2mClient client, Map newNodes, Map nodes, String versionedId , String value) {
// validate value. Must be only primitive, not JsonObject or JsonArray
try {
JsonElement element = JsonUtils.parse(value);
if (!element.isJsonNull() && !element.isJsonPrimitive()) {
throw new IllegalArgumentException(String.format("nodes: %s is not validate value. " +
"The WriteComposite operation is only used for SingleResources or/and ResourceInstance.", nodes.toString()));
}
// convert value from JsonPrimitive() to resource/ResourceInstance type
ResourceModel resourceModel = client.getResourceModel(versionedId, modelProvider);
Object newValue = convertValueByTypeResource(value, resourceModel.type, versionedId);
// add new value after convert
newNodes.put(fromVersionedIdToObjectId(versionedId), newValue);
} catch (JsonSyntaxException jse) {
newNodes.put(fromVersionedIdToObjectId(versionedId), value);
}
}
private void sendCancelObserveRequest(LwM2mClient client, TransportProtos.ToDeviceRpcRequestMsg requestMsg, String versionedId) {
TbLwM2MCancelObserveRequest downlink = TbLwM2MCancelObserveRequest.builder().versionedId(versionedId).timeout(clientContext.getRequestTimeout(client)).build();
var mainCallback = new TbLwM2MCancelObserveCallback(logService, client, versionedId);
@ -353,6 +394,25 @@ public class DefaultLwM2MRpcRequestHandler implements LwM2MRpcRequestHandler {
downlinkHandler.sendCancelObserveRequest(client, downlink, rpcCallback);
}
private void sendObserveCompositeRequest(LwM2mClient client, TransportProtos.ToDeviceRpcRequestMsg requestMsg) {
String[] versionedIds = getIdsFromParameters(client, requestMsg);
TbLwM2MObserveCompositeRequest request = TbLwM2MObserveCompositeRequest.builder().versionedIds(versionedIds).timeout(clientContext.getRequestTimeout(client)).build();
var mainCallback = new TbLwM2MObserveCompositeCallback(uplinkHandler, logService, client, versionedIds);
var rpcCallback = new RpcObserveResponseCompositeCallback(transportService, client, requestMsg, mainCallback);
downlinkHandler.sendObserveCompositeRequest(client, request, rpcCallback);
}
private void sendCancelObserveCompositeRequest(LwM2mClient client, TransportProtos.ToDeviceRpcRequestMsg requestMsg) {
String[] versionedIds = getIdsFromParameters(client, requestMsg);
TbLwM2MCancelObserveCompositeRequest request = TbLwM2MCancelObserveCompositeRequest.builder().versionedIds(versionedIds).timeout(clientContext.getRequestTimeout(client)).build();
var mainCallback = new TbLwM2MCancelObserveCompositeCallback(logService, client, versionedIds);
var rpcCallback = new RpcCancelObserveCompositeCallback(transportService, client, requestMsg, mainCallback);
downlinkHandler.sendCancelObserveCompositeRequest(client, request, rpcCallback);
}
private void sendDeleteRequest(LwM2mClient client, TransportProtos.ToDeviceRpcRequestMsg requestMsg, String versionedId) {
TbLwM2MDeleteRequest downlink = TbLwM2MDeleteRequest.builder().versionedId(versionedId).timeout(clientContext.getRequestTimeout(client)).build();
var mainCallback = new TbLwM2MDeleteCallback(logService, client, versionedId);
@ -364,7 +424,7 @@ public class DefaultLwM2MRpcRequestHandler implements LwM2MRpcRequestHandler {
TbLwM2MCancelAllRequest downlink = TbLwM2MCancelAllRequest.builder().timeout(clientContext.getRequestTimeout(client)).build();
var mainCallback = new TbLwM2MCancelAllObserveCallback(logService, client);
var rpcCallback = new RpcCancelAllObserveCallback(transportService, client, requestMsg, mainCallback);
downlinkHandler.sendCancelAllRequest(client, downlink, rpcCallback);
downlinkHandler.sendCancelObserveAllRequest(client, downlink, rpcCallback);
}
private String getIdFromParameters(LwM2mClient client, LwM2MRpcRequestHeader header) {
@ -412,16 +472,4 @@ public class DefaultLwM2MRpcRequestHandler implements LwM2MRpcRequestHandler {
public void onToServerRpcResponse(TransportProtos.ToServerRpcResponseMsg toServerResponse) {
log.info("[{}] toServerRpcResponse", toServerResponse);
}
private ContentFormat getCompositeContentFormat(LwM2mClient client) {
if (client.getClientSupportContentFormats().contains(ContentFormat.SENML_JSON)) {
return ContentFormat.SENML_JSON;
}
else if (client.getClientSupportContentFormats().contains(ContentFormat.SENML_CBOR)) {
return ContentFormat.SENML_CBOR;
}
else {
return null;
}
}
}

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

@ -35,7 +35,7 @@ public class RpcDiscoverCallback extends RpcLwM2MDownlinkCallback<DiscoverReques
}
protected Optional<String> serializeSuccessfulResponse(DiscoverResponse response) {
return Optional.of(serializer.serialize(response.getObjectLinks()));
return Optional.of(serializer.serializeCoreLinkFormat(response.getObjectLinks()));
}
}

37
common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/rpc/composite/RpcCancelObserveCompositeCallback.java

@ -0,0 +1,37 @@
/**
* Copyright © 2016-2024 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.transport.lwm2m.server.rpc.composite;
import org.eclipse.leshan.core.ResponseCode;
import org.thingsboard.server.common.transport.TransportService;
import org.thingsboard.server.gen.transport.TransportProtos;
import org.thingsboard.server.transport.lwm2m.server.client.LwM2mClient;
import org.thingsboard.server.transport.lwm2m.server.downlink.DownlinkRequestCallback;
import org.thingsboard.server.transport.lwm2m.server.downlink.composite.TbLwM2MCancelObserveCompositeRequest;
import org.thingsboard.server.transport.lwm2m.server.rpc.LwM2MRpcResponseBody;
import org.thingsboard.server.transport.lwm2m.server.rpc.RpcDownlinkRequestCallbackProxy;
public class RpcCancelObserveCompositeCallback extends RpcDownlinkRequestCallbackProxy<TbLwM2MCancelObserveCompositeRequest, Integer> {
public RpcCancelObserveCompositeCallback(TransportService transportService, LwM2mClient client, TransportProtos.ToDeviceRpcRequestMsg requestMsg, DownlinkRequestCallback<TbLwM2MCancelObserveCompositeRequest, Integer> callback) {
super(transportService, client, requestMsg, callback);
}
@Override
protected void sendRpcReplyOnSuccess(Integer response) {
reply(LwM2MRpcResponseBody.builder().result(ResponseCode.CONTENT.getName()).value(response.toString()).build());
}
}

40
common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/rpc/composite/RpcObserveResponseCompositeCallback.java

@ -0,0 +1,40 @@
/**
* Copyright © 2016-2024 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.transport.lwm2m.server.rpc.composite;
import org.eclipse.leshan.core.request.LwM2mRequest;
import org.eclipse.leshan.core.response.ObserveCompositeResponse;
import org.thingsboard.server.common.transport.TransportService;
import org.thingsboard.server.gen.transport.TransportProtos;
import org.thingsboard.server.transport.lwm2m.server.client.LwM2mClient;
import org.thingsboard.server.transport.lwm2m.server.downlink.DownlinkRequestCallback;
import org.thingsboard.server.transport.lwm2m.server.rpc.RpcLwM2MDownlinkCallback;
import java.util.Optional;
import static org.thingsboard.server.transport.lwm2m.utils.LwM2MTransportUtil.contentToString;
public class RpcObserveResponseCompositeCallback<R extends LwM2mRequest<T>, T extends ObserveCompositeResponse> extends RpcLwM2MDownlinkCallback<R, T> {
public RpcObserveResponseCompositeCallback(TransportService transportService, LwM2mClient client, TransportProtos.ToDeviceRpcRequestMsg requestMsg, DownlinkRequestCallback<R, T> callback) {
super(transportService, client, requestMsg, callback);
}
@Override
protected Optional<String> serializeSuccessfulResponse(T response) {
return contentToString(response.getContent());
}
}

619
common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbInMemoryRegistrationStore.java

@ -0,0 +1,619 @@
/**
* Copyright © 2016-2024 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.transport.lwm2m.server.store;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.eclipse.californium.core.coap.Token;
import org.eclipse.californium.core.network.RandomTokenGenerator;
import org.eclipse.californium.core.network.TokenGenerator;
import org.eclipse.californium.core.network.TokenGenerator.Scope;
import org.eclipse.leshan.core.Destroyable;
import org.eclipse.leshan.core.Startable;
import org.eclipse.leshan.core.Stoppable;
import org.eclipse.leshan.core.model.ObjectModel;
import org.eclipse.leshan.core.model.ResourceModel;
import org.eclipse.leshan.core.node.LwM2mPath;
import org.eclipse.leshan.core.observation.CompositeObservation;
import org.eclipse.leshan.core.observation.Observation;
import org.eclipse.leshan.core.observation.ObservationIdentifier;
import org.eclipse.leshan.core.observation.SingleObservation;
import org.eclipse.leshan.core.peer.LwM2mIdentity;
import org.eclipse.leshan.core.request.ContentFormat;
import org.eclipse.leshan.core.util.NamedThreadFactory;
import org.eclipse.leshan.server.registration.Deregistration;
import org.eclipse.leshan.server.registration.ExpirationListener;
import org.eclipse.leshan.server.registration.Registration;
import org.eclipse.leshan.server.registration.RegistrationStore;
import org.eclipse.leshan.server.registration.RegistrationUpdate;
import org.eclipse.leshan.server.registration.UpdatedRegistration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.transport.lwm2m.config.LwM2MTransportServerConfig;
import org.thingsboard.server.transport.lwm2m.server.LwM2mVersionedModelProvider;
import java.net.InetSocketAddress;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import static org.eclipse.leshan.core.californium.ObserveUtil.CTX_CF_OBERSATION;
import static org.eclipse.leshan.core.californium.ObserveUtil.extractSerializedObservation;
public class TbInMemoryRegistrationStore implements RegistrationStore, Startable, Stoppable, Destroyable {
private final Logger LOG = LoggerFactory.getLogger(TbInMemoryRegistrationStore.class);
// Data structure
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<LwM2mIdentity, Registration> regsByIdentity = new HashMap<>();
private final Map<ObservationIdentifier, Observation> obsByToken = new HashMap<>();
private final Map<String, Set<ObservationIdentifier>> tokensByRegId = new HashMap<>();
private final ReadWriteLock lock = new ReentrantReadWriteLock();
// Listener use to notify when a registration expires
private ExpirationListener expirationListener;
private final ScheduledExecutorService schedExecutor;
private ScheduledFuture<?> cleanerTask;
private boolean started = false;
private final long cleanPeriod; // in seconds
private final LwM2MTransportServerConfig config;
private final LwM2mVersionedModelProvider modelProvider;
private TokenGenerator tokenGenerator;
public TbInMemoryRegistrationStore() {
this(null, 2, null); // default clean period : 2s
}
public TbInMemoryRegistrationStore(LwM2MTransportServerConfig config, long cleanPeriodInSec, LwM2mVersionedModelProvider modelProvider) {
this(config, Executors.newScheduledThreadPool(1,
new NamedThreadFactory(String.format("TbInMemoryRegistrationStore Cleaner (%ds)", cleanPeriodInSec))),
cleanPeriodInSec, modelProvider);
}
public TbInMemoryRegistrationStore(LwM2MTransportServerConfig config, ScheduledExecutorService schedExecutor, long cleanPeriodInSec, LwM2mVersionedModelProvider modelProvider) {
this.schedExecutor = schedExecutor;
this.cleanPeriod = cleanPeriodInSec;
this.modelProvider = modelProvider;
this.config = config;
}
/* *************** Leshan Registration API **************** */
@Override
public Deregistration addRegistration(Registration registration) {
try {
lock.writeLock().lock();
Registration registrationRemoved = regsByEp.put(registration.getEndpoint(), registration);
regsByRegId.put(registration.getId(), registration);
regsByIdentity.put(registration.getClientTransportData().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);
if (registrationRemoved != null) {
Collection<Observation> observationsRemoved = unsafeRemoveAllObservations(registrationRemoved.getId());
if (!registrationRemoved.getSocketAddress().equals(registration.getSocketAddress())) {
removeFromMap(regsByAddr, registrationRemoved.getSocketAddress(), registrationRemoved);
}
if (!registrationRemoved.getId().equals(registration.getId())) {
removeFromMap(regsByRegId, registrationRemoved.getId(), registrationRemoved);
}
if (!registrationRemoved.getClientTransportData().getIdentity()
.equals(registration.getClientTransportData().getIdentity())) {
removeFromMap(regsByIdentity, registrationRemoved.getClientTransportData().getIdentity(),
registrationRemoved);
}
return new Deregistration(registrationRemoved, observationsRemoved);
}
} finally {
lock.writeLock().unlock();
}
return null;
}
@Override
public UpdatedRegistration updateRegistration(RegistrationUpdate update) {
try {
lock.writeLock().lock();
Registration registration = getRegistration(update.getRegistrationId());
if (registration == null) {
return null;
} else {
Registration updatedRegistration = update.update(registration);
regsByEp.put(updatedRegistration.getEndpoint(), updatedRegistration);
// If registration is already associated to this address we don't care as we only want to keep the most
// recent binding.
regsByAddr.put(updatedRegistration.getSocketAddress(), updatedRegistration);
if (!registration.getSocketAddress().equals(updatedRegistration.getSocketAddress())) {
removeFromMap(regsByAddr, registration.getSocketAddress(), registration);
}
regsByIdentity.put(updatedRegistration.getClientTransportData().getIdentity(), updatedRegistration);
if (!registration.getClientTransportData().getIdentity()
.equals(updatedRegistration.getClientTransportData().getIdentity())) {
removeFromMap(regsByIdentity, registration.getClientTransportData().getIdentity(), registration);
}
regsByRegId.put(updatedRegistration.getId(), updatedRegistration);
return new UpdatedRegistration(registration, updatedRegistration);
}
} finally {
lock.writeLock().unlock();
}
}
@Override
public Registration getRegistration(String registrationId) {
try {
lock.readLock().lock();
return regsByRegId.get(registrationId);
} finally {
lock.readLock().unlock();
}
}
@Override
public Registration getRegistrationByEndpoint(String endpoint) {
try {
lock.readLock().lock();
return regsByEp.get(endpoint);
} finally {
lock.readLock().unlock();
}
}
@Override
public Registration getRegistrationByAdress(InetSocketAddress address) {
try {
lock.readLock().lock();
return regsByAddr.get(address);
} finally {
lock.readLock().unlock();
}
}
@Override
public Registration getRegistrationByIdentity(LwM2mIdentity identity) {
try {
lock.readLock().lock();
return regsByIdentity.get(identity);
} finally {
lock.readLock().unlock();
}
}
@Override
public Iterator<Registration> getAllRegistrations() {
try {
lock.readLock().lock();
return new ArrayList<>(regsByEp.values()).iterator();
} finally {
lock.readLock().unlock();
}
}
@Override
public Deregistration removeRegistration(String registrationId) {
try {
lock.writeLock().lock();
Registration registration = getRegistration(registrationId);
if (registration != null) {
Collection<Observation> observationsRemoved = unsafeRemoveAllObservations(registration.getId());
regsByEp.remove(registration.getEndpoint());
removeFromMap(regsByAddr, registration.getSocketAddress(), registration);
removeFromMap(regsByRegId, registration.getId(), registration);
removeFromMap(regsByIdentity, registration.getClientTransportData().getIdentity(), registration);
return new Deregistration(registration, observationsRemoved);
}
return null;
} finally {
lock.writeLock().unlock();
}
}
/* *************** Leshan Observation API **************** */
@Override
public Collection<Observation> addObservation(String registrationId, Observation observation, boolean addIfAbsent) {
List<Observation> removed = new ArrayList<>();
try {
lock.writeLock().lock();
if (!regsByRegId.containsKey(registrationId)) {
throw new IllegalStateException(String.format(
"can not add observation %s there is no registration with id %s", observation, registrationId));
}
if (observation instanceof SingleObservation) {
if (validateObserveResource(((SingleObservation)observation).getPath(), registrationId)) {
updateSingleObservation(registrationId, (SingleObservation) observation, addIfAbsent, removed);
// cancel existing observations for the same path and registration id.
cancelObservation (observation, registrationId, removed);
}
} else {
ContentFormat ct = ((CompositeObservation) observation).getResponseContentFormat();
Map<String, String> ctx = observation.getContext();
String serializedObservation = extractSerializedObservation(observation);
JsonNode nodeSerObs = JacksonUtil.toJsonNode(serializedObservation);
((CompositeObservation)observation).getPaths().forEach(path -> {
if (validateObserveResource(path, registrationId)) {
String serializedObs = createSerializedSingleObservation(nodeSerObs, path.toString());
Observation singleObservation = createSingleObservation(registrationId, path, ct, ctx, serializedObs, getTokenGenerator());
updateSingleObservation(registrationId, (SingleObservation) singleObservation, addIfAbsent, removed);
// cancel existing observations for the same path and registration id.
cancelObservation (singleObservation, registrationId, removed);
}
});
}
} finally {
lock.writeLock().unlock();
}
return removed;
}
private boolean validateObserveResource(LwM2mPath path, String registrationId){
// check if the resource is readable.
if (path.isResource() || path.isResourceInstance()) {
ObjectModel objectModel = modelProvider.getObjectModel(getRegistration(registrationId)).getObjectModel(path.getObjectId());
ResourceModel resourceModel = objectModel == null ? null : objectModel.resources.get(path.getResourceId());
if (resourceModel == null) {
return false;
} else if (!resourceModel.operations.isReadable()) {
return false;
} else if (path.isResourceInstance() && !resourceModel.multiple) {
return false;
}
}
return true;
}
private void updateSingleObservation (String registrationId, SingleObservation observation, boolean addIfAbsent, List<Observation> removed) {
// Absorption by existing Observations
Observation previousObservation = null;
SingleObservation existingObservation = null;
ObservationIdentifier id = observation.getId();
if (addIfAbsent) {
if (!obsByToken.containsKey(id)) {
existingObservation = validateByAbsorptionExistingObservations(observation);
if (existingObservation == null) {
obsByToken.put(id, observation);
} else if (!existingObservation.getPath().equals(observation.getPath())){
obsByToken.put(id, observation);
previousObservation = obsByToken.get(existingObservation.getId());
}
}
} else {
previousObservation = obsByToken.put(id, observation);
}
if (!tokensByRegId.containsKey(registrationId)) {
tokensByRegId.put(registrationId, new HashSet<ObservationIdentifier>());
}
if (existingObservation == null || !existingObservation.getPath().equals(observation.getPath())) {
tokensByRegId.get(registrationId).add(id);
}
// log any collisions
if (addIfAbsent && previousObservation != null) {
if (!existingObservation.getPath().equals(observation.getPath())) {
removed.add(previousObservation);
LOG.warn("Token collision ? observation [{}] will be replaced by observation [{}], that this observation includes input observation [{}]!",
previousObservation, observation, observation);
} else {
LOG.warn("Token collision ? existing observation [{}] includes input observation [{}]",
existingObservation, observation);
}
}
}
private SingleObservation validateByAbsorptionExistingObservations (SingleObservation observation) {
LwM2mPath pathObservation = observation.getPath();
AtomicReference<SingleObservation> result = new AtomicReference<>();
obsByToken.values().stream().forEach(obs -> {
LwM2mPath pathObs = ((SingleObservation)obs).getPath();
if ((!pathObservation.equals(pathObs) && pathObs.startWith(pathObservation)) || // pathObs = "3/0/9"-> pathObservation = "3"
(pathObservation.equals(pathObs) && !observation.getId().equals(obs.getId()))) {
result.set((SingleObservation)obs);
} else if (!pathObservation.equals(pathObs) && pathObservation.startWith(pathObs)) { // pathObs = "3" -> pathObservation = "3/0/9"
result.set(observation);
}
});
return result.get();
}
private TokenGenerator getTokenGenerator(){
if (this.tokenGenerator == null) {
this.tokenGenerator = new RandomTokenGenerator(config.getCoapConfig());
}
return this.tokenGenerator;
}
public static SingleObservation createSingleObservation(String registrationId, LwM2mPath target, ContentFormat ct,
Map<String, String> ctx, String serializedObservation, TokenGenerator tokenGenerator) {
Token token = tokenGenerator.createToken(Scope.SHORT_TERM);
Map<String, String> protocolData = Collections.emptyMap();
if (serializedObservation != null) {
protocolData = new HashMap<>();
protocolData.put(CTX_CF_OBERSATION, serializedObservation);
}
return new SingleObservation(new ObservationIdentifier(token.getBytes()), registrationId, target, ct, ctx, protocolData);
}
public static String createSerializedSingleObservation(JsonNode nodeSerObs, String path){
if (nodeSerObs.has("context")){
((ObjectNode) nodeSerObs.get("context")).put("leshan-path", path + "\n");
return JacksonUtil.toString(nodeSerObs);
}
return null;
}
@Override
public Observation removeObservation(String registrationId, ObservationIdentifier observationId) {
try {
lock.writeLock().lock();
Observation observation = unsafeGetObservation(observationId);
if (observation != null && registrationId.equals(observation.getRegistrationId())) {
unsafeRemoveObservation(observationId);
return observation;
}
return null;
} finally {
lock.writeLock().unlock();
}
}
@Override
public Observation getObservation(String registrationId, ObservationIdentifier observationId) {
try {
lock.readLock().lock();
Observation observation = unsafeGetObservation(observationId);
if (observation != null && registrationId.equals(observation.getRegistrationId())) {
return observation;
}
return null;
} finally {
lock.readLock().unlock();
}
}
@Override
public Observation getObservation(ObservationIdentifier observationId) {
try {
lock.readLock().lock();
Observation observation = unsafeGetObservation(observationId);
if (observation != null) {
return observation;
}
return null;
} finally {
lock.readLock().unlock();
}
}
/**
* Prepare for Cancel one Observation
* @param registrationId
* @return
*/
@Override
public Collection<Observation> getObservations(String registrationId) {
try {
lock.readLock().lock();
return unsafeGetObservations(registrationId);
} finally {
lock.readLock().unlock();
}
}
/**
* CancelAllObservation
* @param registrationId
* @return
*/
@Override
public Collection<Observation> removeObservations(String registrationId) {
try {
lock.writeLock().lock();
return unsafeRemoveAllObservations(registrationId);
} finally {
lock.writeLock().unlock();
}
}
/* *************** Observation utility functions **************** */
private Observation unsafeGetObservation(ObservationIdentifier token) {
Observation obs = obsByToken.get(token);
return obs;
}
private void cancelObservation (Observation observation, String registrationId, List<Observation> removed) {
for (Observation obs : unsafeGetObservations(registrationId)) {
cancelExistingObservation(observation, obs, removed);
}
}
private void cancelExistingObservation(Observation observation, Observation obs, List<Observation> removed) {
LwM2mPath pathObservation = ((SingleObservation)observation).getPath();
LwM2mPath pathObs = ((SingleObservation)obs).getPath();
if ((!pathObservation.equals(pathObs) && pathObs.startWith(pathObservation)) || // pathObservation = "3", pathObs = "3/0/9"
(pathObservation.equals(pathObs) && !observation.getId().equals(obs.getId()))) {
unsafeRemoveObservation(obs.getId());
removed.add(obs);
} else if (!pathObservation.equals(pathObs) && pathObservation.startWith(pathObs)) { // pathObservation = "3/0/9", pathObs = "3"
unsafeRemoveObservation(observation.getId());
}
}
private void unsafeRemoveObservation(ObservationIdentifier observationId) {
Observation removed = obsByToken.remove(observationId);
if (removed != null) {
String registrationId = removed.getRegistrationId();
Set<ObservationIdentifier> tokens = tokensByRegId.get(registrationId);
tokens.remove(observationId);
if (tokens.isEmpty()) {
tokensByRegId.remove(registrationId);
}
}
}
/**
* CancelAllObservation
* @param registrationId
* @return
*/
private Collection<Observation> unsafeRemoveAllObservations(String registrationId) {
Collection<Observation> removed = new ArrayList<>();
Set<ObservationIdentifier> ids = tokensByRegId.get(registrationId);
if (ids != null) {
for (ObservationIdentifier id : ids) {
Observation observationRemoved = obsByToken.remove(id);
if (observationRemoved != null) {
removed.add(observationRemoved);
}
}
}
tokensByRegId.remove(registrationId);
return removed;
}
private Collection<Observation> unsafeGetObservations(String registrationId) {
Collection<Observation> result = new ArrayList<>();
Set<ObservationIdentifier> ids = tokensByRegId.get(registrationId);
if (ids != null) {
for (ObservationIdentifier id : ids) {
Observation obs = unsafeGetObservation(id);
if (obs != null) {
result.add(obs);
}
}
}
return result;
}
/* *************** Expiration handling **************** */
@Override
public void setExpirationListener(ExpirationListener listener) {
this.expirationListener = listener;
}
/**
* start the registration store, will start regular cleanup of dead registrations.
*/
@Override
public synchronized void start() {
if (!started) {
started = true;
cleanerTask = schedExecutor.scheduleAtFixedRate(new TbInMemoryRegistrationStore.Cleaner(), cleanPeriod, cleanPeriod, TimeUnit.SECONDS);
}
}
/**
* Stop the underlying cleanup of the registrations.
*/
@Override
public synchronized void stop() {
if (started) {
started = false;
if (cleanerTask != null) {
cleanerTask.cancel(false);
cleanerTask = null;
}
}
}
/**
* Destroy "cleanup" scheduler.
*/
@Override
public synchronized void destroy() {
started = false;
schedExecutor.shutdownNow();
try {
schedExecutor.awaitTermination(5, TimeUnit.SECONDS);
} catch (InterruptedException e) {
LOG.warn("Destroying InMemoryRegistrationStore was interrupted.", e);
}
}
private class Cleaner implements Runnable {
@Override
public void run() {
try {
Collection<Registration> allRegs = new ArrayList<>();
try {
lock.readLock().lock();
allRegs.addAll(regsByEp.values());
} finally {
lock.readLock().unlock();
}
for (Registration reg : allRegs) {
if (!reg.isAlive()) {
// force de-registration
Deregistration removedRegistration = removeRegistration(reg.getId());
expirationListener.registrationExpired(removedRegistration.getRegistration(),
removedRegistration.getObservations());
}
}
} catch (Exception e) {
LOG.warn("Unexpected Exception while registration cleaning", e);
}
}
}
// boolean remove(Object key, Object value) exist only since java8
// So this method is here only while we want to support java 7
protected <K, V> boolean removeFromMap(Map<K, V> map, K key, V value) {
if (map.containsKey(key) && Objects.equals(map.get(key), value)) {
map.remove(key);
return true;
} else
return false;
}
}

16
common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbInMemorySecurityStore.java

@ -16,6 +16,7 @@
package org.thingsboard.server.transport.lwm2m.server.store;
import org.eclipse.leshan.core.SecurityMode;
import org.eclipse.leshan.core.peer.OscoreIdentity;
import org.eclipse.leshan.server.security.NonUniqueSecurityInfoException;
import org.eclipse.leshan.server.security.SecurityInfo;
import org.thingsboard.server.transport.lwm2m.secure.TbLwM2MSecurityInfo;
@ -83,25 +84,30 @@ public class TbInMemorySecurityStore implements TbEditableSecurityStore {
}
}
@Override
public SecurityInfo getByOscoreIdentity(OscoreIdentity oscoreIdentity) {
return null;
}
@Override
public void put(TbLwM2MSecurityInfo tbSecurityInfo) throws NonUniqueSecurityInfoException {
writeLock.lock();
try {
String identity = null;
if (tbSecurityInfo.getSecurityInfo() != null) {
identity = tbSecurityInfo.getSecurityInfo().getIdentity();
identity = tbSecurityInfo.getSecurityInfo().getPskIdentity();
if (identity != null) {
TbLwM2MSecurityInfo infoByIdentity = securityByIdentity.get(identity);
if (infoByIdentity != null && !tbSecurityInfo.getSecurityInfo().getEndpoint().equals(infoByIdentity.getEndpoint())) {
throw new NonUniqueSecurityInfoException("PSK Identity " + identity + " is already used");
}
securityByIdentity.put(tbSecurityInfo.getSecurityInfo().getIdentity(), tbSecurityInfo);
securityByIdentity.put(tbSecurityInfo.getSecurityInfo().getPskIdentity(), tbSecurityInfo);
}
}
TbLwM2MSecurityInfo previous = securityByEp.put(tbSecurityInfo.getEndpoint(), tbSecurityInfo);
if (previous != null && previous.getSecurityInfo() != null) {
String previousIdentity = previous.getSecurityInfo().getIdentity();
String previousIdentity = previous.getSecurityInfo().getPskIdentity();
if (previousIdentity != null && !previousIdentity.equals(identity)) {
securityByIdentity.remove(previousIdentity);
}
@ -116,8 +122,8 @@ public class TbInMemorySecurityStore implements TbEditableSecurityStore {
writeLock.lock();
try {
TbLwM2MSecurityInfo securityInfo = securityByEp.remove(endpoint);
if (securityInfo != null && securityInfo.getSecurityInfo() != null && securityInfo.getSecurityInfo().getIdentity() != null) {
securityByIdentity.remove(securityInfo.getSecurityInfo().getIdentity());
if (securityInfo != null && securityInfo.getSecurityInfo() != null && securityInfo.getSecurityInfo().getPskIdentity() != null) {
securityByIdentity.remove(securityInfo.getSecurityInfo().getPskIdentity());
}
} finally {
writeLock.unlock();

395
common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbLwM2mRedisRegistrationStore.java

@ -15,26 +15,34 @@
*/
package org.thingsboard.server.transport.lwm2m.server.store;
import com.fasterxml.jackson.databind.JsonNode;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.californium.core.coap.Token;
import org.eclipse.californium.core.observe.ObservationStoreException;
import org.eclipse.californium.elements.EndpointContext;
import org.eclipse.californium.core.network.RandomTokenGenerator;
import org.eclipse.californium.core.network.TokenGenerator;
import org.eclipse.californium.core.network.serialization.UdpDataParser;
import org.eclipse.californium.core.network.serialization.UdpDataSerializer;
import org.eclipse.leshan.core.Destroyable;
import org.eclipse.leshan.core.Startable;
import org.eclipse.leshan.core.Stoppable;
import org.eclipse.leshan.core.californium.ObserveUtil;
import org.eclipse.leshan.core.model.ObjectModel;
import org.eclipse.leshan.core.model.ResourceModel;
import org.eclipse.leshan.core.node.LwM2mPath;
import org.eclipse.leshan.core.observation.CompositeObservation;
import org.eclipse.leshan.core.observation.Observation;
import org.eclipse.leshan.core.observation.ObservationIdentifier;
import org.eclipse.leshan.core.observation.SingleObservation;
import org.eclipse.leshan.core.request.Identity;
import org.eclipse.leshan.core.peer.LwM2mIdentity;
import org.eclipse.leshan.core.request.ContentFormat;
import org.eclipse.leshan.core.util.NamedThreadFactory;
import org.eclipse.leshan.core.util.Validate;
import org.eclipse.leshan.server.californium.registration.CaliforniumRegistrationStore;
import org.eclipse.leshan.server.redis.RedisRegistrationStore;
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;
import org.eclipse.leshan.server.registration.ExpirationListener;
import org.eclipse.leshan.server.registration.Registration;
import org.eclipse.leshan.server.registration.RegistrationStore;
import org.eclipse.leshan.server.registration.RegistrationUpdate;
import org.eclipse.leshan.server.registration.UpdatedRegistration;
import org.slf4j.Logger;
@ -45,6 +53,9 @@ import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.Cursor;
import org.springframework.data.redis.core.ScanOptions;
import org.springframework.integration.redis.util.RedisLockRegistry;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.transport.lwm2m.config.LwM2MTransportServerConfig;
import org.thingsboard.server.transport.lwm2m.server.LwM2mVersionedModelProvider;
import java.net.InetSocketAddress;
import java.util.ArrayList;
@ -54,16 +65,22 @@ import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.locks.Lock;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.eclipse.leshan.core.californium.ObserveUtil.extractSerializedObservation;
import static org.thingsboard.server.transport.lwm2m.server.store.TbInMemoryRegistrationStore.createSerializedSingleObservation;
import static org.thingsboard.server.transport.lwm2m.server.store.TbInMemoryRegistrationStore.createSingleObservation;
public class TbLwM2mRedisRegistrationStore implements CaliforniumRegistrationStore, Startable, Stoppable, Destroyable {
@Slf4j
public class TbLwM2mRedisRegistrationStore implements RegistrationStore, Startable, Stoppable, Destroyable {
/** Default time in seconds between 2 cleaning tasks (used to remove expired registration). */
public static final long DEFAULT_CLEAN_PERIOD = 60;
public static final int DEFAULT_CLEAN_LIMIT = 500;
@ -79,10 +96,15 @@ public class TbLwM2mRedisRegistrationStore implements CaliforniumRegistrationSto
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 byte[] OBS_TKN_GET_ALL = "OBS:TKN:*".getBytes(UTF_8);
private static final String OBS_TKNS_REGID_IDX = "TKNS:REGID:"; // secondary index (token list by registration)
private static final byte[] EXP_EP = "EXP:EP".getBytes(UTF_8); // a sorted set used for registration expiration
// (expiration date, Endpoint)
private final RegistrationSerDes registrationSerDes = new RegistrationSerDes();
private final ObservationSerDes observationSerDes = new ObservationSerDes();
private final org.eclipse.leshan.server.californium.observation.ObservationSerDes observationSerDesCoap =
new org.eclipse.leshan.server.californium.observation.ObservationSerDes(new UdpDataParser(), new UdpDataSerializer());
private final RedisConnectionFactory connectionFactory;
// Listener use to notify when a registration expires
@ -98,24 +120,31 @@ public class TbLwM2mRedisRegistrationStore implements CaliforniumRegistrationSto
private final RedisLockRegistry redisLock;
public TbLwM2mRedisRegistrationStore(RedisConnectionFactory connectionFactory) {
this(connectionFactory, DEFAULT_CLEAN_PERIOD, DEFAULT_GRACE_PERIOD, DEFAULT_CLEAN_LIMIT); // default clean period 60s
private final LwM2MTransportServerConfig config;
private TokenGenerator tokenGenerator;
private final LwM2mVersionedModelProvider modelProvider;
public TbLwM2mRedisRegistrationStore(LwM2MTransportServerConfig config, RedisConnectionFactory connectionFactory, LwM2mVersionedModelProvider modelProvider) {
this(config, connectionFactory, DEFAULT_CLEAN_PERIOD, DEFAULT_GRACE_PERIOD, DEFAULT_CLEAN_LIMIT, modelProvider); // default clean period 60s
}
public TbLwM2mRedisRegistrationStore(RedisConnectionFactory connectionFactory, long cleanPeriodInSec, long lifetimeGracePeriodInSec, int cleanLimit) {
this(connectionFactory, Executors.newScheduledThreadPool(1,
public TbLwM2mRedisRegistrationStore(LwM2MTransportServerConfig config, RedisConnectionFactory connectionFactory, long cleanPeriodInSec, long lifetimeGracePeriodInSec, int cleanLimit, LwM2mVersionedModelProvider modelProvider) {
this(config, connectionFactory, Executors.newScheduledThreadPool(1,
new NamedThreadFactory(String.format("RedisRegistrationStore Cleaner (%ds)", cleanPeriodInSec))),
cleanPeriodInSec, lifetimeGracePeriodInSec, cleanLimit);
cleanPeriodInSec, lifetimeGracePeriodInSec, cleanLimit, modelProvider);
}
public TbLwM2mRedisRegistrationStore(RedisConnectionFactory connectionFactory, ScheduledExecutorService schedExecutor, long cleanPeriodInSec,
long lifetimeGracePeriodInSec, int cleanLimit) {
public TbLwM2mRedisRegistrationStore(LwM2MTransportServerConfig config, RedisConnectionFactory connectionFactory, ScheduledExecutorService schedExecutor, long cleanPeriodInSec,
long lifetimeGracePeriodInSec, int cleanLimit, LwM2mVersionedModelProvider modelProvider) {
this.connectionFactory = connectionFactory;
this.schedExecutor = schedExecutor;
this.cleanPeriod = cleanPeriodInSec;
this.cleanLimit = cleanLimit;
this.gracePeriod = lifetimeGracePeriodInSec;
this.redisLock = new RedisLockRegistry(connectionFactory, "Registration");
this.config = config;
this.modelProvider = modelProvider;
}
/* *************** Redis Key utility function **************** */
@ -159,7 +188,7 @@ public class TbLwM2mRedisRegistrationStore implements CaliforniumRegistrationSto
connection.set(regid_idx, registration.getEndpoint().getBytes(UTF_8));
byte[] addr_idx = toRegAddrKey(registration.getSocketAddress());
connection.set(addr_idx, registration.getEndpoint().getBytes(UTF_8));
byte[] identity_idx = toRegIdentityKey(registration.getIdentity());
byte[] identity_idx = toRegIdentityKey(registration.getClientTransportData().getIdentity());
connection.set(identity_idx, registration.getEndpoint().getBytes(UTF_8));
// Add or update expiration
@ -173,7 +202,7 @@ public class TbLwM2mRedisRegistrationStore implements CaliforniumRegistrationSto
if (!oldRegistration.getSocketAddress().equals(registration.getSocketAddress())) {
removeAddrIndex(connection, oldRegistration);
}
if (!oldRegistration.getIdentity().equals(registration.getIdentity())) {
if (!oldRegistration.getClientTransportData().getIdentity().equals(registration.getClientTransportData().getIdentity())) {
removeIdentityIndex(connection, oldRegistration);
}
// remove old observation
@ -231,7 +260,7 @@ public class TbLwM2mRedisRegistrationStore implements CaliforniumRegistrationSto
if (!r.getSocketAddress().equals(updatedRegistration.getSocketAddress())) {
removeAddrIndex(connection, r);
}
if (!r.getIdentity().equals(updatedRegistration.getIdentity())) {
if (!r.getClientTransportData().getIdentity().equals(updatedRegistration.getClientTransportData().getIdentity())) {
removeIdentityIndex(connection, r);
}
@ -251,6 +280,18 @@ public class TbLwM2mRedisRegistrationStore implements CaliforniumRegistrationSto
return getRegistration(connection, registrationId);
}
}
private Registration getRegistration(RedisConnection connection, String registrationId) {
byte[] ep = connection.get(toRegIdKey(registrationId));
if (ep == null) {
return null;
}
byte[] data = connection.get(toEndpointKey(ep));
if (data == null) {
return null;
}
return deserializeReg(data);
}
@Override
public Registration getRegistrationByEndpoint(String endpoint) {
@ -281,7 +322,7 @@ public class TbLwM2mRedisRegistrationStore implements CaliforniumRegistrationSto
}
@Override
public Registration getRegistrationByIdentity(Identity identity) {
public Registration getRegistrationByIdentity(LwM2mIdentity identity) {
Validate.notNull(identity);
try (var connection = connectionFactory.getConnection()) {
byte[] ep = connection.get(toRegIdentityKey(identity));
@ -327,6 +368,7 @@ public class TbLwM2mRedisRegistrationStore implements CaliforniumRegistrationSto
}
}
private Deregistration removeRegistration(RedisConnection connection, String registrationId, boolean removeOnlyIfNotAlive) {
// fetch the client ep by registration ID index
byte[] ep = connection.get(toRegIdKey(registrationId));
@ -371,7 +413,7 @@ public class TbLwM2mRedisRegistrationStore implements CaliforniumRegistrationSto
}
private void removeIdentityIndex(RedisConnection connection, Registration r) {
removeSecondaryIndex(connection, toRegIdentityKey(r.getIdentity()), r.getEndpoint());
removeSecondaryIndex(connection, toRegIdentityKey(r.getClientTransportData().getIdentity()), r.getEndpoint());
}
//TODO: JedisCluster didn't implement Transaction, maybe should use some advanced key creation strategies
@ -410,8 +452,8 @@ public class TbLwM2mRedisRegistrationStore implements CaliforniumRegistrationSto
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[] toRegIdentityKey(LwM2mIdentity identity) {
return toKey(REG_EP_IDENTITY, identity.toString());
}
private byte[] toEndpointKey(String endpoint) {
@ -423,11 +465,11 @@ public class TbLwM2mRedisRegistrationStore implements CaliforniumRegistrationSto
}
private byte[] serializeReg(Registration registration) {
return RegistrationSerDes.bSerialize(registration);
return registrationSerDes.bSerialize(registration);
}
private Registration deserializeReg(byte[] data) {
return RegistrationSerDes.deserialize(data);
return registrationSerDes.deserialize(data);
}
/* *************** Leshan Observation API **************** */
@ -436,15 +478,17 @@ public class TbLwM2mRedisRegistrationStore implements CaliforniumRegistrationSto
* The observation is not persisted here, it is done by the Californium layer (in the implementation of the
* org.eclipse.californium.core.observe.ObservationStore#add method)
*/
@Override
public Collection<Observation> addObservation(String registrationId, Observation observation) {
public Collection<Observation> addObservation(String registrationId, Observation observation, boolean addIfAbsent) {
List<Observation> removed = new ArrayList<>();
try (var connection = connectionFactory.getConnection()) {
// fetch the client ep by registration ID index
byte[] ep = connection.get(toRegIdKey(registrationId));
byte[] ep = connection.commands().get(toRegIdKey(registrationId));
if (ep == null) {
return null;
throw new IllegalStateException(String.format(
"can not add observation %s there is no registration with id %s", observation, registrationId));
}
Lock lock = null;
@ -453,17 +497,27 @@ public class TbLwM2mRedisRegistrationStore implements CaliforniumRegistrationSto
try {
lock = redisLock.obtain(lockKey);
lock.lock();
// cancel existing observations for the same path and registration id.
for (Observation obs : getObservations(connection, registrationId)) {
//TODO: should be able to use CompositeObservation
if (((SingleObservation)observation).getPath().equals(((SingleObservation)obs).getPath())
&& !Arrays.equals(observation.getId(), obs.getId())) {
removed.add(obs);
unsafeRemoveObservation(connection, registrationId, obs.getId());
if (observation instanceof SingleObservation) {
if (validateObserveResource(((SingleObservation)observation).getPath(), registrationId)) {
updateSingleObservation(registrationId, (SingleObservation)observation, addIfAbsent, removed, connection);
// cancel existing observations for the same path and registration id.
cancelObservation(observation, registrationId, removed, connection);
}
} else {
ContentFormat ct = ((CompositeObservation) observation).getResponseContentFormat();
Map<String, String> ctx = observation.getContext();
String serializedObservation = extractSerializedObservation(observation);
JsonNode nodeSerObs = JacksonUtil.toJsonNode(serializedObservation);
((CompositeObservation)observation).getPaths().forEach(path -> {
if (validateObserveResource(path, registrationId)) {
String serializedObs = createSerializedSingleObservation(nodeSerObs, path.toString());
SingleObservation singleObservation = createSingleObservation(registrationId, path, ct, ctx, serializedObs, getTokenGenerator());
updateSingleObservation(registrationId, singleObservation, addIfAbsent, removed, connection);
// cancel existing observations for the same path and registration id.
cancelObservation (singleObservation, registrationId, removed, connection);
}
});
}
} finally {
if (lock != null) {
lock.unlock();
@ -473,7 +527,89 @@ public class TbLwM2mRedisRegistrationStore implements CaliforniumRegistrationSto
return removed;
}
private boolean validateObserveResource(LwM2mPath path, String registrationId){
// check if the resource is readable.
if (path.isResource() || path.isResourceInstance()) {
ObjectModel objectModel = modelProvider.getObjectModel(getRegistration(registrationId)).getObjectModel(path.getObjectId());
ResourceModel resourceModel = objectModel == null ? null : objectModel.resources.get(path.getResourceId());
if (resourceModel == null) {
return false;
} else if (!resourceModel.operations.isReadable()) {
return false;
} else if (path.isResourceInstance() && !resourceModel.multiple) {
return false;
}
}
return true;
}
private void updateSingleObservation (String registrationId, SingleObservation observation, boolean addIfAbsent,
List<Observation> removed, RedisConnection connection) {
// Add and Get previous observation
byte[] previousValue;
byte[] key = toKey(OBS_TKN, observation.getId().getBytes());
byte[] serializeObs = serializeObs(observation);
// we analyze the present previous value
SingleObservation existingObservation = null;
if (addIfAbsent){
previousValue = connection.stringCommands().get(key);
if (previousValue == null) {
existingObservation = validateByAbsorptionExistingObservations(observation, connection);
if (existingObservation == null){
connection.stringCommands().set(key, serializeObs);
} else if(!existingObservation.getPath().equals(observation.getPath())) {
connection.stringCommands().set(key, serializeObs);
previousValue = serializeObs(existingObservation);
}
}
} else {
previousValue = connection.stringCommands().getSet(key, serializeObs);
}
// secondary index to get the list by registrationId
connection.listCommands().lPush(toKey(OBS_TKNS_REGID_IDX, registrationId), observation.getId().getBytes());
// log any collisions
if (addIfAbsent && previousValue != null) {
if (!existingObservation.getPath().equals(observation.getPath())) {
Observation previousObservation = deserializeObs(previousValue);
removed.add(previousObservation);
LOG.warn("Token collision ? observation [{}] will be replaced by observation [{}], that this observation includes input observation [{}]!",
previousObservation, observation, observation);
} else {
LOG.warn("Token collision ? existing observation [{}] includes input observation [{}]",
existingObservation, observation);
}
}
}
@Override
public Collection<Observation> getObservations(String registrationId) {
try (var connection = connectionFactory.getConnection()) {
return getObservations(connection, registrationId);
}
}
@Override
public Observation getObservation(String registrationId, ObservationIdentifier observationId) {
return getObservations(registrationId).stream().filter(
o -> o.getId().getAsHexString().equals(observationId.getAsHexString())).findFirst().get();
}
@Override
public Observation getObservation(ObservationIdentifier observationId) {
try (var connection = connectionFactory.getConnection()) {
byte[] observationValue = connection.get(toKey(OBS_TKN, observationId.getBytes()));
return deserializeObs(observationValue);
}
}
@Override
public Observation removeObservation(String registrationId, ObservationIdentifier observationId) {
return removeObservation(registrationId, observationId.getBytes());
}
public Observation removeObservation(String registrationId, byte[] observationId) {
try (var connection = connectionFactory.getConnection()) {
@ -490,7 +626,7 @@ public class TbLwM2mRedisRegistrationStore implements CaliforniumRegistrationSto
lock = redisLock.obtain(lockKey);
lock.lock();
Observation observation = build(get(new Token(observationId)));
Observation observation = get(new Token(observationId));
if (observation != null && registrationId.equals(observation.getRegistrationId())) {
unsafeRemoveObservation(connection, registrationId, observationId);
return observation;
@ -505,152 +641,62 @@ public class TbLwM2mRedisRegistrationStore implements CaliforniumRegistrationSto
}
}
@Override
public Observation getObservation(String registrationId, byte[] observationId) {
return build(get(new Token(observationId)));
}
@Override
public Collection<Observation> getObservations(String registrationId) {
try (var connection = connectionFactory.getConnection()) {
return getObservations(connection, registrationId);
}
}
private Collection<Observation> getObservations(RedisConnection connection, String registrationId) {
Collection<Observation> result = new ArrayList<>();
for (byte[] token : connection.lRange(toKey(OBS_TKNS_REGID_IDX, registrationId), 0, -1)) {
byte[] obs = connection.get(toKey(OBS_TKN, token));
for (byte[] token : connection.listCommands().lRange(toKey(OBS_TKNS_REGID_IDX, registrationId), 0, -1)) {
byte[] obs = connection.stringCommands().get(toKey(OBS_TKN, token));
if (obs != null) {
result.add(build(deserializeObs(obs)));
result.add(deserializeObs(obs));
}
}
return result;
}
@Override
public Collection<Observation> removeObservations(String registrationId) {
try (var connection = connectionFactory.getConnection()) {
// check registration exists
Registration registration = getRegistration(connection, registrationId);
if (registration == null)
return Collections.emptyList();
// get endpoint and create lock
String endpoint = registration.getEndpoint();
Lock lock = null;
String lockKey = toLockKey(endpoint);
try {
lock = redisLock.obtain(lockKey);
lock.lock();
return unsafeRemoveAllObservations(connection, registrationId);
} finally {
if (lock != null) {
lock.unlock();
}
private SingleObservation validateByAbsorptionExistingObservations(SingleObservation observation, RedisConnection connection) {
LwM2mPath pathObservation = observation.getPath();
AtomicReference<SingleObservation> result = new AtomicReference<>();
Collection<Observation> observations = getObservations(connection, observation.getRegistrationId());
observations.stream().forEach(obs -> {
LwM2mPath pathObs = ((SingleObservation)obs).getPath();
if ((!pathObservation.equals(pathObs) && pathObs.startWith(pathObservation)) || // pathObs = "3/0/9"-> pathObservation = "3"
(pathObservation.equals(pathObs) && !observation.getId().equals(obs.getId()))) {
result.set((SingleObservation)obs);
} else if (!pathObservation.equals(pathObs) && pathObservation.startWith(pathObs)) { // pathObs = "3" -> pathObservation = "3/0/9"
result.set(observation);
}
}
}
/* *************** Californium ObservationStore API **************** */
});
return result.get();
@Override
public org.eclipse.californium.core.observe.Observation putIfAbsent(Token token,
org.eclipse.californium.core.observe.Observation obs) throws ObservationStoreException {
return add(obs, true);
}
@Override
public org.eclipse.californium.core.observe.Observation put(Token token,
org.eclipse.californium.core.observe.Observation obs) throws ObservationStoreException {
return add(obs, false);
}
private org.eclipse.californium.core.observe.Observation add(org.eclipse.californium.core.observe.Observation obs, boolean ifAbsent) throws ObservationStoreException {
String endpoint = ObserveUtil.validateCoapObservation(obs);
org.eclipse.californium.core.observe.Observation previousObservation = null;
try (var connection = connectionFactory.getConnection()) {
Lock lock = null;
String lockKey = toLockKey(endpoint);
try {
lock = redisLock.obtain(lockKey);
lock.lock();
String registrationId = ObserveUtil.extractRegistrationId(obs);
if (!connection.exists(toRegIdKey(registrationId)))
throw new ObservationStoreException("no registration for this Id");
byte[] key = toKey(OBS_TKN, obs.getRequest().getToken().getBytes());
byte[] serializeObs = serializeObs(obs);
byte[] previousValue;
if (ifAbsent) {
previousValue = connection.get(key);
if (previousValue == null || previousValue.length == 0) {
connection.set(key, serializeObs);
} else {
return deserializeObs(previousValue);
}
} else {
previousValue = connection.getSet(key, serializeObs);
}
// secondary index to get the list by registrationId
connection.lPush(toKey(OBS_TKNS_REGID_IDX, registrationId), obs.getRequest().getToken().getBytes());
// log any collisions
if (previousValue != null && previousValue.length != 0) {
previousObservation = deserializeObs(previousValue);
LOG.warn(
"Token collision ? observation from request [{}] will be replaced by observation from request [{}] ",
previousObservation.getRequest(), obs.getRequest());
}
} finally {
if (lock != null) {
lock.unlock();
}
}
}
return previousObservation;
}
@Override
public void remove(Token token) {
public Collection<Observation> removeObservations(String registrationId) {
try (var connection = connectionFactory.getConnection()) {
byte[] tokenKey = toKey(OBS_TKN, token.getBytes());
// fetch the observation by token
byte[] serializedObs = connection.get(tokenKey);
if (serializedObs == null)
return;
org.eclipse.californium.core.observe.Observation obs = deserializeObs(serializedObs);
String registrationId = ObserveUtil.extractRegistrationId(obs);
// check registration exists
Registration registration = getRegistration(connection, registrationId);
if (registration == null) {
LOG.warn("Unable to remove observation {}, registration {} does not exist anymore", obs.getRequest(),
registrationId);
return;
}
if (registration == null)
return Collections.emptyList();
// get endpoint and create lock
String endpoint = registration.getEndpoint();
Lock lock = null;
String lockKey = toLockKey(endpoint);
try {
lock = redisLock.obtain(lockKey);
lock.lock();
unsafeRemoveObservation(connection, registrationId, token.getBytes());
return unsafeRemoveAllObservations(connection, registrationId);
} finally {
if (lock != null) {
lock.unlock();
}
}
}
}
@Override
public org.eclipse.californium.core.observe.Observation get(Token token) {
public Observation get(Token token) {
try (var connection = connectionFactory.getConnection()) {
byte[] obs = connection.get(toKey(OBS_TKN, token.getBytes()));
if (obs == null) {
@ -663,22 +709,16 @@ public class TbLwM2mRedisRegistrationStore implements CaliforniumRegistrationSto
/* *************** Observation utility functions **************** */
private Registration getRegistration(RedisConnection connection, String registrationId) {
byte[] ep = connection.get(toRegIdKey(registrationId));
if (ep == null) {
return null;
}
byte[] data = connection.get(toEndpointKey(ep));
if (data == null) {
return null;
private TokenGenerator getTokenGenerator(){
if (this.tokenGenerator == null) {
this.tokenGenerator = new RandomTokenGenerator(config.getCoapConfig());
}
return deserializeReg(data);
return this.tokenGenerator;
}
private void unsafeRemoveObservation(RedisConnection connection, String registrationId, byte[] observationId) {
if (connection.del(toKey(OBS_TKN, observationId)) > 0L) {
connection.lRem(toKey(OBS_TKNS_REGID_IDX, registrationId), 0, observationId);
if (connection.commands().del(toKey(OBS_TKN, observationId)) > 0L) {
connection.listCommands().lRem(toKey(OBS_TKNS_REGID_IDX, registrationId), 0, observationId);
}
}
@ -690,7 +730,7 @@ public class TbLwM2mRedisRegistrationStore implements CaliforniumRegistrationSto
for (byte[] token : connection.lRange(regIdKey, 0, -1)) {
byte[] obs = connection.get(toKey(OBS_TKN, token));
if (obs != null) {
removed.add(build(deserializeObs(obs)));
removed.add(deserializeObs(obs));
}
connection.del(toKey(OBS_TKN, token));
}
@ -699,24 +739,30 @@ public class TbLwM2mRedisRegistrationStore implements CaliforniumRegistrationSto
return removed;
}
@Override
public void setContext(Token token, EndpointContext correlationContext) {
// In Leshan we always set context when we send the request, so this should not be needed to implement this.
private byte[] serializeObs(Observation obs) {
return observationSerDes.serialize(obs);
}
private byte[] serializeObs(org.eclipse.californium.core.observe.Observation obs) {
return ObservationSerDes.serialize(obs);
private void cancelObservation(Observation observation, String registrationId, List<Observation> removed, RedisConnection connection) {
for (Observation obs : getObservations(connection, registrationId)) {
cancelExistingObservation(connection, observation, obs, removed);
}
}
private org.eclipse.californium.core.observe.Observation deserializeObs(byte[] data) {
return ObservationSerDes.deserialize(data);
private void cancelExistingObservation(RedisConnection connection, Observation observation, Observation obs, List<Observation> removed) {
LwM2mPath pathObservation = ((SingleObservation)observation).getPath();
LwM2mPath pathObs = ((SingleObservation)obs).getPath();
if ((!pathObservation.equals(pathObs) && pathObs.startWith(pathObservation)) || // pathObservation = "3", pathObs = "3/0/9"
(pathObservation.equals(pathObs) && !observation.getId().equals(obs.getId()))) {
unsafeRemoveObservation(connection, obs.getRegistrationId(), obs.getId().getBytes());
removed.add(obs);
} else if (!pathObservation.equals(pathObs) && pathObservation.startWith(pathObs)) { // pathObservation = "3/0/9", pathObs = "3"
unsafeRemoveObservation(connection, obs.getRegistrationId(), observation.getId().getBytes());
}
}
private Observation build(org.eclipse.californium.core.observe.Observation cfObs) {
if (cfObs == null)
return null;
return ObserveUtil.createLwM2mObservation(cfObs.getRequest());
private Observation deserializeObs(byte[] data) {
return data == null ? null : observationSerDes.deserialize(data);
}
/* *************** Expiration handling **************** */
@ -787,7 +833,6 @@ public class TbLwM2mRedisRegistrationStore implements CaliforniumRegistrationSto
expirationListener = listener;
}
@Override
public void setExecutor(ScheduledExecutorService executor) {
// TODO should we reuse californium executor ?
}

22
common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbLwM2mRedisSecurityStore.java

@ -16,6 +16,7 @@
package org.thingsboard.server.transport.lwm2m.server.store;
import org.eclipse.leshan.core.SecurityMode;
import org.eclipse.leshan.core.peer.OscoreIdentity;
import org.eclipse.leshan.server.security.NonUniqueSecurityInfoException;
import org.eclipse.leshan.server.security.SecurityInfo;
import org.springframework.data.redis.connection.RedisConnectionFactory;
@ -86,6 +87,11 @@ public class TbLwM2mRedisSecurityStore implements TbEditableSecurityStore {
}
}
@Override
public SecurityInfo getByOscoreIdentity(OscoreIdentity oscoreIdentity) {
return null;
}
@Override
public void put(TbLwM2MSecurityInfo tbSecurityInfo) throws NonUniqueSecurityInfoException {
SecurityInfo info = tbSecurityInfo.getSecurityInfo();
@ -94,21 +100,21 @@ public class TbLwM2mRedisSecurityStore implements TbEditableSecurityStore {
try (var connection = connectionFactory.getConnection()) {
lock = redisLock.obtain(tbSecurityInfo.getEndpoint());
lock.lock();
if (info != null && info.getIdentity() != null) {
byte[] oldEndpointBytes = connection.hGet(PSKID_SEC.getBytes(), info.getIdentity().getBytes());
if (info != null && info.getPskIdentity() != null) {
byte[] oldEndpointBytes = connection.hGet(PSKID_SEC.getBytes(), info.getPskIdentity().getBytes());
if (oldEndpointBytes != null) {
String oldEndpoint = new String(oldEndpointBytes);
if (!oldEndpoint.equals(info.getEndpoint())) {
throw new NonUniqueSecurityInfoException("PSK Identity " + info.getIdentity() + " is already used");
throw new NonUniqueSecurityInfoException("PSK Identity " + info.getPskIdentity() + " is already used");
}
connection.hSet(PSKID_SEC.getBytes(), info.getIdentity().getBytes(), info.getEndpoint().getBytes());
connection.hSet(PSKID_SEC.getBytes(), info.getPskIdentity().getBytes(), info.getEndpoint().getBytes());
}
}
byte[] previousData = connection.getSet((SEC_EP + tbSecurityInfo.getEndpoint()).getBytes(), tbSecurityInfoSerialized);
if (previousData != null && info != null) {
String previousIdentity = ((TbLwM2MSecurityInfo) JavaSerDesUtil.decode(previousData)).getSecurityInfo().getIdentity();
if (previousIdentity != null && !previousIdentity.equals(info.getIdentity())) {
String previousIdentity = ((TbLwM2MSecurityInfo) JavaSerDesUtil.decode(previousData)).getSecurityInfo().getPskIdentity();
if (previousIdentity != null && !previousIdentity.equals(info.getPskIdentity())) {
connection.hDel(PSKID_SEC.getBytes(), previousIdentity.getBytes());
}
}
@ -147,8 +153,8 @@ public class TbLwM2mRedisSecurityStore implements TbEditableSecurityStore {
byte[] data = connection.get((SEC_EP + endpoint).getBytes());
if (data != null && data.length > 0) {
SecurityInfo info = ((TbLwM2MSecurityInfo) JavaSerDesUtil.decode(data)).getSecurityInfo();
if (info != null && info.getIdentity() != null) {
connection.hDel(PSKID_SEC.getBytes(), info.getIdentity().getBytes());
if (info != null && info.getPskIdentity() != null) {
connection.hDel(PSKID_SEC.getBytes(), info.getPskIdentity().getBytes());
}
connection.del((SEC_EP + endpoint).getBytes());
}

8
common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbLwM2mSecurityStore.java

@ -17,6 +17,7 @@ package org.thingsboard.server.transport.lwm2m.server.store;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.leshan.core.SecurityMode;
import org.eclipse.leshan.core.peer.OscoreIdentity;
import org.eclipse.leshan.server.security.NonUniqueSecurityInfoException;
import org.eclipse.leshan.server.security.SecurityInfo;
import org.thingsboard.server.transport.lwm2m.secure.LwM2mCredentialsSecurityInfoValidator;
@ -60,7 +61,7 @@ public class TbLwM2mSecurityStore implements TbMainSecurityStore {
if (securityInfo == null) {
securityInfo = fetchAndPutSecurityInfo(endpoint);
} else if (securityInfo.usePSK() && securityInfo.getEndpoint().equals(SecurityMode.NO_SEC.toString())
&& securityInfo.getIdentity().equals(SecurityMode.NO_SEC.toString())
&& securityInfo.getPskIdentity().equals(SecurityMode.NO_SEC.toString())
&& Arrays.equals(SecurityMode.NO_SEC.toString().getBytes(), securityInfo.getPreSharedKey())) {
return null;
}
@ -81,6 +82,11 @@ public class TbLwM2mSecurityStore implements TbMainSecurityStore {
return securityInfo;
}
@Override
public SecurityInfo getByOscoreIdentity(OscoreIdentity oscoreIdentity) {
return null;
}
public SecurityInfo fetchAndPutSecurityInfo(String credentialsId) {
TbLwM2MSecurityInfo securityInfo = validator.getEndpointSecurityInfoByCredentialsId(credentialsId, CLIENT);
doPut(securityInfo);

12
common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbLwM2mStoreFactory.java

@ -16,8 +16,8 @@
package org.thingsboard.server.transport.lwm2m.server.store;
import lombok.RequiredArgsConstructor;
import org.eclipse.leshan.server.californium.registration.CaliforniumRegistrationStore;
import org.eclipse.leshan.server.californium.registration.InMemoryRegistrationStore;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.leshan.server.registration.RegistrationStore;
import org.springframework.context.annotation.Bean;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.stereotype.Component;
@ -25,9 +25,11 @@ import org.thingsboard.server.cache.TBRedisCacheConfiguration;
import org.thingsboard.server.queue.util.TbLwM2mTransportComponent;
import org.thingsboard.server.transport.lwm2m.config.LwM2MTransportServerConfig;
import org.thingsboard.server.transport.lwm2m.secure.LwM2mCredentialsSecurityInfoValidator;
import org.thingsboard.server.transport.lwm2m.server.LwM2mVersionedModelProvider;
import java.util.Optional;
@Slf4j
@Component
@TbLwM2mTransportComponent
@RequiredArgsConstructor
@ -36,11 +38,13 @@ public class TbLwM2mStoreFactory {
private final Optional<TBRedisCacheConfiguration> redisConfiguration;
private final LwM2MTransportServerConfig config;
private final LwM2mCredentialsSecurityInfoValidator validator;
private final LwM2mVersionedModelProvider modelProvider;
@Bean
private CaliforniumRegistrationStore registrationStore() {
private RegistrationStore registrationStore() {
return redisConfiguration.isPresent() ?
new TbLwM2mRedisRegistrationStore(getConnectionFactory()) : new InMemoryRegistrationStore(config.getCleanPeriodInSec());
new TbLwM2mRedisRegistrationStore(config, getConnectionFactory(), modelProvider) :
new TbInMemoryRegistrationStore(config, config.getCleanPeriodInSec(), modelProvider);
}
@Bean

383
common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/util/LwM2MClientSerDes.java

@ -15,9 +15,8 @@
*/
package org.thingsboard.server.transport.lwm2m.server.store.util;
import com.eclipsesource.json.Json;
import com.eclipsesource.json.JsonObject;
import com.eclipsesource.json.JsonValue;
import com.fasterxml.jackson.annotation.JsonValue;
import com.google.gson.JsonObject;
import com.google.protobuf.util.JsonFormat;
import lombok.SneakyThrows;
import org.eclipse.leshan.core.model.ResourceModel;
@ -26,113 +25,107 @@ import org.eclipse.leshan.core.node.LwM2mNodeException;
import org.eclipse.leshan.core.node.LwM2mResource;
import org.eclipse.leshan.core.node.LwM2mSingleResource;
import org.eclipse.leshan.core.node.ObjectLink;
import org.eclipse.leshan.core.util.datatype.ULong;
import org.eclipse.leshan.server.redis.serialization.RegistrationSerDes;
import org.thingsboard.server.common.data.device.data.PowerMode;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.gen.transport.TransportProtos;
import org.thingsboard.server.transport.lwm2m.server.client.LwM2MClientState;
import org.thingsboard.server.transport.lwm2m.server.client.LwM2mClient;
import org.thingsboard.server.transport.lwm2m.server.client.ResourceValue;
import java.lang.reflect.Field;
import java.util.Base64;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicLong;
public class LwM2MClientSerDes {
public static final String VALUE = "value";
@SneakyThrows
public static byte[] serialize(LwM2mClient client) {
JsonObject o = Json.object();
o.add("nodeId", client.getNodeId());
o.add("endpoint", client.getEndpoint());
JsonObject o = new JsonObject();
o.addProperty("nodeId", "client.getNodeId()");
o.addProperty("endpoint", client.getEndpoint());
JsonObject resources = Json.object();
JsonObject resources = new JsonObject();
client.getResources().forEach((k, v) -> {
JsonObject resourceValue = Json.object();
JsonObject resourceValue = new JsonObject();
resourceValue.add("lwM2mResource", serialize(v.getLwM2mResource()));
resourceValue.add("resourceModel", serialize(v.getResourceModel()));
resources.add(k, resourceValue);
});
o.add("resources", resources);
JsonObject sharedAttributes = Json.object();
JsonObject sharedAttributes = new JsonObject();
for (Map.Entry<String, TransportProtos.TsKvProto> entry : client.getSharedAttributes().entrySet()) {
sharedAttributes.add(entry.getKey(), JsonFormat.printer().print(entry.getValue()));
sharedAttributes.addProperty(entry.getKey(), JsonFormat.printer().print(entry.getValue()));
}
o.add("sharedAttributes", sharedAttributes);
JsonObject keyTsLatestMap = Json.object();
JsonObject keyTsLatestMap = new JsonObject();
client.getKeyTsLatestMap().forEach((k, v) -> {
keyTsLatestMap.add(k, v.get());
keyTsLatestMap.addProperty(k, v.get());
});
o.add("keyTsLatestMap", keyTsLatestMap);
o.add("state", client.getState().toString());
o.addProperty("state", client.getState().toString());
if (client.getSession() != null) {
o.add("session", JsonFormat.printer().print(client.getSession()));
o.addProperty("session", JsonFormat.printer().print(client.getSession()));
}
if (client.getTenantId() != null) {
o.add("tenantId", client.getTenantId().toString());
o.addProperty("tenantId", client.getTenantId().toString());
}
if (client.getDeviceId() != null) {
o.add("deviceId", client.getDeviceId().toString());
o.addProperty("deviceId", client.getDeviceId().toString());
}
if (client.getProfileId() != null) {
o.add("profileId", client.getProfileId().toString());
o.addProperty("profileId", client.getProfileId().toString());
}
if (client.getPowerMode() != null) {
o.add("powerMode", client.getPowerMode().toString());
o.addProperty("powerMode", client.getPowerMode().toString());
}
if (client.getEdrxCycle() != null) {
o.add("edrxCycle", client.getEdrxCycle());
o.addProperty("edrxCycle", client.getEdrxCycle());
}
if (client.getPsmActivityTimer() != null) {
o.add("psmActivityTimer", client.getPsmActivityTimer());
o.addProperty("psmActivityTimer", client.getPsmActivityTimer());
}
if (client.getPagingTransmissionWindow() != null) {
o.add("pagingTransmissionWindow", client.getPagingTransmissionWindow());
o.addProperty("pagingTransmissionWindow", client.getPagingTransmissionWindow());
}
if (client.getRegistration() != null) {
o.add("registration", RegistrationSerDes.jSerialize(client.getRegistration()));
RegistrationSerDes regDez = new RegistrationSerDes();
o.addProperty("registration", regDez.jSerialize(client.getRegistration()).toString());
}
o.add("asleep", client.isAsleep());
o.add("lastUplinkTime", client.getLastUplinkTime());
o.addProperty("asleep", client.isAsleep());
o.addProperty("lastUplinkTime", client.getLastUplinkTime());
Field firstEdrxDownlink = LwM2mClient.class.getDeclaredField("firstEdrxDownlink");
firstEdrxDownlink.setAccessible(true);
o.add("firstEdrxDownlink", (boolean) firstEdrxDownlink.get(client));
o.add("retryAttempts", client.getRetryAttempts().get());
o.addProperty("firstEdrxDownlink", (boolean) firstEdrxDownlink.get(client));
o.addProperty("retryAttempts", client.getRetryAttempts().get());
if (client.getLastSentRpcId() != null) {
o.add("lastSentRpcId", client.getLastSentRpcId().toString());
o.addProperty("lastSentRpcId", client.getLastSentRpcId().toString());
}
return o.toString().getBytes();
}
private static JsonObject serialize(LwM2mResource resource) {
JsonObject o = Json.object();
o.add("id", resource.getId());
o.add("type", resource.getType().toString());
JsonObject o = new JsonObject();
o.addProperty("id", resource.getId());
o.addProperty("type", resource.getType().toString());
if (resource.isMultiInstances()) {
o.add("multiInstances", true);
JsonObject instances = Json.object();
o.addProperty("multiInstances", true);
JsonObject instances = new JsonObject();
resource.getInstances().forEach((id, in) -> {
JsonObject instance = Json.object();
instance.add("id", in.getId());
JsonObject instance = new JsonObject();
instance.addProperty("id", in.getId());
addValue(instance, in.getType(), in.getValue());
instances.add(id.toString(), instance);
});
o.add("instances", instances);
} else {
o.add("multiInstances", false);
o.addProperty("multiInstances", false);
addValue(o, resource.getType(), resource.getValue());
}
@ -140,95 +133,95 @@ public class LwM2MClientSerDes {
}
private static LwM2mResource parseLwM2mResource(JsonObject o) {
boolean multiInstances = o.get("multiInstances").asBoolean();
int id = o.get("id").asInt();
ResourceModel.Type type = ResourceModel.Type.valueOf(o.get("type").asString());
boolean multiInstances = o.get("multiInstances").getAsBoolean();
int id = o.get("id").getAsInt();
ResourceModel.Type type = ResourceModel.Type.valueOf(o.get("type").getAsString());
if (multiInstances) {
Map<Integer, Object> instances = new HashMap<>();
o.get("instances").asObject().forEach(entry -> {
instances.put(Integer.valueOf(entry.getName()), parseValue(type, entry.getValue()));
o.get("instances").getAsJsonArray().forEach(entry -> {
// instances.put(Integer.valueOf(entry.getAsJsonObject().), parseValue(type, entry.getValue()));
});
return LwM2mMultipleResource.newResource(id, instances, type);
} else {
return LwM2mSingleResource.newResource(id, parseValue(type, o.get(VALUE)));
return LwM2mSingleResource.newResource(id, parseValue(type, (JsonValue) o.get(VALUE)));
}
}
private static Object parseValue(ResourceModel.Type type, JsonValue value) {
switch (type) {
case INTEGER:
return value.asLong();
case FLOAT:
return value.asDouble();
case BOOLEAN:
return value.asBoolean();
case OPAQUE:
return Base64.getDecoder().decode(value.asString());
case STRING:
return value.asString();
case TIME:
return new Date(value.asLong());
case OBJLNK:
return ObjectLink.decodeFromString(value.asString());
case UNSIGNED_INTEGER:
return ULong.valueOf(value.asString());
return value.value();
// case FLOAT:
// return value.value();
// case BOOLEAN:
// return value.asBoolean();
// case OPAQUE:
// return Base64.getDecoder().decode(value.asString());
// case STRING:
// return value.asString();
// case TIME:
// return new Date(value.asLong());
// case OBJLNK:
// return ObjectLink.decodeFromString(value.asString());
// case UNSIGNED_INTEGER:
// return ULong.valueOf(value.asString());
default:
throw new LwM2mNodeException(String.format("Type %s is not supported", type.name()));
}
}
private static JsonObject serialize(ResourceModel resourceModel) {
JsonObject o = Json.object();
o.add("id", resourceModel.id);
o.add("name", resourceModel.name);
o.add("operations", resourceModel.operations.toString());
o.add("multiple", resourceModel.multiple);
o.add("mandatory", resourceModel.mandatory);
o.add("type", resourceModel.type.toString());
o.add("rangeEnumeration", resourceModel.rangeEnumeration);
o.add("units", resourceModel.units);
o.add("description", resourceModel.description);
JsonObject o = new JsonObject();
o.addProperty("id", resourceModel.id);
o.addProperty("name", resourceModel.name);
o.addProperty("operations", resourceModel.operations.toString());
o.addProperty("multiple", resourceModel.multiple);
o.addProperty("mandatory", resourceModel.mandatory);
o.addProperty("type", resourceModel.type.toString());
o.addProperty("rangeEnumeration", resourceModel.rangeEnumeration);
o.addProperty("units", resourceModel.units);
o.addProperty("description", resourceModel.description);
return o;
}
private static ResourceModel parseResourceModel(JsonObject o) {
Integer id = o.get("id").asInt();
String name = o.get("name").asString();
ResourceModel.Operations operations = ResourceModel.Operations.valueOf(o.get("operations").asString());
Boolean multiple = o.get("multiple").asBoolean();
Boolean mandatory = o.get("mandatory").asBoolean();
ResourceModel.Type type = ResourceModel.Type.valueOf(o.get("type").asString());
String rangeEnumeration = o.get("rangeEnumeration").asString();
String units = o.get("units").asString();
String description = o.get("description").asString();
Integer id = o.get("id").getAsInt();
String name = o.get("name").getAsString();
ResourceModel.Operations operations = ResourceModel.Operations.valueOf(o.get("operations").getAsString());
Boolean multiple = o.get("multiple").getAsBoolean();
Boolean mandatory = o.get("mandatory").getAsBoolean();
ResourceModel.Type type = ResourceModel.Type.valueOf(o.get("type").getAsString());
String rangeEnumeration = o.get("rangeEnumeration").getAsString();
String units = o.get("units").getAsString();
String description = o.get("description").getAsString();
return new ResourceModel(id, name, operations, multiple, mandatory, type, rangeEnumeration, units, description);
}
private static void addValue(JsonObject o, ResourceModel.Type type, Object value) {
switch (type) {
case INTEGER:
o.add(VALUE, (Long) value);
o.addProperty(VALUE, (Long) value);
break;
case FLOAT:
o.add(VALUE, (Double) value);
o.addProperty(VALUE, (Double) value);
break;
case BOOLEAN:
o.add(VALUE, (Boolean) value);
o.addProperty(VALUE, (Boolean) value);
break;
case OPAQUE:
o.add(VALUE, Base64.getEncoder().encodeToString((byte[]) value));
o.addProperty(VALUE, Base64.getEncoder().encodeToString((byte[]) value));
break;
case STRING:
o.add(VALUE, (String) value);
o.addProperty(VALUE, (String) value);
break;
case TIME:
o.add(VALUE, ((Date) value).getTime());
o.addProperty(VALUE, ((Date) value).getTime());
break;
case OBJLNK:
o.add(VALUE, ((ObjectLink) value).encodeToString());
o.addProperty(VALUE, ((ObjectLink) value).encodeToString());
break;
case UNSIGNED_INTEGER:
o.add(VALUE, value.toString());
o.addProperty(VALUE, value.toString());
break;
default:
throw new LwM2mNodeException(String.format("Type %s is not supported", type.name()));
@ -237,111 +230,111 @@ public class LwM2MClientSerDes {
@SneakyThrows
public static LwM2mClient deserialize(byte[] data) {
JsonObject o = Json.parse(new String(data)).asObject();
LwM2mClient lwM2mClient = new LwM2mClient(o.get("nodeId").asString(), o.get("endpoint").asString());
o.get("resources").asObject().forEach(entry -> {
JsonObject resource = entry.getValue().asObject();
LwM2mResource lwM2mResource = parseLwM2mResource(resource.get("lwM2mResource").asObject());
ResourceModel resourceModel = parseResourceModel(resource.get("resourceModel").asObject());
ResourceValue resourceValue = new ResourceValue(lwM2mResource, resourceModel);
lwM2mClient.getResources().put(entry.getName(), resourceValue);
});
for (JsonObject.Member entry : o.get("sharedAttributes").asObject()) {
TransportProtos.TsKvProto.Builder builder = TransportProtos.TsKvProto.newBuilder();
JsonFormat.parser().merge(entry.getValue().asString(), builder);
lwM2mClient.getSharedAttributes().put(entry.getName(), builder.build());
}
o.get("keyTsLatestMap").asObject().forEach(entry -> {
lwM2mClient.getKeyTsLatestMap().put(entry.getName(), new AtomicLong(entry.getValue().asLong()));
});
lwM2mClient.setState(LwM2MClientState.valueOf(o.get("state").asString()));
Class<LwM2mClient> lwM2mClientClass = LwM2mClient.class;
JsonValue session = o.get("session");
if (session != null) {
TransportProtos.SessionInfoProto.Builder builder = TransportProtos.SessionInfoProto.newBuilder();
JsonFormat.parser().merge(session.asString(), builder);
Field sessionField = lwM2mClientClass.getDeclaredField("session");
sessionField.setAccessible(true);
sessionField.set(lwM2mClient, builder.build());
}
JsonValue tenantId = o.get("tenantId");
if (tenantId != null) {
Field tenantIdField = lwM2mClientClass.getDeclaredField("tenantId");
tenantIdField.setAccessible(true);
tenantIdField.set(lwM2mClient, new TenantId(UUID.fromString(tenantId.asString())));
}
JsonValue deviceId = o.get("deviceId");
if (tenantId != null) {
Field deviceIdField = lwM2mClientClass.getDeclaredField("deviceId");
deviceIdField.setAccessible(true);
deviceIdField.set(lwM2mClient, UUID.fromString(deviceId.asString()));
}
JsonValue profileId = o.get("profileId");
if (tenantId != null) {
Field profileIdField = lwM2mClientClass.getDeclaredField("profileId");
profileIdField.setAccessible(true);
profileIdField.set(lwM2mClient, UUID.fromString(profileId.asString()));
}
JsonValue powerMode = o.get("powerMode");
if (powerMode != null) {
Field powerModeField = lwM2mClientClass.getDeclaredField("powerMode");
powerModeField.setAccessible(true);
powerModeField.set(lwM2mClient, PowerMode.valueOf(powerMode.asString()));
}
JsonValue edrxCycle = o.get("edrxCycle");
if (edrxCycle != null) {
Field edrxCycleField = lwM2mClientClass.getDeclaredField("edrxCycle");
edrxCycleField.setAccessible(true);
edrxCycleField.set(lwM2mClient, edrxCycle.asLong());
}
JsonValue psmActivityTimer = o.get("psmActivityTimer");
if (psmActivityTimer != null) {
Field psmActivityTimerField = lwM2mClientClass.getDeclaredField("psmActivityTimer");
psmActivityTimerField.setAccessible(true);
psmActivityTimerField.set(lwM2mClient, psmActivityTimer.asLong());
}
JsonValue pagingTransmissionWindow = o.get("pagingTransmissionWindow");
if (pagingTransmissionWindow != null) {
Field pagingTransmissionWindowField = lwM2mClientClass.getDeclaredField("pagingTransmissionWindow");
pagingTransmissionWindowField.setAccessible(true);
pagingTransmissionWindowField.set(lwM2mClient, pagingTransmissionWindow.asLong());
}
JsonValue registration = o.get("registration");
if (registration != null) {
lwM2mClient.setRegistration(RegistrationSerDes.deserialize(registration.asObject()));
}
lwM2mClient.setAsleep(o.get("asleep").asBoolean());
Field lastUplinkTimeField = lwM2mClientClass.getDeclaredField("lastUplinkTime");
lastUplinkTimeField.setAccessible(true);
lastUplinkTimeField.setLong(lwM2mClient, o.get("lastUplinkTime").asLong());
Field firstEdrxDownlinkField = lwM2mClientClass.getDeclaredField("firstEdrxDownlink");
firstEdrxDownlinkField.setAccessible(true);
firstEdrxDownlinkField.setBoolean(lwM2mClient, o.get("firstEdrxDownlink").asBoolean());
lwM2mClient.getRetryAttempts().set(o.get("retryAttempts").asInt());
JsonValue lastSentRpcId = o.get("lastSentRpcId");
if (lastSentRpcId != null) {
lwM2mClient.setLastSentRpcId(UUID.fromString(lastSentRpcId.asString()));
}
// JsonObject o = new JsonObject(new String(data)));
// LwM2mClient lwM2mClient = new LwM2mClient(o.get("nodeId").getAsString(), o.get("endpoint").getAsString());
LwM2mClient lwM2mClient = new LwM2mClient("nodeId", "endpoint");
// o.get("resources").getAsJsonObject().forEach(entry -> {
// JsonObject resource = entry.getValue().asObject();
// LwM2mResource lwM2mResource = parseLwM2mResource(resource.get("lwM2mResource").getAsJsonObject());
// ResourceModel resourceModel = parseResourceModel(resource.get("resourceModel").asObject());
// ResourceValue resourceValue = new ResourceValue(lwM2mResource, resourceModel);
// lwM2mClient.getResources().put(entry.getName(), resourceValue);
// });
//
// for (JsonObject.Member entry : o.get("sharedAttributes").asObject()) {
// TransportProtos.TsKvProto.Builder builder = TransportProtos.TsKvProto.newBuilder();
// JsonFormat.parser().merge(entry.getValue().getAsString(), builder);
// lwM2mClient.getSharedAttributes().put(entry.getName(), builder.build());
// }
//
// o.get("keyTsLatestMap").asObject().forEach(entry -> {
// lwM2mClient.getKeyTsLatestMap().put(entry.getName(), new AtomicLong(entry.getValue().asLong()));
// });
//
// lwM2mClient.setState(LwM2MClientState.valueOf(o.get("state").getAsString()));
//
// Class<LwM2mClient> lwM2mClientClass = LwM2mClient.class;
//
// JsonValue session = o.get("session");
// if (session != null) {
// TransportProtos.SessionInfoProto.Builder builder = TransportProtos.SessionInfoProto.newBuilder();
// JsonFormat.parser().merge(session.asString(), builder);
//
// Field sessionField = lwM2mClientClass.getDeclaredField("session");
// sessionField.setAccessible(true);
// sessionField.set(lwM2mClient, builder.build());
// }
//
// JsonValue tenantId = o.get("tenantId");
// if (tenantId != null) {
// Field tenantIdField = lwM2mClientClass.getDeclaredField("tenantId");
// tenantIdField.setAccessible(true);
// tenantIdField.set(lwM2mClient, new TenantId(UUID.fromString(tenantId.asString())));
// }
//
// JsonValue deviceId = o.get("deviceId");
// if (tenantId != null) {
// Field deviceIdField = lwM2mClientClass.getDeclaredField("deviceId");
// deviceIdField.setAccessible(true);
// deviceIdField.set(lwM2mClient, UUID.fromString(deviceId.asString()));
// }
//
// JsonValue profileId = o.get("profileId");
// if (tenantId != null) {
// Field profileIdField = lwM2mClientClass.getDeclaredField("profileId");
// profileIdField.setAccessible(true);
// profileIdField.set(lwM2mClient, UUID.fromString(profileId.asString()));
// }
//
// JsonValue powerMode = o.get("powerMode");
// if (powerMode != null) {
// Field powerModeField = lwM2mClientClass.getDeclaredField("powerMode");
// powerModeField.setAccessible(true);
// powerModeField.set(lwM2mClient, PowerMode.valueOf(powerMode.asString()));
// }
//
// JsonValue edrxCycle = o.get("edrxCycle");
// if (edrxCycle != null) {
// Field edrxCycleField = lwM2mClientClass.getDeclaredField("edrxCycle");
// edrxCycleField.setAccessible(true);
// edrxCycleField.set(lwM2mClient, edrxCycle.asLong());
// }
//
// JsonValue psmActivityTimer = o.get("psmActivityTimer");
// if (psmActivityTimer != null) {
// Field psmActivityTimerField = lwM2mClientClass.getDeclaredField("psmActivityTimer");
// psmActivityTimerField.setAccessible(true);
// psmActivityTimerField.set(lwM2mClient, psmActivityTimer.asLong());
// }
//
// JsonValue pagingTransmissionWindow = o.get("pagingTransmissionWindow");
// if (pagingTransmissionWindow != null) {
// Field pagingTransmissionWindowField = lwM2mClientClass.getDeclaredField("pagingTransmissionWindow");
// pagingTransmissionWindowField.setAccessible(true);
// pagingTransmissionWindowField.set(lwM2mClient, pagingTransmissionWindow.asLong());
// }
//
// JsonValue registration = o.get("registration");
// if (registration != null) {
// lwM2mClient.setRegistration(RegistrationSerDes.deserialize(registration.asObject()));
// }
//
// lwM2mClient.setAsleep(o.get("asleep").getAsBoolean());
//
// Field lastUplinkTimeField = lwM2mClientClass.getDeclaredField("lastUplinkTime");
// lastUplinkTimeField.setAccessible(true);
// lastUplinkTimeField.setLong(lwM2mClient, o.get("lastUplinkTime").asLong());
//
// Field firstEdrxDownlinkField = lwM2mClientClass.getDeclaredField("firstEdrxDownlink");
// firstEdrxDownlinkField.setAccessible(true);
// firstEdrxDownlinkField.setBoolean(lwM2mClient, o.get("firstEdrxDownlink").getAsBoolean());
//
// lwM2mClient.getRetryAttempts().set(o.get("retryAttempts").asInt());
//
// JsonValue lastSentRpcId = o.get("lastSentRpcId");
// if (lastSentRpcId != null) {
// lwM2mClient.setLastSentRpcId(UUID.fromString(lastSentRpcId.asString()));
// }
return lwM2mClient;
}

33
common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/uplink/DefaultLwM2mUplinkMsgHandler.java

@ -20,6 +20,8 @@ import com.google.gson.GsonBuilder;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.reflect.TypeToken;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@ -100,8 +102,6 @@ import org.thingsboard.server.transport.lwm2m.server.store.TbLwM2mSecurityStore;
import org.thingsboard.server.transport.lwm2m.utils.LwM2MTransportUtil;
import org.thingsboard.server.transport.lwm2m.utils.LwM2mValueConverterImpl;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
@ -341,6 +341,8 @@ public class DefaultLwM2mUplinkMsgHandler extends LwM2MExecutorAwareService impl
} else if (v instanceof LwM2mResource) {
this.updateResourcesValue(lwM2MClient, (LwM2mResource) v, k.toString(), Mode.UPDATE, responseCode);
}
} else {
this.onErrorObservation(registration, k + ": value in composite response is null");
}
});
clientContext.update(lwM2MClient);
@ -348,6 +350,12 @@ public class DefaultLwM2mUplinkMsgHandler extends LwM2MExecutorAwareService impl
}
}
public void onErrorObservation(Registration registration, String errorMsg) {
LwM2mClient lwM2MClient = this.clientContext.getClientByEndpoint(registration.getEndpoint());
logService.log(lwM2MClient, LOG_LWM2M_ERROR + ": " + errorMsg);
}
/**
* Sending updated value to thingsboard from SendListener.dataReceived: object, instance, SingleResource or MultipleResource
*
@ -356,7 +364,7 @@ public class DefaultLwM2mUplinkMsgHandler extends LwM2MExecutorAwareService impl
*/
@Override
public void onUpdateValueWithSendRequest(Registration registration, SendRequest sendRequest) {
for(var entry : sendRequest.getNodes().entrySet()) {
for(var entry : sendRequest.getTimestampedNodes().getNodes().entrySet()) {
LwM2mPath path = entry.getKey();
LwM2mNode node = entry.getValue();
LwM2mClient lwM2MClient = clientContext.getClientByEndpoint(registration.getEndpoint());
@ -557,7 +565,7 @@ public class DefaultLwM2mUplinkMsgHandler extends LwM2MExecutorAwareService impl
private void updateObjectInstanceResourceValue(LwM2mClient client, LwM2mObjectInstance lwM2mObjectInstance, String pathIdVer, int code) {
LwM2mPath pathIds = new LwM2mPath(fromVersionedIdToObjectId(pathIdVer));
lwM2mObjectInstance.getResources().forEach((resourceId, resource) -> {
String pathRez = pathIds.toString() + "/" + resourceId;
String pathRez = pathIdVer + "/" + resourceId;
this.updateResourcesValue(client, resource, pathRez, Mode.UPDATE, code);
});
}
@ -600,7 +608,7 @@ public class DefaultLwM2mUplinkMsgHandler extends LwM2MExecutorAwareService impl
otaService.onCurrentSoftwareResultUpdate(lwM2MClient, (Long) lwM2mResource.getValue());
}
if (ResponseCode.BAD_REQUEST.getCode() > code) {
this.updateAttrTelemetry(registration, Collections.singleton(path));
this.updateAttrTelemetry(registration, path);
}
} else {
log.error("Fail update path [{}] Resource [{}]", path, lwM2mResource);
@ -617,9 +625,12 @@ public class DefaultLwM2mUplinkMsgHandler extends LwM2MExecutorAwareService impl
*
* @param registration - Registration LwM2M Client
*/
private void updateAttrTelemetry(Registration registration, Set<String> paths) {
public void updateAttrTelemetry(Registration registration, String path) {
try {
ResultsAddKeyValueProto results = this.getParametersFromProfile(registration, paths);
ResultsAddKeyValueProto results = this.getParametersFromProfile(registration, path);
if (path.equals("/3_1.2/0/9")) {
log.info("UpdateTelemetry paths [{}] key: [{}] value [{}]", path, results.getResultTelemetries().get(0).getKey(), results.getResultTelemetries().get(0).getLongV());
}
SessionInfoProto sessionInfo = this.getSessionInfoOrCloseSession(registration);
if (results != null && sessionInfo != null) {
if (results.getResultAttributes().size() > 0) {
@ -669,13 +680,13 @@ public class DefaultLwM2mUplinkMsgHandler extends LwM2MExecutorAwareService impl
* @param registration - Registration LwM2M Client
* @param path -
*/
private ResultsAddKeyValueProto getParametersFromProfile(Registration registration, Set<String> path) {
if (path != null && path.size() > 0) {
private ResultsAddKeyValueProto getParametersFromProfile(Registration registration, String path) {
if (!path.isEmpty()) {
ResultsAddKeyValueProto results = new ResultsAddKeyValueProto();
var profile = clientContext.getProfile(registration);
List<TransportProtos.KeyValueProto> resultAttributes = new ArrayList<>();
profile.getObserveAttr().getAttribute().forEach(pathIdVer -> {
if (path.contains(pathIdVer)) {
if (path.equals(pathIdVer)) {
TransportProtos.KeyValueProto kvAttr = this.getKvToThingsBoard(pathIdVer, registration);
if (kvAttr != null) {
resultAttributes.add(kvAttr);
@ -777,7 +788,7 @@ public class DefaultLwM2mUplinkMsgHandler extends LwM2MExecutorAwareService impl
this.updateResourcesValue(client, (LwM2mResource) v, k.toString(), Mode.REPLACE, code);
} else {
LwM2mResourceInstance resourceInstance = (LwM2mResourceInstance) v;
LwM2mMultipleResource multipleResource = new LwM2mMultipleResource(v.getId(), resourceInstance.getType(), resourceInstance);
LwM2mMultipleResource multipleResource = new LwM2mMultipleResource(((LwM2mResourceInstance) v).getId(), resourceInstance.getType(), resourceInstance);
this.updateResourcesValue(client, multipleResource, k.toString(), Mode.REPLACE, code);
}
});

1
common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/uplink/LwM2mUplinkMsgHandler.java

@ -47,6 +47,7 @@ public interface LwM2mUplinkMsgHandler {
void onUpdateValueAfterReadResponse(Registration registration, String path, ReadResponse response);
void onUpdateValueAfterReadCompositeResponse(Registration registration, ReadCompositeResponse response);
void onErrorObservation(Registration registration, String errorMsg);
void onUpdateValueWithSendRequest(Registration registration, SendRequest sendRequest);

115
common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/utils/LwM2MTransportUtil.java

@ -15,11 +15,8 @@
*/
package org.thingsboard.server.transport.lwm2m.utils;
import com.fasterxml.jackson.core.type.TypeReference;
import com.google.gson.JsonElement;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.leshan.core.attributes.Attribute;
import org.eclipse.leshan.core.attributes.AttributeSet;
import org.eclipse.leshan.core.model.LwM2mModel;
import org.eclipse.leshan.core.model.ObjectLoader;
import org.eclipse.leshan.core.model.ObjectModel;
@ -30,8 +27,6 @@ import org.eclipse.leshan.core.node.LwM2mPath;
import org.eclipse.leshan.core.node.LwM2mResource;
import org.eclipse.leshan.core.node.LwM2mSingleResource;
import org.eclipse.leshan.core.node.codec.CodecException;
import org.eclipse.leshan.core.request.SimpleDownlinkRequest;
import org.eclipse.leshan.core.request.WriteAttributesRequest;
import org.eclipse.leshan.core.util.Hex;
import org.eclipse.leshan.server.registration.Registration;
import org.thingsboard.common.util.JacksonUtil;
@ -52,9 +47,7 @@ import org.thingsboard.server.transport.lwm2m.server.ota.firmware.FirmwareUpdate
import org.thingsboard.server.transport.lwm2m.server.ota.firmware.FirmwareUpdateState;
import org.thingsboard.server.transport.lwm2m.server.ota.software.SoftwareUpdateResult;
import org.thingsboard.server.transport.lwm2m.server.ota.software.SoftwareUpdateState;
import org.thingsboard.server.transport.lwm2m.server.uplink.LwM2mUplinkMsgHandler;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.HashMap;
@ -62,13 +55,6 @@ import java.util.List;
import java.util.Map;
import java.util.Optional;
import static org.eclipse.leshan.core.attributes.Attribute.DIMENSION;
import static org.eclipse.leshan.core.attributes.Attribute.GREATER_THAN;
import static org.eclipse.leshan.core.attributes.Attribute.LESSER_THAN;
import static org.eclipse.leshan.core.attributes.Attribute.MAXIMUM_PERIOD;
import static org.eclipse.leshan.core.attributes.Attribute.MINIMUM_PERIOD;
import static org.eclipse.leshan.core.attributes.Attribute.OBJECT_VERSION;
import static org.eclipse.leshan.core.attributes.Attribute.STEP;
import static org.eclipse.leshan.core.model.ResourceModel.Type.BOOLEAN;
import static org.eclipse.leshan.core.model.ResourceModel.Type.FLOAT;
import static org.eclipse.leshan.core.model.ResourceModel.Type.INTEGER;
@ -83,6 +69,7 @@ import static org.thingsboard.server.transport.lwm2m.server.ota.DefaultLwM2MOtaU
import static org.thingsboard.server.transport.lwm2m.server.ota.DefaultLwM2MOtaUpdateService.SW_RESULT_ID;
import static org.thingsboard.server.transport.lwm2m.server.ota.DefaultLwM2MOtaUpdateService.SW_STATE_ID;
@Slf4j
public class LwM2MTransportUtil {
@ -92,7 +79,7 @@ public class LwM2MTransportUtil {
public static final String LOG_LWM2M_INFO = "info";
public static final String LOG_LWM2M_ERROR = "error";
public static final String LOG_LWM2M_WARN = "warn";
public static final int BOOTSTRAP_DEFAULT_SHORT_ID = 0;
public static final int BOOTSTRAP_DEFAULT_SHORT_ID_0 = 0;
public enum LwM2MClientStrategy {
CLIENT_STRATEGY_1(1, "Read only resources marked as observation"),
@ -235,7 +222,10 @@ public class LwM2MTransportUtil {
}
public static String convertObjectIdToVersionedId(String path, Registration registration) {
String ver = registration.getSupportedObject().get(new LwM2mPath(path).getObjectId());
String ver = String.valueOf(registration.getSupportedObject().get(new LwM2mPath(path).getObjectId()));
return convertObjectIdToVerId(path, ver);
}
public static String convertObjectIdToVerId(String path, String ver) {
ver = ver != null ? ver : TbLwM2mVersion.VERSION_1_0.getVersion().toString();
try {
String[] keyArray = path.split(LWM2M_SEPARATOR_PATH);
@ -258,60 +248,6 @@ public class LwM2MTransportUtil {
}
}
/**
* As example:
* a)Write-Attributes/3/0/9?pmin=1 means the Battery Level value will be notified
* to the Server with a minimum interval of 1sec;
* this value is set at theResource level.
* b)Write-Attributes/3/0/9?pmin means the Battery Level will be notified
* to the Server with a minimum value (pmin) given by the default one
* (resource 2 of Object Server ID=1),
* or with another value if this Attribute has been set at another level
* (Object or Object Instance: see section5.1.1).
* c)Write-Attributes/3/0?pmin=10 means that all Resources of Instance 0 of the Object ‘Device (ID:3)’
* will be notified to the Server with a minimum interval of 10 sec;
* this value is set at the Object Instance level.
* d)Write-Attributes /3/0/9?gt=45&st=10 means the Battery Level will be notified to the Server
* when:
* a.old value is 20 and new value is 35 due to step condition
* b.old value is 45 and new value is 50 due to gt condition
* c.old value is 50 and new value is 40 due to both gt and step conditions
* d.old value is 35 and new value is 20 due to step conditione)
* Write-Attributes /3/0/9?lt=20&gt=85&st=10 means the Battery Level will be notified to the Server
* when:
* a.old value is 17 and new value is 24 due to lt condition
* b.old value is 75 and new value is 90 due to both gt and step conditions
* String uriQueries = "pmin=10&pmax=60";
* AttributeSet attributes = AttributeSet.parse(uriQueries);
* WriteAttributesRequest request = new WriteAttributesRequest(target, attributes);
* Attribute gt = new Attribute(GREATER_THAN, Double.valueOf("45"));
* Attribute st = new Attribute(LESSER_THAN, Double.valueOf("10"));
* Attribute pmax = new Attribute(MAXIMUM_PERIOD, "60");
* Attribute [] attrs = {gt, st};
*/
public static SimpleDownlinkRequest createWriteAttributeRequest(String target, Object params, LwM2mUplinkMsgHandler serviceImpl) {
AttributeSet attrSet = new AttributeSet(createWriteAttributes(params, serviceImpl, target));
return attrSet.getAttributes().size() > 0 ? new WriteAttributesRequest(target, attrSet) : null;
}
private static Attribute[] createWriteAttributes(Object params, LwM2mUplinkMsgHandler serviceImpl, String target) {
List<Attribute> attributeLists = new ArrayList<>();
Map<String, Object> map = JacksonUtil.convertValue(params, new TypeReference<>() {
});
map.forEach((k, v) -> {
if (StringUtils.trimToNull(v.toString()) != null) {
Object attrValue = convertWriteAttributes(k, v, serviceImpl, target);
if (attrValue != null) {
Attribute attribute = createAttribute(k, attrValue);
if (attribute != null) {
attributeLists.add(new Attribute(k, attrValue));
}
}
}
});
return attributeLists.toArray(Attribute[]::new);
}
/**
* "UNSIGNED_INTEGER": // Number -> Integer Example:
* Alarm Timestamp [32-bit unsigned integer]
@ -360,45 +296,14 @@ public class LwM2MTransportUtil {
public static Map<Integer, Object> convertMultiResourceValuesFromJson(JsonElement newValProto, ResourceModel.Type type, String versionedId) {
Map<Integer, Object> newValues = new HashMap<>();
newValProto.getAsJsonObject().entrySet().forEach((obj) -> {
newValues.put(Integer.valueOf(obj.getKey()), LwM2mValueConverterImpl.getInstance().convertValue(obj.getValue().getAsString(),
STRING, type, new LwM2mPath(fromVersionedIdToObjectId(versionedId))));
newValues.put(Integer.valueOf(obj.getKey()), convertValueByTypeResource (obj.getValue().getAsString(), type, versionedId));
});
return newValues;
}
public static Object convertWriteAttributes(String type, Object value, LwM2mUplinkMsgHandler serviceImpl, String target) {
switch (type) {
/** Integer [0:255]; */
case DIMENSION:
Long dim = (Long) serviceImpl.getConverter().convertValue(value, equalsResourceTypeGetSimpleName(value), INTEGER, new LwM2mPath(target));
return dim >= 0 && dim <= 255 ? dim : null;
/**String;*/
case OBJECT_VERSION:
return serviceImpl.getConverter().convertValue(value, equalsResourceTypeGetSimpleName(value), STRING, new LwM2mPath(target));
/**INTEGER */
case MINIMUM_PERIOD:
case MAXIMUM_PERIOD:
return serviceImpl.getConverter().convertValue(value, equalsResourceTypeGetSimpleName(value), INTEGER, new LwM2mPath(target));
/**Float; */
case GREATER_THAN:
case LESSER_THAN:
case STEP:
if (value.getClass().getSimpleName().equals("String")) {
value = Double.valueOf((String) value);
}
return serviceImpl.getConverter().convertValue(value, equalsResourceTypeGetSimpleName(value), FLOAT, new LwM2mPath(target));
default:
return null;
}
}
private static Attribute createAttribute(String key, Object attrValue) {
try {
return new Attribute(key, attrValue);
} catch (Exception e) {
log.error("CreateAttribute, not valid parameter key: [{}], attrValue: [{}], error: [{}]", key, attrValue, e.getMessage());
return null;
}
public static Object convertValueByTypeResource (String value, ResourceModel.Type type, String versionedId) {
return LwM2mValueConverterImpl.getInstance().convertValue(value,
STRING, type, new LwM2mPath(fromVersionedIdToObjectId(versionedId)));
}
/**

9
dao/src/main/java/org/thingsboard/server/dao/service/validator/DeviceProfileDataValidator.java

@ -327,10 +327,13 @@ public class DeviceProfileDataValidator extends AbstractHasOtaPackageValidator<D
if (!isBootstrapServerUpdateEnable && serverConfig.isBootstrapServerIs()) {
throw new DeviceCredentialsValidationException("Bootstrap config must not include \"Bootstrap Server\". \"Include Bootstrap Server updates\" is " + isBootstrapServerUpdateEnable + ".");
}
String server = serverConfig.isBootstrapServerIs() ? "Bootstrap Server" : "LwM2M Server" + " shortServerId: " + serverConfig.getShortServerId() + ":";
if (serverConfig.getShortServerId() < 1 || serverConfig.getShortServerId() > 65534) {
throw new DeviceCredentialsValidationException(server + " ShortServerId must not be less than 1 and more than 65534!");
if (!serverConfig.isBootstrapServerIs() && (serverConfig.getShortServerId() < 1 || serverConfig.getShortServerId() > 65534)) {
throw new DeviceCredentialsValidationException("LwM2M Server ShortServerId must not be less than 1 and more than 65534!");
}
if (serverConfig.isBootstrapServerIs() && !(serverConfig.getShortServerId() == null || serverConfig.getShortServerId() ==0)) {
throw new DeviceCredentialsValidationException("Bootstrap Server ShortServerId must be null or '0'!");
}
String server = serverConfig.isBootstrapServerIs() ? "Bootstrap Server" : "LwM2M Server";
if (!shortServerIds.add(serverConfig.getShortServerId())) {
throw new DeviceCredentialsValidationException(server + " \"Short server Id\" value = " + serverConfig.getShortServerId() + ". This value must be a unique value for all servers!");
}

81
msa/black-box-tests/src/test/java/org/thingsboard/server/msa/AbstractCoapClientTest.java

@ -0,0 +1,81 @@
/**
* Copyright © 2016-2024 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.msa;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.eclipse.californium.core.CoapClient;
import org.eclipse.californium.core.coap.MediaTypeRegistry;
import org.eclipse.californium.core.config.CoapConfig;
import org.eclipse.californium.elements.config.Configuration;
import org.eclipse.californium.elements.config.Configuration.ModuleDefinitionsProvider;
import org.eclipse.californium.elements.config.IntegerDefinition;
import org.eclipse.californium.elements.config.TcpConfig;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.msg.session.FeatureType;
public abstract class AbstractCoapClientTest extends AbstractContainerTest{
private static final String COAP_BASE_URL = "coap://localhost:5683/api/v1/";
private static final long CLIENT_REQUEST_TIMEOUT = 60000L;
private static final String COAP_CLIENT_TEST = "COAP_CLIENT_TEST.";
private static final IntegerDefinition COAP_PORT_DEF = CoapConfig.COAP_PORT;
private static final ModuleDefinitionsProvider MODULE_DEFINITIONS_PROVIDER = new ModuleDefinitionsProvider() {
@Override
public String getModule() {
return COAP_CLIENT_TEST;
}
@Override
public void applyDefinitions(Configuration config) {
TcpConfig.register();
config.set(COAP_PORT_DEF, 5683);
}
};
protected CoapClient client;
protected byte[] createCoapClientAndPublish(String deviceName) throws Exception {
String provisionRequestMsg = createTestProvisionMessage(deviceName);
Configuration.addDefaultModule(MODULE_DEFINITIONS_PROVIDER);
String featureTokenUrl = COAP_BASE_URL + FeatureType.PROVISION.name().toLowerCase();
client = new CoapClient(featureTokenUrl);
return client.setTimeout(CLIENT_REQUEST_TIMEOUT)
.post(provisionRequestMsg.getBytes(), MediaTypeRegistry.APPLICATION_JSON)
.getPayload();
}
protected void disconnect() {
if (client != null) {
client.shutdown();
}
}
private String createTestProvisionMessage(String deviceName) {
ObjectNode provisionRequest = JacksonUtil.newObjectNode();
provisionRequest.put("provisionDeviceKey", TEST_PROVISION_DEVICE_KEY);
provisionRequest.put("provisionDeviceSecret", TEST_PROVISION_DEVICE_SECRET);
if (deviceName != null) {
provisionRequest.put("deviceName", deviceName);
}
return provisionRequest.toString();
}
}

122
msa/black-box-tests/src/test/java/org/thingsboard/server/msa/TestCoapClient.java

@ -1,122 +0,0 @@
/**
* Copyright © 2016-2024 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.msa;
import org.eclipse.californium.core.CoapClient;
import org.eclipse.californium.core.CoapHandler;
import org.eclipse.californium.core.CoapObserveRelation;
import org.eclipse.californium.core.CoapResponse;
import org.eclipse.californium.core.coap.CoAP;
import org.eclipse.californium.core.coap.MediaTypeRegistry;
import org.eclipse.californium.core.coap.Request;
import org.eclipse.californium.elements.exception.ConnectorException;
import org.thingsboard.server.common.msg.session.FeatureType;
import java.io.IOException;
public class TestCoapClient {
private static final String COAP_BASE_URL = "coap://localhost:5683/api/v1/";
private static final long CLIENT_REQUEST_TIMEOUT = 60000L;
private final CoapClient client;
public TestCoapClient(){
this.client = createClient();
}
public TestCoapClient(String accessToken, FeatureType featureType) {
this.client = createClient(getFeatureTokenUrl(accessToken, featureType));
}
public TestCoapClient(String featureTokenUrl) {
this.client = createClient(featureTokenUrl);
}
public void connectToCoap(String accessToken) {
setURI(accessToken, null);
}
public void connectToCoap(String accessToken, FeatureType featureType) {
setURI(accessToken, featureType);
}
public void disconnect() {
if (client != null) {
client.shutdown();
}
}
public CoapResponse postMethod(String requestBody) throws ConnectorException, IOException {
return this.postMethod(requestBody.getBytes());
}
public CoapResponse postMethod(byte[] requestBodyBytes) throws ConnectorException, IOException {
return client.setTimeout(CLIENT_REQUEST_TIMEOUT).post(requestBodyBytes, MediaTypeRegistry.APPLICATION_JSON);
}
public void postMethod(CoapHandler handler, String payload, int format) {
client.post(handler, payload, format);
}
public void postMethod(CoapHandler handler, byte[] payload, int format) {
client.post(handler, payload, format);
}
public CoapResponse getMethod() throws ConnectorException, IOException {
return client.setTimeout(CLIENT_REQUEST_TIMEOUT).get();
}
public CoapObserveRelation getObserveRelation(TestCoapClientCallback callback){
Request request = Request.newGet().setObserve();
request.setType(CoAP.Type.CON);
return client.observe(request, callback);
}
public void setURI(String featureTokenUrl) {
if (client == null) {
throw new RuntimeException("Failed to connect! CoapClient is not initialized!");
}
client.setURI(featureTokenUrl);
}
public void setURI(String accessToken, FeatureType featureType) {
if (featureType == null){
featureType = FeatureType.ATTRIBUTES;
}
setURI(getFeatureTokenUrl(accessToken, featureType));
}
private CoapClient createClient() {
return new CoapClient();
}
private CoapClient createClient(String featureTokenUrl) {
return new CoapClient(featureTokenUrl);
}
public static String getFeatureTokenUrl(FeatureType featureType) {
return COAP_BASE_URL + featureType.name().toLowerCase();
}
public static String getFeatureTokenUrl(String token, FeatureType featureType) {
return COAP_BASE_URL + token + "/" + featureType.name().toLowerCase();
}
public static String getFeatureTokenUrl(String token, FeatureType featureType, int requestId) {
return COAP_BASE_URL + token + "/" + featureType.name().toLowerCase() + "/" + requestId;
}
}

68
msa/black-box-tests/src/test/java/org/thingsboard/server/msa/TestCoapClientCallback.java

@ -1,68 +0,0 @@
/**
* Copyright © 2016-2024 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.msa;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.californium.core.CoapHandler;
import org.eclipse.californium.core.CoapResponse;
import org.eclipse.californium.core.coap.CoAP;
import java.util.concurrent.CountDownLatch;
@Slf4j
@Data
public class TestCoapClientCallback implements CoapHandler {
protected final CountDownLatch latch;
protected Integer observe;
protected byte[] payloadBytes;
protected CoAP.ResponseCode responseCode;
public TestCoapClientCallback() {
this.latch = new CountDownLatch(1);
}
public TestCoapClientCallback(int subscribeCount) {
this.latch = new CountDownLatch(subscribeCount);
}
public Integer getObserve() {
return observe;
}
public byte[] getPayloadBytes() {
return payloadBytes;
}
public CoAP.ResponseCode getResponseCode() {
return responseCode;
}
@Override
public void onLoad(CoapResponse response) {
observe = response.getOptions().getObserve();
payloadBytes = response.getPayload();
responseCode = response.getCode();
latch.countDown();
}
@Override
public void onError() {
log.warn("Command Response Ack Error, No connect");
}
}

28
msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/CoapClientTest.java

@ -16,7 +16,6 @@
package org.thingsboard.server.msa.connectivity;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.gson.JsonObject;
import org.testng.annotations.AfterMethod;
import org.testng.annotations.BeforeMethod;
@ -26,18 +25,14 @@ import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.DeviceProfile;
import org.thingsboard.server.common.data.DeviceProfileProvisionType;
import org.thingsboard.server.common.data.security.DeviceCredentials;
import org.thingsboard.server.common.msg.session.FeatureType;
import org.thingsboard.server.msa.AbstractContainerTest;
import org.thingsboard.server.msa.AbstractCoapClientTest;
import org.thingsboard.server.msa.DisableUIListeners;
import org.thingsboard.server.msa.TestCoapClient;
import static org.assertj.core.api.Assertions.assertThat;
import static org.thingsboard.server.msa.prototypes.DevicePrototypes.defaultDevicePrototype;
@DisableUIListeners
public class CoapClientTest extends AbstractContainerTest {
private TestCoapClient client;
public class CoapClientTest extends AbstractCoapClientTest{
private Device device;
@BeforeMethod
public void setUp() throws Exception {
@ -48,6 +43,7 @@ public class CoapClientTest extends AbstractContainerTest {
@AfterMethod
public void tearDown() {
testRestClient.deleteDeviceIfExists(device.getId());
disconnect();
}
@Test
@ -101,21 +97,5 @@ public class CoapClientTest extends AbstractContainerTest {
assertThat(response.get("status").asText()).isEqualTo("NOT_FOUND");
}
private byte[] createCoapClientAndPublish(String deviceName) throws Exception {
String provisionRequestMsg = createTestProvisionMessage(deviceName);
client = new TestCoapClient(TestCoapClient.getFeatureTokenUrl(FeatureType.PROVISION));
return client.postMethod(provisionRequestMsg.getBytes()).getPayload();
}
private String createTestProvisionMessage(String deviceName) {
ObjectNode provisionRequest = JacksonUtil.newObjectNode();
provisionRequest.put("provisionDeviceKey", TEST_PROVISION_DEVICE_KEY);
provisionRequest.put("provisionDeviceSecret", TEST_PROVISION_DEVICE_SECRET);
if (deviceName != null) {
provisionRequest.put("deviceName", deviceName);
}
return provisionRequest.toString();
}
}

13
pom.xml

@ -40,7 +40,7 @@
<pkg.installFolder>/usr/share/${pkg.name}</pkg.installFolder>
<jakarta-annotation.version>2.1.1</jakarta-annotation.version>
<jakarta.xml.bind-api.version>4.0.0</jakarta.xml.bind-api.version>
<javax.xml.bind-api.version>2.3.1</javax.xml.bind-api.version>
<javax.xml.bind-api.version>2.4.0-b180830.0359</javax.xml.bind-api.version>
<jaxb-runtime.version>4.0.2</jaxb-runtime.version>
<spring-boot.version>3.1.0</spring-boot.version>
<spring-data.version>3.1.0</spring-data.version>
@ -67,13 +67,13 @@
<apache-httpclient.version>4.5.13</apache-httpclient.version>
<apache-httpcore.version>4.4.14</apache-httpcore.version>
<joda-time.version>2.8.1</joda-time.version>
<jackson.version>2.13.4</jackson.version>
<jackson-databind.version>2.13.4.2</jackson-databind.version>
<jackson.version>2.15.3</jackson.version>
<jackson-databind.version>2.15.3</jackson-databind.version>
<fasterxml-classmate.version>1.3.4</fasterxml-classmate.version>
<auth0-jwt.version>4.2.1</auth0-jwt.version>
<json-schema-validator.version>2.2.6</json-schema-validator.version>
<californium.version>3.0.0</californium.version>
<leshan.version>2.0.0-M5</leshan.version>
<californium.version>3.10.0</californium.version>
<leshan.version>2.0.0-M14</leshan.version>
<gson.version>2.9.0</gson.version>
<freemarker.version>2.3.30</freemarker.version>
<mail.version>2.0.1</mail.version>
@ -131,7 +131,7 @@
<antisamy.version>1.7.3</antisamy.version>
<snmp4j.version>2.8.5</snmp4j.version>
<!-- TEST SCOPE -->
<awaitility.version>4.1.0</awaitility.version>
<awaitility.version>4.2.0</awaitility.version>
<dbunit.version>2.7.2</dbunit.version>
<java-websocket.version>1.5.2</java-websocket.version>
<jupiter.version>5.9.3</jupiter.version> <!-- keep the same version as spring-boot-starter-test depend on jupiter-->
@ -829,6 +829,7 @@
<exclude>src/main/scripts/control/**</exclude>
<exclude>src/main/scripts/windows/**</exclude>
<exclude>src/main/resources/public/static/rulenode/**</exclude>
<exclude>src/main/java/org/eclipse/leshan/server/observation//**</exclude>
<exclude>**/*.proto.js</exclude>
<exclude>docker/haproxy/**</exclude>
<exclude>docker/tb-node/**</exclude>

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save