diff --git a/application/src/main/java/org/eclipse/leshan/server/observation/ObservationServiceImpl.java b/application/src/main/java/org/eclipse/leshan/server/observation/ObservationServiceImpl.java new file mode 100644 index 0000000000..571b0e09bd --- /dev/null +++ b/application/src/main/java/org/eclipse/leshan/server/observation/ObservationServiceImpl.java @@ -0,0 +1,281 @@ +/** + * Copyright © 2016-2023 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.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 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 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 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 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 endpoints = endpointProvider.getEndpoints(); + for (LwM2mServerEndpoint lwM2mEndpoint : endpoints) { + lwM2mEndpoint.cancelObservation(observation); + } + + for (ObservationListener listener : listeners) { + listener.cancelled(observation); + } + } + + @Override + public Set getObservations(Registration registration) { + return getObservations(registration.getId()); + } + + private Set getObservations(String registrationId) { + if (registrationId == null) + return Collections.emptySet(); + + return new HashSet<>(registrationStore.getObservations(registrationId)); + } + + private Set getCompositeObservationsForCancel(String registrationId, String[] nodePaths) { + if (registrationId == null || nodePaths == null) + return Collections.emptySet(); + + // array of String to array of LWM2M path + List lwPaths = new ArrayList<>(nodePaths.length); + for (int i = 0; i < nodePaths.length; i++) { + lwPaths.add(new LwM2mPath(nodePaths[i])); + } + + // search composite-observation + Set 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 getObservationsForCancel(String registrationId, String nodePath) { + if (registrationId == null || nodePath == null) + return Collections.emptySet(); + + Set 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: There is registration with id %s for observation path %s, that includes this observation path %s", + registrationId, lwPath, lwPathObs); + 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); + } + + } +} diff --git a/application/src/test/java/org/thingsboard/server/edge/DeviceProfileEdgeTest.java b/application/src/test/java/org/thingsboard/server/edge/DeviceProfileEdgeTest.java index 97c4b0b333..fc877ae6f8 100644 --- a/application/src/test/java/org/thingsboard/server/edge/DeviceProfileEdgeTest.java +++ b/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); diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/AbstractLwM2MIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/AbstractLwM2MIntegrationTest.java index 9cd6a66cb5..267accc51c 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/AbstractLwM2MIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/AbstractLwM2MIntegrationTest.java @@ -145,8 +145,7 @@ 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" + @@ -403,17 +402,24 @@ public abstract class AbstractLwM2MIntegrationTest extends AbstractTransportInte } protected void awaitObserveReadAll(int cntObserve, String deviceIdStr) throws Exception { - await("ObserveReadAll after start client: countObserve " + cntObserve) + await("ObserveReadAll after start client/test: countObserve " + cntObserve) .atMost(40, TimeUnit.SECONDS) .until(() -> cntObserve == getCntObserveAll(deviceIdStr)); } - protected int getCntObserveAll(String deviceIdStr) throws Exception { + 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() : 0; + 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 { diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/Lwm2mTestHelper.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/Lwm2mTestHelper.java index 6eb8dab73f..7b80b677cc 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/Lwm2mTestHelper.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/Lwm2mTestHelper.java @@ -34,6 +34,7 @@ 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; @@ -41,6 +42,7 @@ public class Lwm2mTestHelper { public static final int RESOURCE_ID_11 = 11; public static final int RESOURCE_ID_14 = 14; public static final int RESOURCE_ID_15 = 15; + public static final int RESOURCE_INSTANCE_ID_0 = 0; public static final int RESOURCE_INSTANCE_ID_2 = 2; public static final String RESOURCE_ID_NAME_3_9 = "batteryLevel"; diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/SimpleLwM2MDevice.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/SimpleLwM2MDevice.java index 89833a43a6..e6fe6598aa 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/SimpleLwM2MDevice.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/SimpleLwM2MDevice.java @@ -58,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(); diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/ota/AbstractOtaLwM2MIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/ota/AbstractOtaLwM2MIntegrationTest.java index 6b9bb2ec02..7cbd7af5a7 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/ota/AbstractOtaLwM2MIntegrationTest.java +++ b/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); } diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/ota/sql/OtaLwM2MIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/ota/sql/OtaLwM2MIntegrationTest.java index 6e6b0f2c2a..fed02cd8ca 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/ota/sql/OtaLwM2MIntegrationTest.java +++ b/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 expectedStatuses; @Test diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/AbstractRpcLwM2MIntegrationObserveTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/AbstractRpcLwM2MIntegrationObserveTest.java new file mode 100644 index 0000000000..63558fd489 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/AbstractRpcLwM2MIntegrationObserveTest.java @@ -0,0 +1,34 @@ +/** + * Copyright © 2016-2023 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); + } + +} diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2MIntegrationObserveCompositeTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2MIntegrationObserveCompositeTest.java new file mode 100644 index 0000000000..ed7d71caee --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2MIntegrationObserveCompositeTest.java @@ -0,0 +1,446 @@ +/** + * Copyright © 2016-2023 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/0/9", "/3/0/14", "/5/0/3", "/3/0/15", "/19/1/0/0"]} - Ok + * 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()); + } +} diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationDiscoverTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationDiscoverTest.java index 6da48b0a79..039af357cc 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationDiscoverTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationDiscoverTest.java @@ -35,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 { @@ -132,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 + ">"; diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationObserveTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationObserveTest.java index d9679c92c4..c6d96f2756 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationObserveTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationObserveTest.java @@ -17,84 +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&ObserveCancelAll + * Observe "3_1.2/0/9" * @throws Exception */ @Test - public void testObserveReadAllNothingObservation_Result_CONTENT_Value_Count_0() throws Exception { - awaitObserveReadAll(2, deviceId); - String idVer_3_0_0 = objectInstanceIdVer_3 + "/" + RESOURCE_ID_0; - String actualResult = sendRpcObserve("Observe", idVer_3_0_0); + 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()); assertTrue(rpcActualResult.get("value").asText().contains("LwM2mSingleResource")); - assertEquals(3, getCntObserveAll(deviceId)); - actualResult = sendRpcObserve("ObserveCancelAll", null); - 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); - assertEquals(0, getCntObserveAll(deviceId)); + 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_9 + "/" + 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)); } + /** - * Observe {"id":"/3_1.1/0/13"} + * Repeated request on Observe + * Observe {"id":"/3_1.2/0/0"} * @throws Exception */ @Test - public void testObserveWithBadVersion_Result_BadRequest_ErrorMsg_BadVersionMustBe1_0() throws Exception { + 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.?/0/13"} + * @throws Exception + */ + @Test + 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()); } @@ -128,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); @@ -141,54 +197,173 @@ 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_1.2/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; 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.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()); + String expected = "LwM2mSingleResource [id=0"; + assertTrue(rpcActualResult.get("value").asText().contains(expected)); } /** - * ObserveReadAll + * Observe {"id":["3"]} - Ok + * PreviousObservation contains "3/0/0" * @throws Exception */ @Test - public void testObserveReadAll_Result_CONTENT_Value_Contains_Paths_Count_ObserveReadAll() throws Exception { - String expectedId = objectInstanceIdVer_3 + "/" + RESOURCE_ID_14; - sendRpcObserve("Observe", expectedId); + public void testObserve_Result_CONTENT_ONE_PATH_PreviousObservation_CONTAINCE_OTHER_CurrentObservation() throws Exception { + sendCancelObserveAllWithAwait(deviceId); + // "3/0/9" + String idVer_3_0_0 = objectInstanceIdVer_3 + "/" + RESOURCE_ID_0; + String actualResult3_0_0 = sendRpcObserve("Observe", idVer_3_0_0); + ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult3_0_0, 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/0" 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(3, 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" + 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()); + // PreviousObservation "3" contains CurrentObservation "3/0/0" + 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"} + */ + @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 = "for observation path " + fromVersionedIdToObjectId(expectedId_3_0_9) + ", that includes this observation path " + fromVersionedIdToObjectId(objectIdVer_3); + 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()); } diff --git a/application/src/test/resources/lwm2m/5.xml b/application/src/test/resources/lwm2m/5.xml index 5133f7d9ab..ddcea323b0 100644 --- a/application/src/test/resources/lwm2m/5.xml +++ b/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 5 urn:oma:lwm2m:oma:5 1.0 - 1.0 + 1.2 Single Optional diff --git a/application/src/test/resources/lwm2m/9.xml b/application/src/test/resources/lwm2m/9.xml index 4186b1248d..7c78620d85 100644 --- a/application/src/test/resources/lwm2m/9.xml +++ b/application/src/test/resources/lwm2m/9.xml @@ -64,7 +64,7 @@ LEGAL DISCLAIMER 9 urn:oma:lwm2m:oma:9 1.0 - 1.0 + 1.1 Multiple Optional diff --git a/common/transport/lwm2m/pom.xml b/common/transport/lwm2m/pom.xml index 5c37bbcf89..ca4a9ab2c7 100644 --- a/common/transport/lwm2m/pom.xml +++ b/common/transport/lwm2m/pom.xml @@ -97,7 +97,7 @@ org.awaitility awaitility - test + 4.2.0 org.eclipse.californium diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/LwM2MTransportBootstrapService.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/LwM2MTransportBootstrapService.java index 1f1382ecf5..0d7b25616e 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/LwM2MTransportBootstrapService.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/LwM2MTransportBootstrapService.java @@ -131,6 +131,7 @@ public class LwM2MTransportBootstrapService { // Set Californium Configuration endpointsBuilder.setConfiguration(serverCoapConfig); + serverConfig.setCoapConfig(serverCoapConfig); // Create CoAP endpoint diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/store/LwM2MBootstrapConfigStoreTaskProvider.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/store/LwM2MBootstrapConfigStoreTaskProvider.java index 018648100d..845a1e725b 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/store/LwM2MBootstrapConfigStoreTaskProvider.java +++ b/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 { @@ -106,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); @@ -170,7 +170,7 @@ public class LwM2MBootstrapConfigStoreTaskProvider implements LwM2MBootstrapTask 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()); } @@ -244,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)); diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/config/LwM2MTransportServerConfig.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/config/LwM2MTransportServerConfig.java index 2b7ce44e10..f73a419e3e 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/config/LwM2MTransportServerConfig.java +++ b/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 networkConfig; + @Getter + @Setter + private Configuration coapConfig; + @Bean @ConfigurationProperties(prefix = "transport.lwm2m.server.security.credentials") public SslCredentialsConfig lwm2mServerCredentials() { diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mServerListener.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mServerListener.java index a29a37c766..392f3158fe 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mServerListener.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mServerListener.java @@ -93,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.info("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) { @@ -106,21 +107,25 @@ 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()); } }; diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mSessionMsgListener.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mSessionMsgListener.java index 7b9eff0cc5..9c8b37b206 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mSessionMsgListener.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mSessionMsgListener.java @@ -55,7 +55,7 @@ public class LwM2mSessionMsgListener implements GenericFutureListener 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()); } @@ -197,11 +202,72 @@ public class DefaultLwM2mDownlinkMsgHandler extends LwM2MExecutorAwareService im @Override public void sendObserveAllRequest(LwM2mClient client, TbLwM2MObserveAllRequest request, DownlinkRequestCallback> callback) { Set observations = context.getServer().getObservationService().getObservations(client.getRegistration()); - //TODO: should be able to use CompositeObservation - Set paths = observations.stream().map(observation -> ((SingleObservation)observation).getPath().toString()).collect(Collectors.toUnmodifiableSet()); + Set paths = new LinkedHashSet<>(); + observations.stream().forEach(observation -> { + if (observation instanceof SingleObservation) { + paths.add("SingleObservation:" + ((SingleObservation) observation).getPath().toString()); + } else { + List listPath = ((CompositeObservation) observation).getPaths(); + List pathsComposite = listPath.stream().map(lwM2mPath -> (lwM2mPath.toString())).collect(Collectors.toList()); + Set pathsCompositeSort = new TreeSet<>(); + if (pathsComposite.size() == 1) { + pathsCompositeSort.add("CompositeObservation: [" + pathsComposite.get(0) + "]"); + } else if (pathsComposite.size() > 1) { + List 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 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 callback) { + Set observations = context.getServer().getObservationService().getObservations(client.getRegistration()); + List listPath = LwM2mPath.getLwM2mPathList(Arrays.asList(request.getObjectIds())); + Optional 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 lwPaths = new HashSet<>(); + observations.forEach(obs -> { + LwM2mPath lwPathObs = ((SingleObservation) obs).getPath(); + listPath.forEach(nodePath -> { + if(nodePath.equals(lwPathObs) || lwPathObs.startWith(nodePath)){ // nodePath = "3", lwPathObs = "3/0/9": cancel for tne all lwPathObs + lwPaths.add(nodePath.toString()); + } + }); + }); + for (String nodePath : lwPaths) { + cnt += context.getServer().getObservationService().cancelObservations(client.getRegistration(), nodePath); + } + } + callback.onSuccess(request, cnt); + } + @Override public void sendDiscoverAllRequest(LwM2mClient client, TbLwM2MDiscoverAllRequest request, DownlinkRequestCallback> callback) { callback.onSuccess(request, Arrays.stream(client.getRegistration().getSortedObjectLinks()).map(Link::toCoreLinkFormat).collect(Collectors.toList())); @@ -258,7 +324,7 @@ public class DefaultLwM2mDownlinkMsgHandler extends LwM2MExecutorAwareService im } @Override - public void sendCancelAllRequest(LwM2mClient client, TbLwM2MCancelAllRequest request, DownlinkRequestCallback callback) { + public void sendCancelObserveAllRequest(LwM2mClient client, TbLwM2MCancelAllRequest request, DownlinkRequestCallback callback) { int observeCancelCnt = context.getServer().getObservationService().cancelObservations(client.getRegistration()); callback.onSuccess(request, observeCancelCnt); } @@ -709,4 +775,13 @@ public class DefaultLwM2mDownlinkMsgHandler extends LwM2MExecutorAwareService im }); return compositeContentFormat.get() != null ? compositeContentFormat.get() : defaultValue; } + + private static boolean containsObserveComposite(List l1, List l2) { + for (T elem : l1) { + if (l2.contains(elem)) { + return true; + } + } + return false; + } } diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/downlink/LwM2mDownlinkMsgHandler.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/downlink/LwM2mDownlinkMsgHandler.java index a9d49e6dae..2987de1fc2 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/downlink/LwM2mDownlinkMsgHandler.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/downlink/LwM2mDownlinkMsgHandler.java @@ -19,6 +19,7 @@ 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; @@ -29,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; @@ -36,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; @@ -58,7 +62,11 @@ public interface LwM2mDownlinkMsgHandler { void sendCancelObserveRequest(LwM2mClient client, TbLwM2MCancelObserveRequest request, DownlinkRequestCallback callback); - void sendCancelAllRequest(LwM2mClient client, TbLwM2MCancelAllRequest request, DownlinkRequestCallback callback); + void sendCancelObserveAllRequest(LwM2mClient client, TbLwM2MCancelAllRequest request, DownlinkRequestCallback callback); + + void sendObserveCompositeRequest(LwM2mClient client, TbLwM2MObserveCompositeRequest request, DownlinkRequestCallback callback); + + void sendCancelObserveCompositeRequest(LwM2mClient client, TbLwM2MCancelObserveCompositeRequest request, DownlinkRequestCallback callback); void sendDiscoverRequest(LwM2mClient client, TbLwM2MDiscoverRequest request, DownlinkRequestCallback callback); diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/downlink/composite/TbLwM2MCancelObserveCompositeCallback.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/downlink/composite/TbLwM2MCancelObserveCompositeCallback.java new file mode 100644 index 0000000000..9eb3797ea4 --- /dev/null +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/downlink/composite/TbLwM2MCancelObserveCompositeCallback.java @@ -0,0 +1,40 @@ +/** + * Copyright © 2016-2023 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 { + + 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)); + } +} diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/downlink/composite/TbLwM2MCancelObserveCompositeRequest.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/downlink/composite/TbLwM2MCancelObserveCompositeRequest.java new file mode 100644 index 0000000000..c3d4608bf5 --- /dev/null +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/downlink/composite/TbLwM2MCancelObserveCompositeRequest.java @@ -0,0 +1,32 @@ +/** + * Copyright © 2016-2023 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; + } +} diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/downlink/composite/TbLwM2MObserveCompositeCallback.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/downlink/composite/TbLwM2MObserveCompositeCallback.java new file mode 100644 index 0000000000..6b3855bb3c --- /dev/null +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/downlink/composite/TbLwM2MObserveCompositeCallback.java @@ -0,0 +1,39 @@ +/** + * Copyright © 2016-2023 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 { + + 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); + } + +} diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/downlink/composite/TbLwM2MObserveCompositeRequest.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/downlink/composite/TbLwM2MObserveCompositeRequest.java new file mode 100644 index 0000000000..93f9ddf726 --- /dev/null +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/downlink/composite/TbLwM2MObserveCompositeRequest.java @@ -0,0 +1,51 @@ +/** + * Copyright © 2016-2023 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 implements HasContentFormat { + + + private final Optional 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 getRequestContentFormat() { + return this.requestContentFormatOpt; + } +} diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/ota/DefaultLwM2MOtaUpdateService.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/ota/DefaultLwM2MOtaUpdateService.java index 4e43d7b653..734522134f 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/ota/DefaultLwM2MOtaUpdateService.java +++ b/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 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 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 getAttributeValue(List attrs, String keyName) { diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/rpc/DefaultLwM2MRpcRequestHandler.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/rpc/DefaultLwM2MRpcRequestHandler.java index c725d4f070..514ee8719b 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/rpc/DefaultLwM2MRpcRequestHandler.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/rpc/DefaultLwM2MRpcRequestHandler.java @@ -58,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; @@ -162,6 +168,12 @@ public class DefaultLwM2MRpcRequestHandler implements LwM2MRpcRequestHandler { case WRITE_COMPOSITE: 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()); } @@ -382,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); @@ -393,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) { diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/rpc/composite/RpcCancelObserveCompositeCallback.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/rpc/composite/RpcCancelObserveCompositeCallback.java new file mode 100644 index 0000000000..b011317ee3 --- /dev/null +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/rpc/composite/RpcCancelObserveCompositeCallback.java @@ -0,0 +1,37 @@ +/** + * Copyright © 2016-2023 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 { + + public RpcCancelObserveCompositeCallback(TransportService transportService, LwM2mClient client, TransportProtos.ToDeviceRpcRequestMsg requestMsg, DownlinkRequestCallback callback) { + super(transportService, client, requestMsg, callback); + } + + @Override + protected void sendRpcReplyOnSuccess(Integer response) { + reply(LwM2MRpcResponseBody.builder().result(ResponseCode.CONTENT.getName()).value(response.toString()).build()); + } +} diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/rpc/composite/RpcObserveResponseCompositeCallback.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/rpc/composite/RpcObserveResponseCompositeCallback.java new file mode 100644 index 0000000000..6768a70bdf --- /dev/null +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/rpc/composite/RpcObserveResponseCompositeCallback.java @@ -0,0 +1,40 @@ +/** + * Copyright © 2016-2023 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, T extends ObserveCompositeResponse> extends RpcLwM2MDownlinkCallback { + + public RpcObserveResponseCompositeCallback(TransportService transportService, LwM2mClient client, TransportProtos.ToDeviceRpcRequestMsg requestMsg, DownlinkRequestCallback callback) { + super(transportService, client, requestMsg, callback); + } + + @Override + protected Optional serializeSuccessfulResponse(T response) { + return contentToString(response.getContent()); + } +} diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbInMemoryRegistrationStore.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbInMemoryRegistrationStore.java new file mode 100644 index 0000000000..d44e6a95fe --- /dev/null +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbInMemoryRegistrationStore.java @@ -0,0 +1,577 @@ +/** + * Copyright © 2016-2023 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.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.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.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 regsByEp = new HashMap<>(); + private final Map regsByAddr = new HashMap<>(); + private final Map regsByRegId = new HashMap<>(); + private final Map regsByIdentity = new HashMap<>(); + private final Map obsByToken = new HashMap<>(); + private final Map> 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 TokenGenerator tokenGenerator; + + private final LwM2mVersionedModelProvider modelProvider; + + public TbInMemoryRegistrationStore() { + this(null, 2, null); // default clean period : 2s + } + + public TbInMemoryRegistrationStore(TokenGenerator tokenGenerator, long cleanPeriodInSec, LwM2mVersionedModelProvider modelProvider) { + this(tokenGenerator, Executors.newScheduledThreadPool(1, + new NamedThreadFactory(String.format("TbInMemoryRegistrationStore Cleaner (%ds)", cleanPeriodInSec))), + cleanPeriodInSec, modelProvider); + } + + public TbInMemoryRegistrationStore(TokenGenerator tokenGenerator, ScheduledExecutorService schedExecutor, long cleanPeriodInSec, LwM2mVersionedModelProvider modelProvider) { + this.schedExecutor = schedExecutor; + this.cleanPeriod = cleanPeriodInSec; + this.modelProvider = modelProvider; + this.tokenGenerator = tokenGenerator; + } + + /* *************** 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 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 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 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 addObservation(String registrationId, Observation observation, boolean addIfAbsent) { + List 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, observation, addIfAbsent, removed); + // cancel existing observations for the same path and registration id. + cancelObservation (observation, registrationId, removed); + } + } else { + ContentFormat ct = ((CompositeObservation) observation).getResponseContentFormat(); + Map 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); + updateSingleObservation(registrationId, 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, Observation observation, boolean addIfAbsent, List removed) { + Observation previousObservation; + + ObservationIdentifier id = observation.getId(); + if (addIfAbsent) { + if (!obsByToken.containsKey(id)) + previousObservation = obsByToken.put(id, observation); + else + previousObservation = obsByToken.get(id); + + } else { + previousObservation = obsByToken.put(id, observation); + } + if (!tokensByRegId.containsKey(registrationId)) { + tokensByRegId.put(registrationId, new HashSet()); + } + tokensByRegId.get(registrationId).add(id); + + // log any collisions + if (previousObservation != null) { + removed.add(previousObservation); + LOG.warn("Token collision ? observation [{}] will be replaced by observation [{}] ", + previousObservation, observation); + } + } + + private Observation createSingleObservation(String registrationId, LwM2mPath target, ContentFormat ct, + Map ctx, String serializedObservation) { + + Token token = tokenGenerator.createToken(Scope.SHORT_TERM); + Map 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); + } + + private 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 getObservations(String registrationId) { + try { + lock.readLock().lock(); + return unsafeGetObservations(registrationId); + } finally { + lock.readLock().unlock(); + } + } + + /** + * CancelAllObservation + * @param registrationId + * @return + */ + + @Override + public Collection 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 removed) { + for (Observation obs : unsafeGetObservations(registrationId)) { + cancelExistingObservation(observation, obs, removed); + } + } + + private void cancelExistingObservation(Observation observation, Observation obs, List 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 tokens = tokensByRegId.get(registrationId); + tokens.remove(observationId); + if (tokens.isEmpty()) { + tokensByRegId.remove(registrationId); + } + } + } + + + /** + * CancelAllObservation + * @param registrationId + * @return + */ + private Collection unsafeRemoveAllObservations(String registrationId) { + Collection removed = new ArrayList<>(); + Set 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 unsafeGetObservations(String registrationId) { + Collection result = new ArrayList<>(); + Set 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 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 boolean removeFromMap(Map map, K key, V value) { + if (map.containsKey(key) && Objects.equals(map.get(key), value)) { + map.remove(key); + return true; + } else + return false; + } +} diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbLwM2mRedisRegistrationStore.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbLwM2mRedisRegistrationStore.java index d8be30f86d..308cfe24ab 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbLwM2mRedisRegistrationStore.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbLwM2mRedisRegistrationStore.java @@ -19,11 +19,11 @@ import lombok.extern.slf4j.Slf4j; import org.eclipse.californium.core.coap.Token; import org.eclipse.californium.core.network.serialization.UdpDataParser; import org.eclipse.californium.core.network.serialization.UdpDataSerializer; -import org.eclipse.californium.core.observe.ObservationStoreException; 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.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; @@ -82,6 +82,7 @@ public class TbLwM2mRedisRegistrationStore implements RegistrationStore, Startab 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) @@ -477,16 +478,56 @@ public class TbLwM2mRedisRegistrationStore implements RegistrationStore, Startab lock.lock(); // Add and Get previous observation - byte[] previousValue; + + byte[] previousValue = null; +// byte[] key; +// byte[] serializeObs; byte[] key = toKey(OBS_TKN, observation.getId().getBytes()); byte[] serializeObs = serializeObs(observation); - if (addIfAbsent) { + // we analyze the present previous value + Collection previousPaths = getPreviousPaths(observation, connection); + if (observation instanceof SingleObservation) { + if (previousPaths == null) { + connection.stringCommands().set(key, serializeObs); + } else if (!addIfAbsent) { + + } + + previousValue = connection.get(key); if (previousValue == null || previousValue.length == 0) { - connection.set(key, serializeObs); + if (observation instanceof CompositeObservation) { + List paths = ((CompositeObservation) observation).getPaths(); + if (paths.size()==1) { + key = toKey(OBS_TKN, observation.getId().getBytes()); + serializeObs = serializeObs(observation); + previousValue = connection.stringCommands().get(key); + if (previousValue == null || previousValue.length == 0) { +// connection.set(key, serializeObs); + connection.stringCommands().set(key, serializeObs); + } + paths.forEach(pObs -> { + + }); + } + else { + paths.forEach(pObs -> { + + }); + } + } else if (observation instanceof SingleObservation) { +// previousValue = connection.get(key); + previousValue = connection.stringCommands().get(key); + if (previousValue == null || previousValue.length == 0) { + connection.stringCommands().set(key, serializeObs); +// connection.set(key, serializeObs); + } + } } + } else { - previousValue = connection.getSet(key, serializeObs); +// previousValue = connection.getSet(key, serializeObs); + previousValue = connection.stringCommands().getSet(key, serializeObs); } // secondary index to get the list by registrationId @@ -501,11 +542,36 @@ public class TbLwM2mRedisRegistrationStore implements RegistrationStore, Startab } // 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()) - && !observation.getId().equals(obs.getId())) { - removed.add(obs); - unsafeRemoveObservation(connection, registrationId, obs.getId().getBytes()); + if (obs instanceof CompositeObservation){ + ((CompositeObservation)obs).getPaths().forEach(pObs -> { + if (observation instanceof CompositeObservation) { + ((CompositeObservation)observation).getPaths().forEach(pObservation -> { + //TODO include obs every (SingleObservation) in observation Composite + }); + log.info("observation obs -> CompositeObservation"); + } else if (observation instanceof SingleObservation) { + if (((SingleObservation) observation).getPath().equals(((SingleObservation)obs).getPath()) + && !observation.getId().equals(obs.getId())){ + removed.add(obs); + unsafeRemoveObservation(connection, registrationId, obs.getId().getBytes()); + } + } + + }); + + } else if (obs instanceof SingleObservation) { + if (observation instanceof CompositeObservation) { + ((CompositeObservation)observation).getPaths().forEach(pObservation -> { + //TODO include obs every (SingleObservation) in observation Composite + }); + log.info("observation -> CompositeObservation, obs -> SingleObservation"); + } else if (observation instanceof SingleObservation) { + if (((SingleObservation) observation).getPath().equals(((SingleObservation)obs).getPath()) + && !observation.getId().equals(obs.getId())){ + removed.add(obs); + unsafeRemoveObservation(connection, registrationId, obs.getId().getBytes()); + } + } } } } finally { @@ -573,10 +639,11 @@ public class TbLwM2mRedisRegistrationStore implements RegistrationStore, Startab } } + private Collection getObservations(RedisConnection connection, String registrationId) { Collection 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(deserializeObs(obs)); } @@ -584,127 +651,156 @@ public class TbLwM2mRedisRegistrationStore implements RegistrationStore, Startab return result; } - @Override - public Collection removeObservations(String registrationId) { - try (var connection = connectionFactory.getConnection()) { - // check registration exists - Registration registration = getRegistration(connection, registrationId); - if (registration == null) - return Collections.emptyList(); + private Collection getPreviousPaths(Observation observation, RedisConnection connection) { + Collection result = new ArrayList<>(); + Collection observations = getObservations(connection, observation.getRegistrationId()); + observations.forEach(obs -> { - // 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(); - } + Collection prevPath = getPreviousPaths( observation, obs); + if (prevPath != null){ + result.addAll(prevPath); } - } - } - - /* *************** Californium ObservationStore API **************** */ - - public org.eclipse.californium.core.observe.Observation putIfAbsent(Token token, - org.eclipse.californium.core.observe.Observation obs) throws ObservationStoreException { - return add(obs, true); + }); + return result.size() > 0 ? result : null; } - 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(); + private Collection getPreviousPaths(Observation observation, Observation prevObs) { + Collection prevPath = new ArrayList<>(); + if (observation instanceof SingleObservation && prevObs instanceof SingleObservation) { - 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()); - Observation obsLeshan = buildLwM2mObservationFromCfToLeshanCore(obs); - byte[] serializeObs = serializeObs(obsLeshan); - byte[] previousValue; - if (ifAbsent) { - previousValue = connection.get(key); - if (previousValue == null || previousValue.length == 0) { - connection.set(key, serializeObs); - previousValue = connection.getSet(key, serializeObs); - } else { - return buildCoapObservationFromLeshanCoreToCfCore(deserializeObs(previousValue)); - } - } else { - previousValue = connection.getSet(key, serializeObs); - } + } else if (observation instanceof CompositeObservation) { + if (prevObs instanceof CompositeObservation) { // observation instanceof CompositeObservation && prevObs instanceof CompositeObservation - // secondary index to get the list by registrationId - connection.lPush(toKey(OBS_TKNS_REGID_IDX, registrationId), obs.getRequest().getToken().getBytes()); + } else { // observation instanceof CompositeObservation && prevObs instanceof SingleObservation - // log any collisions - if (previousValue != null && previousValue.length != 0) { - previousObservation = buildCoapObservationFromLeshanCoreToCfCore(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(); - } } + } else { // observation instanceof SingleObservation prevObs instanceof CompositeObservation + } - return previousObservation; + return prevPath.size() > 0 ? prevPath : null; } - public void remove(Token token) { + @Override + public Collection 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); - org.eclipse.californium.core.observe.Observation obs = null; - 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(); } } } - } + /* *************** Californium ObservationStore API **************** */ + +// public org.eclipse.californium.core.observe.Observation putIfAbsent(Token token, +// org.eclipse.californium.core.observe.Observation obs) throws ObservationStoreException { +// return add(obs, true); +// } + +// 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()); +// Observation obsLeshan = buildLwM2mObservationFromCfToLeshanCore(obs); +// byte[] serializeObs = serializeObs(obsLeshan); +// byte[] previousValue; +// if (ifAbsent) { +// previousValue = connection.get(key); +// if (previousValue == null || previousValue.length == 0) { +// connection.set(key, serializeObs); +// previousValue = connection.getSet(key, serializeObs); +// } else { +// return buildCoapObservationFromLeshanCoreToCfCore(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 = buildCoapObservationFromLeshanCoreToCfCore(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; +// } +// +// public void remove(Token token) { +// 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); +// org.eclipse.californium.core.observe.Observation obs = null; +// String registrationId = ObserveUtil.extractRegistrationId(obs); +// Registration registration = getRegistration(connection, registrationId); +// if (registration == null) { +// LOG.warn("Unable to remove observation {}, registration {} does not exist anymore", obs.getRequest(), +// registrationId); +// return; +// } +// +// String endpoint = registration.getEndpoint(); +// Lock lock = null; +// String lockKey = toLockKey(endpoint); +// try { +// lock = redisLock.obtain(lockKey); +// lock.lock(); +// +// unsafeRemoveObservation(connection, registrationId, token.getBytes()); +// } finally { +// if (lock != null) { +// lock.unlock(); +// } +// } +// } +// +// } + public Observation get(Token token) { try (var connection = connectionFactory.getConnection()) { byte[] obs = connection.get(toKey(OBS_TKN, token.getBytes())); @@ -745,15 +841,15 @@ public class TbLwM2mRedisRegistrationStore implements RegistrationStore, Startab return observationSerDes.serialize(obs); } - private org.eclipse.leshan.core.observation.Observation buildLwM2mObservationFromCfToLeshanCore( - org.eclipse.californium.core.observe.Observation observation) { - String serializedObservation = observationSerDesCoap.serialize(observation); - return serializedObservation == null ? null : ObserveUtil.createLwM2mObservation(observation, serializedObservation); - } - private org.eclipse.californium.core.observe.Observation buildCoapObservationFromLeshanCoreToCfCore(Observation obs) { - String serializedObservation = ObserveUtil.extractSerializedObservation(obs); - return serializedObservation == null ? null : observationSerDesCoap.deserialize(serializedObservation); - } +// private Observation buildLwM2mObservationFromCfToLeshanCore( +// org.eclipse.californium.core.observe.Observation observation) { +// String serializedObservation = observationSerDesCoap.serialize(observation); +// return serializedObservation == null ? null : ObserveUtil.createLwM2mObservation(observation, serializedObservation); +// } +// private org.eclipse.californium.core.observe.Observation buildCoapObservationFromLeshanCoreToCfCore(Observation obs) { +// String serializedObservation = ObserveUtil.extractSerializedObservation(obs); +// return serializedObservation == null ? null : observationSerDesCoap.deserialize(serializedObservation); +// } private Observation deserializeObs(byte[] data) { return data == null ? null : observationSerDes.deserialize(data); diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbLwM2mStoreFactory.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbLwM2mStoreFactory.java index 9aaebaa8bc..016fe09c22 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbLwM2mStoreFactory.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbLwM2mStoreFactory.java @@ -16,7 +16,7 @@ package org.thingsboard.server.transport.lwm2m.server.store; import lombok.RequiredArgsConstructor; -import org.eclipse.leshan.server.registration.InMemoryRegistrationStore; +import org.eclipse.californium.core.network.RandomTokenGenerator; import org.eclipse.leshan.server.registration.RegistrationStore; import org.springframework.context.annotation.Bean; import org.springframework.data.redis.connection.RedisConnectionFactory; @@ -25,6 +25,7 @@ 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; @@ -37,10 +38,12 @@ public class TbLwM2mStoreFactory { private final LwM2MTransportServerConfig config; private final LwM2mCredentialsSecurityInfoValidator validator; + private final LwM2mVersionedModelProvider modelProvider; + @Bean private RegistrationStore registrationStore() { return redisConfiguration.isPresent() ? - new TbLwM2mRedisRegistrationStore(getConnectionFactory()) : new InMemoryRegistrationStore(config.getCleanPeriodInSec()); + new TbLwM2mRedisRegistrationStore(getConnectionFactory()) : new TbInMemoryRegistrationStore(new RandomTokenGenerator(config.getCoapConfig()), config.getCleanPeriodInSec(), modelProvider); } @Bean diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/uplink/DefaultLwM2mUplinkMsgHandler.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/uplink/DefaultLwM2mUplinkMsgHandler.java index 84e1bfa53d..603128dbfc 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/uplink/DefaultLwM2mUplinkMsgHandler.java +++ b/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 * @@ -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 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 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 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); diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/uplink/LwM2mUplinkMsgHandler.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/uplink/LwM2mUplinkMsgHandler.java index 9beebe5279..e95bb979ce 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/uplink/LwM2mUplinkMsgHandler.java +++ b/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); diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/utils/LwM2MTransportUtil.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/utils/LwM2MTransportUtil.java index 201d41ce61..9f7003cbb3 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/utils/LwM2MTransportUtil.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/utils/LwM2MTransportUtil.java @@ -79,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"), @@ -223,6 +223,9 @@ public class LwM2MTransportUtil { public static String convertObjectIdToVersionedId(String path, Registration registration) { 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); @@ -245,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>=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 attributeLists = new ArrayList<>(); -// Map 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] @@ -357,41 +306,6 @@ public class LwM2MTransportUtil { STRING, type, new LwM2mPath(fromVersionedIdToObjectId(versionedId))); } -// 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; -// } -// } - /** * @param lwM2MClient - * @param path -