Browse Source

Merge branch 'master' into develop/3.7.2

pull/11670/head
Andrii Shvaika 2 years ago
parent
commit
573e95c201
  1. 46
      application/src/main/java/org/thingsboard/server/service/entitiy/alarm/DefaultTbAlarmService.java
  2. 4
      application/src/main/java/org/thingsboard/server/service/entitiy/alarm/TbAlarmService.java
  3. 30
      application/src/main/java/org/thingsboard/server/service/housekeeper/processor/AlarmsUnassignTaskProcessor.java
  4. 7
      application/src/test/java/org/thingsboard/server/service/entitiy/alarm/DefaultTbAlarmServiceTest.java
  5. 37
      application/src/test/java/org/thingsboard/server/service/housekeeper/HousekeeperServiceTest.java
  6. 30
      application/src/test/java/org/thingsboard/server/transport/lwm2m/AbstractLwM2MIntegrationTest.java
  7. 1
      application/src/test/java/org/thingsboard/server/transport/lwm2m/Lwm2mTestHelper.java
  8. 19
      application/src/test/java/org/thingsboard/server/transport/lwm2m/client/LwM2mBinaryAppDataContainer.java
  9. 7
      application/src/test/java/org/thingsboard/server/transport/lwm2m/client/SimpleLwM2MDevice.java
  10. 2
      application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/AbstractRpcLwM2MIntegrationObserveTest.java
  11. 19
      application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/AbstractRpcLwM2MIntegrationTest.java
  12. 502
      application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2MIntegrationObserveCompositeTest.java
  13. 20
      application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationDiscoverTest.java
  14. 315
      application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationObserveTest.java
  15. 3
      common/dao-api/src/main/java/org/thingsboard/server/dao/alarm/AlarmService.java
  16. 20
      common/data/src/main/java/org/thingsboard/server/common/data/housekeeper/AlarmsUnassignHousekeeperTask.java
  17. 201
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/downlink/DefaultLwM2mDownlinkMsgHandler.java
  18. 166
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbInMemoryRegistrationStore.java
  19. 135
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbLwM2mRedisRegistrationStore.java
  20. 22
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/uplink/DefaultLwM2mUplinkMsgHandler.java
  21. 36
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/utils/LwM2MTransportUtil.java
  22. 2
      dao/src/main/java/org/thingsboard/server/dao/alarm/AlarmDao.java
  23. 7
      dao/src/main/java/org/thingsboard/server/dao/alarm/BaseAlarmService.java
  24. 19
      dao/src/main/java/org/thingsboard/server/dao/sql/alarm/AlarmRepository.java
  25. 12
      dao/src/main/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmDao.java
  26. 320
      msa/black-box-tests/src/test/java/org/thingsboard/server/msa/AbstractLwm2mClientTest.java
  27. 11
      msa/black-box-tests/src/test/java/org/thingsboard/server/msa/TestRestClient.java
  28. 493
      msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/lwm2m/AbstractLwm2mClientTest.java
  29. 35
      msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/lwm2m/Lwm2mDevicesForTest.java
  30. 2
      msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/lwm2m/client/FwLwM2MDevice.java
  31. 74
      msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/lwm2m/client/LwM2MTestClient.java
  32. 230
      msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/lwm2m/client/LwM2mBinaryAppDataContainer.java
  33. 190
      msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/lwm2m/client/LwM2mTemperatureSensor.java
  34. 2
      msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/lwm2m/client/LwM2mValueConverterImpl.java
  35. 51
      msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/lwm2m/client/Lwm2mTestHelper.java
  36. 38
      msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/lwm2m/client/SimpleLwM2MDevice.java
  37. 52
      msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/lwm2m/rpc/Lwm2mObserveCompositeTest.java
  38. 52
      msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/lwm2m/rpc/Lwm2mObserveTest.java
  39. 15
      msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/lwm2m/security/Lwm2mClientNoSecTest.java
  40. 14
      msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/lwm2m/security/Lwm2mClientPskTest.java
  41. 144
      msa/black-box-tests/src/test/resources/lwm2m-registry/19.xml
  42. 103
      msa/black-box-tests/src/test/resources/lwm2m-registry/3303.xml
  43. 22
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/FetchMode.java
  44. 149
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetTelemetryNode.java
  45. 17
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetTelemetryNodeConfiguration.java
  46. 644
      rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/TbGetTelemetryNodeTest.java
  47. 9
      ui-ngx/src/app/core/utils.ts
  48. 23
      ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.ts
  49. 11
      ui-ngx/src/app/modules/home/components/dashboard/dashboard.component.ts
  50. 4
      ui-ngx/src/app/modules/home/components/widget/lib/scada/scada-symbol-widget.component.ts
  51. 738
      ui-ngx/src/app/modules/home/components/widget/lib/scada/scada-symbol.models.ts
  52. 46
      ui-ngx/src/app/modules/home/components/widget/widget-container.component.ts
  53. 30
      ui-ngx/src/app/modules/home/models/dashboard-component.models.ts
  54. 7
      ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol-editor.component.ts
  55. 80
      ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol-editor.models.ts

46
application/src/main/java/org/thingsboard/server/service/entitiy/alarm/DefaultTbAlarmService.java

@ -37,13 +37,10 @@ import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.AlarmId; import org.thingsboard.server.common.data.id.AlarmId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.page.SortOrder;
import org.thingsboard.server.service.entitiy.AbstractTbEntityService; import org.thingsboard.server.service.entitiy.AbstractTbEntityService;
import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.UUID;
@Service @Service
@AllArgsConstructor @AllArgsConstructor
@ -176,20 +173,20 @@ public class DefaultTbAlarmService extends AbstractTbEntityService implements Tb
} }
@Override @Override
public List<AlarmId> unassignDeletedUserAlarms(TenantId tenantId, UserId userId, String userTitle, long unassignTs) { public void unassignDeletedUserAlarms(TenantId tenantId, UserId userId, String userTitle, List<UUID> alarms, long unassignTs) {
List<AlarmId> totalAlarmIds = new ArrayList<>(); for (UUID alarmId : alarms) {
PageLink pageLink = new PageLink(100, 0, null, new SortOrder("id", SortOrder.Direction.ASC)); log.trace("[{}] Unassigning alarm {} from user {}", tenantId, alarmId, userId);
while (true) { AlarmApiCallResult result = alarmSubscriptionService.unassignAlarm(tenantId, new AlarmId(alarmId), unassignTs);
PageData<AlarmId> pageData = alarmService.findAlarmIdsByAssigneeId(tenantId, userId, pageLink); if (!result.isSuccessful()) {
List<AlarmId> alarmIds = pageData.getData(); log.error("[{}] Cannot unassign alarm {} from user {}", tenantId, alarmId, userId);
if (alarmIds.isEmpty()) { continue;
break; }
if (result.isModified()) {
String comment = String.format("Alarm was unassigned because user %s - was deleted", userTitle);
addSystemAlarmComment(result.getAlarm(), null, "ASSIGN", comment);
logEntityActionService.logEntityAction(result.getAlarm().getTenantId(), result.getAlarm().getOriginator(), result.getAlarm(), result.getAlarm().getCustomerId(), ActionType.ALARM_UNASSIGNED, null);
} }
processAlarmsUnassignment(tenantId, userId, userTitle, alarmIds, unassignTs);
totalAlarmIds.addAll(alarmIds);
pageLink = pageLink.nextPageLink();
} }
return totalAlarmIds;
} }
@Override @Override
@ -204,22 +201,6 @@ public class DefaultTbAlarmService extends AbstractTbEntityService implements Tb
return ts > 0 ? ts : System.currentTimeMillis(); return ts > 0 ? ts : System.currentTimeMillis();
} }
private void processAlarmsUnassignment(TenantId tenantId, UserId userId, String userTitle, List<AlarmId> alarmIds, long unassignTs) {
for (AlarmId alarmId : alarmIds) {
log.trace("[{}] Unassigning alarm {} userId {}", tenantId, alarmId, userId);
AlarmApiCallResult result = alarmSubscriptionService.unassignAlarm(tenantId, alarmId, unassignTs);
if (!result.isSuccessful()) {
log.error("[{}] Cannot unassign alarm {} userId {}", tenantId, alarmId, userId);
continue;
}
if (result.isModified()) {
String comment = String.format("Alarm was unassigned because user %s - was deleted", userTitle);
addSystemAlarmComment(result.getAlarm(), null, "ASSIGN", comment);
logEntityActionService.logEntityAction(result.getAlarm().getTenantId(), result.getAlarm().getOriginator(), result.getAlarm(), result.getAlarm().getCustomerId(), ActionType.ALARM_UNASSIGNED, null);
}
}
}
private void addSystemAlarmComment(Alarm alarm, User user, String subType, String commentText) { private void addSystemAlarmComment(Alarm alarm, User user, String subType, String commentText) {
addSystemAlarmComment(alarm, user, subType, commentText, null); addSystemAlarmComment(alarm, user, subType, commentText, null);
} }
@ -245,4 +226,5 @@ public class DefaultTbAlarmService extends AbstractTbEntityService implements Tb
log.error("Failed to save alarm comment", e); log.error("Failed to save alarm comment", e);
} }
} }
} }

4
application/src/main/java/org/thingsboard/server/service/entitiy/alarm/TbAlarmService.java

@ -19,11 +19,11 @@ import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.alarm.Alarm;
import org.thingsboard.server.common.data.alarm.AlarmInfo; import org.thingsboard.server.common.data.alarm.AlarmInfo;
import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.AlarmId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.id.UserId;
import java.util.List; import java.util.List;
import java.util.UUID;
public interface TbAlarmService { public interface TbAlarmService {
@ -41,7 +41,7 @@ public interface TbAlarmService {
AlarmInfo unassign(Alarm alarm, long unassignTs, User user) throws ThingsboardException; AlarmInfo unassign(Alarm alarm, long unassignTs, User user) throws ThingsboardException;
List<AlarmId> unassignDeletedUserAlarms(TenantId tenantId, UserId userId, String userTitle, long unassignTs); void unassignDeletedUserAlarms(TenantId tenantId, UserId userId, String userTitle, List<UUID> alarms, long unassignTs);
Boolean delete(Alarm alarm, User user); Boolean delete(Alarm alarm, User user);
} }

30
application/src/main/java/org/thingsboard/server/service/housekeeper/processor/AlarmsUnassignTaskProcessor.java

@ -21,22 +21,46 @@ import org.springframework.stereotype.Component;
import org.thingsboard.server.common.data.housekeeper.AlarmsUnassignHousekeeperTask; import org.thingsboard.server.common.data.housekeeper.AlarmsUnassignHousekeeperTask;
import org.thingsboard.server.common.data.housekeeper.HousekeeperTaskType; import org.thingsboard.server.common.data.housekeeper.HousekeeperTaskType;
import org.thingsboard.server.common.data.id.AlarmId; import org.thingsboard.server.common.data.id.AlarmId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.util.TbPair;
import org.thingsboard.server.dao.alarm.AlarmService;
import org.thingsboard.server.service.entitiy.alarm.TbAlarmService; import org.thingsboard.server.service.entitiy.alarm.TbAlarmService;
import java.util.List; import java.util.List;
import java.util.UUID;
@Component @Component
@RequiredArgsConstructor @RequiredArgsConstructor
@Slf4j @Slf4j
public class AlarmsUnassignTaskProcessor extends HousekeeperTaskProcessor<AlarmsUnassignHousekeeperTask> { public class AlarmsUnassignTaskProcessor extends HousekeeperTaskProcessor<AlarmsUnassignHousekeeperTask> {
private final TbAlarmService alarmService; private final TbAlarmService tbAlarmService;
private final AlarmService alarmService;
@Override @Override
public void process(AlarmsUnassignHousekeeperTask task) throws Exception { public void process(AlarmsUnassignHousekeeperTask task) throws Exception {
List<AlarmId> alarms = alarmService.unassignDeletedUserAlarms(task.getTenantId(), (UserId) task.getEntityId(), task.getUserTitle(), task.getTs()); TenantId tenantId = task.getTenantId();
log.debug("[{}][{}] Unassigned {} alarms", task.getTenantId(), task.getEntityId(), alarms.size()); UserId userId = (UserId) task.getEntityId();
if (task.getAlarms() == null) {
AlarmId lastId = null;
long lastCreatedTime = 0;
while (true) {
List<TbPair<UUID, Long>> alarms = alarmService.findAlarmIdsByAssigneeId(tenantId, userId, lastCreatedTime, lastId, 64);
if (alarms.isEmpty()) {
break;
}
housekeeperClient.submitTask(new AlarmsUnassignHousekeeperTask(tenantId, userId, task.getUserTitle(), alarms.stream().map(TbPair::getFirst).toList()));
TbPair<UUID, Long> last = alarms.get(alarms.size() - 1);
lastId = new AlarmId(last.getFirst());
lastCreatedTime = last.getSecond();
log.debug("[{}][{}] Submitted task for unassigning {} alarms", tenantId, userId, alarms.size());
}
} else {
tbAlarmService.unassignDeletedUserAlarms(tenantId, userId, task.getUserTitle(), task.getAlarms(), task.getTs());
log.debug("[{}][{}] Unassigned {} alarms", tenantId, userId, task.getAlarms().size());
}
} }
@Override @Override

7
application/src/test/java/org/thingsboard/server/service/entitiy/alarm/DefaultTbAlarmServiceTest.java

@ -37,7 +37,6 @@ import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.AlarmId; import org.thingsboard.server.common.data.id.AlarmId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.dao.alarm.AlarmService; import org.thingsboard.server.dao.alarm.AlarmService;
import org.thingsboard.server.dao.customer.CustomerService; import org.thingsboard.server.dao.customer.CustomerService;
import org.thingsboard.server.dao.edge.EdgeService; import org.thingsboard.server.dao.edge.EdgeService;
@ -46,7 +45,6 @@ import org.thingsboard.server.service.executors.DbCallbackExecutorService;
import org.thingsboard.server.service.sync.vc.EntitiesVersionControlService; import org.thingsboard.server.service.sync.vc.EntitiesVersionControlService;
import org.thingsboard.server.service.telemetry.AlarmSubscriptionService; import org.thingsboard.server.service.telemetry.AlarmSubscriptionService;
import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.UUID; import java.util.UUID;
@ -164,16 +162,13 @@ public class DefaultTbAlarmServiceTest {
AlarmInfo alarm = new AlarmInfo(); AlarmInfo alarm = new AlarmInfo();
alarm.setId(new AlarmId(UUID.randomUUID())); alarm.setId(new AlarmId(UUID.randomUUID()));
when(alarmService.findAlarmIdsByAssigneeId(any(), any(), any()))
.thenReturn(new PageData<>(List.of(alarm.getId()), 0, 1, false))
.thenReturn(new PageData<>(Collections.EMPTY_LIST, 0, 0, false));
when(alarmSubscriptionService.unassignAlarm(any(), any(), anyLong())) when(alarmSubscriptionService.unassignAlarm(any(), any(), anyLong()))
.thenReturn(AlarmApiCallResult.builder().successful(true).modified(true).alarm(alarm).build()); .thenReturn(AlarmApiCallResult.builder().successful(true).modified(true).alarm(alarm).build());
User user = new User(); User user = new User();
user.setEmail("testEmail@gmail.com"); user.setEmail("testEmail@gmail.com");
user.setId(new UserId(UUID.randomUUID())); user.setId(new UserId(UUID.randomUUID()));
service.unassignDeletedUserAlarms(new TenantId(UUID.randomUUID()), user.getId(), user.getTitle(), System.currentTimeMillis()); service.unassignDeletedUserAlarms(new TenantId(UUID.randomUUID()), user.getId(), user.getTitle(), List.of(alarm.getUuidId()), System.currentTimeMillis());
ObjectNode commentNode = JacksonUtil.newObjectNode(); ObjectNode commentNode = JacksonUtil.newObjectNode();
commentNode.put("subtype", "ASSIGN"); commentNode.put("subtype", "ASSIGN");

37
application/src/test/java/org/thingsboard/server/service/housekeeper/HousekeeperServiceTest.java

@ -55,7 +55,6 @@ import org.thingsboard.server.common.data.kv.BasicTsKvEntry;
import org.thingsboard.server.common.data.kv.StringDataEntry; import org.thingsboard.server.common.data.kv.StringDataEntry;
import org.thingsboard.server.common.data.kv.TsKvEntry; import org.thingsboard.server.common.data.kv.TsKvEntry;
import org.thingsboard.server.common.data.msg.TbNodeConnectionType; import org.thingsboard.server.common.data.msg.TbNodeConnectionType;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.page.TimePageLink; import org.thingsboard.server.common.data.page.TimePageLink;
import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.relation.EntityRelation;
import org.thingsboard.server.common.data.relation.RelationTypeGroup; import org.thingsboard.server.common.data.relation.RelationTypeGroup;
@ -63,6 +62,7 @@ import org.thingsboard.server.common.data.rule.RuleChain;
import org.thingsboard.server.common.data.rule.RuleChainMetaData; import org.thingsboard.server.common.data.rule.RuleChainMetaData;
import org.thingsboard.server.common.data.rule.RuleChainType; import org.thingsboard.server.common.data.rule.RuleChainType;
import org.thingsboard.server.common.data.rule.RuleNode; import org.thingsboard.server.common.data.rule.RuleNode;
import org.thingsboard.server.common.data.util.TbPair;
import org.thingsboard.server.controller.AbstractControllerTest; import org.thingsboard.server.controller.AbstractControllerTest;
import org.thingsboard.server.dao.alarm.AlarmDao; import org.thingsboard.server.dao.alarm.AlarmDao;
import org.thingsboard.server.dao.alarm.AlarmService; import org.thingsboard.server.dao.alarm.AlarmService;
@ -184,23 +184,32 @@ public class HousekeeperServiceTest extends AbstractControllerTest {
Device device = createDevice("test", "test"); Device device = createDevice("test", "test");
UserId userId = customerUserId; UserId userId = customerUserId;
createRelatedData(userId); createRelatedData(userId);
Alarm alarm = Alarm.builder()
.type("test") List<AlarmId> alarms = new ArrayList<>();
.tenantId(tenantId) int count = 112;
.originator(device.getId()) for (int i = 0; i < count; i++) {
.severity(AlarmSeverity.MAJOR) Alarm alarm = Alarm.builder()
.build(); .type("test" + i)
alarm = doPost("/api/alarm", alarm, Alarm.class); .tenantId(tenantId)
AlarmId alarmId = alarm.getId(); .originator(device.getId())
alarm = doPost("/api/alarm/" + alarmId + "/assign/" + userId, "", Alarm.class); .severity(AlarmSeverity.MAJOR)
assertThat(alarm.getAssigneeId()).isEqualTo(userId); .build();
assertThat(alarmService.findAlarmIdsByAssigneeId(tenantId, userId, new PageLink(100)).getData()).isNotEmpty(); alarm = doPost("/api/alarm", alarm, Alarm.class);
AlarmId alarmId = alarm.getId();
alarm = doPost("/api/alarm/" + alarmId + "/assign/" + userId, "", Alarm.class);
assertThat(alarm.getAssigneeId()).isEqualTo(userId);
alarms.add(alarmId);
}
List<AlarmId> assignedAlarms = alarmService.findAlarmIdsByAssigneeId(tenantId, userId, 0, null, 5000).stream()
.map(TbPair::getFirst).map(AlarmId::new).toList();
assertThat(assignedAlarms).size().isEqualTo(count);
assertThat(assignedAlarms).containsAll(alarms);
doDelete("/api/user/" + userId).andExpect(status().isOk()); doDelete("/api/user/" + userId).andExpect(status().isOk());
await().atMost(10, TimeUnit.SECONDS).untilAsserted(() -> { await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> {
verifyNoRelatedData(userId); verifyNoRelatedData(userId);
assertThat(alarmService.findAlarmById(tenantId, alarmId).getAssigneeId()).isNull(); assertThat(alarmService.findAlarmIdsByAssigneeId(tenantId, userId, 0, null, 5000)).size().isZero();
}); });
} }

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

@ -411,27 +411,36 @@ public abstract class AbstractLwM2MIntegrationTest extends AbstractTransportInte
} }
protected void awaitObserveReadAll(int cntObserve, String deviceIdStr) throws Exception { protected void awaitObserveReadAll(int cntObserve, String deviceIdStr) throws Exception {
await("ObserveReadAll after start client/test: countObserve " + cntObserve) await("ObserveReadAll: countObserve " + cntObserve)
.atMost(40, TimeUnit.SECONDS) .atMost(40, TimeUnit.SECONDS)
.until(() -> cntObserve == getCntObserveAll(deviceIdStr)); .until(() -> cntObserve == getCntObserveAll(deviceIdStr));
} }
protected Integer getCntObserveAll(String deviceIdStr) throws Exception { protected Integer getCntObserveAll(String deviceIdStr) throws Exception {
String actualResultBefore = sendObserve("ObserveReadAll", null, deviceIdStr); String actualResult = sendObserveOK("ObserveReadAll", null, deviceIdStr);
ObjectNode rpcActualResultBefore = JacksonUtil.fromString(actualResultBefore, ObjectNode.class); ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class);
assertEquals(ResponseCode.CONTENT.getName(), rpcActualResultBefore.get("result").asText()); assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText());
JsonElement element = JsonUtils.parse(rpcActualResultBefore.get("value").asText()); JsonElement element = JsonUtils.parse(rpcActualResult.get("value").asText());
return element.isJsonArray() ? ((JsonArray)element).size() : null; return element.isJsonArray() ? ((JsonArray)element).size() : null;
} }
protected void sendCancelObserveAllWithAwait(String deviceIdStr) throws Exception { protected void sendObserveCancelAllWithAwait(String deviceIdStr) throws Exception {
String actualResultCancelAll = sendObserve("ObserveCancelAll", null, deviceIdStr); String actualResultCancelAll = sendObserveOK("ObserveCancelAll", null, deviceIdStr);
ObjectNode rpcActualResultCancelAll = JacksonUtil.fromString(actualResultCancelAll, ObjectNode.class); ObjectNode rpcActualResultCancelAll = JacksonUtil.fromString(actualResultCancelAll, ObjectNode.class);
assertEquals(ResponseCode.CONTENT.getName(), rpcActualResultCancelAll.get("result").asText()); assertEquals(ResponseCode.CONTENT.getName(), rpcActualResultCancelAll.get("result").asText());
awaitObserveReadAll(0, deviceId); awaitObserveReadAll(0, deviceId);
} }
protected String sendObserve(String method, String params, String deviceIdStr) throws Exception { protected String sendRpcObserveOkWithResultValue(String method, String params) throws Exception {
String actualResultReadAll = sendRpcObserveOk(method, params);
ObjectNode rpcActualResult = JacksonUtil.fromString(actualResultReadAll, ObjectNode.class);
assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText());
return rpcActualResult.get("value").asText();
}
protected String sendRpcObserveOk(String method, String params) throws Exception {
return sendObserveOK(method, params, deviceId);
}
protected String sendObserveOK(String method, String params, String deviceIdStr) throws Exception {
String sendRpcRequest; String sendRpcRequest;
if (params == null) { if (params == null) {
sendRpcRequest = "{\"method\": \"" + method + "\"}"; sendRpcRequest = "{\"method\": \"" + method + "\"}";
@ -442,4 +451,9 @@ public abstract class AbstractLwM2MIntegrationTest extends AbstractTransportInte
return doPostAsync("/api/plugins/rpc/twoway/" + deviceIdStr, sendRpcRequest, String.class, status().isOk()); return doPostAsync("/api/plugins/rpc/twoway/" + deviceIdStr, sendRpcRequest, String.class, status().isOk());
} }
protected ObjectNode sendRpcObserveWithResult(String method, String params) throws Exception {
String actualResultReadAll = sendRpcObserveOk(method, params);
return JacksonUtil.fromString(actualResultReadAll, ObjectNode.class);
}
} }

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

@ -48,6 +48,7 @@ public class Lwm2mTestHelper {
public static final String RESOURCE_ID_NAME_3_9 = "batteryLevel"; public static final String RESOURCE_ID_NAME_3_9 = "batteryLevel";
public static final String RESOURCE_ID_NAME_3_14 = "UtfOffset"; public static final String RESOURCE_ID_NAME_3_14 = "UtfOffset";
public static final String RESOURCE_ID_NAME_19_0_0 = "dataRead"; public static final String RESOURCE_ID_NAME_19_0_0 = "dataRead";
public static final String RESOURCE_ID_NAME_19_0_2 = "dataCreationTime";
public static final String RESOURCE_ID_NAME_19_1_0 = "dataWrite"; public static final String RESOURCE_ID_NAME_19_1_0 = "dataWrite";
public static final String RESOURCE_ID_NAME_19_0_3 = "dataDescription"; public static final String RESOURCE_ID_NAME_19_0_3 = "dataDescription";

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

@ -27,8 +27,10 @@ import org.eclipse.leshan.core.response.WriteResponse;
import javax.security.auth.Destroyable; import javax.security.auth.Destroyable;
import java.sql.Time; import java.sql.Time;
import java.time.Instant;
import java.time.LocalTime;
import java.time.ZoneId;
import java.util.Arrays; import java.util.Arrays;
import java.util.Date;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@ -85,7 +87,8 @@ public class LwM2mBinaryAppDataContainer extends BaseInstanceEnabler implements
fireResourceChange(0); fireResourceChange(0);
fireResourceChange(2); fireResourceChange(2);
} }
, 1800000, 1800000, TimeUnit.MILLISECONDS); // 30 MIN , 1, 1, TimeUnit.SECONDS); // 1 sec
// , 1800000, 1800000, TimeUnit.MILLISECONDS); // 30 MIN
} catch (Throwable e) { } catch (Throwable e) {
log.error("[{}]Throwable", e.toString()); log.error("[{}]Throwable", e.toString());
e.printStackTrace(); e.printStackTrace();
@ -123,6 +126,7 @@ public class LwM2mBinaryAppDataContainer extends BaseInstanceEnabler implements
switch (resourceId) { switch (resourceId) {
case 0: case 0:
if (setData(value, replace)) { if (setData(value, replace)) {
fireResourceChange(resourceId);
return WriteResponse.success(); return WriteResponse.success();
} else { } else {
WriteResponse.badRequest("Invalidate value ..."); WriteResponse.badRequest("Invalidate value ...");
@ -132,7 +136,7 @@ public class LwM2mBinaryAppDataContainer extends BaseInstanceEnabler implements
fireResourceChange(resourceId); fireResourceChange(resourceId);
return WriteResponse.success(); return WriteResponse.success();
case 2: case 2:
setTimestamp(((Date) value.getValue()).getTime()); setTimestamp();
fireResourceChange(resourceId); fireResourceChange(resourceId);
return WriteResponse.success(); return WriteResponse.success();
case 3: case 3:
@ -177,12 +181,15 @@ public class LwM2mBinaryAppDataContainer extends BaseInstanceEnabler implements
return this.description; return this.description;
} }
private void setTimestamp(long time) { private void setTimestamp() {
this.timestamp = new Time(time); long currentTimeMillis = System.currentTimeMillis();
this.timestamp = new Time(currentTimeMillis);
} }
private Time getTimestamp() { private Time getTimestamp() {
return this.timestamp != null ? this.timestamp : new Time(new Date().getTime()); LocalTime localTime = LocalTime.ofInstant(Instant.now(), ZoneId.systemDefault());
this.timestamp = Time.valueOf(localTime);
return this.timestamp;
} }
private boolean setData(LwM2mResource value, boolean replace) { private boolean setData(LwM2mResource value, boolean replace) {

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

@ -58,7 +58,7 @@ public class SimpleLwM2MDevice extends BaseInstanceEnabler implements Destroyabl
executorService.scheduleWithFixedDelay(() -> { executorService.scheduleWithFixedDelay(() -> {
fireResourceChange(9); fireResourceChange(9);
} }
, 1, 1, TimeUnit.SECONDS); // 30 MIN , 1, 1, TimeUnit.SECONDS); // 2 sec
// , 1800000, 1800000, TimeUnit.MILLISECONDS); // 30 MIN // , 1800000, 1800000, TimeUnit.MILLISECONDS); // 30 MIN
} catch (Throwable e) { } catch (Throwable e) {
log.error("[{}]Throwable", e.toString()); log.error("[{}]Throwable", e.toString());
@ -169,8 +169,9 @@ public class SimpleLwM2MDevice extends BaseInstanceEnabler implements Destroyabl
} }
private int getBatteryLevel() { private int getBatteryLevel() {
return randomIterator.nextInt(); int valBattery = randomIterator.nextInt();
// return 42; log.trace("Send from client [3/0/9] val: [{}]", valBattery);
return valBattery;
} }
private long getMemoryFree() { private long getMemoryFree() {

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

@ -28,7 +28,7 @@ public abstract class AbstractRpcLwM2MIntegrationObserveTest extends AbstractRpc
@Before @Before
public void initTest () throws Exception { public void initTest () throws Exception {
awaitObserveReadAll(2, deviceId); awaitObserveReadAll(4, deviceId);
} }
} }

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

@ -42,13 +42,16 @@ import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.OBJECT_INST
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.OBJECT_INSTANCE_ID_1; 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_0;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_14; 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_9; 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_0_0;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_NAME_19_0_2;
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_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_14;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_NAME_3_9; import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_NAME_3_9;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.TEMPERATURE_SENSOR; import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.TEMPERATURE_SENSOR;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.resources; import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.resources;
import static org.thingsboard.server.transport.lwm2m.utils.LwM2MTransportUtil.fromVersionedIdToObjectId;
@DaoSqlTest @DaoSqlTest
public abstract class AbstractRpcLwM2MIntegrationTest extends AbstractLwM2MIntegrationTest { public abstract class AbstractRpcLwM2MIntegrationTest extends AbstractLwM2MIntegrationTest {
@ -74,7 +77,11 @@ public abstract class AbstractRpcLwM2MIntegrationTest extends AbstractLwM2MInteg
protected String objectIdVer_3303; protected String objectIdVer_3303;
protected static AtomicInteger endpointSequence = new AtomicInteger(); protected static AtomicInteger endpointSequence = new AtomicInteger();
protected static String DEVICE_ENDPOINT_RPC_PREF = "deviceEndpointRpc"; protected static String DEVICE_ENDPOINT_RPC_PREF = "deviceEndpointRpc";
protected String idVer_3_0_0;
protected String idVer_3_0_9; protected String idVer_3_0_9;
protected String id_3_0_9;
protected String idVer_19_0_0; protected String idVer_19_0_0;
public AbstractRpcLwM2MIntegrationTest() { public AbstractRpcLwM2MIntegrationTest() {
@ -125,7 +132,9 @@ public abstract class AbstractRpcLwM2MIntegrationTest extends AbstractLwM2MInteg
objectInstanceIdVer_5 = (String) expectedObjectIdVerInstances.stream().filter(path -> ((String) path).startsWith("/" + FIRMWARE)).findFirst().get(); objectInstanceIdVer_5 = (String) expectedObjectIdVerInstances.stream().filter(path -> ((String) path).startsWith("/" + FIRMWARE)).findFirst().get();
objectInstanceIdVer_9 = (String) expectedObjectIdVerInstances.stream().filter(path -> ((String) path).startsWith("/" + SOFTWARE_MANAGEMENT)).findFirst().get(); objectInstanceIdVer_9 = (String) expectedObjectIdVerInstances.stream().filter(path -> ((String) path).startsWith("/" + SOFTWARE_MANAGEMENT)).findFirst().get();
idVer_3_0_0 = objectIdVer_3 + "/" + OBJECT_INSTANCE_ID_0 + "/" + RESOURCE_ID_0;
idVer_3_0_9 = objectIdVer_3 + "/" + OBJECT_INSTANCE_ID_0 + "/" + RESOURCE_ID_9; idVer_3_0_9 = objectIdVer_3 + "/" + OBJECT_INSTANCE_ID_0 + "/" + RESOURCE_ID_9;
id_3_0_9 = fromVersionedIdToObjectId(idVer_3_0_9);
idVer_19_0_0 = objectIdVer_19 + "/" + OBJECT_INSTANCE_ID_0 + "/" + RESOURCE_ID_0; idVer_19_0_0 = objectIdVer_19 + "/" + OBJECT_INSTANCE_ID_0 + "/" + RESOURCE_ID_0;
OBSERVE_ATTRIBUTES_WITH_PARAMS_RPC = OBSERVE_ATTRIBUTES_WITH_PARAMS_RPC =
@ -134,14 +143,18 @@ public abstract class AbstractRpcLwM2MIntegrationTest extends AbstractLwM2MInteg
" \"" + idVer_3_0_9 + "\": \"" + RESOURCE_ID_NAME_3_9 + "\",\n" + " \"" + idVer_3_0_9 + "\": \"" + RESOURCE_ID_NAME_3_9 + "\",\n" +
" \"" + objectIdVer_3 + "/" + OBJECT_INSTANCE_ID_0 + "/" + RESOURCE_ID_14 + "\": \"" + RESOURCE_ID_NAME_3_14 + "\",\n" + " \"" + objectIdVer_3 + "/" + OBJECT_INSTANCE_ID_0 + "/" + RESOURCE_ID_14 + "\": \"" + RESOURCE_ID_NAME_3_14 + "\",\n" +
" \"" + idVer_19_0_0 + "\": \"" + RESOURCE_ID_NAME_19_0_0 + "\",\n" + " \"" + idVer_19_0_0 + "\": \"" + RESOURCE_ID_NAME_19_0_0 + "\",\n" +
" \"" + objectIdVer_19 + "/" + OBJECT_INSTANCE_ID_1 + "/" + RESOURCE_ID_0 + "\": \"" + RESOURCE_ID_NAME_19_1_0 + "\"\n" + " \"" + objectIdVer_19 + "/" + OBJECT_INSTANCE_ID_1 + "/" + RESOURCE_ID_0 + "\": \"" + RESOURCE_ID_NAME_19_1_0 + "\",\n" +
" \"" + objectIdVer_19 + "/" + OBJECT_INSTANCE_ID_0 + "/" + RESOURCE_ID_2 + "\": \"" + RESOURCE_ID_NAME_19_0_2 + "\"\n" +
" },\n" + " },\n" +
" \"observe\": [\n" + " \"observe\": [\n" +
" \"" + idVer_3_0_9 + "\",\n" + " \"" + idVer_3_0_9 + "\",\n" +
" \"" + idVer_19_0_0 + "\"\n" + " \"" + idVer_19_0_0 + "\",\n" +
" \"" + objectIdVer_19 + "/" + OBJECT_INSTANCE_ID_1 + "/" + RESOURCE_ID_0 + "\",\n" +
" \"" + objectIdVer_19 + "/" + OBJECT_INSTANCE_ID_0 + "/" + RESOURCE_ID_2 + "\"\n" +
" ],\n" + " ],\n" +
" \"attribute\": [\n" + " \"attribute\": [\n" +
" \"" + objectIdVer_3 + "/" + OBJECT_INSTANCE_ID_0 + "/" + RESOURCE_ID_14 + "\"\n" + " \"" + objectIdVer_3 + "/" + OBJECT_INSTANCE_ID_0 + "/" + RESOURCE_ID_14 + "\",\n" +
" \"" + objectIdVer_19 + "/" + OBJECT_INSTANCE_ID_0 + "/" + RESOURCE_ID_2 + "\"\n" +
" ],\n" + " ],\n" +
" \"telemetry\": [\n" + " \"telemetry\": [\n" +
" \"" + idVer_3_0_9 + "\",\n" + " \"" + idVer_3_0_9 + "\",\n" +

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

@ -15,12 +15,22 @@
*/ */
package org.thingsboard.server.transport.lwm2m.rpc.sql; package org.thingsboard.server.transport.lwm2m.rpc.sql;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode; import com.fasterxml.jackson.databind.node.ObjectNode;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.leshan.core.ResponseCode; import org.eclipse.leshan.core.ResponseCode;
import org.junit.Test; 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.common.util.JacksonUtil;
import org.thingsboard.server.transport.lwm2m.rpc.AbstractRpcLwM2MIntegrationObserveTest; import org.thingsboard.server.transport.lwm2m.rpc.AbstractRpcLwM2MIntegrationObserveTest;
import org.thingsboard.server.transport.lwm2m.server.uplink.DefaultLwM2mUplinkMsgHandler;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
import static org.awaitility.Awaitility.await;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue; import static org.junit.Assert.assertTrue;
@ -29,21 +39,25 @@ import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.OBJECT_INST
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.OBJECT_INSTANCE_ID_1; 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_0;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_14; 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_2;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_3; 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_5;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_7; 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_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_0_0;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_NAME_19_0_2;
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_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_14;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_NAME_3_9; 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.Lwm2mTestHelper.RESOURCE_INSTANCE_ID_0;
import static org.thingsboard.server.transport.lwm2m.utils.LwM2MTransportUtil.fromVersionedIdToObjectId; import static org.thingsboard.server.transport.lwm2m.utils.LwM2MTransportUtil.fromVersionedIdToObjectId;
@Slf4j
public class RpcLwm2MIntegrationObserveCompositeTest extends AbstractRpcLwM2MIntegrationObserveTest { public class RpcLwm2MIntegrationObserveCompositeTest extends AbstractRpcLwM2MIntegrationObserveTest {
@SpyBean
DefaultLwM2mUplinkMsgHandler defaultUplinkMsgHandlerTest;
/** /**
* ObserveComposite {"ids":["5/0/7", "5/0/5", "5/0/3", "3/0/9", "19/1/0/0"]} - Ok * ObserveComposite {"ids":["5/0/7", "5/0/5", "5/0/3", "3/0/9", "19/1/0/0"]} - Ok
@ -51,13 +65,13 @@ public class RpcLwm2MIntegrationObserveCompositeTest extends AbstractRpcLwM2MInt
*/ */
@Test @Test
public void testObserveCompositeAnyResources_Result_CONTENT_Value_LwM2mSingleResource_LwM2mResourceInstance() throws Exception { public void testObserveCompositeAnyResources_Result_CONTENT_Value_LwM2mSingleResource_LwM2mResourceInstance() throws Exception {
sendCancelObserveAllWithAwait(deviceId); sendObserveCancelAllWithAwait(deviceId);
String expectedIdVer5_0_7 = objectInstanceIdVer_5 + "/" + RESOURCE_ID_7; String expectedIdVer5_0_7 = objectInstanceIdVer_5 + "/" + RESOURCE_ID_7;
String expectedIdVer5_0_5= objectInstanceIdVer_5 + "/" + RESOURCE_ID_5; String expectedIdVer5_0_5 = objectInstanceIdVer_5 + "/" + RESOURCE_ID_5;
String expectedIdVer5_0_3 = objectInstanceIdVer_5 + "/" + RESOURCE_ID_3; 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 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 expectedIds = "[\"" + expectedIdVer5_0_7 + "\", \"" + expectedIdVer5_0_5 + "\", \"" + expectedIdVer5_0_3 + "\", \"" + idVer_3_0_9 + "\", \"" + expectedIdVer19_1_0_0 + "\"]";
String actualResult = sendCompositeRPCByIds("ObserveComposite", expectedIds); String actualResult = sendCompositeRPCByIds("ObserveComposite", expectedIds);
ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class);
assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText()); assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText());
String actualValues = rpcActualResult.get("value").asText(); String actualValues = rpcActualResult.get("value").asText();
@ -74,30 +88,30 @@ public class RpcLwm2MIntegrationObserveCompositeTest extends AbstractRpcLwM2MInt
*/ */
@Test @Test
public void testObserveComposite_ObjectInstanceWithOtherObjectResourceInstance_Result_CONTENT_Ok() throws Exception { public void testObserveComposite_ObjectInstanceWithOtherObjectResourceInstance_Result_CONTENT_Ok() throws Exception {
sendCancelObserveAllWithAwait(deviceId); sendObserveCancelAllWithAwait(deviceId);
String expectedIdVer19_1_0 = objectIdVer_19 + "/" + OBJECT_INSTANCE_ID_1 + "/" + RESOURCE_ID_0; String expectedIdVer19_1_0 = objectIdVer_19 + "/" + OBJECT_INSTANCE_ID_1 + "/" + RESOURCE_ID_0;
String expectedIdVer5_0 = objectInstanceIdVer_5; String expectedIdVer5_0 = objectInstanceIdVer_5;
String expectedIds = "[\"" + expectedIdVer19_1_0 + "\", \"" + expectedIdVer5_0 + "\"]"; String expectedIds = "[\"" + expectedIdVer19_1_0 + "\", \"" + expectedIdVer5_0 + "\"]";
String actualResult = sendCompositeRPCByIds("ObserveComposite", expectedIds); String actualResult = sendCompositeRPCByIds("ObserveComposite", expectedIds);
ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class);
assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText()); assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText());
String actual= rpcActualResult.get("value").asText(); String actual = rpcActualResult.get("value").asText();
assertTrue(actual.contains(fromVersionedIdToObjectId(expectedIdVer19_1_0) + "=LwM2mMultipleResource")); assertTrue(actual.contains(fromVersionedIdToObjectId(expectedIdVer19_1_0) + "=LwM2mMultipleResource"));
assertTrue(actual.contains(fromVersionedIdToObjectId(expectedIdVer5_0) + "=LwM2mObjectInstance")); assertTrue(actual.contains(fromVersionedIdToObjectId(expectedIdVer5_0) + "=LwM2mObjectInstance"));
} }
/** /**
* ObserveComposite {"ids":["5/0/7", "5/0/2"]} - Ok * ObserveComposite {"ids":["5/0/7", "5/0/2"]} - Ok
* "5/0/2" - Execute^ result == null * "5/0/2" - Execute result == null
* @throws Exception * @throws Exception
*/ */
@Test @Test
public void testObserveCompositeAnyResources_Result_CONTENT_Value_LwM2mSingleResource_If_Error_Null() throws Exception { public void testObserveReadAll_AfterCompositeObservation_WithResourceNotReadable_Result_CONTENT_ObserveResourceNotReadableIsNull() throws Exception {
// sendCancelObserveAllWithAwait(deviceId); sendObserveCancelAllWithAwait(deviceId);
String expectedIdVer5_0_7 = objectInstanceIdVer_5 + "/" + RESOURCE_ID_7; String expectedIdVer5_0_7 = objectInstanceIdVer_5 + "/" + RESOURCE_ID_7;
String expectedIdVer5_0_2 = objectInstanceIdVer_5 + "/" + RESOURCE_ID_2; String expectedIdVer5_0_2 = objectInstanceIdVer_5 + "/" + RESOURCE_ID_2;
String expectedIds = "[\"" + expectedIdVer5_0_7 + "\", \"" + expectedIdVer5_0_2 + "\"]"; String expectedIds = "[\"" + expectedIdVer5_0_7 + "\", \"" + expectedIdVer5_0_2 + "\"]";
String actualResult = sendCompositeRPCByIds("ObserveComposite", expectedIds); String actualResult = sendCompositeRPCByIds("ObserveComposite", expectedIds);
ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class);
assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText()); assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText());
String actualValues = rpcActualResult.get("value").asText(); String actualValues = rpcActualResult.get("value").asText();
@ -113,34 +127,102 @@ public class RpcLwm2MIntegrationObserveCompositeTest extends AbstractRpcLwM2MInt
*/ */
@Test @Test
public void testObserveComposite_Result_BAD_REQUEST_ONE_PATH_CONTAINCE_OTHER() throws Exception { public void testObserveComposite_Result_BAD_REQUEST_ONE_PATH_CONTAINCE_OTHER() throws Exception {
sendCancelObserveAllWithAwait(deviceId); sendObserveCancelAllWithAwait(deviceId);
String expectedIdVer5_0 = objectInstanceIdVer_5; String expectedIdVer5_0 = objectInstanceIdVer_5;
String expectedIdVer5_0_2 = objectInstanceIdVer_5 + "/" + RESOURCE_ID_2; String expectedIdVer5_0_2 = objectInstanceIdVer_5 + "/" + RESOURCE_ID_2;
String expectedIds = "[\"" + expectedIdVer5_0 + "\", \"" + expectedIdVer5_0_2 + "\"]"; String expectedIds = "[\"" + expectedIdVer5_0 + "\", \"" + expectedIdVer5_0_2 + "\"]";
String actualResult = sendCompositeRPCByIds("ObserveComposite", expectedIds); String actualResult = sendCompositeRPCByIds("ObserveComposite", expectedIds);
ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class);
assertEquals(ResponseCode.BAD_REQUEST.getName(), rpcActualResult.get("result").asText()); assertEquals(ResponseCode.BAD_REQUEST.getName(), rpcActualResult.get("result").asText());
String actual= rpcActualResult.get("error").asText(); String actual = rpcActualResult.get("error").asText();
String expected = "Invalid path list : /5/0 and /5/0/2 are overlapped paths"; String expected = "Invalid path list : /5/0 and /5/0/2 are overlapped paths";
assertTrue(expected.equals(actual)); assertTrue(expected.equals(actual));
} }
/** /**
* Previous -> "3/0/9" * Previous -> "3/0/9", "19/0/2", "19/1/0", "19/0/0", All only SingleObservation;
* ObserveComposite {"ids":["5/0/7", "5/0/5", "5/0/3", "3/0/9"]} - CONTENT * if at least one of the resource objectIds (Composite) in SingleObservation or CompositeObservation is already registered - return BAD REQUEST
* ObserveComposite {"ids":["5/0/7", "5/0/5", "5/0/3", "3/0/9"]}
* @throws Exception * @throws Exception
*/ */
@Test @Test
public void testObserveCompositeThereAreObservationOneResource_Result_CONTENT_Value_ObservationAddIfAbsent() throws Exception { public void testObserveComposite_IfLeastOneResourceIsAlreadyRegistered_return_BadRequest() throws Exception {
// Verify after start
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 expectedIdVer19_0_2 = objectIdVer_19 + "/" + OBJECT_INSTANCE_ID_0 + "/" + RESOURCE_ID_2;
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(expectedIdVer19_0_2)));
assertTrue(actualValues.contains("SingleObservation:" + fromVersionedIdToObjectId(expectedIdVer19_1_0)));
assertTrue(actualValues.contains("SingleObservation:" + fromVersionedIdToObjectId(idVer_19_0_0)));
// Send Observe composite with "/3/0/9"
String expectedIdVer5_0_7 = objectInstanceIdVer_5 + "/" + RESOURCE_ID_7; String expectedIdVer5_0_7 = objectInstanceIdVer_5 + "/" + RESOURCE_ID_7;
String expectedIdVer5_0_5= objectInstanceIdVer_5 + "/" + RESOURCE_ID_5; String expectedIdVer5_0_5 = objectInstanceIdVer_5 + "/" + RESOURCE_ID_5;
String expectedIdVer5_0_3 = objectInstanceIdVer_5 + "/" + RESOURCE_ID_3; 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 expectedIds = "[\"" + expectedIdVer5_0_7 + "\", \"" + expectedIdVer5_0_5 + "\", \"" + expectedIdVer5_0_3 + "\", \"" + idVer_3_0_9 + "\"]";
String actualResult = sendCompositeRPCByIds("ObserveComposite", expectedIds); String actualResult = sendCompositeRPCByIds("ObserveComposite", expectedIds);
ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class);
assertEquals(ResponseCode.BAD_REQUEST.getName(), rpcActualResult.get("result").asText());
assertTrue(rpcActualResult.get("error").asText().contains(fromVersionedIdToObjectId(idVer_3_0_9)));
assertTrue(rpcActualResult.get("error").asText().contains("is already registered"));
// verify after send Observe composite
actualResultReadAll = sendCompositeRPCByKeys("ObserveReadAll", null);
rpcActualResultReadAll = JacksonUtil.fromString(actualResultReadAll, ObjectNode.class);
assertEquals(ResponseCode.CONTENT.getName(), rpcActualResultReadAll.get("result").asText());
actualValues = rpcActualResultReadAll.get("value").asText();
assertTrue(actualValues.contains("SingleObservation:" + fromVersionedIdToObjectId(idVer_3_0_9)));
assertTrue(actualValues.contains("SingleObservation:" + fromVersionedIdToObjectId(expectedIdVer19_0_2)));
assertTrue(actualValues.contains("SingleObservation:" + fromVersionedIdToObjectId(expectedIdVer19_1_0)));
assertTrue(actualValues.contains("SingleObservation:" + fromVersionedIdToObjectId(idVer_19_0_0)));
}
/**
* Previous -> ["5/0/7", "5/0/5", "5/0/3"], CompositeObservation *
* if the resource SingleObservation is already registered in CompositeObservation - return BAD REQUEST
* SingleObservation {"id":["5/0/7"}
* @throws Exception
*/
@Test
public void testObserveSingle_IfResourceIsAlreadyRegisteredInComposite_return_BadRequest() throws Exception {
// Send Observe Composite
String expectedIdVer5_0_7 = objectInstanceIdVer_5 + "/" + RESOURCE_ID_7;
String expectedId5_0_7 = fromVersionedIdToObjectId(expectedIdVer5_0_7);
String expectedIdVer5_0_5 = objectInstanceIdVer_5 + "/" + RESOURCE_ID_5;
String expectedId5_0_5 = fromVersionedIdToObjectId(expectedIdVer5_0_5);
String expectedIdVer5_0_3 = objectInstanceIdVer_5 + "/" + RESOURCE_ID_3;
String expectedId5_0_3 = fromVersionedIdToObjectId(expectedIdVer5_0_3);
String expectedIds = "[\"" + expectedIdVer5_0_7 + "\", \"" + expectedIdVer5_0_5 + "\", \"" + expectedIdVer5_0_3 + "\"]";
String actualResult = sendCompositeRPCByIds("ObserveComposite", expectedIds);
ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class);
assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText()); assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText());
String expectedResult = "/3/0/9=LwM2mSingleResource [id=9"; String actualValues = rpcActualResult.get("value").asText();
assertTrue(rpcActualResult.get("value").asText().contains(expectedResult)); assertTrue(actualValues.contains(expectedId5_0_7));
assertTrue(actualValues.contains(expectedId5_0_5));
assertTrue(actualValues.contains(expectedId5_0_3));
// Send Observe Single
actualResult = sendObserve("Observe", expectedIdVer5_0_7);
rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class);
assertEquals(ResponseCode.BAD_REQUEST.getName(), rpcActualResult.get("result").asText());
assertTrue(rpcActualResult.get("error").asText().contains(expectedId5_0_7));
assertTrue(rpcActualResult.get("error").asText().contains("is already registered"));
// verify after send Observe Single
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();
String expectedIdVer19_0_2 = objectIdVer_19 + "/" + OBJECT_INSTANCE_ID_0 + "/" + RESOURCE_ID_2;
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(expectedIdVer19_0_2)));
assertTrue(actualValues.contains("SingleObservation:" + fromVersionedIdToObjectId(expectedIdVer19_1_0)));
assertTrue(actualValues.contains("SingleObservation:" + fromVersionedIdToObjectId(idVer_19_0_0)));
assertTrue(actualValues.contains("CompositeObservation:"));
assertTrue(actualValues.contains(expectedId5_0_7));
assertTrue(actualValues.contains(expectedId5_0_5));
assertTrue(actualValues.contains(expectedId5_0_3));
} }
/** /**
@ -149,14 +231,14 @@ public class RpcLwm2MIntegrationObserveCompositeTest extends AbstractRpcLwM2MInt
*/ */
@Test @Test
public void testObserveCompositeAnyResources_Result_CONTENT_Value_LwM2mSingleResource_LwM2mMultipleResource() throws Exception { public void testObserveCompositeAnyResources_Result_CONTENT_Value_LwM2mSingleResource_LwM2mMultipleResource() throws Exception {
sendCancelObserveAllWithAwait(deviceId); sendObserveCancelAllWithAwait(deviceId);
String expectedIdVer5_0_7 = objectInstanceIdVer_5 + "/" + RESOURCE_ID_7; String expectedIdVer5_0_7 = objectInstanceIdVer_5 + "/" + RESOURCE_ID_7;
String expectedIdVer5_0_5= objectInstanceIdVer_5 + "/" + RESOURCE_ID_5; String expectedIdVer5_0_5 = objectInstanceIdVer_5 + "/" + RESOURCE_ID_5;
String expectedIdVer5_0_3 = objectInstanceIdVer_5 + "/" + RESOURCE_ID_3; String expectedIdVer5_0_3 = objectInstanceIdVer_5 + "/" + RESOURCE_ID_3;
String expectedIdVer19_1_0 = objectIdVer_19 + "/" + OBJECT_INSTANCE_ID_1 + "/" + RESOURCE_ID_0; 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 expectedIds = "[\"" + expectedIdVer5_0_7 + "\", \"" + expectedIdVer5_0_5 + "\", \"" + expectedIdVer5_0_3 + "\", \"" + idVer_3_0_9 + "\", \"" + expectedIdVer19_1_0 + "\"]";
String actualResult = sendCompositeRPCByIds("ObserveComposite", expectedIds); String actualResult = sendCompositeRPCByIds("ObserveComposite", expectedIds);
ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class);
assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText()); assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText());
String actualValues = rpcActualResult.get("value").asText(); String actualValues = rpcActualResult.get("value").asText();
@ -173,7 +255,7 @@ public class RpcLwm2MIntegrationObserveCompositeTest extends AbstractRpcLwM2MInt
*/ */
@Test @Test
public void testObserveCompositeWithKeyName_Result_CONTENT_Value_SingleResources() throws Exception { public void testObserveCompositeWithKeyName_Result_CONTENT_Value_SingleResources() throws Exception {
sendCancelObserveAllWithAwait(deviceId); sendObserveCancelAllWithAwait(deviceId);
String expectedKey3_0_9 = RESOURCE_ID_NAME_3_9; String expectedKey3_0_9 = RESOURCE_ID_NAME_3_9;
String expectedKey3_0_14 = RESOURCE_ID_NAME_3_14; String expectedKey3_0_14 = RESOURCE_ID_NAME_3_14;
@ -198,18 +280,16 @@ public class RpcLwm2MIntegrationObserveCompositeTest extends AbstractRpcLwM2MInt
* @throws Exception * @throws Exception
*/ */
@Test @Test
public void testObserveCompositeWithKeyNameThereAreObservationOneResource_Result_CONTENT_Value_ObservationAddIfAbsent() throws Exception { public void testObserveCompositeWithKeyName_IfLeastOneResourceIsAlreadyRegistered_return_BadRequest() throws Exception {
String expectedKey3_0_9 = RESOURCE_ID_NAME_3_9; String expectedKey3_0_9 = RESOURCE_ID_NAME_3_9;
String expectedKey3_0_14 = RESOURCE_ID_NAME_3_14; String expectedKey3_0_14 = RESOURCE_ID_NAME_3_14;
String expectedKey19_0_0 = RESOURCE_ID_NAME_19_0_0; String expectedKey19_0_0 = RESOURCE_ID_NAME_19_0_0;
String expectedKey19_1_0 = RESOURCE_ID_NAME_19_1_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 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); String actualResult = sendCompositeRPCByKeys("ObserveComposite", expectedKeys);
ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class);
assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText()); assertEquals(ResponseCode.BAD_REQUEST.getName(), rpcActualResult.get("result").asText());
String actual = rpcActualResult.get("value").asText(); assertTrue(rpcActualResult.get("error").asText().contains("is already registered"));
assertTrue(actual.contains(fromVersionedIdToObjectId(expectedIdVer19_1_0) + "=LwM2mMultipleResource"));
} }
/** /**
@ -218,8 +298,8 @@ public class RpcLwm2MIntegrationObserveCompositeTest extends AbstractRpcLwM2MInt
* @throws Exception * @throws Exception
*/ */
@Test @Test
public void testObserveReadAll_AfterCompositeObservation_Result_CONTENT_Value_SingleObservation_Only() throws Exception { public void testObserveReadAll_AfterbserveCancelAllAndCompositeObservation_Result_CONTENT_Value_CompositeObservation_Only() throws Exception {
sendCancelObserveAllWithAwait(deviceId); sendObserveCancelAllWithAwait(deviceId);
String expectedKey3_0_9 = RESOURCE_ID_NAME_3_9; String expectedKey3_0_9 = RESOURCE_ID_NAME_3_9;
String expectedKey3_0_14 = RESOURCE_ID_NAME_3_14; String expectedKey3_0_14 = RESOURCE_ID_NAME_3_14;
@ -232,76 +312,15 @@ public class RpcLwm2MIntegrationObserveCompositeTest extends AbstractRpcLwM2MInt
String actualResultReadAll = sendCompositeRPCByKeys("ObserveReadAll", null); String actualResultReadAll = sendCompositeRPCByKeys("ObserveReadAll", null);
ObjectNode rpcActualResultReadAll = JacksonUtil.fromString(actualResultReadAll, ObjectNode.class); ObjectNode rpcActualResultReadAll = JacksonUtil.fromString(actualResultReadAll, ObjectNode.class);
assertEquals(ResponseCode.CONTENT.getName(), rpcActualResultReadAll.get("result").asText()); assertEquals(ResponseCode.CONTENT.getName(), rpcActualResultReadAll.get("result").asText());
String actualValues = rpcActualResultReadAll.get("value").asText(); String actualValues = rpcActualResultReadAll.get("value").asText();
String expectedIdVer3_0_14 = objectInstanceIdVer_3 + "/" + RESOURCE_ID_14; String expectedIdVer3_0_14 = objectInstanceIdVer_3 + "/" + RESOURCE_ID_14;
String expectedIdVer19_1_0 = objectIdVer_19 + "/" + OBJECT_INSTANCE_ID_1 + "/" + RESOURCE_ID_0; 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("CompositeObservation:"));
assertTrue(actualValues.contains("SingleObservation:" + fromVersionedIdToObjectId(expectedIdVer3_0_14))); assertFalse(actualValues.contains("SingleObservation"));
assertTrue(actualValues.contains("SingleObservation:" + fromVersionedIdToObjectId(expectedIdVer19_1_0))); assertTrue(actualValues.contains(Objects.requireNonNull(fromVersionedIdToObjectId(idVer_3_0_9))));
assertTrue(actualValues.contains("SingleObservation:" + fromVersionedIdToObjectId(idVer_19_0_0))); assertTrue(actualValues.contains(Objects.requireNonNull(fromVersionedIdToObjectId(expectedIdVer3_0_14))));
} assertTrue(actualValues.contains(Objects.requireNonNull(fromVersionedIdToObjectId(expectedIdVer19_1_0))));
assertTrue(actualValues.contains(Objects.requireNonNull(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)));
} }
/** /**
@ -310,166 +329,179 @@ public class RpcLwm2MIntegrationObserveCompositeTest extends AbstractRpcLwM2MInt
* @throws Exception * @throws Exception
*/ */
@Test @Test
public void testObserveCompositeAnyResources_Result_CONTENT_CancelObserveComposite_This_Result_Content_Count_5() throws Exception { public void testObserveCancelAllThenObserveCompositeAnyResources_Result_CONTENT_CancelObserveComposite_This_Result_Content_Count_1() throws Exception {
sendCancelObserveAllWithAwait(deviceId); sendObserveCancelAllWithAwait(deviceId);
// ObserveComposite // ObserveComposite
String expectedIdVer5_0_7 = objectInstanceIdVer_5 + "/" + RESOURCE_ID_7; String expectedIdVer5_0_7 = objectInstanceIdVer_5 + "/" + RESOURCE_ID_7;
String expectedIdVer5_0_5= objectInstanceIdVer_5 + "/" + RESOURCE_ID_5; String expectedIdVer5_0_5 = objectInstanceIdVer_5 + "/" + RESOURCE_ID_5;
String expectedIdVer5_0_3 = objectInstanceIdVer_5 + "/" + RESOURCE_ID_3; 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 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 expectedIds = "[\"" + expectedIdVer5_0_7 + "\", \"" + expectedIdVer5_0_5 + "\", \"" + expectedIdVer5_0_3 + "\", \"" + idVer_3_0_9 + "\", \"" + expectedIdVer19_1_0_0 + "\"]";
String actualResult = sendCompositeRPCByIds("ObserveComposite", expectedIds); 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); ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class);
assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText()); assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText());
// ObserveCompositeCancel // ObserveCompositeCancel
actualResult = sendCompositeRPCByIds("ObserveCompositeCancel", expectedIds); actualResult = sendCompositeRPCByIds("ObserveCompositeCancel", expectedIds);
rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class);
assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText()); assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText());
assertEquals("3", rpcActualResult.get("value").asText()); assertEquals("1", rpcActualResult.get("value").asText());
assertEquals(0, (Object) getCntObserveAll(deviceId)); assertEquals(0, (Object) getCntObserveAll(deviceId));
} }
/** /**
* ObserveComposite {"ids":["/3/0/9", "/5/0/5", "/5/0/3", "/5/0/7", "/19/1/0/0"]} - Ok * ObserveComposite {"ids":["/3/0/9", "/5/0/5", "/5/0/3", "/5/0/7", "/19/1/0/0"]} - Ok
* ObserveCompositeCancel {"ids":["/5", "/19/1/0/0"]} - Ok * ObserveCompositeCancel {"ids":["/5", "/19/1/0/0"]} - BadRequest
* last Observation
* @throws Exception * @throws Exception
*/ */
@Test @Test
public void testObserveCompositeAnyResources_Result_CONTENT_CancelObserveComposite_OneObjectAnyResource_Result_Content_Count_4() throws Exception { public void testObserveCompositeFiveResources_Result_CONTENT_CancelObserveComposite_TwoAnyResource_Result_BadRequest() throws Exception {
sendCancelObserveAllWithAwait(deviceId); sendObserveCancelAllWithAwait(deviceId);
// ObserveComposite // ObserveComposite five
String expectedIdVer5_0_7 = objectInstanceIdVer_5 + "/" + RESOURCE_ID_7; String expectedIdVer5_0_7 = objectInstanceIdVer_5 + "/" + RESOURCE_ID_7;
String expectedIdVer5_0_5 = objectInstanceIdVer_5 + "/" + RESOURCE_ID_5; String expectedIdVer5_0_5 = objectInstanceIdVer_5 + "/" + RESOURCE_ID_5;
String expectedIdVer5_0_3 = objectInstanceIdVer_5 + "/" + RESOURCE_ID_3; String expectedIdVer5_0_3 = objectInstanceIdVer_5 + "/" + RESOURCE_ID_3;
String expectedIdVer3_0_9 = objectInstanceIdVer_3 + "/" + RESOURCE_ID_9; 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 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 expectedIds = "[\"" + expectedIdVer5_0_7 + "\", \"" + expectedIdVer5_0_5 + "\", \"" + expectedIdVer5_0_3 + "\", \"" + expectedIdVer3_0_9 + "\", \"" + expectedIdVer19_1_0_0 + "\"]";
String actualResult = sendCompositeRPCByIds("ObserveComposite", expectedIds); String actualResult = sendCompositeRPCByIds("ObserveComposite", expectedIds);
ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class);
assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText()); assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText());
awaitObserveReadAll(5, deviceId); awaitObserveReadAll(1, deviceId);
// ObserveCompositeCancel // ObserveCompositeCancel two
expectedIds = "[\"" + objectInstanceIdVer_5 + "\", \"" + expectedIdVer19_1_0_0 + "\"]"; expectedIds = "[\"" + objectInstanceIdVer_5 + "\", \"" + expectedIdVer19_1_0_0 + "\"]";
actualResult = sendCompositeRPCByIds("ObserveCompositeCancel", expectedIds); actualResult = sendCompositeRPCByIds("ObserveCompositeCancel", expectedIds);
rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class);
assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText()); assertEquals(ResponseCode.BAD_REQUEST.getName(), rpcActualResult.get("result").asText());
assertEquals("4", rpcActualResult.get("value").asText()); // CNT = 4 ("/5/0/5", "/5/0/3", "/5/0/7", "/19/1/0/0"9) assertTrue(rpcActualResult.get("error").asText().contains(objectInstanceIdVer_5)); // CNT = 4 ("/5/0/5", "/5/0/3", "/5/0/7", "/19/1/0/0"9)
assertTrue(rpcActualResult.get("error").asText().contains(expectedIdVer19_1_0_0)); // CNT = 4 ("/5/0/5", "/5/0/3", "/5/0/7", "/19/1/0/0"9)
String actualResultReadAll = sendCompositeRPCByKeys("ObserveReadAll", null);
ObjectNode rpcActualResultReadAll = JacksonUtil.fromString(actualResultReadAll, ObjectNode.class);
assertEquals(ResponseCode.CONTENT.getName(), rpcActualResultReadAll.get("result").asText());
String actualValues = rpcActualResultReadAll.get("value").asText();
assertEquals("[\"SingleObservation:" + fromVersionedIdToObjectId(expectedIdVer3_0_9) + "\"]", actualValues);
} }
/** /**
* ObserveComposite {"ids":["/3", "/5/0/3", "/19/1/0/0"]} - Ok * ObserveComposite {"ids":["/3", "/5/0/3", "/19/1/0/0"]} - Ok
* ObserveCompositeCancel {"ids":["/3/0/9", "/5/0/3", "/19/1/0/0"} -> BAD_REQUEST * ObserveCompositeCancel {"ids":["/19/1/0/0", "/3/0/9"} -> BAD_REQUEST
* ObserveCompositeCancel {"ids":["/3"} -> CONTENT
*/ */
@Test @Test
public void testObserveOneObjectAnyResources_Result_CONTENT_Cancel_OneResourceFromObjectAnyResource_Result_BAD_REQUEST_Cancel_OneObject_Result_CONTENT() throws Exception { public void testObserveOneObjectAnyResources_Result_CONTENT_Cancel_OneResourceFromObjectAnyResource_Result_BAD_REQUEST_Cancel_OneObject_Result_CONTENT() throws Exception {
sendCancelObserveAllWithAwait(deviceId); sendObserveCancelAllWithAwait(deviceId);
// ObserveComposite // ObserveComposite
sendCancelObserveAllWithAwait(deviceId);
String expectedIdVer5_0_3 = objectInstanceIdVer_5 + "/" + RESOURCE_ID_3; 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 expectedIdVer19_1_0_0 = objectIdVer_19 + "/" + OBJECT_INSTANCE_ID_1 + "/" + RESOURCE_ID_0 + "/" + RESOURCE_INSTANCE_ID_0;
String expectedIds = "[\"" + objectIdVer_3 + "\", \"" + expectedIdVer5_0_3 + "\", \"" + expectedIdVer19_1_0_0 + "\"]"; String expectedIds = "[\"" + objectIdVer_3 + "\", \"" + expectedIdVer5_0_3 + "\", \"" + expectedIdVer19_1_0_0 + "\"]";
String actualResult = sendCompositeRPCByIds("ObserveComposite", expectedIds); String actualResult = sendCompositeRPCByIds("ObserveComposite", expectedIds);
ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class);
assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText()); assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText());
// ObserveCompositeCancel // ObserveCompositeCancel
expectedIds = "[\"" + expectedIdVer19_1_0_0 + "\", \"" + idVer_3_0_9 + "\"]"; String sendIds = "[\"" + expectedIdVer19_1_0_0 + "\", \"" + idVer_3_0_9 + "\"]";
actualResult = sendCompositeRPCByIds("ObserveCompositeCancel", expectedIds); actualResult = sendCompositeRPCByIds("ObserveCompositeCancel", sendIds);
rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class);
assertEquals(ResponseCode.BAD_REQUEST.getName(), rpcActualResult.get("result").asText()); assertEquals(ResponseCode.BAD_REQUEST.getName(), rpcActualResult.get("result").asText());
String expectedValue = "for observation path [" + fromVersionedIdToObjectId(objectIdVer_3) + "], that includes this observation path [" + fromVersionedIdToObjectId(idVer_3_0_9); String expectedValue = "Could not find active Observe Composite component with paths: [/19_1.1/1/0/0, /3_1.2/0/9]";
assertTrue(rpcActualResult.get("error").asText().contains(expectedValue)); assertTrue(rpcActualResult.get("error").asText().contains(expectedValue));
// ObserveCompositeCancel // "ObserveReadAll"
expectedIds = "[\"" + objectIdVer_3 + "\", \"" + expectedIdVer19_1_0_0 + "\"]";
actualResult = sendCompositeRPCByIds("ObserveCompositeCancel", expectedIds);
rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class);
assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText());
assertEquals("2", rpcActualResult.get("value").asText());
String actualResultReadAll = sendCompositeRPCByKeys("ObserveReadAll", null); String actualResultReadAll = sendCompositeRPCByKeys("ObserveReadAll", null);
ObjectNode rpcActualResultReadAll = JacksonUtil.fromString(actualResultReadAll, ObjectNode.class); ObjectNode rpcActualResultReadAll = JacksonUtil.fromString(actualResultReadAll, ObjectNode.class);
assertEquals(ResponseCode.CONTENT.getName(), rpcActualResultReadAll.get("result").asText()); assertEquals(ResponseCode.CONTENT.getName(), rpcActualResultReadAll.get("result").asText());
String actualValues = rpcActualResultReadAll.get("value").asText(); String actualValues = rpcActualResultReadAll.get("value").asText();
assertEquals("[\"SingleObservation:" + fromVersionedIdToObjectId(expectedIdVer5_0_3) + "\"]", actualValues); assertTrue(actualValues.contains("CompositeObservation:"));
} }
/**
* ObserveComposite {"ids":["/3/0/9", "/3/0/14", "/5/0/3", "/3/0/15", "/19/1/0/0"]} - Ok
* ObserveCancel {"id":"/3/0/9"} -> INTERNAL_SERVER_ERROR
* ObserveCompositeCancel {"ids":["/3/0/9", "/19/1/0/0", "/3]} - Ok
* last Observation
* @throws Exception
*/
@Test @Test
public void testObserveCompositeAnyResources_Result_CONTENT_CancelObserveComposite_OneResource_OneObjectAnyResource_Result_Content_Count_4() throws Exception { public void testObserveCompositeResource_Update_After_Registration_UpdateRegistration() throws Exception {
sendCancelObserveAllWithAwait(deviceId); String id_3_0_9 = fromVersionedIdToObjectId(idVer_3_0_9);
// ObserveComposite String id_19_0_0 = fromVersionedIdToObjectId(idVer_19_0_0);
sendCancelObserveAllWithAwait(deviceId); String idVer_19_1_0 = objectIdVer_19 + "/" + OBJECT_INSTANCE_ID_1 + "/" + RESOURCE_ID_0;
String expectedIdVer3_0_14 = objectIdVer_3 + "/" + OBJECT_INSTANCE_ID_0 + "/" + RESOURCE_ID_14; String id_19_1_0 = fromVersionedIdToObjectId(idVer_19_1_0);
String expectedIdVer3_0_15= objectIdVer_3 + "/" + OBJECT_INSTANCE_ID_0 + "/" + RESOURCE_ID_15; String idVer_19_0_2 = objectIdVer_19 + "/" + OBJECT_INSTANCE_ID_0 + "/" + RESOURCE_ID_2;
String expectedIdVer5_0_3 = objectInstanceIdVer_5 + "/" + RESOURCE_ID_3; String id_19_0_2 = fromVersionedIdToObjectId(idVer_19_0_2);
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 + "\"]"; // 1 - "ObserveReadAll": at least one update value of all resources we observe - after connection
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); String actualResultReadAll = sendCompositeRPCByKeys("ObserveReadAll", null);
ObjectNode rpcActualResultReadAll = JacksonUtil.fromString(actualResultReadAll, ObjectNode.class); ObjectNode rpcActualResultReadAll = JacksonUtil.fromString(actualResultReadAll, ObjectNode.class);
assertEquals(ResponseCode.CONTENT.getName(), rpcActualResultReadAll.get("result").asText()); assertEquals(ResponseCode.CONTENT.getName(), rpcActualResultReadAll.get("result").asText());
String actualValues = rpcActualResultReadAll.get("value").asText(); String rpcActualVValuesReadAll = rpcActualResultReadAll.get("value").asText();
assertEquals("[\"SingleObservation:" + fromVersionedIdToObjectId(expectedIdVer5_0_3) + "\"]", actualValues); ArrayNode rpcactualValues = JacksonUtil.fromString(rpcActualVValuesReadAll, ArrayNode.class);
assertEquals(rpcactualValues.size(), 4);
assertTrue(actualResultReadAll.contains("SingleObservation:" + id_3_0_9));
assertTrue(actualResultReadAll.contains("SingleObservation:" + id_19_1_0));
assertTrue(actualResultReadAll.contains("SingleObservation:" + id_19_0_2));
assertTrue(actualResultReadAll.contains("SingleObservation:" + id_19_0_0));
long initAttrTelemetryAtCount = countUpdateAttrTelemetryAll();
long initAttrTelemetryAtCount_3_0_9 = countUpdateAttrTelemetryResource(idVer_3_0_9);
long initAttrTelemetryAtCount_19_0_0 = countUpdateAttrTelemetryResource(idVer_19_0_0);
long initAttrTelemetryAtCount_19_1_0 = countUpdateAttrTelemetryResource(idVer_19_1_0);
long initAttrTelemetryAtCount_19_0_2 = countUpdateAttrTelemetryResource(idVer_19_0_2);
updateRegAtLeastOnceAfterAction();
updateAttrTelemetryAllAtLeastOnceAfterAction(initAttrTelemetryAtCount);
updateAttrTelemetryResourceAtLeastOnceAfterAction(initAttrTelemetryAtCount_3_0_9, idVer_3_0_9);
updateAttrTelemetryResourceAtLeastOnceAfterAction(initAttrTelemetryAtCount_19_0_0, idVer_19_0_0);
updateAttrTelemetryResourceAtLeastOnceAfterAction(initAttrTelemetryAtCount_19_1_0, idVer_19_1_0);
updateAttrTelemetryResourceAtLeastOnceAfterAction(initAttrTelemetryAtCount_19_0_2, idVer_19_0_2);
// 2 - "ObserveReadAll": No update of all resources we are observing - after "ObserveReadCancelAll"
sendObserveCancelAllWithAwait(deviceId);
updateRegAtLeastOnceAfterAction();
actualResultReadAll = sendCompositeRPCByKeys("ObserveReadAll", null);
rpcActualResultReadAll = JacksonUtil.fromString(actualResultReadAll, ObjectNode.class);
assertEquals(ResponseCode.CONTENT.getName(), rpcActualResultReadAll.get("result").asText());
rpcActualVValuesReadAll = rpcActualResultReadAll.get("value").asText();
rpcactualValues = JacksonUtil.fromString(rpcActualVValuesReadAll, ArrayNode.class);
assertEquals(rpcactualValues.size(), 0);
// 2.1 - ObserveComposite: observeCancelAll verify"
initAttrTelemetryAtCount = countUpdateAttrTelemetryAll();
initAttrTelemetryAtCount_3_0_9 = countUpdateAttrTelemetryResource(idVer_3_0_9);
initAttrTelemetryAtCount_19_0_0 = countUpdateAttrTelemetryResource(idVer_19_0_0);
initAttrTelemetryAtCount_19_1_0 = countUpdateAttrTelemetryResource(idVer_19_1_0);
initAttrTelemetryAtCount_19_0_2 = countUpdateAttrTelemetryResource(idVer_19_0_2);
updateRegAtLeastOnceAfterAction();
assertEquals(countUpdateAttrTelemetryAll(), initAttrTelemetryAtCount);
assertEquals(countUpdateAttrTelemetryResource(idVer_3_0_9), initAttrTelemetryAtCount_3_0_9);
assertEquals(countUpdateAttrTelemetryResource(idVer_19_0_0), initAttrTelemetryAtCount_19_0_0);
assertEquals(countUpdateAttrTelemetryResource(idVer_19_1_0), initAttrTelemetryAtCount_19_1_0);
assertEquals(countUpdateAttrTelemetryResource(idVer_19_0_2), initAttrTelemetryAtCount_19_0_2);
// 3 - ObserveComposite: at least one update value of all resources we observe - after ObserveComposite"
String expectedKeys = "[\"" + RESOURCE_ID_NAME_3_9 + "\", \"" + RESOURCE_ID_NAME_19_0_0 + "\", \"" + RESOURCE_ID_NAME_19_0_2 + "\", \"" + RESOURCE_ID_NAME_19_1_0 + "\"]";
String actualResult = sendCompositeRPCByKeys("ObserveComposite", expectedKeys);
assertTrue(actualResult.contains(id_3_0_9 + "=LwM2mSingleResource"));
assertTrue(actualResult.contains(id_19_0_0 + "=LwM2mMultipleResource"));
assertTrue(actualResult.contains(id_19_1_0 + "=LwM2mMultipleResource"));
assertTrue(actualResult.contains(id_19_0_2 + "=LwM2mSingleResource"));
// 3.1 - ObserveComposite: - verify");
actualResultReadAll = sendCompositeRPCByKeys("ObserveReadAll", null);
rpcActualResultReadAll = JacksonUtil.fromString(actualResultReadAll, ObjectNode.class);
assertEquals(ResponseCode.CONTENT.getName(), rpcActualResultReadAll.get("result").asText());
rpcActualVValuesReadAll = rpcActualResultReadAll.get("value").asText();
rpcactualValues = JacksonUtil.fromString(rpcActualVValuesReadAll, ArrayNode.class);
assertEquals(rpcactualValues.size(), 1);
assertFalse(actualResultReadAll.contains("SingleObservation"));
assertTrue(actualResultReadAll.contains("CompositeObservation:"));
assertTrue(actualResultReadAll.contains(id_19_0_2));
assertTrue(actualResultReadAll.contains(id_19_1_0));
assertTrue(actualResultReadAll.contains(id_19_0_0));
assertTrue(actualResultReadAll.contains(id_3_0_9));
initAttrTelemetryAtCount = countUpdateAttrTelemetryAll();
initAttrTelemetryAtCount_3_0_9 = countUpdateAttrTelemetryResource(idVer_3_0_9);
initAttrTelemetryAtCount_19_0_0 = countUpdateAttrTelemetryResource(idVer_19_0_0);
initAttrTelemetryAtCount_19_1_0 = countUpdateAttrTelemetryResource(idVer_19_1_0);
initAttrTelemetryAtCount_19_0_2 = countUpdateAttrTelemetryResource(idVer_19_0_2);
updateRegAtLeastOnceAfterAction();
updateAttrTelemetryAllAtLeastOnceAfterAction(initAttrTelemetryAtCount);
updateAttrTelemetryResourceAtLeastOnceAfterAction(initAttrTelemetryAtCount_3_0_9, idVer_3_0_9);
updateAttrTelemetryResourceAtLeastOnceAfterAction(initAttrTelemetryAtCount_19_0_0, idVer_19_0_0);
updateAttrTelemetryResourceAtLeastOnceAfterAction(initAttrTelemetryAtCount_19_1_0, idVer_19_1_0);
updateAttrTelemetryResourceAtLeastOnceAfterAction(initAttrTelemetryAtCount_19_0_2, idVer_19_0_2);
} }
private String sendObserve(String method, String params) throws Exception { private String sendObserve(String method, String params) throws Exception {
String sendRpcRequest; String sendRpcRequest;
if (params == null) { if (params == null) {
sendRpcRequest = "{\"method\": \"" + method + "\"}"; sendRpcRequest = "{\"method\": \"" + method + "\"}";
} } else {
else {
sendRpcRequest = "{\"method\": \"" + method + "\", \"params\": {\"id\": \"" + params + "\"}}"; sendRpcRequest = "{\"method\": \"" + method + "\", \"params\": {\"id\": \"" + params + "\"}}";
} }
return doPostAsync("/api/plugins/rpc/twoway/" + deviceId, sendRpcRequest, String.class, status().isOk()); return doPostAsync("/api/plugins/rpc/twoway/" + deviceId, sendRpcRequest, String.class, status().isOk());
@ -481,7 +513,71 @@ public class RpcLwm2MIntegrationObserveCompositeTest extends AbstractRpcLwM2MInt
} }
private String sendCompositeRPCByKeys(String method, String keys) throws Exception { private String sendCompositeRPCByKeys(String method, String keys) throws Exception {
String setRpcRequest = "{\"method\": \"" + method + "\", \"params\": {\"keys\":" + keys + "}}"; String sendRpcRequest = "{\"method\": \"" + method + "\", \"params\": {\"keys\":" + keys + "}}";
return doPostAsync("/api/plugins/rpc/twoway/" + deviceId, setRpcRequest, String.class, status().isOk()); return doPostAsync("/api/plugins/rpc/twoway/" + deviceId, sendRpcRequest, String.class, status().isOk());
}
private long countUpdateAttrTelemetryAll() {
return Mockito.mockingDetails(defaultUplinkMsgHandlerTest)
.getInvocations().stream()
.filter(invocation -> invocation.getMethod().getName().equals("updateAttrTelemetry"))
.count();
}
private void updateAttrTelemetryAllAtLeastOnceAfterAction(long initialInvocationCount) {
AtomicLong newInvocationCount = new AtomicLong(initialInvocationCount);
log.warn("countUpdateAttrTelemetryAllAtLeastOnceAfterAction: initialInvocationCount [{}]", initialInvocationCount);
await("Update AttrTelemetryAll at-least-once after action")
.atMost(50, TimeUnit.SECONDS)
.until(() -> {
newInvocationCount.set(countUpdateAttrTelemetryAll());
return newInvocationCount.get() > initialInvocationCount;
});
log.warn("countUpdateAttrTelemetryAllAtLeastOnceAfterAction: newInvocationCount [{}]", newInvocationCount.get());
}
private long countUpdateAttrTelemetryResource(String idVerRez) {
return Mockito.mockingDetails(defaultUplinkMsgHandlerTest)
.getInvocations().stream()
.filter(invocation ->
invocation.getMethod().getName().equals("updateAttrTelemetry") &&
invocation.getArguments().length > 1 &&
idVerRez.equals(invocation.getArguments()[1])
)
.count();
}
private void updateAttrTelemetryResourceAtLeastOnceAfterAction(long initialInvocationCount, String idVerRez) {
AtomicLong newInvocationCount = new AtomicLong(initialInvocationCount);
log.warn("countUpdateAttrTelemetryResourceAtLeastOnceAfterAction: initialInvocationCount [{}]", initialInvocationCount);
await("Update AttrTelemetryResource at-least-once after action")
.atMost(50, TimeUnit.SECONDS)
.until(() -> {
newInvocationCount.set(countUpdateAttrTelemetryResource(idVerRez));
return newInvocationCount.get() > initialInvocationCount;
});
log.warn("countUpdateAttrTelemetryResourceAtLeastOnceAfterAction: newInvocationCount [{}]", newInvocationCount.get());
}
private long countUpdateReg() {
return Mockito.mockingDetails(defaultUplinkMsgHandlerTest)
.getInvocations().stream()
.filter(invocation -> invocation.getMethod().getName().equals("updatedReg"))
.count();
}
private void updateRegAtLeastOnceAfterAction() {
long initialInvocationCount = countUpdateReg();
AtomicLong newInvocationCount = new AtomicLong(initialInvocationCount);
log.warn("updateRegAtLeastOnceAfterAction: initialInvocationCount [{}]", initialInvocationCount);
await("Update Registration at-least-once after action")
.atMost(50, TimeUnit.SECONDS)
.until(() -> {
newInvocationCount.set(countUpdateReg());
return newInvocationCount.get() > initialInvocationCount;
});
log.warn("updateRegAtLeastOnceAfterAction: newInvocationCount [{}]", newInvocationCount.get());
} }
} }

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

@ -23,6 +23,8 @@ import org.eclipse.leshan.core.link.LinkParseException;
import org.eclipse.leshan.core.node.LwM2mPath; import org.eclipse.leshan.core.node.LwM2mPath;
import org.junit.Test; import org.junit.Test;
import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.transport.lwm2m.config.TbLwM2mVersion;
import org.thingsboard.server.transport.lwm2m.rpc.AbstractRpcLwM2MIntegrationTest; import org.thingsboard.server.transport.lwm2m.rpc.AbstractRpcLwM2MIntegrationTest;
import java.util.Arrays; import java.util.Arrays;
@ -33,9 +35,10 @@ import java.util.stream.Collectors;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue; import static org.junit.Assert.assertTrue;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import static org.thingsboard.server.common.data.lwm2m.LwM2mConstants.LWM2M_SEPARATOR_KEY;
import static org.thingsboard.server.common.data.lwm2m.LwM2mConstants.LWM2M_SEPARATOR_PATH;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.OBJECT_INSTANCE_ID_0; 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.Lwm2mTestHelper.RESOURCE_ID_2;
import static org.thingsboard.server.transport.lwm2m.utils.LwM2MTransportUtil.convertObjectIdToVerId;
public class RpcLwm2mIntegrationDiscoverTest extends AbstractRpcLwM2MIntegrationTest { public class RpcLwm2mIntegrationDiscoverTest extends AbstractRpcLwM2MIntegrationTest {
@ -172,4 +175,19 @@ public class RpcLwm2mIntegrationDiscoverTest extends AbstractRpcLwM2MIntegration
String setRpcRequest = "{\"method\": \"Discover\", \"params\": {\"id\": \"" + path + "\"}}"; String setRpcRequest = "{\"method\": \"Discover\", \"params\": {\"id\": \"" + path + "\"}}";
return doPostAsync("/api/plugins/rpc/twoway/" + deviceId, setRpcRequest, String.class, status().isOk()); return doPostAsync("/api/plugins/rpc/twoway/" + deviceId, setRpcRequest, String.class, status().isOk());
} }
private String convertObjectIdToVerId(String path, String ver) {
ver = ver != null ? ver : TbLwM2mVersion.VERSION_1_0.getVersion().toString();
try {
String[] keyArray = path.split(LWM2M_SEPARATOR_PATH);
if (keyArray.length > 1) {
keyArray[1] = keyArray[1] + LWM2M_SEPARATOR_KEY + ver;
return StringUtils.join(keyArray, LWM2M_SEPARATOR_PATH);
} else {
return path;
}
} catch (Exception e) {
return null;
}
}
} }

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

@ -25,7 +25,6 @@ import org.eclipse.leshan.server.registration.Registration;
import org.junit.Test; import org.junit.Test;
import org.mockito.Mockito; import org.mockito.Mockito;
import org.springframework.boot.test.mock.mockito.SpyBean; import org.springframework.boot.test.mock.mockito.SpyBean;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.transport.lwm2m.rpc.AbstractRpcLwM2MIntegrationObserveTest; import org.thingsboard.server.transport.lwm2m.rpc.AbstractRpcLwM2MIntegrationObserveTest;
import org.thingsboard.server.transport.lwm2m.server.uplink.DefaultLwM2mUplinkMsgHandler; import org.thingsboard.server.transport.lwm2m.server.uplink.DefaultLwM2mUplinkMsgHandler;
@ -38,10 +37,10 @@ import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.timeout; import static org.mockito.Mockito.timeout;
import static org.mockito.Mockito.verify; 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.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_0;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_2; 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_3;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_9;
import static org.thingsboard.server.transport.lwm2m.utils.LwM2MTransportUtil.fromVersionedIdToObjectId; import static org.thingsboard.server.transport.lwm2m.utils.LwM2MTransportUtil.fromVersionedIdToObjectId;
@Slf4j @Slf4j
@ -51,16 +50,12 @@ public class RpcLwm2mIntegrationObserveTest extends AbstractRpcLwM2MIntegrationO
DefaultLwM2mUplinkMsgHandler defaultUplinkMsgHandlerTest; DefaultLwM2mUplinkMsgHandler defaultUplinkMsgHandlerTest;
@Test @Test
public void testObserveReadAll_Count_2_CancelAll_Count_0_Ok() throws Exception { public void testObserveReadAll_Count_4_CancelAll_Count_0_Ok() throws Exception {
String actualResultReadAll = sendRpcObserve("ObserveReadAll", null); String actualValuesReadAll = sendRpcObserveOkWithResultValue("ObserveReadAll", null);
ObjectNode rpcActualResult = JacksonUtil.fromString(actualResultReadAll, ObjectNode.class); assertEquals(4, actualValuesReadAll.split(",").length);
assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText()); sendObserveCancelAllWithAwait(deviceId);
String actualValuesReadAll = rpcActualResult.get("value").asText(); actualValuesReadAll = sendRpcObserveOkWithResultValue("ObserveReadAll", null);
assertEquals(2, actualValuesReadAll.split(",").length); assertEquals("[]", actualValuesReadAll);
String expected = "\"SingleObservation:/19/0/0\"";
assertTrue(actualValuesReadAll.contains(expected));
expected = "\"SingleObservation:/3/0/9\"";
assertTrue(actualValuesReadAll.contains(expected));
} }
/** /**
@ -69,16 +64,11 @@ public class RpcLwm2mIntegrationObserveTest extends AbstractRpcLwM2MIntegrationO
*/ */
@Test @Test
public void testObserveOneResource_Result_CONTENT_Value_Count_3_After_Cancel_Count_2() throws Exception { public void testObserveOneResource_Result_CONTENT_Value_Count_3_After_Cancel_Count_2() throws Exception {
sendCancelObserveAllWithAwait(deviceId); sendObserveCancelAllWithAwait(deviceId);
String idVer_3_0_9 = objectInstanceIdVer_3 + "/" + RESOURCE_ID_9; sendRpcObserveWithContainsLwM2mSingleResource(idVer_3_0_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(Optional.of(1).get(), Optional.ofNullable(getCntObserveAll(deviceId)).get());
int cntUpdate = 3; int cntUpdate = 3;
verify(defaultUplinkMsgHandlerTest, timeout(10000).times(cntUpdate)) verify(defaultUplinkMsgHandlerTest, timeout(10000).times(cntUpdate))
.onUpdateValueAfterReadResponse(Mockito.any(Registration.class), eq(idVer_3_0_9), Mockito.any(ReadResponse.class)); .onUpdateValueAfterReadResponse(Mockito.any(Registration.class), eq(idVer_3_0_9), Mockito.any(ReadResponse.class));
} }
@ -88,15 +78,12 @@ public class RpcLwm2mIntegrationObserveTest extends AbstractRpcLwM2MIntegrationO
*/ */
@Test @Test
public void testObserveOneObjectInstance_Result_CONTENT_Value_Count_3_After_Cancel_Count_2() throws Exception { public void testObserveOneObjectInstance_Result_CONTENT_Value_Count_3_After_Cancel_Count_2() throws Exception {
sendCancelObserveAllWithAwait(deviceId); sendObserveCancelAllWithAwait(deviceId);
String idVer_3_0 = objectInstanceIdVer_3; String idVer_3_0 = objectInstanceIdVer_3;
String actualResult = sendRpcObserve("Observe", idVer_3_0); sendRpcObserveWithContainsLwM2mSingleResource(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; int cntUpdate = 3;
verify(defaultUplinkMsgHandlerTest, timeout(10000).times(cntUpdate)) verify(defaultUplinkMsgHandlerTest, timeout(10000).times(cntUpdate))
.updateAttrTelemetry(Mockito.any(Registration.class), eq(idVer_3_0_9)); .updateAttrTelemetry(Mockito.any(Registration.class), eq(idVer_3_0_9));
} }
@ -106,19 +93,15 @@ public class RpcLwm2mIntegrationObserveTest extends AbstractRpcLwM2MIntegrationO
*/ */
@Test @Test
public void testObserveOneObject_Result_CONTENT_Value_Count_3_After_Cancel_Count_2() throws Exception { public void testObserveOneObject_Result_CONTENT_Value_Count_3_After_Cancel_Count_2() throws Exception {
sendCancelObserveAllWithAwait(deviceId); sendObserveCancelAllWithAwait(deviceId);
String idVer_3_0 = objectInstanceIdVer_3; String idVer_3_0 = objectInstanceIdVer_3;
String actualResult = sendRpcObserve("Observe", idVer_3_0); sendRpcObserveWithContainsLwM2mSingleResource(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; int cntUpdate = 3;
verify(defaultUplinkMsgHandlerTest, timeout(10000).times(cntUpdate)) verify(defaultUplinkMsgHandlerTest, timeout(10000).times(cntUpdate))
.updateAttrTelemetry(Mockito.any(Registration.class), eq(idVer_3_0_9)); .updateAttrTelemetry(Mockito.any(Registration.class), eq(idVer_3_0_9));
} }
/** /**
* Repeated request on Observe * Repeated request on Observe
* Observe {"id":"/3_1.2/0/0"} * Observe {"id":"/3_1.2/0/0"}
@ -126,15 +109,10 @@ public class RpcLwm2mIntegrationObserveTest extends AbstractRpcLwM2MIntegrationO
*/ */
@Test @Test
public void testObserveRepeated_Result_CONTENT_AddIfAbsent() throws Exception { public void testObserveRepeated_Result_CONTENT_AddIfAbsent() throws Exception {
String idVer_3_0_0 = objectInstanceIdVer_3 + "/" + RESOURCE_ID_0; sendRpcObserveOkWithResultValue("Observe", idVer_3_0_0);
String actualResult = sendRpcObserve("Observe", idVer_3_0_0); String rpcActualResult = sendRpcObserveOkWithResultValue("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"; String expected = "LwM2mSingleResource [id=0";
assertTrue(rpcActualResult.get("value").asText().contains(expected)); assertTrue(rpcActualResult.contains(expected));
} }
/** /**
@ -143,15 +121,14 @@ public class RpcLwm2mIntegrationObserveTest extends AbstractRpcLwM2MIntegrationO
*/ */
@Test @Test
public void testObserveWithBadVersion_Result_BadRequest_ErrorMsg_BadVersionMustBe_Ver() throws Exception { public void testObserveWithBadVersion_Result_BadRequest_ErrorMsg_BadVersionMustBe_Ver() throws Exception {
String expectedInstance = (String) expectedInstances.stream().filter(path -> !((String)path).contains("_")).findFirst().get(); String expectedInstance = (String) expectedInstances.stream().filter(path -> !((String) path).contains("_")).findFirst().get();
LwM2mPath expectedPath = new LwM2mPath(expectedInstance); LwM2mPath expectedPath = new LwM2mPath(expectedInstance);
int expectedResource = lwM2MTestClient.getLeshanClient().getObjectTree().getObjectEnablers().get(expectedPath.getObjectId()).getObjectModel().resources.entrySet().stream().findAny().get().getKey(); 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 ver = lwM2MTestClient.getLeshanClient().getObjectTree().getObjectEnablers().get(expectedPath.getObjectId()).getObjectModel().version;
String expectedId = "/" + expectedPath.getObjectId() + "_" + Version.MAX + "/" + expectedPath.getObjectInstanceId() + "/" + expectedResource; String expectedId = "/" + expectedPath.getObjectId() + "_" + Version.MAX + "/" + expectedPath.getObjectInstanceId() + "/" + expectedResource;
String actualResult = sendRpcObserve("Observe", expectedId); ObjectNode rpcActualResult = sendRpcObserveWithResult("Observe", expectedId);
ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class);
assertEquals(ResponseCode.BAD_REQUEST.getName(), rpcActualResult.get("result").asText()); assertEquals(ResponseCode.BAD_REQUEST.getName(), rpcActualResult.get("result").asText());
String expected = "Specified resource id " + expectedId +" is not valid version! Must be version: " + ver; String expected = "Specified resource id " + expectedId + " is not valid version! Must be version: " + ver;
assertEquals(expected, rpcActualResult.get("error").asText()); assertEquals(expected, rpcActualResult.get("error").asText());
} }
@ -162,10 +139,9 @@ public class RpcLwm2mIntegrationObserveTest extends AbstractRpcLwM2MIntegrationO
*/ */
@Test @Test
public void testObserveNoImplementedInstanceOnDevice_Result_NotFound() throws Exception { public void testObserveNoImplementedInstanceOnDevice_Result_NotFound() throws Exception {
String objectInstanceIdVer = (String) expectedObjectIdVers.stream().filter(path -> ((String)path).contains("/" + ACCESS_CONTROL)).findFirst().get(); String objectInstanceIdVer = (String) expectedObjectIdVers.stream().filter(path -> ((String) path).contains("/" + ACCESS_CONTROL)).findFirst().get();
String expected = objectInstanceIdVer + "/" + OBJECT_INSTANCE_ID_0; String expected = objectInstanceIdVer + "/" + OBJECT_INSTANCE_ID_0;
String actualResult = sendRpcObserve("Observe", expected); ObjectNode rpcActualResult = sendRpcObserveWithResult("Observe", expected);
ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class);
assertEquals(ResponseCode.NOT_FOUND.getName(), rpcActualResult.get("result").asText()); assertEquals(ResponseCode.NOT_FOUND.getName(), rpcActualResult.get("result").asText());
} }
@ -177,8 +153,7 @@ public class RpcLwm2mIntegrationObserveTest extends AbstractRpcLwM2MIntegrationO
@Test @Test
public void testObserveNoImplementedResourceOnDeviceValueNull_Result_BadRequest() throws Exception { public void testObserveNoImplementedResourceOnDeviceValueNull_Result_BadRequest() throws Exception {
String expected = objectIdVer_19 + "/" + OBJECT_INSTANCE_ID_0 + "/" + RESOURCE_ID_3; String expected = objectIdVer_19 + "/" + OBJECT_INSTANCE_ID_0 + "/" + RESOURCE_ID_3;
String actualResult = sendRpcObserve("Observe", expected); ObjectNode rpcActualResult = sendRpcObserveWithResult("Observe", expected);
ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class);
String expectedValue = "value MUST NOT be null"; String expectedValue = "value MUST NOT be null";
assertEquals(ResponseCode.BAD_REQUEST.getName(), rpcActualResult.get("result").asText()); assertEquals(ResponseCode.BAD_REQUEST.getName(), rpcActualResult.get("result").asText());
assertEquals(expectedValue, rpcActualResult.get("error").asText()); assertEquals(expectedValue, rpcActualResult.get("error").asText());
@ -191,9 +166,7 @@ public class RpcLwm2mIntegrationObserveTest extends AbstractRpcLwM2MIntegrationO
@Test @Test
public void testObserveResourceNotRead_Result_METHOD_NOT_ALLOWED() throws Exception { public void testObserveResourceNotRead_Result_METHOD_NOT_ALLOWED() throws Exception {
String expectedId = objectInstanceIdVer_5 + "/" + RESOURCE_ID_0; String expectedId = objectInstanceIdVer_5 + "/" + RESOURCE_ID_0;
sendRpcObserve("Observe", expectedId); ObjectNode rpcActualResult = sendRpcObserveWithResult("Observe", expectedId);
String actualResult = sendRpcObserve("Observe", expectedId);
ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class);
assertEquals(ResponseCode.METHOD_NOT_ALLOWED.getName(), rpcActualResult.get("result").asText()); assertEquals(ResponseCode.METHOD_NOT_ALLOWED.getName(), rpcActualResult.get("result").asText());
} }
@ -204,9 +177,7 @@ public class RpcLwm2mIntegrationObserveTest extends AbstractRpcLwM2MIntegrationO
@Test @Test
public void testObserveExecuteResource_Result_METHOD_NOT_ALLOWED() throws Exception { public void testObserveExecuteResource_Result_METHOD_NOT_ALLOWED() throws Exception {
String expectedId = objectInstanceIdVer_5 + "/" + RESOURCE_ID_2; String expectedId = objectInstanceIdVer_5 + "/" + RESOURCE_ID_2;
sendRpcObserve("Observe", expectedId); ObjectNode rpcActual = sendRpcObserveWithResult("Observe", expectedId);
String actual = sendRpcObserve("Observe", expectedId);
ObjectNode rpcActual = JacksonUtil.fromString(actual, ObjectNode.class);
assertEquals(ResponseCode.METHOD_NOT_ALLOWED.getName(), rpcActual.get("result").asText()); assertEquals(ResponseCode.METHOD_NOT_ALLOWED.getName(), rpcActual.get("result").asText());
} }
@ -217,100 +188,80 @@ public class RpcLwm2mIntegrationObserveTest extends AbstractRpcLwM2MIntegrationO
*/ */
@Test @Test
public void testObserveRepeatedRequestObserveOnDevice_Result_CONTENT_PutIfAbsent() throws Exception { public void testObserveRepeatedRequestObserveOnDevice_Result_CONTENT_PutIfAbsent() throws Exception {
String idVer_3_0_0 = objectInstanceIdVer_3 + "/" + RESOURCE_ID_0; sendRpcObserveOkWithResultValue("Observe", idVer_3_0_0);
String actualResult = sendRpcObserve("Observe", idVer_3_0_0); String rpcActualResult = sendRpcObserveOkWithResultValue("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"; String expected = "LwM2mSingleResource [id=0";
assertTrue(rpcActualResult.get("value").asText().contains(expected)); assertTrue(rpcActualResult.contains(expected));
} }
/** /**
* Observe {"id":["3"]} - Ok
* PreviousObservation contains "3/0/9" * PreviousObservation contains "3/0/9"
* Observe {"id":["19"]} - Bad Request
* Observe {"id":["19/0"]} - Bad Request
* Observe {"id":["19/1"]} - Ok
* @throws Exception * @throws Exception
*/ */
@Test @Test
public void testObserve_Result_CONTENT_ONE_PATH_PreviousObservation_CONTAINCE_OTHER_CurrentObservation() throws Exception { public void testObserves_OverlappedPaths_FirstResource_SecondObjectOrInstance() throws Exception {
sendCancelObserveAllWithAwait(deviceId); sendObserveCancelAllWithAwait(deviceId);
// "3/0/9" // "19/0/0"
String idVer_3_0_9 = objectInstanceIdVer_3 + "/" + RESOURCE_ID_9; sendRpcObserveOkWithResultValue("Observe", idVer_19_0_0);
String actualResult3_0_9 = sendRpcObserve("Observe", idVer_3_0_9); // PreviousObservation "19/0/0" change to CurrentObservation "19" - object
ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult3_0_9, ObjectNode.class); ObjectNode rpcActualResult = sendRpcObserveWithResult("Observe", objectIdVer_19);
assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText()); assertEquals(ResponseCode.BAD_REQUEST.getName(), rpcActualResult.get("result").asText());
// "3" String expected = "Resource [" + fromVersionedIdToObjectId(objectIdVer_19) + "] conflict with is already registered as SingleObservation [" + fromVersionedIdToObjectId(idVer_19_0_0) + "].";
String actualResult3 = sendRpcObserve("Observe", objectIdVer_3); assertEquals(expected, rpcActualResult.get("error").asText());
rpcActualResult = JacksonUtil.fromString(actualResult3, ObjectNode.class); // Verify ObserveReadAll
assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText()); String actualValuesReadAll = sendRpcObserveOkWithResultValue("ObserveReadAll", null);
// PreviousObservation "3/0/9" change to CurrentObservation "3" String expectedReadAll = "[\"SingleObservation:/19/0/0\"]";
String actualResultReadAll = sendRpcObserve("ObserveReadAll", null); assertEquals(expectedReadAll, actualValuesReadAll);
rpcActualResult = JacksonUtil.fromString(actualResultReadAll, ObjectNode.class); // PreviousObservation "19/0/0" change to CurrentObservation "19/0" - instance
assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText()); String expectedIdVer19_0 = objectIdVer_19 + "/" + OBJECT_INSTANCE_ID_0;
String actualValuesReadAll = rpcActualResult.get("value").asText(); rpcActualResult = sendRpcObserveWithResult("Observe", expectedIdVer19_0);
assertEquals(1, actualValuesReadAll.split(",").length); assertEquals(ResponseCode.BAD_REQUEST.getName(), rpcActualResult.get("result").asText());
String expected = "\"SingleObservation:/3\""; expected = "Resource [" + fromVersionedIdToObjectId(expectedIdVer19_0) + "] conflict with is already registered as SingleObservation [" + fromVersionedIdToObjectId(idVer_19_0_0) + "].";
assertTrue(actualValuesReadAll.contains(expected)); assertEquals(expected, rpcActualResult.get("error").asText());
} // Verify ObserveReadAll
actualValuesReadAll = sendRpcObserveOkWithResultValue("ObserveReadAll", null);
/** assertEquals(expectedReadAll, actualValuesReadAll);
* Observe {"id":["3/0/9"]} - Ok // PreviousObservation "19/0/0" add CurrentObservation "19/1" - instance
* PreviousObservation contains "3" String expectedIdVer19_1 = objectIdVer_19 + "/" + OBJECT_INSTANCE_ID_1;
* @throws Exception rpcActualResult = sendRpcObserveWithResult("Observe", expectedIdVer19_1);
*/
@Test
public void testObserve_Result_CONTENT_ONE_PATH_CurrentObservation_CONTAINCE_OTHER_PreviousObservation() throws Exception {
sendCancelObserveAllWithAwait(deviceId);
// "3"
String actualResult3 = sendRpcObserve("Observe", objectIdVer_3);
ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult3, ObjectNode.class);
assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText());
// "3/0/0"; WARN: - Token collision ? existing observation [/3] includes input observation [/3/0/0]
String idVer_3_0_0 = objectInstanceIdVer_3 + "/" + RESOURCE_ID_0;
String actualResult3_0_0 = sendRpcObserve("Observe", idVer_3_0_0);
rpcActualResult = JacksonUtil.fromString(actualResult3_0_0, ObjectNode.class);
assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText());
String actualResultReadAll = sendRpcObserve("ObserveReadAll", null);
rpcActualResult = JacksonUtil.fromString(actualResultReadAll, ObjectNode.class);
assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText()); assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText());
String actualValuesReadAll = rpcActualResult.get("value").asText(); assertTrue(rpcActualResult.get("value").asText().contains("LwM2mObjectInstance"));
assertEquals(1, actualValuesReadAll.split(",").length); // Verify ObserveReadAll
String expected = "\"SingleObservation:/3\""; actualValuesReadAll = sendRpcObserveOkWithResultValue("ObserveReadAll", null);
assertTrue(actualValuesReadAll.contains(expected)); assertTrue(actualValuesReadAll.contains("SingleObservation:/19/1"));
assertTrue(actualValuesReadAll.contains("SingleObservation:/19/0/0"));
// PreviousObservation "19/1/"- instance change to CurrentObservation "19/1/0" - resource
String expectedIdVer19_1_0 = expectedIdVer19_1 + "/" + RESOURCE_ID_0;
rpcActualResult = sendRpcObserveWithResult("Observe", expectedIdVer19_1_0);
assertEquals(ResponseCode.BAD_REQUEST.getName(), rpcActualResult.get("result").asText());
expected = "Resource [" + fromVersionedIdToObjectId(expectedIdVer19_1_0) + "] conflict with is already registered as SingleObservation [" + fromVersionedIdToObjectId(expectedIdVer19_1) + "].";
assertEquals(expected, rpcActualResult.get("error").asText());
// Verify ObserveReadAll
actualValuesReadAll = sendRpcObserveOkWithResultValue("ObserveReadAll", null);
assertTrue(actualValuesReadAll.contains("SingleObservation:/19/1"));
assertTrue(actualValuesReadAll.contains("SingleObservation:/19/0/0"));
} }
/** /**
* Observe {"id":"/3/0/9"} * Observe {"id":"/3/0/9"}
* ObserveCancel {"id":"/3/0/9"} * ObserveCancel {"id":"/3/0/9"}
*/ */
@Test @Test
public void testObserveResource_ObserveCancelResource_Result_CONTENT_Count_1() throws Exception { public void testObserveResource_ObserveCancelResource_Result_CONTENT_Count_1() throws Exception {
sendCancelObserveAllWithAwait(deviceId); sendObserveCancelAllWithAwait(deviceId);
String expectedId_3_0_9 = objectInstanceIdVer_3 + "/" + RESOURCE_ID_9; String actualValuesReadAll = sendRpcObserveReadAllWithResult(idVer_3_0_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); assertEquals(1, actualValuesReadAll.split(",").length);
String expected = "\"SingleObservation:" + fromVersionedIdToObjectId(expectedId_3_0_9) + "\""; String expected = "\"SingleObservation:" + id_3_0_9 + "\"";
assertTrue(actualValuesReadAll.contains(expected)); assertTrue(actualValuesReadAll.contains(expected));
// cancel observe "/3_1.2/0/9" // cancel observe "/3_1.2/0/9"
String actualResult = sendRpcObserve("ObserveCancel", expectedId_3_0_9); sendRpcObserveOkWithResultValue("ObserveCancel", idVer_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"} * Observe {"id":"/3"}
* ObserveCancel {"id":"/3/0/9"} -> INTERNAL_SERVER_ERROR * ObserveCancel {"id":"/3/0/9"} -> INTERNAL_SERVER_ERROR
@ -318,64 +269,90 @@ public class RpcLwm2mIntegrationObserveTest extends AbstractRpcLwM2MIntegrationO
*/ */
@Test @Test
public void testObserveObject_ObserveCancelOneResource_Result_INTERNAL_SERVER_ERROR_Than_Cancel_ObserveObject_Result_CONTENT_Count_1() throws Exception { public void testObserveObject_ObserveCancelOneResource_Result_INTERNAL_SERVER_ERROR_Than_Cancel_ObserveObject_Result_CONTENT_Count_1() throws Exception {
sendCancelObserveAllWithAwait(deviceId); sendObserveCancelAllWithAwait(deviceId);
String expectedId_3 = objectIdVer_3; String actualValuesReadAll = sendRpcObserveReadAllWithResult(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); assertEquals(1, actualValuesReadAll.split(",").length);
String expected = "\"SingleObservation:" + fromVersionedIdToObjectId(expectedId_3) + "\""; String expected = "\"SingleObservation:" + fromVersionedIdToObjectId(objectIdVer_3) + "\"";
assertTrue(actualValuesReadAll.contains(expected)); assertTrue(actualValuesReadAll.contains(expected));
// cancel observe "/3_1.2/0/9" // cancel observe "/3_1.2/0/9"
String expectedId_3_0_9 = objectInstanceIdVer_3 + "/" + RESOURCE_ID_9; ObjectNode rpcActualResult = sendRpcObserveWithResult("ObserveCancel", idVer_3_0_9);
String actualResult = sendRpcObserve("ObserveCancel", expectedId_3_0_9); String expectedValue = "Could not find active Observe component with path: " + idVer_3_0_9;
String expectedValue = "for observation path [" + fromVersionedIdToObjectId(objectIdVer_3) + "], that includes this observation path [" + fromVersionedIdToObjectId(expectedId_3_0_9);
rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class);
assertEquals(ResponseCode.BAD_REQUEST.getName(), rpcActualResult.get("result").asText()); assertEquals(ResponseCode.BAD_REQUEST.getName(), rpcActualResult.get("result").asText());
assertTrue(rpcActualResult.get("error").asText().contains(expectedValue)); assertTrue(rpcActualResult.get("error").asText().contains(expectedValue));
// cancel observe "/3_1.2" // cancel observe "/3_1.2"
actualResult = sendRpcObserve("ObserveCancel", expectedId_3); sendRpcObserveOkWithResultValue("ObserveCancel", objectIdVer_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/0"} * Observe {"id":"/3/0/0"}
* Observe {"id":"/3/0/9"} * Observe {"id":"/3/0/9"}
* ObserveCancel {"id":"/3"} - Ok, cnt = 2 * ObserveCancel {"id":"/3"} - Bad
* ObserveCancel {"/3/0/0"} - Ok
* ObserveCancel {"/3/0/9"} - Ok
*
*/ */
@Test @Test
public void testObserveResource_ObserveCancelObject_Result_CONTENT_Count_1() throws Exception { public void testObserveResource_ObserveCancelObject_Result_CONTENT_Count_1() throws Exception {
sendCancelObserveAllWithAwait(deviceId); sendObserveCancelAllWithAwait(deviceId);
String expectedId_3_0_0 = objectInstanceIdVer_3 + "/" + RESOURCE_ID_0; sendRpcObserveWithWithTwoResource(idVer_3_0_0, idVer_3_0_9);
sendRpcObserve("Observe", expectedId_3_0_0); String rpcActualResul = sendRpcObserveOkWithResultValue("ObserveReadAll", null);
String expectedId_3_0_9 = objectInstanceIdVer_3 + "/" + RESOURCE_ID_9; assertEquals(2, rpcActualResul.split(",").length);
sendRpcObserve("Observe", expectedId_3_0_9); String expected_3_0_0 = "\"SingleObservation:" + fromVersionedIdToObjectId(idVer_3_0_0) + "\"";
String actualResultReadAll = sendRpcObserve("ObserveReadAll", null); String expected_3_0_9 = "\"SingleObservation:" + id_3_0_9 + "\"";
ObjectNode rpcActualResult = JacksonUtil.fromString(actualResultReadAll, ObjectNode.class); assertTrue(rpcActualResul.contains(expected_3_0_0));
assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText()); assertTrue(rpcActualResul.contains(expected_3_0_9));
String actualValuesReadAll = rpcActualResult.get("value").asText(); // cancel observe "/3_1.2"
assertEquals(2, actualValuesReadAll.split(",").length); ObjectNode rpcActualResult = sendRpcObserveWithResult("ObserveCancel", objectIdVer_3);
String expected_3_0_0 = "\"SingleObservation:" + fromVersionedIdToObjectId(expectedId_3_0_0) + "\""; assertEquals(ResponseCode.BAD_REQUEST.getName(), rpcActualResult.get("result").asText());
String expected_3_0_9 = "\"SingleObservation:" + fromVersionedIdToObjectId(expectedId_3_0_9) + "\""; String expected = "Could not find active Observe component with path: " + objectIdVer_3;
assertTrue(actualValuesReadAll.contains(expected_3_0_0)); assertEquals(expected, rpcActualResult.get("error").asText());
assertTrue(actualValuesReadAll.contains(expected_3_0_9)); // Verify ObserveReadAll
rpcActualResul = sendRpcObserveOkWithResultValue("ObserveReadAll", null);
// cancel observe "/3_1.2" String expectedReadAll = "[\"SingleObservation:/19/0/0\"]";
String expectedId_3 = objectIdVer_3; assertTrue(rpcActualResul.contains(expected_3_0_0));
String actualResult = sendRpcObserve("ObserveCancel", expectedId_3); assertTrue(rpcActualResul.contains(expected_3_0_9));
rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); }
/**
* ObserveCancelAll
* Observe {"id":"3_1.2/0/9"}
* updateRegistration
* idResources_3_1.2/0/9 => updateAttrTelemetry >= 10 times
*/
@Test
public void testObserveResource_Update_AfterUpdateRegistration() throws Exception {
sendObserveCancelAllWithAwait(deviceId);
int cntUpdate = 3;
verify(defaultUplinkMsgHandlerTest, timeout(50000).atLeast(cntUpdate))
.updatedReg(Mockito.any(Registration.class));
sendRpcObserveWithContainsLwM2mSingleResource(idVer_3_0_9);
cntUpdate = 10;
verify(defaultUplinkMsgHandlerTest, timeout(50000).atLeast(cntUpdate))
.updateAttrTelemetry(Mockito.any(Registration.class), eq(idVer_3_0_9));
}
private void sendRpcObserveWithWithTwoResource(String expectedId_1, String expectedId_2) throws Exception {
sendRpcObserveOk("Observe", expectedId_1);
sendRpcObserveOk("Observe", expectedId_2);
}
private String sendRpcObserveReadAllWithResult(String params) throws Exception {
sendRpcObserveOk("Observe", params);
ObjectNode rpcActualResult = sendRpcObserveWithResult("ObserveReadAll", null);
assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText()); assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText());
assertEquals("2", rpcActualResult.get("value").asText()); return rpcActualResult.get("value").asText();
} }
private String sendRpcObserve(String method, String params) throws Exception { private void sendRpcObserveWithContainsLwM2mSingleResource(String params) throws Exception {
return sendObserve(method, params, deviceId); String rpcActualResult = sendRpcObserveOkWithResultValue("Observe", params);
assertTrue(rpcActualResult.contains("LwM2mSingleResource"));
assertEquals(Optional.of(1).get(), Optional.ofNullable(getCntObserveAll(deviceId)).get());
} }
} }

3
common/dao-api/src/main/java/org/thingsboard/server/dao/alarm/AlarmService.java

@ -107,7 +107,7 @@ public interface AlarmService extends EntityDaoService {
PageData<AlarmData> findAlarmDataByQueryForEntities(TenantId tenantId, PageData<AlarmData> findAlarmDataByQueryForEntities(TenantId tenantId,
AlarmDataQuery query, Collection<EntityId> orderedEntityIds); AlarmDataQuery query, Collection<EntityId> orderedEntityIds);
PageData<AlarmId> findAlarmIdsByAssigneeId(TenantId tenantId, UserId userId, PageLink pageLink); List<TbPair<UUID, Long>> findAlarmIdsByAssigneeId(TenantId tenantId, UserId userId, long createdTimeOffset, AlarmId idOffset, int limit);
List<TbPair<UUID, Long>> findAlarmIdsByOriginatorId(TenantId tenantId, EntityId originatorId, long createdTimeOffset, AlarmId idOffset, int limit); List<TbPair<UUID, Long>> findAlarmIdsByOriginatorId(TenantId tenantId, EntityId originatorId, long createdTimeOffset, AlarmId idOffset, int limit);
@ -118,4 +118,5 @@ public interface AlarmService extends EntityDaoService {
long countAlarmsByQuery(TenantId tenantId, CustomerId customerId, AlarmCountQuery query); long countAlarmsByQuery(TenantId tenantId, CustomerId customerId, AlarmCountQuery query);
PageData<EntitySubtype> findAlarmTypesByTenantId(TenantId tenantId, PageLink pageLink); PageData<EntitySubtype> findAlarmTypesByTenantId(TenantId tenantId, PageLink pageLink);
} }

20
common/data/src/main/java/org/thingsboard/server/common/data/housekeeper/AlarmsUnassignHousekeeperTask.java

@ -21,6 +21,11 @@ import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
import lombok.ToString; import lombok.ToString;
import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.UserId;
import java.util.List;
import java.util.UUID;
@Data @Data
@ToString(callSuper = true) @ToString(callSuper = true)
@ -29,10 +34,21 @@ import org.thingsboard.server.common.data.User;
public class AlarmsUnassignHousekeeperTask extends HousekeeperTask { public class AlarmsUnassignHousekeeperTask extends HousekeeperTask {
private String userTitle; private String userTitle;
private List<UUID> alarms;
protected AlarmsUnassignHousekeeperTask(User user) { protected AlarmsUnassignHousekeeperTask(User user) {
super(user.getTenantId(), user.getId(), HousekeeperTaskType.UNASSIGN_ALARMS); this(user.getTenantId(), user.getId(), user.getTitle(), null);
this.userTitle = user.getTitle(); }
public AlarmsUnassignHousekeeperTask(TenantId tenantId, UserId userId, String userTitle, List<UUID> alarms) {
super(tenantId, userId, HousekeeperTaskType.UNASSIGN_ALARMS);
this.userTitle = userTitle;
this.alarms = alarms;
}
@Override
public String getDescription() {
return super.getDescription() + (alarms != null ? " (" + alarms + ")" : "");
} }
} }

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

@ -68,9 +68,8 @@ import org.eclipse.leshan.core.util.Hex;
import org.eclipse.leshan.server.model.LwM2mModelProvider; import org.eclipse.leshan.server.model.LwM2mModelProvider;
import org.eclipse.leshan.server.registration.Registration; import org.eclipse.leshan.server.registration.Registration;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.device.profile.lwm2m.ObjectAttributes; import org.thingsboard.server.common.data.device.profile.lwm2m.ObjectAttributes;
import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.queue.util.TbLwM2mTransportComponent; import org.thingsboard.server.queue.util.TbLwM2mTransportComponent;
import org.thingsboard.server.transport.lwm2m.config.LwM2MTransportServerConfig; import org.thingsboard.server.transport.lwm2m.config.LwM2MTransportServerConfig;
import org.thingsboard.server.transport.lwm2m.server.LwM2mTransportContext; import org.thingsboard.server.transport.lwm2m.server.LwM2mTransportContext;
@ -88,14 +87,11 @@ import org.thingsboard.server.transport.lwm2m.utils.LwM2mValueConverterImpl;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collection; import java.util.Collection;
import java.util.Date; import java.util.Date;
import java.util.HashSet;
import java.util.LinkedHashSet; import java.util.LinkedHashSet;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Optional;
import java.util.Set; import java.util.Set;
import java.util.TreeSet;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.RejectedExecutionException; import java.util.concurrent.RejectedExecutionException;
import java.util.function.Function; import java.util.function.Function;
@ -177,24 +173,32 @@ public class DefaultLwM2mDownlinkMsgHandler extends LwM2MExecutorAwareService im
} }
} }
/**
* if resource in CompositeObservation is already registered - return BAD REQUEST
*/
@Override @Override
public void sendObserveRequest(LwM2mClient client, TbLwM2MObserveRequest request, DownlinkRequestCallback<ObserveRequest, ObserveResponse> callback) { public void sendObserveRequest(LwM2mClient client, TbLwM2MObserveRequest request, DownlinkRequestCallback<ObserveRequest, ObserveResponse> callback) {
try { try {
validateVersionedId(client, request); validateVersionedId(client, request);
LwM2mPath resultIds = new LwM2mPath(request.getObjectId()); LwM2mPath resultIds = new LwM2mPath(request.getObjectId());
ObserveRequest downlink; String resourceExisting = checkResourceSingleObservationForExisting(client, resultIds.toString());
ContentFormat contentFormat = getReadRequestContentFormat(client, request, modelProvider); if (StringUtils.isNotBlank(resourceExisting)) {
if (resultIds.isResourceInstance()) { callback.onValidationError(request.toString(), resourceExisting);
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 { } else {
downlink = new ObserveRequest(contentFormat, resultIds.getObjectId()); 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 {
downlink = new ObserveRequest(contentFormat, resultIds.getObjectId());
}
log.info("[{}] Send observation: {}.", client.getEndpoint(), request.getVersionedId());
sendSimpleRequest(client, downlink, request.getTimeout(), callback);
} }
log.info("[{}] Send observation: {}.", client.getEndpoint(), request.getVersionedId());
sendSimpleRequest(client, downlink, request.getTimeout(), callback);
} catch (InvalidRequestException e) { } catch (InvalidRequestException e) {
callback.onValidationError(request.toString(), e.getMessage()); callback.onValidationError(request.toString(), e.getMessage());
} }
@ -208,19 +212,7 @@ public class DefaultLwM2mDownlinkMsgHandler extends LwM2MExecutorAwareService im
if (observation instanceof SingleObservation) { if (observation instanceof SingleObservation) {
paths.add("SingleObservation:" + ((SingleObservation) observation).getPath().toString()); paths.add("SingleObservation:" + ((SingleObservation) observation).getPath().toString());
} else { } else {
List<LwM2mPath> listPath = ((CompositeObservation) observation).getPaths(); paths.add("CompositeObservation: " + ((CompositeObservation) observation).getPaths().toString());
List<String> pathsComposite = listPath.stream().map(lwM2mPath -> (lwM2mPath.toString())).collect(Collectors.toList());
Set <String> pathsCompositeSort = new TreeSet<>();
if (pathsComposite.size() == 1) {
pathsCompositeSort.add("CompositeObservation: [" + pathsComposite.get(0) + "]");
} else if (pathsComposite.size() > 1) {
List <String> sort = new LinkedList<>();
sort.addAll(pathsComposite);
sort.set(0, "CompositeObservation: [" + sort.get(0));
sort.set(pathsComposite.size()-1, sort.get(pathsComposite.size()-1) + "]");
pathsCompositeSort = new LinkedHashSet<>(sort);
}
paths.addAll(pathsCompositeSort);
} }
}); });
@ -234,10 +226,15 @@ public class DefaultLwM2mDownlinkMsgHandler extends LwM2MExecutorAwareService im
public void sendObserveCompositeRequest(LwM2mClient client, TbLwM2MObserveCompositeRequest request, DownlinkRequestCallback<ObserveCompositeRequest, public void sendObserveCompositeRequest(LwM2mClient client, TbLwM2MObserveCompositeRequest request, DownlinkRequestCallback<ObserveCompositeRequest,
ObserveCompositeResponse> callback) { ObserveCompositeResponse> callback) {
try { try {
log.trace("[{}] Send Composite observation: [{}].", client.getEndpoint(), request.getObjectIds());
ContentFormat compositeContentFormat = this.findFirstContentFormatForComposite(client.getClientSupportContentFormats()); ContentFormat compositeContentFormat = this.findFirstContentFormatForComposite(client.getClientSupportContentFormats());
ObserveCompositeRequest downlink = new ObserveCompositeRequest(compositeContentFormat, compositeContentFormat, request.getObjectIds()); String resourceExisting = checkResourceForExistingComposite(client, request.getObjectIds());
sendCompositeRequest(client, downlink, this.config.getTimeout(), callback); if (StringUtils.isNotBlank(resourceExisting)) {
callback.onValidationError(request.toString(), resourceExisting);
} else {
ObserveCompositeRequest downlink = new ObserveCompositeRequest(compositeContentFormat, compositeContentFormat, request.getObjectIds());
log.trace("[{}] Send ObserveComposite: {}.", client.getEndpoint(), request.getVersionedIds());
sendCompositeRequest(client, downlink, this.config.getTimeout(), callback);
}
} catch (InvalidRequestException e) { } catch (InvalidRequestException e) {
callback.onValidationError(request.toString(), e.getMessage()); callback.onValidationError(request.toString(), e.getMessage());
} }
@ -246,44 +243,18 @@ public class DefaultLwM2mDownlinkMsgHandler extends LwM2MExecutorAwareService im
@Override @Override
public void sendCancelObserveCompositeRequest(LwM2mClient client, TbLwM2MCancelObserveCompositeRequest request, DownlinkRequestCallback<TbLwM2MCancelObserveCompositeRequest, Integer> callback) { public void sendCancelObserveCompositeRequest(LwM2mClient client, TbLwM2MCancelObserveCompositeRequest request, DownlinkRequestCallback<TbLwM2MCancelObserveCompositeRequest, Integer> callback) {
try { try {
Set<Observation> observations = context.getServer().getObservationService().getObservations(client.getRegistration()); log.trace("[{}] Send CancelObserveComposite: {}.", client.getEndpoint(), request.getVersionedIds());
List<LwM2mPath> listPath = LwM2mPath.getLwM2mPathList(Arrays.asList(request.getObjectIds())); int cnt = context.getServer().getObservationService().cancelCompositeObservations(client.getRegistration(), request.getObjectIds());
Optional<Observation> observationOpt = Optional.ofNullable(observations.stream().filter(observation -> observation instanceof CompositeObservation && ((CompositeObservation) observation).getPaths().equals(listPath)).findFirst().orElse(null)); if (cnt != 0) {
int cnt = 0;
if (observationOpt.isPresent()) {
cnt = context.getServer().getObservationService().cancelCompositeObservations(client.getRegistration(), request.getObjectIds());
callback.onSuccess(request, cnt); callback.onSuccess(request, cnt);
} else { } else {
Set<String> lwPaths = new HashSet<>(); callback.onValidationError(request.toString(), "Could not find active Observe Composite component with paths: " + Arrays.toString(request.getVersionedIds()));
for (Observation obs : observations) {
LwM2mPath lwPathObs = ((SingleObservation) obs).getPath();
for (LwM2mPath nodePath : listPath) {
String validNodePath = validatePathObserveCancelAny(nodePath, lwPathObs, client);
if (validNodePath != null) lwPaths.add(validNodePath);
}
};
for (String nodePath : lwPaths) {
cnt += context.getServer().getObservationService().cancelObservations(client.getRegistration(), nodePath);
}
} }
callback.onSuccess(request, cnt); } catch (InvalidRequestException e) {
} catch (ThingsboardException e){
callback.onValidationError(request.toString(), e.getMessage()); callback.onValidationError(request.toString(), e.getMessage());
} }
} }
private String validatePathObserveCancelAny(LwM2mPath nodePath, LwM2mPath lwPathObs, LwM2mClient client) throws ThingsboardException {
if (nodePath.equals(lwPathObs) || lwPathObs.startWith(nodePath)) { // nodePath = "3", lwPathObs = "3/0/9": cancel for tne all lwPathObs
return lwPathObs.toString();
} else if (!nodePath.equals(lwPathObs) && nodePath.startWith(lwPathObs)) {
String errorMsg = String.format(
"Unexpected error: There is registration with Endpoint %s for observation path [%s], that includes this observation path [%s]",
client.getRegistration().getEndpoint(), lwPathObs, nodePath);
throw new ThingsboardException(errorMsg, ThingsboardErrorCode.BAD_REQUEST_PARAMS);
}
return null;
}
@Override @Override
public void sendDiscoverAllRequest(LwM2mClient client, TbLwM2MDiscoverAllRequest request, DownlinkRequestCallback<TbLwM2MDiscoverAllRequest, List<String>> callback) { public void sendDiscoverAllRequest(LwM2mClient client, TbLwM2MDiscoverAllRequest request, DownlinkRequestCallback<TbLwM2MDiscoverAllRequest, List<String>> callback) {
callback.onSuccess(request, Arrays.stream(client.getRegistration().getSortedObjectLinks()).map(Link::toCoreLinkFormat).collect(Collectors.toList())); callback.onSuccess(request, Arrays.stream(client.getRegistration().getSortedObjectLinks()).map(Link::toCoreLinkFormat).collect(Collectors.toList()));
@ -334,22 +305,15 @@ public class DefaultLwM2mDownlinkMsgHandler extends LwM2MExecutorAwareService im
@Override @Override
public void sendCancelObserveRequest(LwM2mClient client, TbLwM2MCancelObserveRequest request, DownlinkRequestCallback<TbLwM2MCancelObserveRequest, Integer> callback) { public void sendCancelObserveRequest(LwM2mClient client, TbLwM2MCancelObserveRequest request, DownlinkRequestCallback<TbLwM2MCancelObserveRequest, Integer> callback) {
try{ try {
validateVersionedId(client, request); log.trace("[{}] Send CancelObserve {}.", client.getEndpoint(), request.getVersionedId());
Set<Observation> observations = context.getServer().getObservationService().getObservations(client.getRegistration()); int cnt = context.getServer().getObservationService().cancelObservations(client.getRegistration(), request.getObjectId());
int observeCancelCnt = 0; if (cnt != 0) {
Set<String> lwPaths = new HashSet<>(); callback.onSuccess(request, cnt);
for (Observation obs : observations) { } else {
LwM2mPath lwPathObs = ((SingleObservation) obs).getPath(); callback.onValidationError(request.toString(), "Could not find active Observe component with path: " + request.getVersionedId());
LwM2mPath nodePath = new LwM2mPath(request.getObjectId());
String validNodePath = validatePathObserveCancelAny(nodePath, lwPathObs, client);
if (validNodePath != null) lwPaths.add(validNodePath);
};
for (String nodePath : lwPaths) {
observeCancelCnt += context.getServer().getObservationService().cancelObservations(client.getRegistration(), nodePath);
} }
callback.onSuccess(request, observeCancelCnt); } catch (InvalidRequestException e) {
} catch (ThingsboardException e){
callback.onValidationError(request.toString(), e.getMessage()); callback.onValidationError(request.toString(), e.getMessage());
} }
} }
@ -416,7 +380,7 @@ public class DefaultLwM2mDownlinkMsgHandler extends LwM2MExecutorAwareService im
addAttribute(attributes, OBJECT_VERSION, params.getVer()); // Attachment.OBJECT addAttribute(attributes, OBJECT_VERSION, params.getVer()); // Attachment.OBJECT
} }
return new LwM2mAttributeSet(attributes); return new LwM2mAttributeSet(attributes);
} }
@Override @Override
@ -702,13 +666,13 @@ public class DefaultLwM2mDownlinkMsgHandler extends LwM2MExecutorAwareService im
addAttribute(attributes, attribute, value, null, null); addAttribute(attributes, attribute, value, null, null);
} }
private static <T> void addAttribute(List<LwM2mAttribute<?>> attributes, LwM2mAttributeModel<T> attribute, T value, Function<T, ?> converter) { private static <T> void addAttribute(List<LwM2mAttribute<?>> attributes, LwM2mAttributeModel<T> attribute, T value, Function<T, ?> converter) {
addAttribute(attributes, attribute, value, null, converter); addAttribute(attributes, attribute, value, null, converter);
} }
private static <T> void addAttribute(List<LwM2mAttribute<?>> attributes, LwM2mAttributeModel<T> attributeName, T value, Predicate<T> filter, Function<T, ?> converter) { private static <T> void addAttribute(List<LwM2mAttribute<?>> attributes, LwM2mAttributeModel<T> attributeName, T value, Predicate<T> filter, Function<T, ?> converter) {
if (value != null && ((filter == null) || filter.test(value))) { if (value != null && ((filter == null) || filter.test(value))) {
T valueConvert = (T) converter != null ? (T) converter.apply(value) : value; T valueConvert = (T) converter != null ? (T) converter.apply(value) : value;
attributes.add(new LwM2mAttribute<>(attributeName, valueConvert)); attributes.add(new LwM2mAttribute<>(attributeName, valueConvert));
} }
} }
@ -746,12 +710,12 @@ public class DefaultLwM2mDownlinkMsgHandler extends LwM2MExecutorAwareService im
if (resourceModel != null && (pathIds.isResourceInstance() || (pathIds.isResource() && !resourceModel.multiple))) { if (resourceModel != null && (pathIds.isResourceInstance() || (pathIds.isResource() && !resourceModel.multiple))) {
ContentFormat[] desiredFormats; ContentFormat[] desiredFormats;
if (OBJLNK.equals(resourceModel.type)) { if (OBJLNK.equals(resourceModel.type)) {
desiredFormats = new ContentFormat[]{ContentFormat.LINK, ContentFormat.CBOR, ContentFormat.SENML_CBOR, ContentFormat.SENML_JSON}; desiredFormats = new ContentFormat[]{ContentFormat.LINK, ContentFormat.CBOR, ContentFormat.SENML_CBOR, ContentFormat.SENML_JSON};
} else if (OPAQUE.equals(resourceModel.type)) { } else if (OPAQUE.equals(resourceModel.type)) {
desiredFormats = new ContentFormat[]{ContentFormat.OPAQUE, ContentFormat.CBOR, ContentFormat.SENML_CBOR, ContentFormat.SENML_JSON}; desiredFormats = new ContentFormat[]{ContentFormat.OPAQUE, ContentFormat.CBOR, ContentFormat.SENML_CBOR, ContentFormat.SENML_JSON};
} else { } else {
desiredFormats = new ContentFormat[]{ContentFormat.CBOR, ContentFormat.SENML_CBOR, ContentFormat.SENML_JSON}; desiredFormats = new ContentFormat[]{ContentFormat.CBOR, ContentFormat.SENML_CBOR, ContentFormat.SENML_JSON};
} }
return findFirstContentFormatForComp(client.getClientSupportContentFormats(), client.getDefaultContentFormat(), desiredFormats); return findFirstContentFormatForComp(client.getClientSupportContentFormats(), client.getDefaultContentFormat(), desiredFormats);
} else { } else {
return getContentFormatForComplex(client); return getContentFormatForComplex(client);
@ -775,6 +739,7 @@ public class DefaultLwM2mDownlinkMsgHandler extends LwM2MExecutorAwareService im
throw new RuntimeException("The version " + client.getRegistration().getLwM2mVersion() + " is not supported!"); throw new RuntimeException("The version " + client.getRegistration().getLwM2mVersion() + " is not supported!");
} }
} }
private <R> String toString(R request) { private <R> String toString(R request) {
try { try {
return request != null ? request.toString() : ""; return request != null ? request.toString() : "";
@ -784,7 +749,7 @@ public class DefaultLwM2mDownlinkMsgHandler extends LwM2MExecutorAwareService im
} }
} }
private ContentFormat findFirstContentFormatForComposite (Set<ContentFormat> clientSupportContentFormats) { private ContentFormat findFirstContentFormatForComposite(Set<ContentFormat> clientSupportContentFormats) {
ContentFormat contentFormat = findFirstContentFormatForComp(clientSupportContentFormats, null, ContentFormat.SENML_CBOR, ContentFormat.SENML_JSON); ContentFormat contentFormat = findFirstContentFormatForComp(clientSupportContentFormats, null, ContentFormat.SENML_CBOR, ContentFormat.SENML_JSON);
if (contentFormat != null) { if (contentFormat != null) {
return contentFormat; return contentFormat;
@ -792,8 +757,9 @@ public class DefaultLwM2mDownlinkMsgHandler extends LwM2MExecutorAwareService im
throw new RuntimeException("This device does not support Composite Operation"); throw new RuntimeException("This device does not support Composite Operation");
} }
} }
private static ContentFormat findFirstContentFormatForComp(Set<ContentFormat> clientSupportContentFormats, ContentFormat defaultValue, ContentFormat... desiredFormats) { private static ContentFormat findFirstContentFormatForComp(Set<ContentFormat> clientSupportContentFormats, ContentFormat defaultValue, ContentFormat... desiredFormats) {
List desiredFormatsList = Arrays.asList(desiredFormats); List desiredFormatsList = Arrays.asList(desiredFormats);
for (ContentFormat c : clientSupportContentFormats) { for (ContentFormat c : clientSupportContentFormats) {
if (desiredFormatsList.contains(c)) { if (desiredFormatsList.contains(c)) {
return c; return c;
@ -801,4 +767,61 @@ public class DefaultLwM2mDownlinkMsgHandler extends LwM2MExecutorAwareService im
} }
return defaultValue; return defaultValue;
} }
/**
* Check if at least one of the resource objectIds (Composite) in SingleObservation or CompositeObservation is already registered
* @param objectIds
* @return
*/
private String checkResourceForExistingComposite(LwM2mClient client, String[] objectIds) {
List<String> objectIdsList = Arrays.asList(objectIds);
Set<Observation> observations = context.getServer().getObservationService().getObservations(client.getRegistration());
for (Observation observation : observations) {
if (observation instanceof SingleObservation singleObs) {
String idSingleOb = singleObs.getPath().toString();
if (objectIdsList.contains(idSingleOb)) {
return "Resource [" + idSingleOb + "] is already registered as SingleObservation.";
}
} else if (observation instanceof CompositeObservation compObs) {
String paths = compObs.getPaths().toString();
for (String idCompOb : objectIds) {
if (paths.contains(idCompOb)) {
return "Resource [" + idCompOb + "] is already registered in CompositeObservation.";
}
}
}
}
return null;
}
/**
* Check if the resource SingleObservation is already registered in CompositeObservation
* Check if the resource SingleObservation is already registered in SingleObservation and (not equals path
* @param objectId
* @return
*/
private String checkResourceSingleObservationForExisting(LwM2mClient client, String objectId) {
Set<Observation> observations = context.getServer().getObservationService().getObservations(client.getRegistration());
for (Observation observation : observations) {
if (observation instanceof SingleObservation singleObs) {
LwM2mPath pathSingleOb = singleObs.getPath();
LwM2mPath pathObjectId = new LwM2mPath(objectId);
if (!pathSingleOb.toString().equals(objectId)) {
List paths = Arrays.asList(pathSingleOb, pathObjectId);
try {
LwM2mPath.validateNotOverlapping(paths);
} catch (IllegalArgumentException e){
return "Resource [" + objectId + "] conflict with is already registered as SingleObservation [" + pathSingleOb + "].";
}
}
}
else if (observation instanceof CompositeObservation compObs) {
String paths = compObs.getPaths().toString();
if (paths.contains(objectId)) {
return "Resource [" + objectId + "] is already registered in CompositeObservation.";
}
}
}
return null;
}
} }

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

@ -15,25 +15,16 @@
*/ */
package org.thingsboard.server.transport.lwm2m.server.store; package org.thingsboard.server.transport.lwm2m.server.store;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.eclipse.californium.core.coap.Token;
import org.eclipse.californium.core.network.RandomTokenGenerator;
import org.eclipse.californium.core.network.TokenGenerator; import org.eclipse.californium.core.network.TokenGenerator;
import org.eclipse.californium.core.network.TokenGenerator.Scope;
import org.eclipse.leshan.core.Destroyable; import org.eclipse.leshan.core.Destroyable;
import org.eclipse.leshan.core.Startable; import org.eclipse.leshan.core.Startable;
import org.eclipse.leshan.core.Stoppable; 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.CompositeObservation;
import org.eclipse.leshan.core.observation.Observation; import org.eclipse.leshan.core.observation.Observation;
import org.eclipse.leshan.core.observation.ObservationIdentifier; import org.eclipse.leshan.core.observation.ObservationIdentifier;
import org.eclipse.leshan.core.observation.SingleObservation; import org.eclipse.leshan.core.observation.SingleObservation;
import org.eclipse.leshan.core.peer.LwM2mIdentity; import org.eclipse.leshan.core.peer.LwM2mIdentity;
import org.eclipse.leshan.core.request.ContentFormat;
import org.eclipse.leshan.core.util.NamedThreadFactory; import org.eclipse.leshan.core.util.NamedThreadFactory;
import org.eclipse.leshan.server.registration.Deregistration; import org.eclipse.leshan.server.registration.Deregistration;
import org.eclipse.leshan.server.registration.ExpirationListener; import org.eclipse.leshan.server.registration.ExpirationListener;
@ -41,14 +32,12 @@ import org.eclipse.leshan.server.registration.Registration;
import org.eclipse.leshan.server.registration.RegistrationStore; import org.eclipse.leshan.server.registration.RegistrationStore;
import org.eclipse.leshan.server.registration.RegistrationUpdate; import org.eclipse.leshan.server.registration.RegistrationUpdate;
import org.eclipse.leshan.server.registration.UpdatedRegistration; import org.eclipse.leshan.server.registration.UpdatedRegistration;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.transport.lwm2m.config.LwM2MTransportServerConfig; import org.thingsboard.server.transport.lwm2m.config.LwM2MTransportServerConfig;
import org.thingsboard.server.transport.lwm2m.server.LwM2mVersionedModelProvider; import org.thingsboard.server.transport.lwm2m.server.LwM2mVersionedModelProvider;
import java.net.InetSocketAddress; import java.net.InetSocketAddress;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
import java.util.Iterator; import java.util.Iterator;
@ -60,13 +49,9 @@ import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture; import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.locks.ReadWriteLock; import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock; 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;
@Slf4j @Slf4j
public class TbInMemoryRegistrationStore implements RegistrationStore, Startable, Stoppable, Destroyable { public class TbInMemoryRegistrationStore implements RegistrationStore, Startable, Stoppable, Destroyable {
@ -108,7 +93,7 @@ public class TbInMemoryRegistrationStore implements RegistrationStore, Startable
this.schedExecutor = schedExecutor; this.schedExecutor = schedExecutor;
this.cleanPeriod = cleanPeriodInSec; this.cleanPeriod = cleanPeriodInSec;
this.modelProvider = modelProvider; this.modelProvider = modelProvider;
this.config = config; this.config = config;
} }
/* *************** Leshan Registration API **************** */ /* *************** Leshan Registration API **************** */
@ -255,138 +240,60 @@ public class TbInMemoryRegistrationStore implements RegistrationStore, Startable
List<Observation> removed = new ArrayList<>(); List<Observation> removed = new ArrayList<>();
try { try {
lock.writeLock().lock(); lock.writeLock().lock();
if (!regsByRegId.containsKey(registrationId)) { if (!regsByRegId.containsKey(registrationId)) {
throw new IllegalStateException(String.format( throw new IllegalStateException(String.format(
"can not add observation %s there is no registration with id %s", observation, registrationId)); "can not add observation %s there is no registration with id %s", observation, registrationId));
} }
updateObservation(registrationId, observation, addIfAbsent, removed);
if (observation instanceof SingleObservation) {
if (validateObserveResource(((SingleObservation)observation).getPath(), registrationId)) {
updateSingleObservation(registrationId, (SingleObservation) observation, addIfAbsent, removed);
// cancel existing observations for the same path and registration id.
cancelObservation (observation, registrationId, removed);
}
} else {
ContentFormat ct = ((CompositeObservation) observation).getResponseContentFormat();
Map<String, String> ctx = observation.getContext();
String serializedObservation = extractSerializedObservation(observation);
JsonNode nodeSerObs = JacksonUtil.toJsonNode(serializedObservation);
((CompositeObservation)observation).getPaths().forEach(path -> {
if (validateObserveResource(path, registrationId)) {
String serializedObs = createSerializedSingleObservation(nodeSerObs, path.toString());
Observation singleObservation = createSingleObservation(registrationId, path, ct, ctx, serializedObs, getTokenGenerator());
updateSingleObservation(registrationId, (SingleObservation) singleObservation, addIfAbsent, removed);
// cancel existing observations for the same path and registration id.
cancelObservation (singleObservation, registrationId, removed);
}
});
}
} finally { } finally {
lock.writeLock().unlock(); lock.writeLock().unlock();
} }
return removed; return removed;
} }
private boolean validateObserveResource(LwM2mPath path, String registrationId){ private void updateObservation(String registrationId, Observation observation, boolean addIfAbsent, List<Observation> removed) {
// check if the resource is readable.
if (path.isResource() || path.isResourceInstance()) {
ObjectModel objectModel = modelProvider.getObjectModel(getRegistration(registrationId)).getObjectModel(path.getObjectId());
ResourceModel resourceModel = objectModel == null ? null : objectModel.resources.get(path.getResourceId());
if (resourceModel == null) {
return false;
} else if (!resourceModel.operations.isReadable()) {
return false;
} else if (path.isResourceInstance() && !resourceModel.multiple) {
return false;
}
}
return true;
}
private void updateSingleObservation (String registrationId, SingleObservation observation, boolean addIfAbsent, List<Observation> removed) {
// Absorption by existing Observations
// Absorption by existing Observations
Observation previousObservation = null; Observation previousObservation = null;
SingleObservation existingObservation = null;
ObservationIdentifier id = observation.getId(); ObservationIdentifier id = observation.getId();
if (addIfAbsent) { if (addIfAbsent) {
if (!obsByToken.containsKey(id)) { if (!obsByToken.containsKey(id)) {
existingObservation = validateByAbsorptionExistingObservations(observation); previousObservation = obsByToken.put(id, observation);
if (existingObservation == null) { } else {
obsByToken.put(id, observation); obsByToken.put(id, observation);
} else if (!existingObservation.getPath().equals(observation.getPath())){
obsByToken.put(id, observation);
previousObservation = obsByToken.get(existingObservation.getId());
}
} }
} else { } else {
previousObservation = obsByToken.put(id, observation); previousObservation = obsByToken.put(id, observation);
} }
if (!tokensByRegId.containsKey(registrationId)) { if (!tokensByRegId.containsKey(registrationId)) {
tokensByRegId.put(registrationId, new HashSet<ObservationIdentifier>()); tokensByRegId.put(registrationId, new HashSet<ObservationIdentifier>());
} }
tokensByRegId.get(registrationId).add(id);
if (existingObservation == null || !existingObservation.getPath().equals(observation.getPath())) {
tokensByRegId.get(registrationId).add(id);
}
// log any collisions // log any collisions
if (addIfAbsent && previousObservation != null) { if (previousObservation != null) {
if (!existingObservation.getPath().equals(observation.getPath())) { removed.add(previousObservation);
removed.add(previousObservation); log.warn("Token collision ? observation [{}] will be replaced by observation [{}] ",
log.warn("Token collision ? observation [{}] will be replaced by observation [{}], that this observation includes input observation [{}]!", previousObservation, observation);
previousObservation, observation, observation);
} else {
log.warn("Token collision ? existing observation [{}] includes input observation [{}]",
existingObservation, observation);
}
} }
} // cancel existing observations for the same path and registration id.
for (Observation obs : unsafeGetObservations(registrationId)) {
private SingleObservation validateByAbsorptionExistingObservations (SingleObservation observation) { if (areTheSamePaths(observation, obs) && !observation.getId().equals(obs.getId())) {
LwM2mPath pathObservation = observation.getPath(); unsafeRemoveObservation(obs.getId());
AtomicReference<SingleObservation> result = new AtomicReference<>(); removed.add(obs);
obsByToken.values().stream().forEach(obs -> {
LwM2mPath pathObs = ((SingleObservation)obs).getPath();
if ((!pathObservation.equals(pathObs) && pathObs.startWith(pathObservation)) || // pathObs = "3/0/9"-> pathObservation = "3"
(pathObservation.equals(pathObs) && !observation.getId().equals(obs.getId()))) {
result.set((SingleObservation)obs);
} else if (!pathObservation.equals(pathObs) && pathObservation.startWith(pathObs)) { // pathObs = "3" -> pathObservation = "3/0/9"
result.set(observation);
} }
});
return result.get();
}
private TokenGenerator getTokenGenerator(){
if (this.tokenGenerator == null) {
this.tokenGenerator = new RandomTokenGenerator(config.getCoapConfig());
} }
return this.tokenGenerator;
} }
public static SingleObservation createSingleObservation(String registrationId, LwM2mPath target, ContentFormat ct, private boolean areTheSamePaths(Observation observation, Observation obs) {
Map<String, String> ctx, String serializedObservation, TokenGenerator tokenGenerator) { if (observation instanceof SingleObservation && obs instanceof SingleObservation) {
Token token = tokenGenerator.createToken(Scope.SHORT_TERM); return ((SingleObservation) observation).getPath().equals(((SingleObservation) obs).getPath());
Map<String, String> protocolData = Collections.emptyMap();
if (serializedObservation != null) {
protocolData = new HashMap<>();
protocolData.put(CTX_CF_OBERSATION, serializedObservation);
} }
return new SingleObservation(new ObservationIdentifier(token.getBytes()), registrationId, target, ct, ctx, protocolData); if (observation instanceof CompositeObservation && obs instanceof CompositeObservation) {
} return ((CompositeObservation) observation).getPaths().equals(((CompositeObservation) obs).getPaths());
public static String createSerializedSingleObservation(JsonNode nodeSerObs, String path){
if (nodeSerObs.has("context")){
((ObjectNode) nodeSerObs.get("context")).put("leshan-path", path + "\n");
return JacksonUtil.toString(nodeSerObs);
} }
return null; return false;
} }
@Override @Override
@ -394,6 +301,12 @@ public class TbInMemoryRegistrationStore implements RegistrationStore, Startable
try { try {
lock.writeLock().lock(); lock.writeLock().lock();
Observation observation = unsafeGetObservation(observationId); Observation observation = unsafeGetObservation(observationId);
if (observation instanceof SingleObservation){
log.trace("(SingleObservation) removeObservation: [{}]", ((SingleObservation)observation).getPath());
} else {
log.trace("(CompositeObservation) removeObservation: [{}]", ((CompositeObservation)observation).getPaths());
}
if (observation != null && registrationId.equals(observation.getRegistrationId())) { if (observation != null && registrationId.equals(observation.getRegistrationId())) {
unsafeRemoveObservation(observationId); unsafeRemoveObservation(observationId);
return observation; return observation;
@ -470,24 +383,6 @@ public class TbInMemoryRegistrationStore implements RegistrationStore, Startable
return obs; return obs;
} }
private void cancelObservation (Observation observation, String registrationId, List<Observation> removed) {
for (Observation obs : unsafeGetObservations(registrationId)) {
cancelExistingObservation(observation, obs, removed);
}
}
private void cancelExistingObservation(Observation observation, Observation obs, List<Observation> removed) {
LwM2mPath pathObservation = ((SingleObservation)observation).getPath();
LwM2mPath pathObs = ((SingleObservation)obs).getPath();
if ((!pathObservation.equals(pathObs) && pathObs.startWith(pathObservation)) || // pathObservation = "3", pathObs = "3/0/9"
(pathObservation.equals(pathObs) && !observation.getId().equals(obs.getId()))) {
unsafeRemoveObservation(obs.getId());
removed.add(obs);
} else if (!pathObservation.equals(pathObs) && pathObservation.startWith(pathObs)) { // pathObservation = "3/0/9", pathObs = "3"
unsafeRemoveObservation(observation.getId());
}
}
private void unsafeRemoveObservation(ObservationIdentifier observationId) { private void unsafeRemoveObservation(ObservationIdentifier observationId) {
Observation removed = obsByToken.remove(observationId); Observation removed = obsByToken.remove(observationId);
if (removed != null) { if (removed != null) {
@ -500,7 +395,6 @@ public class TbInMemoryRegistrationStore implements RegistrationStore, Startable
} }
} }
/** /**
* CancelAllObservation * CancelAllObservation
* @param registrationId * @param registrationId

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

@ -15,7 +15,6 @@
*/ */
package org.thingsboard.server.transport.lwm2m.server.store; package org.thingsboard.server.transport.lwm2m.server.store;
import com.fasterxml.jackson.databind.JsonNode;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.eclipse.californium.core.coap.Token; import org.eclipse.californium.core.coap.Token;
import org.eclipse.californium.core.network.RandomTokenGenerator; import org.eclipse.californium.core.network.RandomTokenGenerator;
@ -33,10 +32,8 @@ import org.eclipse.leshan.core.observation.Observation;
import org.eclipse.leshan.core.observation.ObservationIdentifier; import org.eclipse.leshan.core.observation.ObservationIdentifier;
import org.eclipse.leshan.core.observation.SingleObservation; import org.eclipse.leshan.core.observation.SingleObservation;
import org.eclipse.leshan.core.peer.LwM2mIdentity; import org.eclipse.leshan.core.peer.LwM2mIdentity;
import org.eclipse.leshan.core.request.ContentFormat;
import org.eclipse.leshan.core.util.NamedThreadFactory; import org.eclipse.leshan.core.util.NamedThreadFactory;
import org.eclipse.leshan.core.util.Validate; import org.eclipse.leshan.core.util.Validate;
import org.eclipse.leshan.server.redis.RedisRegistrationStore;
import org.eclipse.leshan.server.redis.serialization.ObservationSerDes; import org.eclipse.leshan.server.redis.serialization.ObservationSerDes;
import org.eclipse.leshan.server.redis.serialization.RegistrationSerDes; import org.eclipse.leshan.server.redis.serialization.RegistrationSerDes;
import org.eclipse.leshan.server.registration.Deregistration; import org.eclipse.leshan.server.registration.Deregistration;
@ -45,15 +42,12 @@ import org.eclipse.leshan.server.registration.Registration;
import org.eclipse.leshan.server.registration.RegistrationStore; import org.eclipse.leshan.server.registration.RegistrationStore;
import org.eclipse.leshan.server.registration.RegistrationUpdate; import org.eclipse.leshan.server.registration.RegistrationUpdate;
import org.eclipse.leshan.server.registration.UpdatedRegistration; import org.eclipse.leshan.server.registration.UpdatedRegistration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.redis.connection.RedisClusterConnection; import org.springframework.data.redis.connection.RedisClusterConnection;
import org.springframework.data.redis.connection.RedisConnection; import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.Cursor; import org.springframework.data.redis.core.Cursor;
import org.springframework.data.redis.core.ScanOptions; import org.springframework.data.redis.core.ScanOptions;
import org.springframework.integration.redis.util.RedisLockRegistry; import org.springframework.integration.redis.util.RedisLockRegistry;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.transport.lwm2m.config.LwM2MTransportServerConfig; import org.thingsboard.server.transport.lwm2m.config.LwM2MTransportServerConfig;
import org.thingsboard.server.transport.lwm2m.server.LwM2mVersionedModelProvider; import org.thingsboard.server.transport.lwm2m.server.LwM2mVersionedModelProvider;
@ -65,19 +59,14 @@ import java.util.Collections;
import java.util.Iterator; import java.util.Iterator;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.concurrent.Executors; import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture; import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.Lock;
import static java.nio.charset.StandardCharsets.UTF_8; import static java.nio.charset.StandardCharsets.UTF_8;
import static org.eclipse.leshan.core.californium.ObserveUtil.extractSerializedObservation;
import static org.thingsboard.server.transport.lwm2m.server.store.TbInMemoryRegistrationStore.createSerializedSingleObservation;
import static org.thingsboard.server.transport.lwm2m.server.store.TbInMemoryRegistrationStore.createSingleObservation;
@Slf4j @Slf4j
public class TbLwM2mRedisRegistrationStore implements RegistrationStore, Startable, Stoppable, Destroyable { public class TbLwM2mRedisRegistrationStore implements RegistrationStore, Startable, Stoppable, Destroyable {
@ -87,8 +76,6 @@ public class TbLwM2mRedisRegistrationStore implements RegistrationStore, Startab
/** Defaut Extra time for registration lifetime in seconds */ /** Defaut Extra time for registration lifetime in seconds */
public static final long DEFAULT_GRACE_PERIOD = 0; public static final long DEFAULT_GRACE_PERIOD = 0;
private static final Logger LOG = LoggerFactory.getLogger(RedisRegistrationStore.class);
// Redis key prefixes // Redis key prefixes
public static final String REG_EP = "REG:EP:"; // (Endpoint => Registration) public static final String REG_EP = "REG:EP:"; // (Endpoint => Registration)
private static final String REG_EP_REGID_IDX = "EP:REGID:"; // secondary index key (Registration ID => Endpoint) private static final String REG_EP_REGID_IDX = "EP:REGID:"; // secondary index key (Registration ID => Endpoint)
@ -131,7 +118,7 @@ public class TbLwM2mRedisRegistrationStore implements RegistrationStore, Startab
public TbLwM2mRedisRegistrationStore(LwM2MTransportServerConfig config, RedisConnectionFactory connectionFactory, long cleanPeriodInSec, long lifetimeGracePeriodInSec, int cleanLimit, LwM2mVersionedModelProvider modelProvider) { public TbLwM2mRedisRegistrationStore(LwM2MTransportServerConfig config, RedisConnectionFactory connectionFactory, long cleanPeriodInSec, long lifetimeGracePeriodInSec, int cleanLimit, LwM2mVersionedModelProvider modelProvider) {
this(config, connectionFactory, Executors.newScheduledThreadPool(1, this(config, connectionFactory, Executors.newScheduledThreadPool(1,
new NamedThreadFactory(String.format("RedisRegistrationStore Cleaner (%ds)", cleanPeriodInSec))), new NamedThreadFactory(String.format("RedisRegistrationStore Cleaner (%ds)", cleanPeriodInSec))),
cleanPeriodInSec, lifetimeGracePeriodInSec, cleanLimit, modelProvider); cleanPeriodInSec, lifetimeGracePeriodInSec, cleanLimit, modelProvider);
} }
@ -281,6 +268,7 @@ public class TbLwM2mRedisRegistrationStore implements RegistrationStore, Startab
return getRegistration(connection, registrationId); return getRegistration(connection, registrationId);
} }
} }
private Registration getRegistration(RedisConnection connection, String registrationId) { private Registration getRegistration(RedisConnection connection, String registrationId) {
byte[] ep = connection.get(toRegIdKey(registrationId)); byte[] ep = connection.get(toRegIdKey(registrationId));
if (ep == null) { if (ep == null) {
@ -494,31 +482,10 @@ public class TbLwM2mRedisRegistrationStore implements RegistrationStore, Startab
Lock lock = null; Lock lock = null;
String lockKey = toLockKey(ep); String lockKey = toLockKey(ep);
try { try {
lock = redisLock.obtain(lockKey); lock = redisLock.obtain(lockKey);
lock.lock(); lock.lock();
if (observation instanceof SingleObservation) { updateObservation(registrationId, observation, addIfAbsent, removed, connection);
if (validateObserveResource(((SingleObservation)observation).getPath(), registrationId)) {
updateSingleObservation(registrationId, (SingleObservation)observation, addIfAbsent, removed, connection);
// cancel existing observations for the same path and registration id.
cancelObservation(observation, registrationId, removed, connection);
}
} else {
ContentFormat ct = ((CompositeObservation) observation).getResponseContentFormat();
Map<String, String> ctx = observation.getContext();
String serializedObservation = extractSerializedObservation(observation);
JsonNode nodeSerObs = JacksonUtil.toJsonNode(serializedObservation);
((CompositeObservation)observation).getPaths().forEach(path -> {
if (validateObserveResource(path, registrationId)) {
String serializedObs = createSerializedSingleObservation(nodeSerObs, path.toString());
SingleObservation singleObservation = createSingleObservation(registrationId, path, ct, ctx, serializedObs, getTokenGenerator());
updateSingleObservation(registrationId, singleObservation, addIfAbsent, removed, connection);
// cancel existing observations for the same path and registration id.
cancelObservation (singleObservation, registrationId, removed, connection);
}
});
}
} finally { } finally {
if (lock != null) { if (lock != null) {
lock.unlock(); lock.unlock();
@ -527,43 +494,21 @@ public class TbLwM2mRedisRegistrationStore implements RegistrationStore, Startab
} }
return removed; return removed;
} }
private void updateObservation(String registrationId, Observation observation, boolean addIfAbsent,
private boolean validateObserveResource(LwM2mPath path, String registrationId){ List<Observation> removed, RedisConnection connection) {
// check if the resource is readable.
if (path.isResource() || path.isResourceInstance()) {
ObjectModel objectModel = modelProvider.getObjectModel(getRegistration(registrationId)).getObjectModel(path.getObjectId());
ResourceModel resourceModel = objectModel == null ? null : objectModel.resources.get(path.getResourceId());
if (resourceModel == null) {
return false;
} else if (!resourceModel.operations.isReadable()) {
return false;
} else if (path.isResourceInstance() && !resourceModel.multiple) {
return false;
}
}
return true;
}
private void updateSingleObservation (String registrationId, SingleObservation observation, boolean addIfAbsent,
List<Observation> removed, RedisConnection connection) {
// Add and Get previous observation // Add and Get previous observation
byte[] previousValue; byte[] previousValue;
byte[] key = toKey(OBS_TKN, observation.getId().getBytes()); byte[] key = toKey(OBS_TKN, observation.getId().getBytes());
byte[] serializeObs = serializeObs(observation); byte[] serializeObs = serializeObs(observation);
// we analyze the present previous value // we analyze the present previous value
SingleObservation existingObservation = null; if (addIfAbsent) {
if (addIfAbsent){
previousValue = connection.stringCommands().get(key); previousValue = connection.stringCommands().get(key);
if (previousValue == null) { if (previousValue == null) {
existingObservation = validateByAbsorptionExistingObservations(observation, connection); connection.stringCommands().set(key, serializeObs);
if (existingObservation == null){ previousValue = serializeObs;
connection.stringCommands().set(key, serializeObs); } else {
} else if(!existingObservation.getPath().equals(observation.getPath())) { connection.stringCommands().set(key, serializeObs);
connection.stringCommands().set(key, serializeObs);
previousValue = serializeObs(existingObservation);
}
} }
} else { } else {
previousValue = connection.stringCommands().getSet(key, serializeObs); previousValue = connection.stringCommands().getSet(key, serializeObs);
@ -573,25 +518,38 @@ public class TbLwM2mRedisRegistrationStore implements RegistrationStore, Startab
connection.listCommands().lPush(toKey(OBS_TKNS_REGID_IDX, registrationId), observation.getId().getBytes()); connection.listCommands().lPush(toKey(OBS_TKNS_REGID_IDX, registrationId), observation.getId().getBytes());
// log any collisions // log any collisions
if (addIfAbsent && previousValue != null) { Observation previousObservation;
if (!existingObservation.getPath().equals(observation.getPath())) { if (previousValue != null && previousValue.length != 0) {
Observation previousObservation = deserializeObs(previousValue); previousObservation = deserializeObs(previousValue);
removed.add(previousObservation); log.warn("Token collision ? observation [{}] will be replaced by observation [{}] ",
LOG.warn("Token collision ? observation [{}] will be replaced by observation [{}], that this observation includes input observation [{}]!", previousObservation, observation);
previousObservation, observation, observation); }
} else {
LOG.warn("Token collision ? existing observation [{}] includes input observation [{}]", // cancel existing observations for the same path and registration id.
existingObservation, observation); for (Observation obs : getObservations(connection, registrationId)) {
if (areTheSamePaths(observation, obs) && !observation.getId().equals(obs.getId())) {
removed.add(obs);
unsafeRemoveObservation(connection, registrationId, obs.getId().getBytes());
} }
} }
} }
private boolean areTheSamePaths(Observation observation, Observation obs) {
if (observation instanceof SingleObservation && obs instanceof SingleObservation) {
return ((SingleObservation) observation).getPath().equals(((SingleObservation) obs).getPath());
}
if (observation instanceof CompositeObservation && obs instanceof CompositeObservation) {
return ((CompositeObservation) observation).getPaths().equals(((CompositeObservation) obs).getPaths());
}
return false;
}
@Override @Override
public Collection<Observation> getObservations(String registrationId) { public Collection<Observation> getObservations(String registrationId) {
try (var connection = connectionFactory.getConnection()) { try (var connection = connectionFactory.getConnection()) {
return getObservations(connection, registrationId); return getObservations(connection, registrationId);
} }
} }
@Override @Override
public Observation getObservation(String registrationId, ObservationIdentifier observationId) { public Observation getObservation(String registrationId, ObservationIdentifier observationId) {
return getObservations(registrationId).stream().filter( return getObservations(registrationId).stream().filter(
@ -654,25 +612,6 @@ public class TbLwM2mRedisRegistrationStore implements RegistrationStore, Startab
return result; return result;
} }
private SingleObservation validateByAbsorptionExistingObservations(SingleObservation observation, RedisConnection connection) {
LwM2mPath pathObservation = observation.getPath();
AtomicReference<SingleObservation> result = new AtomicReference<>();
Collection<Observation> observations = getObservations(connection, observation.getRegistrationId());
observations.stream().forEach(obs -> {
LwM2mPath pathObs = ((SingleObservation)obs).getPath();
if ((!pathObservation.equals(pathObs) && pathObs.startWith(pathObservation)) || // pathObs = "3/0/9"-> pathObservation = "3"
(pathObservation.equals(pathObs) && !observation.getId().equals(obs.getId()))) {
result.set((SingleObservation)obs);
} else if (!pathObservation.equals(pathObs) && pathObservation.startWith(pathObs)) { // pathObs = "3" -> pathObservation = "3/0/9"
result.set(observation);
}
});
return result.get();
}
@Override @Override
public Collection<Observation> removeObservations(String registrationId) { public Collection<Observation> removeObservations(String registrationId) {
try (var connection = connectionFactory.getConnection()) { try (var connection = connectionFactory.getConnection()) {
@ -710,7 +649,7 @@ public class TbLwM2mRedisRegistrationStore implements RegistrationStore, Startab
/* *************** Observation utility functions **************** */ /* *************** Observation utility functions **************** */
private TokenGenerator getTokenGenerator(){ private TokenGenerator getTokenGenerator() {
if (this.tokenGenerator == null) { if (this.tokenGenerator == null) {
this.tokenGenerator = new RandomTokenGenerator(config.getCoapConfig()); this.tokenGenerator = new RandomTokenGenerator(config.getCoapConfig());
} }
@ -751,8 +690,8 @@ public class TbLwM2mRedisRegistrationStore implements RegistrationStore, Startab
} }
private void cancelExistingObservation(RedisConnection connection, Observation observation, Observation obs, List<Observation> removed) { private void cancelExistingObservation(RedisConnection connection, Observation observation, Observation obs, List<Observation> removed) {
LwM2mPath pathObservation = ((SingleObservation)observation).getPath(); LwM2mPath pathObservation = ((SingleObservation) observation).getPath();
LwM2mPath pathObs = ((SingleObservation)obs).getPath(); LwM2mPath pathObs = ((SingleObservation) obs).getPath();
if ((!pathObservation.equals(pathObs) && pathObs.startWith(pathObservation)) || // pathObservation = "3", pathObs = "3/0/9" if ((!pathObservation.equals(pathObs) && pathObs.startWith(pathObservation)) || // pathObservation = "3", pathObs = "3/0/9"
(pathObservation.equals(pathObs) && !observation.getId().equals(obs.getId()))) { (pathObservation.equals(pathObs) && !observation.getId().equals(obs.getId()))) {
unsafeRemoveObservation(connection, obs.getRegistrationId(), obs.getId().getBytes()); unsafeRemoveObservation(connection, obs.getRegistrationId(), obs.getId().getBytes());
@ -803,7 +742,7 @@ public class TbLwM2mRedisRegistrationStore implements RegistrationStore, Startab
try { try {
schedExecutor.awaitTermination(5, TimeUnit.SECONDS); schedExecutor.awaitTermination(5, TimeUnit.SECONDS);
} catch (InterruptedException e) { } catch (InterruptedException e) {
LOG.warn("Destroying RedisRegistrationStore was interrupted.", e); log.warn("Destroying RedisRegistrationStore was interrupted.", e);
} }
} }
@ -824,7 +763,7 @@ public class TbLwM2mRedisRegistrationStore implements RegistrationStore, Startab
} }
} }
} catch (Exception e) { } catch (Exception e) {
LOG.warn("Unexpected Exception while registration cleaning", e); log.warn("Unexpected Exception while registration cleaning", e);
} }
} }
} }

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

@ -372,21 +372,19 @@ public class DefaultLwM2mUplinkMsgHandler extends LwM2MExecutorAwareService impl
LwM2mPath path = instant.getKey(); LwM2mPath path = instant.getKey();
LwM2mNode node = instant.getValue(); LwM2mNode node = instant.getValue();
LwM2mClient lwM2MClient = clientContext.getClientByEndpoint(registration.getEndpoint()); LwM2mClient lwM2MClient = clientContext.getClientByEndpoint(registration.getEndpoint());
String stringPath = convertObjectIdToVersionedId(path.toString(), lwM2MClient); ObjectModel objectModelVersion = lwM2MClient.getObjectModel(path.toString(), modelProvider);
ObjectModel objectModelVersion = lwM2MClient.getObjectModel(stringPath, modelProvider);
if (objectModelVersion != null) { if (objectModelVersion != null) {
if (node instanceof LwM2mObject) { if (node instanceof LwM2mObject) {
LwM2mObject lwM2mObject = (LwM2mObject) node; LwM2mObject lwM2mObject = (LwM2mObject) node;
this.updateObjectResourceValue(lwM2MClient, lwM2mObject, stringPath, 0); this.updateObjectResourceValue(lwM2MClient, lwM2mObject, path.toString(), 0);
} else if (node instanceof LwM2mObjectInstance) { } else if (node instanceof LwM2mObjectInstance) {
LwM2mObjectInstance lwM2mObjectInstance = (LwM2mObjectInstance) node; LwM2mObjectInstance lwM2mObjectInstance = (LwM2mObjectInstance) node;
this.updateObjectInstanceResourceValue(lwM2MClient, lwM2mObjectInstance, stringPath, 0); this.updateObjectInstanceResourceValue(lwM2MClient, lwM2mObjectInstance, path.toString(), 0);
} else if (node instanceof LwM2mResource) { } else if (node instanceof LwM2mResource) {
LwM2mResource lwM2mResource = (LwM2mResource) node; LwM2mResource lwM2mResource = (LwM2mResource) node;
this.updateResourcesValue(lwM2MClient, lwM2mResource, stringPath, Mode.UPDATE, 0); this.updateResourcesValue(lwM2MClient, lwM2mResource, path.toString(), Mode.UPDATE, 0);
} }
} }
tryAwake(lwM2MClient); tryAwake(lwM2MClient);
} }
} }
@ -569,7 +567,6 @@ public class DefaultLwM2mUplinkMsgHandler extends LwM2MExecutorAwareService impl
} }
private void updateObjectInstanceResourceValue(LwM2mClient client, LwM2mObjectInstance lwM2mObjectInstance, String pathIdVer, int code) { private void updateObjectInstanceResourceValue(LwM2mClient client, LwM2mObjectInstance lwM2mObjectInstance, String pathIdVer, int code) {
LwM2mPath pathIds = new LwM2mPath(fromVersionedIdToObjectId(pathIdVer));
lwM2mObjectInstance.getResources().forEach((resourceId, resource) -> { lwM2mObjectInstance.getResources().forEach((resourceId, resource) -> {
String pathRez = pathIdVer + "/" + resourceId; String pathRez = pathIdVer + "/" + resourceId;
this.updateResourcesValue(client, resource, pathRez, Mode.UPDATE, code); this.updateResourcesValue(client, resource, pathRez, Mode.UPDATE, code);
@ -584,11 +581,12 @@ public class DefaultLwM2mUplinkMsgHandler extends LwM2MExecutorAwareService impl
* #4 updateAttrTelemetry * #4 updateAttrTelemetry
* @param lwM2MClient - Registration LwM2M Client * @param lwM2MClient - Registration LwM2M Client
* @param lwM2mResource - LwM2mSingleResource response.getContent() * @param lwM2mResource - LwM2mSingleResource response.getContent()
* @param path - resource * @param stringPath - resource
* @param mode - Replace, Update * @param mode - Replace, Update
*/ */
private void updateResourcesValue(LwM2mClient lwM2MClient, LwM2mResource lwM2mResource, String path, Mode mode, int code) { private void updateResourcesValue(LwM2mClient lwM2MClient, LwM2mResource lwM2mResource, String stringPath, Mode mode, int code) {
Registration registration = lwM2MClient.getRegistration(); Registration registration = lwM2MClient.getRegistration();
String path = convertObjectIdToVersionedId(stringPath, lwM2MClient);
if (lwM2MClient.saveResourceValue(path, lwM2mResource, modelProvider, mode)) { if (lwM2MClient.saveResourceValue(path, lwM2mResource, modelProvider, mode)) {
if (path.equals(convertObjectIdToVersionedId(FW_NAME_ID, lwM2MClient))) { if (path.equals(convertObjectIdToVersionedId(FW_NAME_ID, lwM2MClient))) {
otaService.onCurrentFirmwareNameUpdate(lwM2MClient, (String) lwM2mResource.getValue()); otaService.onCurrentFirmwareNameUpdate(lwM2MClient, (String) lwM2mResource.getValue());
@ -632,17 +630,17 @@ public class DefaultLwM2mUplinkMsgHandler extends LwM2MExecutorAwareService impl
* @param registration - Registration LwM2M Client * @param registration - Registration LwM2M Client
*/ */
public void updateAttrTelemetry(Registration registration, String path) { public void updateAttrTelemetry(Registration registration, String path) {
log.trace("UpdateAttrTelemetry paths [{}]", path);
try { try {
ResultsAddKeyValueProto results = this.getParametersFromProfile(registration, path); 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); SessionInfoProto sessionInfo = this.getSessionInfoOrCloseSession(registration);
if (results != null && sessionInfo != null) { if (results != null && sessionInfo != null) {
if (results.getResultAttributes().size() > 0) { if (results.getResultAttributes().size() > 0) {
log.trace("UpdateAttribute paths [{}] value [{}]", path, results.getResultAttributes().get(0).toString());
this.helper.sendParametersOnThingsboardAttribute(results.getResultAttributes(), sessionInfo); this.helper.sendParametersOnThingsboardAttribute(results.getResultAttributes(), sessionInfo);
} }
if (results.getResultTelemetries().size() > 0) { if (results.getResultTelemetries().size() > 0) {
log.trace("UpdateTelemetry paths [{}] value [{}]", path, results.getResultTelemetries().get(0).toString());
this.helper.sendParametersOnThingsboardTelemetry(results.getResultTelemetries(), sessionInfo); this.helper.sendParametersOnThingsboardTelemetry(results.getResultTelemetries(), sessionInfo);
} }
} }

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

@ -156,21 +156,19 @@ public class LwM2MTransportUtil {
} }
public static String convertObjectIdToVersionedId(String path, LwM2mClient lwM2MClient) { public static String convertObjectIdToVersionedId(String path, LwM2mClient lwM2MClient) {
String ver = String.valueOf(lwM2MClient.getSupportedObjectVersion(new LwM2mPath(path).getObjectId())); String[] keyArray = path.split(LWM2M_SEPARATOR_PATH);
return convertObjectIdToVerId(path, ver); if (keyArray.length > 1) {
} try {
public static String convertObjectIdToVerId(String path, String ver) { Integer objectId = Integer.valueOf((keyArray[1].split(LWM2M_SEPARATOR_KEY))[0]);
ver = ver != null ? ver : TbLwM2mVersion.VERSION_1_0.getVersion().toString(); String ver = String.valueOf(lwM2MClient.getSupportedObjectVersion(objectId));
try { ver = ver != null ? ver : TbLwM2mVersion.VERSION_1_0.getVersion().toString();
String[] keyArray = path.split(LWM2M_SEPARATOR_PATH); keyArray[1] = String.valueOf(keyArray[1]).contains(LWM2M_SEPARATOR_KEY) ? keyArray[1] : keyArray[1] + LWM2M_SEPARATOR_KEY + ver;
if (keyArray.length > 1) {
keyArray[1] = keyArray[1] + LWM2M_SEPARATOR_KEY + ver;
return StringUtils.join(keyArray, LWM2M_SEPARATOR_PATH); return StringUtils.join(keyArray, LWM2M_SEPARATOR_PATH);
} else { } catch (Exception e) {
return path; return null;
} }
} catch (Exception e) { } else {
return null; return path;
} }
} }
@ -214,20 +212,20 @@ public class LwM2MTransportUtil {
} }
public static Map<Integer, Object> convertMultiResourceValuesFromRpcBody(Object value, ResourceModel.Type type, String versionedId) throws Exception { public static Map<Integer, Object> convertMultiResourceValuesFromRpcBody(Object value, ResourceModel.Type type, String versionedId) throws Exception {
String valueJsonStr = JacksonUtil.toString(value); String valueJsonStr = JacksonUtil.toString(value);
JsonElement element = JsonUtils.parse(valueJsonStr); JsonElement element = JsonUtils.parse(valueJsonStr);
return convertMultiResourceValuesFromJson(element, type, versionedId); return convertMultiResourceValuesFromJson(element, type, versionedId);
} }
public static Map<Integer, Object> convertMultiResourceValuesFromJson(JsonElement newValProto, ResourceModel.Type type, String versionedId) { public static Map<Integer, Object> convertMultiResourceValuesFromJson(JsonElement newValProto, ResourceModel.Type type, String versionedId) {
Map<Integer, Object> newValues = new HashMap<>(); Map<Integer, Object> newValues = new HashMap<>();
newValProto.getAsJsonObject().entrySet().forEach((obj) -> { newValProto.getAsJsonObject().entrySet().forEach((obj) -> {
newValues.put(Integer.valueOf(obj.getKey()), convertValueByTypeResource (obj.getValue().getAsString(), type, versionedId)); newValues.put(Integer.valueOf(obj.getKey()), convertValueByTypeResource(obj.getValue().getAsString(), type, versionedId));
}); });
return newValues; return newValues;
} }
public static Object convertValueByTypeResource (String value, ResourceModel.Type type, String versionedId) { public static Object convertValueByTypeResource(String value, ResourceModel.Type type, String versionedId) {
return LwM2mValueConverterImpl.getInstance().convertValue(value, return LwM2mValueConverterImpl.getInstance().convertValue(value,
STRING, type, new LwM2mPath(fromVersionedIdToObjectId(versionedId))); STRING, type, new LwM2mPath(fromVersionedIdToObjectId(versionedId)));
} }
@ -356,7 +354,7 @@ public class LwM2MTransportUtil {
serverCoapConfig.setTransient(DTLS_CONNECTION_ID_LENGTH); serverCoapConfig.setTransient(DTLS_CONNECTION_ID_LENGTH);
serverCoapConfig.setTransient(DTLS_CONNECTION_ID_NODE_ID); serverCoapConfig.setTransient(DTLS_CONNECTION_ID_NODE_ID);
serverCoapConfig.set(DTLS_CONNECTION_ID_LENGTH, cIdLength); serverCoapConfig.set(DTLS_CONNECTION_ID_LENGTH, cIdLength);
if ( cIdLength > 4) { if (cIdLength > 4) {
serverCoapConfig.set(DTLS_CONNECTION_ID_NODE_ID, 0); serverCoapConfig.set(DTLS_CONNECTION_ID_NODE_ID, 0);
} else { } else {
serverCoapConfig.set(DTLS_CONNECTION_ID_NODE_ID, null); serverCoapConfig.set(DTLS_CONNECTION_ID_NODE_ID, null);

2
dao/src/main/java/org/thingsboard/server/dao/alarm/AlarmDao.java

@ -79,7 +79,7 @@ public interface AlarmDao extends Dao<Alarm> {
PageData<AlarmId> findAlarmsIdsByEndTsBeforeAndTenantId(Long time, TenantId tenantId, PageLink pageLink); PageData<AlarmId> findAlarmsIdsByEndTsBeforeAndTenantId(Long time, TenantId tenantId, PageLink pageLink);
PageData<AlarmId> findAlarmIdsByAssigneeId(TenantId tenantId, UUID userId, PageLink pageLink); PageData<TbPair<UUID, Long>> findAlarmIdsByAssigneeId(TenantId tenantId, UserId userId, long createdTimeOffset, AlarmId idOffset, int limit);
PageData<TbPair<UUID, Long>> findAlarmIdsByOriginatorId(TenantId tenantId, EntityId originatorId, long createdTimeOffset, AlarmId idOffset, int limit); PageData<TbPair<UUID, Long>> findAlarmIdsByOriginatorId(TenantId tenantId, EntityId originatorId, long createdTimeOffset, AlarmId idOffset, int limit);

7
dao/src/main/java/org/thingsboard/server/dao/alarm/BaseAlarmService.java

@ -308,10 +308,10 @@ public class BaseAlarmService extends AbstractCachedEntityService<TenantId, Page
} }
@Override @Override
public PageData<AlarmId> findAlarmIdsByAssigneeId(TenantId tenantId, UserId userId, PageLink pageLink) { public List<TbPair<UUID, Long>> findAlarmIdsByAssigneeId(TenantId tenantId, UserId userId, long createdTimeOffset, AlarmId idOffset, int limit) {
log.trace("[{}] Executing findAlarmIdsByAssigneeId [{}]", tenantId, userId); log.trace("[{}] Executing findAlarmIdsByAssigneeId [{}][{}]", tenantId, userId, idOffset);
validateId(userId, id -> "Incorrect userId " + id); validateId(userId, id -> "Incorrect userId " + id);
return alarmDao.findAlarmIdsByAssigneeId(tenantId, userId.getId(), pageLink); return alarmDao.findAlarmIdsByAssigneeId(tenantId, userId, createdTimeOffset, idOffset, limit).getData();
} }
@Override @Override
@ -476,4 +476,5 @@ public class BaseAlarmService extends AbstractCachedEntityService<TenantId, Page
request.setEndTs(request.getStartTs()); request.setEndTs(request.getStartTs());
} }
} }
} }

19
dao/src/main/java/org/thingsboard/server/dao/sql/alarm/AlarmRepository.java

@ -331,8 +331,23 @@ public interface AlarmRepository extends JpaRepository<AlarmEntity, UUID> {
@Query(value = "SELECT a FROM AlarmInfoEntity a WHERE a.tenantId = :tenantId AND a.id = :alarmId") @Query(value = "SELECT a FROM AlarmInfoEntity a WHERE a.tenantId = :tenantId AND a.id = :alarmId")
AlarmInfoEntity findAlarmInfoById(@Param("tenantId") UUID tenantId, @Param("alarmId") UUID alarmId); AlarmInfoEntity findAlarmInfoById(@Param("tenantId") UUID tenantId, @Param("alarmId") UUID alarmId);
@Query("SELECT a.id FROM AlarmEntity a WHERE a.tenantId = :tenantId AND a.assigneeId = :assigneeId") // using Slice so that count query is not executed
Page<UUID> findAlarmIdsByAssigneeId(@Param("tenantId") UUID tenantId, @Param("assigneeId") UUID assigneeId, Pageable pageable); @Query("SELECT new org.thingsboard.server.common.data.util.TbPair(a.id, a.createdTime) " +
"FROM AlarmEntity a WHERE a.tenantId = :tenantId AND a.assigneeId = :assigneeId")
Slice<TbPair<UUID, Long>> findAlarmIdsByAssigneeId(@Param("tenantId") UUID tenantId,
@Param("assigneeId") UUID assigneeId,
Pageable pageable);
// using Slice so that count query is not executed
@Query("SELECT new org.thingsboard.server.common.data.util.TbPair(a.id, a.createdTime) " +
"FROM AlarmEntity a WHERE a.tenantId = :tenantId AND a.assigneeId = :assigneeId " +
"AND (a.createdTime > :createdTimeOffset OR " +
"(a.createdTime = :createdTimeOffset AND a.id > :idOffset))")
Slice<TbPair<UUID, Long>> findAlarmIdsByAssigneeId(@Param("tenantId") UUID tenantId,
@Param("assigneeId") UUID assigneeId,
@Param("createdTimeOffset") long createdTimeOffset,
@Param("idOffset") UUID idOffset,
Pageable pageable);
// using Slice so that count query is not executed // using Slice so that count query is not executed
@Query("SELECT new org.thingsboard.server.common.data.util.TbPair(a.id, a.createdTime) " + @Query("SELECT new org.thingsboard.server.common.data.util.TbPair(a.id, a.createdTime) " +

12
dao/src/main/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmDao.java

@ -296,9 +296,15 @@ public class JpaAlarmDao extends JpaAbstractDao<AlarmEntity, Alarm> implements A
} }
@Override @Override
public PageData<AlarmId> findAlarmIdsByAssigneeId(TenantId tenantId, UUID userId, PageLink pageLink) { public PageData<TbPair<UUID, Long>> findAlarmIdsByAssigneeId(TenantId tenantId, UserId userId, long createdTimeOffset, AlarmId idOffset, int limit) {
return DaoUtil.pageToPageData(alarmRepository.findAlarmIdsByAssigneeId(tenantId.getId(), userId, DaoUtil.toPageable(pageLink))) Slice<TbPair<UUID, Long>> result;
.mapData(AlarmId::new); Pageable pageRequest = toPageable(new PageLink(limit), List.of(SortOrder.of("createdTime", ASC), SortOrder.of("id", ASC)));
if (idOffset == null) {
result = alarmRepository.findAlarmIdsByAssigneeId(tenantId.getId(), userId.getId(), pageRequest);
} else {
result = alarmRepository.findAlarmIdsByAssigneeId(tenantId.getId(), userId.getId(), createdTimeOffset, idOffset.getId(), pageRequest);
}
return DaoUtil.pageToPageData(result);
} }
@Override @Override

320
msa/black-box-tests/src/test/java/org/thingsboard/server/msa/AbstractLwm2mClientTest.java

@ -1,320 +0,0 @@
/**
* Copyright © 2016-2024 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.msa;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.IOUtils;
import org.eclipse.leshan.client.object.Security;
import org.eclipse.leshan.core.util.Hex;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.common.util.ThingsBoardThreadFactory;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.DeviceProfile;
import org.thingsboard.server.common.data.DeviceProfileProvisionType;
import org.thingsboard.server.common.data.DeviceProfileType;
import org.thingsboard.server.common.data.DeviceTransportType;
import org.thingsboard.server.common.data.ResourceType;
import org.thingsboard.server.common.data.TbResource;
import org.thingsboard.server.common.data.device.credentials.lwm2m.LwM2MBootstrapClientCredentials;
import org.thingsboard.server.common.data.device.credentials.lwm2m.LwM2MClientCredential;
import org.thingsboard.server.common.data.device.credentials.lwm2m.LwM2MDeviceCredentials;
import org.thingsboard.server.common.data.device.credentials.lwm2m.NoSecBootstrapClientCredential;
import org.thingsboard.server.common.data.device.credentials.lwm2m.NoSecClientCredential;
import org.thingsboard.server.common.data.device.credentials.lwm2m.PSKBootstrapClientCredential;
import org.thingsboard.server.common.data.device.credentials.lwm2m.PSKClientCredential;
import org.thingsboard.server.common.data.device.profile.DefaultDeviceProfileConfiguration;
import org.thingsboard.server.common.data.device.profile.DeviceProfileData;
import org.thingsboard.server.common.data.device.profile.DisabledDeviceProfileProvisionConfiguration;
import org.thingsboard.server.common.data.device.profile.Lwm2mDeviceProfileTransportConfiguration;
import org.thingsboard.server.common.data.device.profile.lwm2m.OtherConfiguration;
import org.thingsboard.server.common.data.device.profile.lwm2m.TelemetryMappingConfiguration;
import org.thingsboard.server.common.data.device.profile.lwm2m.bootstrap.LwM2MBootstrapServerCredential;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.security.DeviceCredentials;
import org.thingsboard.server.common.data.security.DeviceCredentialsType;
import org.thingsboard.server.msa.connectivity.lwm2m.LwM2MTestClient;
import org.thingsboard.server.msa.connectivity.lwm2m.Lwm2mTestHelper.LwM2MClientState;
import java.net.ServerSocket;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import static org.assertj.core.api.Assertions.assertThat;
import static org.awaitility.Awaitility.await;
import static org.eclipse.leshan.client.object.Security.psk;
import static org.thingsboard.server.msa.connectivity.lwm2m.Lwm2mTestHelper.CLIENT_ENDPOINT_NO_SEC;
import static org.thingsboard.server.msa.connectivity.lwm2m.Lwm2mTestHelper.CLIENT_ENDPOINT_PSK;
import static org.thingsboard.server.msa.connectivity.lwm2m.Lwm2mTestHelper.CLIENT_LWM2M_SETTINGS;
import static org.thingsboard.server.msa.connectivity.lwm2m.Lwm2mTestHelper.CLIENT_PSK_IDENTITY;
import static org.thingsboard.server.msa.connectivity.lwm2m.Lwm2mTestHelper.CLIENT_PSK_KEY;
import static org.thingsboard.server.msa.connectivity.lwm2m.Lwm2mTestHelper.LwM2MClientState.ON_INIT;
import static org.thingsboard.server.msa.connectivity.lwm2m.Lwm2mTestHelper.LwM2MClientState.ON_REGISTRATION_STARTED;
import static org.thingsboard.server.msa.connectivity.lwm2m.Lwm2mTestHelper.LwM2MClientState.ON_REGISTRATION_SUCCESS;
import static org.thingsboard.server.msa.connectivity.lwm2m.Lwm2mTestHelper.LwM2MClientState.ON_UPDATE_SUCCESS;
import static org.thingsboard.server.msa.connectivity.lwm2m.Lwm2mTestHelper.OBSERVE_ATTRIBUTES_WITHOUT_PARAMS;
import static org.thingsboard.server.msa.connectivity.lwm2m.Lwm2mTestHelper.SECURE_URI;
import static org.thingsboard.server.msa.connectivity.lwm2m.Lwm2mTestHelper.SECURITY_NO_SEC;
import static org.thingsboard.server.msa.connectivity.lwm2m.Lwm2mTestHelper.resources;
import static org.thingsboard.server.msa.connectivity.lwm2m.Lwm2mTestHelper.shortServerId;
@Slf4j
public class AbstractLwm2mClientTest extends AbstractContainerTest{
protected ScheduledExecutorService executor;
protected Security security;
protected final PageLink pageLink = new PageLink(30);
protected TenantId tenantId;
protected DeviceProfile lwm2mDeviceProfile;
protected Device lwM2MDeviceTest;
protected LwM2MTestClient lwM2MTestClient;
public final Set<LwM2MClientState> expectedStatusesRegistrationLwm2mSuccess = new HashSet<>(Arrays.asList(ON_INIT, ON_REGISTRATION_STARTED, ON_REGISTRATION_SUCCESS));
public void connectLwm2mClientNoSec() throws Exception {
LwM2MDeviceCredentials deviceCredentials = getDeviceCredentialsNoSec(createNoSecClientCredentials(CLIENT_ENDPOINT_NO_SEC));
basicTestConnection(SECURITY_NO_SEC,
deviceCredentials,
CLIENT_ENDPOINT_NO_SEC,
"TestConnection Lwm2m NoSec (msa)");
}
public void connectLwm2mClientPsk() throws Exception {
String clientEndpoint = CLIENT_ENDPOINT_PSK;
String identity = CLIENT_PSK_IDENTITY;
String keyPsk = CLIENT_PSK_KEY;
PSKClientCredential clientCredentials = new PSKClientCredential();
clientCredentials.setEndpoint(clientEndpoint);
clientCredentials.setIdentity(identity);
clientCredentials.setKey(keyPsk);
Security security = psk(SECURE_URI,
shortServerId,
identity.getBytes(StandardCharsets.UTF_8),
Hex.decodeHex(keyPsk.toCharArray()));
LwM2MDeviceCredentials deviceCredentials = getDeviceCredentialsSecurePsk(clientCredentials);
basicTestConnection(security,
deviceCredentials,
clientEndpoint,
"TestConnection Lwm2m Rpc (msa)");
}
public void basicTestConnection(Security security,
LwM2MDeviceCredentials deviceCredentials,
String clientEndpoint, String alias) throws Exception {
// create lwm2mClient and lwM2MDevice
lwM2MDeviceTest = createDeviceWithCredentials(deviceCredentials, clientEndpoint);
lwM2MTestClient = createNewClient(security, clientEndpoint, executor);
LwM2MClientState finishState = ON_REGISTRATION_SUCCESS;
await(alias + " - " + ON_REGISTRATION_STARTED)
.atMost(40, TimeUnit.SECONDS)
.until(() -> {
log.warn("msa basicTestConnection started -> finishState: [{}] states: {}", finishState, lwM2MTestClient.getClientStates());
return lwM2MTestClient.getClientStates().contains(finishState) || lwM2MTestClient.getClientStates().contains(ON_REGISTRATION_STARTED);
});
await(alias + " - " + ON_UPDATE_SUCCESS)
.atMost(40, TimeUnit.SECONDS)
.until(() -> {
log.warn("msa basicTestConnection update -> finishState: [{}] states: {}", finishState, lwM2MTestClient.getClientStates());
return lwM2MTestClient.getClientStates().contains(finishState) || lwM2MTestClient.getClientStates().contains(ON_UPDATE_SUCCESS);
});
assertThat(lwM2MTestClient.getClientStates()).containsAll(expectedStatusesRegistrationLwm2mSuccess);
}
public LwM2MTestClient createNewClient(Security security,
String endpoint, ScheduledExecutorService executor) throws Exception {
this.executor = executor;
LwM2MTestClient lwM2MTestClient = new LwM2MTestClient(endpoint);
try (ServerSocket socket = new ServerSocket(0)) {
int clientPort = socket.getLocalPort();
lwM2MTestClient.init(security, clientPort);
}
return lwM2MTestClient;
}
protected void destroyAfter(){
clientDestroy();
deviceDestroy();
deviceProfileDestroy();
if (executor != null) {
executor.shutdown();
}
}
protected void clientDestroy() {
try {
if (lwM2MTestClient != null) {
lwM2MTestClient.destroy();
}
} catch (Exception e) {
log.error("Failed client Destroy", e);
}
}
protected void deviceDestroy() {
try {
if (lwM2MDeviceTest != null) {
testRestClient.deleteDeviceIfExists(lwM2MDeviceTest.getId());
}
} catch (Exception e) {
log.error("Failed device Delete", e);
}
}
protected void initTest(String deviceProfileName) throws Exception {
if (executor != null) {
executor.shutdown();
}
executor = Executors.newScheduledThreadPool(10, ThingsBoardThreadFactory.forName("test-scheduled-" + deviceProfileName));
lwm2mDeviceProfile = getDeviceProfile(deviceProfileName);
tenantId = lwm2mDeviceProfile.getTenantId();
for (String resourceName : resources) {
TbResource lwModel = new TbResource();
lwModel.setResourceType(ResourceType.LWM2M_MODEL);
lwModel.setTitle(resourceName);
lwModel.setFileName(resourceName);
lwModel.setTenantId(tenantId);
byte[] bytes = IOUtils.toByteArray(AbstractLwm2mClientTest.class.getClassLoader().getResourceAsStream("lwm2m-registry/" + resourceName));
lwModel.setData(bytes);
testRestClient.postTbResourceIfNotExists(lwModel);
}
}
protected DeviceProfile getDeviceProfile(String deviceProfileName) throws Exception {
DeviceProfile deviceProfile = getDeviceProfileIfExists(deviceProfileName);
if (deviceProfile == null) {
deviceProfile = testRestClient.postDeviceProfile(createDeviceProfile(deviceProfileName));
}
return deviceProfile;
}
protected DeviceProfile getDeviceProfileIfExists(String deviceProfileName) throws Exception {
return testRestClient.getDeviceProfiles(pageLink).getData().stream()
.filter(x -> x.getName().equals(deviceProfileName))
.findFirst()
.orElse(null);
}
protected DeviceProfile createDeviceProfile(String deviceProfileName) throws Exception {
DeviceProfile deviceProfile = new DeviceProfile();
deviceProfile.setName(deviceProfileName);
deviceProfile.setType(DeviceProfileType.DEFAULT);
deviceProfile.setTransportType(DeviceTransportType.LWM2M);
deviceProfile.setProvisionType(DeviceProfileProvisionType.DISABLED);
deviceProfile.setDescription(deviceProfile.getName());
DeviceProfileData deviceProfileData = new DeviceProfileData();
deviceProfileData.setConfiguration(new DefaultDeviceProfileConfiguration());
deviceProfileData.setProvisionConfiguration(new DisabledDeviceProfileProvisionConfiguration(null));
deviceProfileData.setTransportConfiguration(getTransportConfiguration());
deviceProfile.setProfileData(deviceProfileData);
return deviceProfile;
}
protected void deviceProfileDestroy(){
try {
if (lwm2mDeviceProfile != null) {
testRestClient.deleteDeviceProfileIfExists(lwm2mDeviceProfile);
}
} catch (Exception e) {
log.error("Failed deviceProfile Delete", e);
}
}
protected Device createDeviceWithCredentials(LwM2MDeviceCredentials deviceCredentials, String clientEndpoint) throws Exception {
Device device = createDevice(deviceCredentials, clientEndpoint);
return device;
}
protected Device createDevice(LwM2MDeviceCredentials credentials, String clientEndpoint) throws Exception {
Device device = testRestClient.getDeviceByNameIfExists(clientEndpoint);
if (device == null) {
device = new Device();
device.setName(clientEndpoint);
device.setDeviceProfileId(lwm2mDeviceProfile.getId());
device.setTenantId(tenantId);
device = testRestClient.postDevice("", device);
}
DeviceCredentials deviceCredentials = testRestClient.getDeviceCredentialsByDeviceId(device.getId());
deviceCredentials.setCredentialsType(DeviceCredentialsType.LWM2M_CREDENTIALS);
deviceCredentials.setCredentialsValue(JacksonUtil.toString(credentials));
deviceCredentials = testRestClient.postDeviceCredentials(deviceCredentials);
assertThat(deviceCredentials).isNotNull();
return device;
}
protected LwM2MDeviceCredentials getDeviceCredentialsNoSec(LwM2MClientCredential clientCredentials) {
LwM2MDeviceCredentials credentials = new LwM2MDeviceCredentials();
credentials.setClient(clientCredentials);
LwM2MBootstrapClientCredentials bootstrapCredentials = new LwM2MBootstrapClientCredentials();
NoSecBootstrapClientCredential serverCredentials = new NoSecBootstrapClientCredential();
bootstrapCredentials.setBootstrapServer(serverCredentials);
bootstrapCredentials.setLwm2mServer(serverCredentials);
credentials.setBootstrap(bootstrapCredentials);
return credentials;
}
public NoSecClientCredential createNoSecClientCredentials(String clientEndpoint) {
NoSecClientCredential clientCredentials = new NoSecClientCredential();
clientCredentials.setEndpoint(clientEndpoint);
return clientCredentials;
}
protected Lwm2mDeviceProfileTransportConfiguration getTransportConfiguration() {
List<LwM2MBootstrapServerCredential> bootstrapServerCredentials = new ArrayList<>();
Lwm2mDeviceProfileTransportConfiguration transportConfiguration = new Lwm2mDeviceProfileTransportConfiguration();
TelemetryMappingConfiguration observeAttrConfiguration = JacksonUtil.fromString(OBSERVE_ATTRIBUTES_WITHOUT_PARAMS, TelemetryMappingConfiguration.class);
OtherConfiguration clientLwM2mSettings = JacksonUtil.fromString(CLIENT_LWM2M_SETTINGS, OtherConfiguration.class);
transportConfiguration.setBootstrapServerUpdateEnable(true);
transportConfiguration.setObserveAttr(observeAttrConfiguration);
transportConfiguration.setClientLwM2mSettings(clientLwM2mSettings);
transportConfiguration.setBootstrap(bootstrapServerCredentials);
return transportConfiguration;
}
protected LwM2MDeviceCredentials getDeviceCredentialsSecurePsk(LwM2MClientCredential clientCredentials) {
LwM2MDeviceCredentials credentials = new LwM2MDeviceCredentials();
credentials.setClient(clientCredentials);
LwM2MBootstrapClientCredentials bootstrapCredentials;
bootstrapCredentials = getBootstrapClientCredentialsPsk(clientCredentials);
credentials.setBootstrap(bootstrapCredentials);
return credentials;
}
private LwM2MBootstrapClientCredentials getBootstrapClientCredentialsPsk(LwM2MClientCredential clientCredentials) {
LwM2MBootstrapClientCredentials bootstrapCredentials = new LwM2MBootstrapClientCredentials();
PSKBootstrapClientCredential serverCredentials = new PSKBootstrapClientCredential();
if (clientCredentials != null) {
serverCredentials.setClientSecretKey(((PSKClientCredential) clientCredentials).getKey());
serverCredentials.setClientPublicKeyOrId(((PSKClientCredential) clientCredentials).getIdentity());
}
bootstrapCredentials.setBootstrapServer(serverCredentials);
bootstrapCredentials.setLwm2mServer(serverCredentials);
return bootstrapCredentials;
}
}

11
msa/black-box-tests/src/test/java/org/thingsboard/server/msa/TestRestClient.java

@ -16,6 +16,7 @@
package org.thingsboard.server.msa; package org.thingsboard.server.msa;
import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import io.restassured.RestAssured; import io.restassured.RestAssured;
import io.restassured.common.mapper.TypeRef; import io.restassured.common.mapper.TypeRef;
import io.restassured.config.HeaderConfig; import io.restassured.config.HeaderConfig;
@ -116,6 +117,16 @@ public class TestRestClient {
.as(Device.class); .as(Device.class);
} }
public ObjectNode postRpcLwm2mParams(String deviceIdStr, String body) {
return given().spec(requestSpec).body(body)
.post("/api/plugins/rpc/twoway/" + deviceIdStr)
.then()
.statusCode(HTTP_OK)
.extract()
.as(ObjectNode.class);
}
public Device getDeviceByName(String deviceName) { public Device getDeviceByName(String deviceName) {
return given().spec(requestSpec).pathParam("deviceName", deviceName) return given().spec(requestSpec).pathParam("deviceName", deviceName)
.get("/api/tenant/devices?deviceName={deviceName}") .get("/api/tenant/devices?deviceName={deviceName}")

493
msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/lwm2m/AbstractLwm2mClientTest.java

@ -0,0 +1,493 @@
/**
* Copyright © 2016-2024 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.msa.connectivity.lwm2m;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.common.collect.Sets;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonParser;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.IOUtils;
import org.eclipse.leshan.client.object.Security;
import org.eclipse.leshan.core.ResponseCode;
import org.eclipse.leshan.core.util.Hex;
import org.testcontainers.shaded.org.apache.commons.lang3.RandomStringUtils;
import org.testng.Assert;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.common.util.ThingsBoardThreadFactory;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.DeviceProfile;
import org.thingsboard.server.common.data.DeviceProfileProvisionType;
import org.thingsboard.server.common.data.DeviceProfileType;
import org.thingsboard.server.common.data.DeviceTransportType;
import org.thingsboard.server.common.data.ResourceType;
import org.thingsboard.server.common.data.TbResource;
import org.thingsboard.server.common.data.device.credentials.lwm2m.LwM2MBootstrapClientCredentials;
import org.thingsboard.server.common.data.device.credentials.lwm2m.LwM2MClientCredential;
import org.thingsboard.server.common.data.device.credentials.lwm2m.LwM2MDeviceCredentials;
import org.thingsboard.server.common.data.device.credentials.lwm2m.NoSecBootstrapClientCredential;
import org.thingsboard.server.common.data.device.credentials.lwm2m.NoSecClientCredential;
import org.thingsboard.server.common.data.device.credentials.lwm2m.PSKBootstrapClientCredential;
import org.thingsboard.server.common.data.device.credentials.lwm2m.PSKClientCredential;
import org.thingsboard.server.common.data.device.profile.DefaultDeviceProfileConfiguration;
import org.thingsboard.server.common.data.device.profile.DeviceProfileData;
import org.thingsboard.server.common.data.device.profile.DisabledDeviceProfileProvisionConfiguration;
import org.thingsboard.server.common.data.device.profile.Lwm2mDeviceProfileTransportConfiguration;
import org.thingsboard.server.common.data.device.profile.lwm2m.OtherConfiguration;
import org.thingsboard.server.common.data.device.profile.lwm2m.TelemetryMappingConfiguration;
import org.thingsboard.server.common.data.device.profile.lwm2m.bootstrap.LwM2MBootstrapServerCredential;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.DeviceProfileId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.security.DeviceCredentials;
import org.thingsboard.server.common.data.security.DeviceCredentialsType;
import org.thingsboard.server.msa.AbstractContainerTest;
import org.thingsboard.server.msa.WsClient;
import org.thingsboard.server.msa.connectivity.lwm2m.client.LwM2MTestClient;
import org.thingsboard.server.msa.connectivity.lwm2m.client.Lwm2mTestHelper.LwM2MClientState;
import org.thingsboard.server.msa.mapper.WsTelemetryResponse;
import java.net.ServerSocket;
import java.nio.charset.StandardCharsets;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import static org.assertj.core.api.Assertions.assertThat;
import static org.awaitility.Awaitility.await;
import static org.eclipse.leshan.client.object.Security.psk;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.testng.AssertJUnit.assertEquals;
import static org.thingsboard.server.msa.connectivity.lwm2m.client.Lwm2mTestHelper.CLIENT_ENDPOINT_PSK;
import static org.thingsboard.server.msa.connectivity.lwm2m.client.Lwm2mTestHelper.CLIENT_LWM2M_SETTINGS;
import static org.thingsboard.server.msa.connectivity.lwm2m.client.Lwm2mTestHelper.CLIENT_PSK_IDENTITY;
import static org.thingsboard.server.msa.connectivity.lwm2m.client.Lwm2mTestHelper.CLIENT_PSK_KEY;
import static org.thingsboard.server.msa.connectivity.lwm2m.client.Lwm2mTestHelper.LwM2MClientState.ON_INIT;
import static org.thingsboard.server.msa.connectivity.lwm2m.client.Lwm2mTestHelper.LwM2MClientState.ON_REGISTRATION_STARTED;
import static org.thingsboard.server.msa.connectivity.lwm2m.client.Lwm2mTestHelper.LwM2MClientState.ON_REGISTRATION_SUCCESS;
import static org.thingsboard.server.msa.connectivity.lwm2m.client.Lwm2mTestHelper.LwM2MClientState.ON_UPDATE_SUCCESS;
import static org.thingsboard.server.msa.connectivity.lwm2m.client.Lwm2mTestHelper.OBSERVE_ATTRIBUTES_WITH_PARAMS;
import static org.thingsboard.server.msa.connectivity.lwm2m.client.Lwm2mTestHelper.RESOURCE_ID_NAME_19_0_0;
import static org.thingsboard.server.msa.connectivity.lwm2m.client.Lwm2mTestHelper.RESOURCE_ID_NAME_19_0_2;
import static org.thingsboard.server.msa.connectivity.lwm2m.client.Lwm2mTestHelper.RESOURCE_ID_NAME_19_1_0;
import static org.thingsboard.server.msa.connectivity.lwm2m.client.Lwm2mTestHelper.RESOURCE_ID_NAME_3_14;
import static org.thingsboard.server.msa.connectivity.lwm2m.client.Lwm2mTestHelper.RESOURCE_ID_NAME_3_9;
import static org.thingsboard.server.msa.connectivity.lwm2m.client.Lwm2mTestHelper.SECURE_URI;
import static org.thingsboard.server.msa.connectivity.lwm2m.client.Lwm2mTestHelper.SECURITY_NO_SEC;
import static org.thingsboard.server.msa.connectivity.lwm2m.client.Lwm2mTestHelper.resources;
import static org.thingsboard.server.msa.connectivity.lwm2m.client.Lwm2mTestHelper.shortServerId;
@Slf4j
public class AbstractLwm2mClientTest extends AbstractContainerTest {
protected ScheduledExecutorService executor;
protected Security security;
protected final PageLink pageLink = new PageLink(30);
protected TenantId tenantId;
public final Set<LwM2MClientState> expectedStatusesRegistrationLwm2mSuccess = new HashSet<>(Arrays.asList(ON_INIT, ON_REGISTRATION_STARTED, ON_REGISTRATION_SUCCESS));
public void createLwm2mDevicesForConnectNoSec(String name, Lwm2mDevicesForTest devicesForTest) throws Exception {
String clientEndpoint = name + "-" + RandomStringUtils.randomAlphanumeric(7);
LwM2MDeviceCredentials deviceCredentials = getDeviceCredentialsNoSec(createNoSecClientCredentials(clientEndpoint));
Device lwM2MDeviceTest = createDeviceWithCredentials(deviceCredentials, clientEndpoint, devicesForTest.getLwm2mDeviceProfile().getId());
LwM2MTestClient lwM2MTestClient = createNewClient(SECURITY_NO_SEC, clientEndpoint, executor);
devicesForTest.setLwM2MDeviceTest(lwM2MDeviceTest);
devicesForTest.setLwM2MTestClient(lwM2MTestClient);
}
public void createLwm2mDevicesForConnectPsk(Lwm2mDevicesForTest devicesForTest) throws Exception {
String clientEndpoint = CLIENT_ENDPOINT_PSK + "-" + RandomStringUtils.randomAlphanumeric(7);
String identity = CLIENT_PSK_IDENTITY;
String keyPsk = CLIENT_PSK_KEY;
PSKClientCredential clientCredentials = new PSKClientCredential();
clientCredentials.setEndpoint(clientEndpoint);
clientCredentials.setIdentity(identity);
clientCredentials.setKey(keyPsk);
Security security = psk(SECURE_URI,
shortServerId,
identity.getBytes(StandardCharsets.UTF_8),
Hex.decodeHex(keyPsk.toCharArray()));
LwM2MDeviceCredentials deviceCredentials = getDeviceCredentialsSecurePsk(clientCredentials);
Device lwM2MDeviceTest = createDeviceWithCredentials(deviceCredentials, clientEndpoint, devicesForTest.getLwm2mDeviceProfile().getId());
LwM2MTestClient lwM2MTestClient = createNewClient(security, clientEndpoint, executor);
devicesForTest.setLwM2MDeviceTest(lwM2MDeviceTest);
devicesForTest.setLwM2MTestClient(lwM2MTestClient);
}
/**
* Observe {"id":"/3/0/0"}
* Observe {"id":"/3/0/9"}
* ObserveCancel {"id":"/3"} - Bad
* ObserveCancel {"/3/0/0"} - Ok
* ObserveCancelAl - Ok
*
* @param lwM2MTestClient
* @param deviceId
* @throws Exception
*/
public void observeResource_Update_AfterUpdateRegistration_test(LwM2MTestClient lwM2MTestClient, DeviceId deviceId) throws Exception {
awaitUpdateRegistrationSuccess(lwM2MTestClient, 5);
sendCancelObserveAllWithAwait(deviceId.toString());
awaitUpdateRegistrationSuccess(lwM2MTestClient, 1);
long tsBefore = Instant.now().toEpochMilli();
String param = "/3_1.2/0/9";
sendRpcObserveWithContainsLwM2mSingleResource(param, deviceId.toString(), 1);
awaitUpdateRegistrationSuccess(lwM2MTestClient, 1);
checkLatestTelemetryUploaded(deviceId, tsBefore, RESOURCE_ID_NAME_3_9);
param = "/3_1.2/0/0";
sendRpcObserveWithContainsLwM2mSingleResource(param, deviceId.toString(), 2);
awaitUpdateRegistrationSuccess(lwM2MTestClient, 2);
param = "/3_1.2";
String expected = "Could not find active Observe component with path: " + param;
String actual = sendObserveCancel_BadRequest("ObserveCancel", param, deviceId.toString());
assertEquals(expected, actual);
param = "/3_1.2/0/0";
sendObserveCancel_Ok("ObserveCancel", param, deviceId.toString());
sendCancelObserveAllWithAwait(deviceId.toString());
awaitUpdateRegistrationSuccess(lwM2MTestClient, 1);
}
public void observeCompositeResource_Update_AfterUpdateRegistration_test(LwM2MTestClient lwM2MTestClient, DeviceId deviceId) throws Exception {
String id_3_0_9 = "/3/0/9";
String id_3_0_14 = "/3/0/14";
String id_19_0_0 = "/19/0/0";
String id_19_1_0 = "/19/1/0";
String id_19_0_2 = "/19/0/2";
awaitUpdateRegistrationSuccess(lwM2MTestClient, 5);
sendCancelObserveAllWithAwait(deviceId.toString());
awaitUpdateRegistrationSuccess(lwM2MTestClient, 1);
long tsBefore = Instant.now().toEpochMilli();
String expectedKeys = "[\"" + RESOURCE_ID_NAME_3_9 + "\", \"" + RESOURCE_ID_NAME_3_14 + "\", \"" + RESOURCE_ID_NAME_19_0_0 + "\", \"" + RESOURCE_ID_NAME_19_0_2 + "\", \"" +RESOURCE_ID_NAME_19_1_0 + "\"]";
String actualResult = sendRpcObserveCompositeWithResultValue(expectedKeys, deviceId.toString());
assertTrue(actualResult.contains(id_3_0_9 + "=LwM2mSingleResource"));
assertTrue(actualResult.contains(id_3_0_14 + "=LwM2mSingleResource"));
assertTrue(actualResult.contains(id_19_0_0 + "=LwM2mMultipleResource"));
assertTrue(actualResult.contains(id_19_1_0 + "=LwM2mMultipleResource"));
assertTrue(actualResult.contains(id_19_0_2 + "=LwM2mSingleResource"));
// ObserveComposite: - verify
ObjectNode rpcActualResultBefore = sendRpcObserve("ObserveReadAll", null, deviceId.toString());
assertEquals(ResponseCode.CONTENT.getName(), rpcActualResultBefore.get("result").asText());
JsonElement element = JsonParser.parseString(rpcActualResultBefore.get("value").asText());
assertEquals(1, ((JsonArray) element).size());
actualResult = ((JsonArray) element).asList().get(0).getAsString();
assertTrue(actualResult.contains("CompositeObservation:"));
checkLatestTelemetryUploaded(deviceId, tsBefore, RESOURCE_ID_NAME_3_9, RESOURCE_ID_NAME_3_14,
RESOURCE_ID_NAME_19_0_0, RESOURCE_ID_NAME_19_0_2, RESOURCE_ID_NAME_19_1_0);
awaitUpdateRegistrationSuccess(lwM2MTestClient, 1);
sendCancelObserveAllWithAwait(deviceId.toString());
awaitUpdateRegistrationSuccess(lwM2MTestClient, 1);
assertEquals(0, (Object) Optional.ofNullable(getCntObserveAll(deviceId.toString())).get());
}
public void basicTestConnection(LwM2MTestClient lwM2MTestClient, String alias) throws Exception {
LwM2MClientState finishState = ON_REGISTRATION_SUCCESS;
await(alias + " - " + ON_REGISTRATION_STARTED)
.atMost(40, TimeUnit.SECONDS)
.until(() -> {
log.warn("msa basicTestConnection started -> finishState: [{}] states: {}", finishState, lwM2MTestClient.getClientStates());
return lwM2MTestClient.getClientStates().contains(finishState) || lwM2MTestClient.getClientStates().contains(ON_REGISTRATION_STARTED);
});
await(alias + " - " + ON_UPDATE_SUCCESS)
.atMost(40, TimeUnit.SECONDS)
.until(() -> {
log.warn("msa basicTestConnection update -> finishState: [{}] states: {}", finishState, lwM2MTestClient.getClientStates());
return lwM2MTestClient.getClientStates().contains(finishState) || lwM2MTestClient.getClientStates().contains(ON_UPDATE_SUCCESS);
});
assertThat(lwM2MTestClient.getClientStates()).containsAll(expectedStatusesRegistrationLwm2mSuccess);
}
public LwM2MTestClient createNewClient(Security security,
String endpoint, ScheduledExecutorService executor) throws Exception {
this.executor = executor;
LwM2MTestClient lwM2MTestClient = new LwM2MTestClient(executor, endpoint);
try (ServerSocket socket = new ServerSocket(0)) {
int clientPort = socket.getLocalPort();
lwM2MTestClient.init(security, clientPort);
}
return lwM2MTestClient;
}
protected void destroyAfter(Lwm2mDevicesForTest devicesForTest) {
clientDestroy(devicesForTest.getLwM2MTestClient());
deviceDestroy(devicesForTest.getLwM2MDeviceTest());
deviceProfileDestroy(devicesForTest.getLwm2mDeviceProfile());
if (executor != null) {
executor.shutdown();
}
}
protected void clientDestroy(LwM2MTestClient lwM2MTestClient) {
try {
if (lwM2MTestClient != null) {
lwM2MTestClient.destroy();
}
} catch (Exception e) {
log.error("Failed client Destroy", e);
}
}
protected void deviceDestroy(Device lwM2MDeviceTest) {
try {
if (lwM2MDeviceTest != null) {
testRestClient.deleteDeviceIfExists(lwM2MDeviceTest.getId());
}
} catch (Exception e) {
log.error("Failed device Delete", e);
}
}
protected DeviceProfile initTest(String deviceProfileName) throws Exception {
if (executor != null) {
executor.shutdown();
}
executor = Executors.newScheduledThreadPool(10, ThingsBoardThreadFactory.forName("test-scheduled-" + deviceProfileName));
DeviceProfile lwm2mDeviceProfile = getDeviceProfile(deviceProfileName);
tenantId = lwm2mDeviceProfile.getTenantId();
for (String resourceName : resources) {
TbResource lwModel = new TbResource();
lwModel.setResourceType(ResourceType.LWM2M_MODEL);
lwModel.setTitle(resourceName);
lwModel.setFileName(resourceName);
lwModel.setTenantId(tenantId);
byte[] bytes = IOUtils.toByteArray(AbstractLwm2mClientTest.class.getClassLoader().getResourceAsStream("lwm2m-registry/" + resourceName));
lwModel.setData(bytes);
testRestClient.postTbResourceIfNotExists(lwModel);
}
return lwm2mDeviceProfile;
}
protected DeviceProfile getDeviceProfile(String deviceProfileName) throws Exception {
DeviceProfile deviceProfile = getDeviceProfileIfExists(deviceProfileName);
if (deviceProfile == null) {
deviceProfile = testRestClient.postDeviceProfile(createDeviceProfile(deviceProfileName));
}
return deviceProfile;
}
protected DeviceProfile getDeviceProfileIfExists(String deviceProfileName) throws Exception {
return testRestClient.getDeviceProfiles(pageLink).getData().stream()
.filter(x -> x.getName().equals(deviceProfileName))
.findFirst()
.orElse(null);
}
protected DeviceProfile createDeviceProfile(String deviceProfileName) throws Exception {
DeviceProfile deviceProfile = new DeviceProfile();
deviceProfile.setName(deviceProfileName);
deviceProfile.setType(DeviceProfileType.DEFAULT);
deviceProfile.setTransportType(DeviceTransportType.LWM2M);
deviceProfile.setProvisionType(DeviceProfileProvisionType.DISABLED);
deviceProfile.setDescription(deviceProfile.getName());
DeviceProfileData deviceProfileData = new DeviceProfileData();
deviceProfileData.setConfiguration(new DefaultDeviceProfileConfiguration());
deviceProfileData.setProvisionConfiguration(new DisabledDeviceProfileProvisionConfiguration(null));
deviceProfileData.setTransportConfiguration(getTransportConfiguration());
deviceProfile.setProfileData(deviceProfileData);
return deviceProfile;
}
protected void deviceProfileDestroy(DeviceProfile lwm2mDeviceProfile) {
try {
if (lwm2mDeviceProfile != null) {
testRestClient.deleteDeviceProfileIfExists(lwm2mDeviceProfile);
}
} catch (Exception e) {
log.error("Failed deviceProfile Delete", e);
}
}
protected Device createDeviceWithCredentials(LwM2MDeviceCredentials deviceCredentials, String clientEndpoint, DeviceProfileId profileId) throws Exception {
Device device = createDevice(deviceCredentials, clientEndpoint, profileId);
return device;
}
protected Device createDevice(LwM2MDeviceCredentials credentials, String clientEndpoint, DeviceProfileId profileId) throws Exception {
Device device = testRestClient.getDeviceByNameIfExists(clientEndpoint);
if (device == null) {
device = new Device();
device.setName(clientEndpoint);
device.setDeviceProfileId(profileId);
device.setTenantId(tenantId);
device = testRestClient.postDevice("", device);
}
DeviceCredentials deviceCredentials = testRestClient.getDeviceCredentialsByDeviceId(device.getId());
deviceCredentials.setCredentialsType(DeviceCredentialsType.LWM2M_CREDENTIALS);
deviceCredentials.setCredentialsValue(JacksonUtil.toString(credentials));
deviceCredentials = testRestClient.postDeviceCredentials(deviceCredentials);
assertThat(deviceCredentials).isNotNull();
return device;
}
protected LwM2MDeviceCredentials getDeviceCredentialsNoSec(LwM2MClientCredential clientCredentials) {
LwM2MDeviceCredentials credentials = new LwM2MDeviceCredentials();
credentials.setClient(clientCredentials);
LwM2MBootstrapClientCredentials bootstrapCredentials = new LwM2MBootstrapClientCredentials();
NoSecBootstrapClientCredential serverCredentials = new NoSecBootstrapClientCredential();
bootstrapCredentials.setBootstrapServer(serverCredentials);
bootstrapCredentials.setLwm2mServer(serverCredentials);
credentials.setBootstrap(bootstrapCredentials);
return credentials;
}
public NoSecClientCredential createNoSecClientCredentials(String clientEndpoint) {
NoSecClientCredential clientCredentials = new NoSecClientCredential();
clientCredentials.setEndpoint(clientEndpoint);
return clientCredentials;
}
protected Lwm2mDeviceProfileTransportConfiguration getTransportConfiguration() {
List<LwM2MBootstrapServerCredential> bootstrapServerCredentials = new ArrayList<>();
Lwm2mDeviceProfileTransportConfiguration transportConfiguration = new Lwm2mDeviceProfileTransportConfiguration();
TelemetryMappingConfiguration observeAttrConfiguration = JacksonUtil.fromString(OBSERVE_ATTRIBUTES_WITH_PARAMS, TelemetryMappingConfiguration.class);
OtherConfiguration clientLwM2mSettings = JacksonUtil.fromString(CLIENT_LWM2M_SETTINGS, OtherConfiguration.class);
transportConfiguration.setBootstrapServerUpdateEnable(true);
transportConfiguration.setObserveAttr(observeAttrConfiguration);
transportConfiguration.setClientLwM2mSettings(clientLwM2mSettings);
transportConfiguration.setBootstrap(bootstrapServerCredentials);
return transportConfiguration;
}
protected LwM2MDeviceCredentials getDeviceCredentialsSecurePsk(LwM2MClientCredential clientCredentials) {
LwM2MDeviceCredentials credentials = new LwM2MDeviceCredentials();
credentials.setClient(clientCredentials);
LwM2MBootstrapClientCredentials bootstrapCredentials;
bootstrapCredentials = getBootstrapClientCredentialsPsk(clientCredentials);
credentials.setBootstrap(bootstrapCredentials);
return credentials;
}
private LwM2MBootstrapClientCredentials getBootstrapClientCredentialsPsk(LwM2MClientCredential clientCredentials) {
LwM2MBootstrapClientCredentials bootstrapCredentials = new LwM2MBootstrapClientCredentials();
PSKBootstrapClientCredential serverCredentials = new PSKBootstrapClientCredential();
if (clientCredentials != null) {
serverCredentials.setClientSecretKey(((PSKClientCredential) clientCredentials).getKey());
serverCredentials.setClientPublicKeyOrId(((PSKClientCredential) clientCredentials).getIdentity());
}
bootstrapCredentials.setBootstrapServer(serverCredentials);
bootstrapCredentials.setLwm2mServer(serverCredentials);
return bootstrapCredentials;
}
protected String sendObserveCancel_BadRequest(String method, String params, String deviceIdStr) throws Exception {
ObjectNode rpcActualResult = sendRpcObserve(method, params, deviceIdStr);
assertEquals(ResponseCode.BAD_REQUEST.getName(), rpcActualResult.get("result").asText());
return rpcActualResult.get("error").asText();
}
protected void sendObserveCancel_Ok(String method, String params, String deviceIdStr) throws Exception {
ObjectNode rpcActualResult = sendRpcObserve(method, params, deviceIdStr);
assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText());
assertEquals("1", rpcActualResult.get("value").asText());
}
protected void sendCancelObserveAllWithAwait(String deviceIdStr) throws Exception {
ObjectNode rpcActualResultCancelAll = sendRpcObserve("ObserveCancelAll", null, deviceIdStr);
assertEquals(ResponseCode.CONTENT.getName(), rpcActualResultCancelAll.get("result").asText());
awaitObserveReadAll(0, deviceIdStr);
}
protected void awaitObserveReadAll(int cntObserve, String deviceIdStr) throws Exception {
await("ObserveReadAll after start client/test: countObserve " + cntObserve)
.atMost(40, TimeUnit.SECONDS)
.until(() -> cntObserve == getCntObserveAll(deviceIdStr));
}
protected void awaitUpdateRegistrationSuccess(LwM2MTestClient lwM2MTestClient, int cntUpdate) throws Exception {
cntUpdate = cntUpdate + lwM2MTestClient.getCountUpdateRegistrationSuccess();
int finalCntUpdate = cntUpdate;
await("Update Registration client: countUpdateSuccess " + finalCntUpdate)
.atMost(40, TimeUnit.SECONDS)
.until(() -> finalCntUpdate <= lwM2MTestClient.getCountUpdateRegistrationSuccess());
}
protected void awaitObserveReadResource_3_0_9(int cntRead, String deviceIdStr) throws Exception {
await("Read value 3/0/9 after start observe: countRead " + cntRead)
.atMost(40, TimeUnit.SECONDS)
.until(() -> cntRead == getCntObserveAll(deviceIdStr));
}
protected Integer getCntObserveAll(String deviceIdStr) throws Exception {
ObjectNode rpcActualResultBefore = sendRpcObserve("ObserveReadAll", null, deviceIdStr);
assertEquals(ResponseCode.CONTENT.getName(), rpcActualResultBefore.get("result").asText());
JsonElement element = JsonParser.parseString(rpcActualResultBefore.get("value").asText());
return element.isJsonArray() ? ((JsonArray) element).size() : null;
}
private void sendRpcObserveWithContainsLwM2mSingleResource(String params, String deviceIdStr, int cnt) throws Exception {
String rpcActualResult = sendRpcObserveWithResultValue(params, deviceIdStr);
assertTrue(rpcActualResult.contains("LwM2mSingleResource"));
assertEquals(Optional.of(cnt).get(), Optional.ofNullable(getCntObserveAll(deviceIdStr)).get());
}
private String sendRpcObserveWithResultValue(String params, String deviceIdStr) throws Exception {
ObjectNode rpcActualResult = sendRpcObserve("Observe", params, deviceIdStr);
assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText());
return rpcActualResult.get("value").asText();
}
private String sendRpcObserveCompositeWithResultValue(String params, String deviceIdStr) throws Exception {
ObjectNode rpcActualResult = sendRpcObserveComposite(params, deviceIdStr);
assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText());
return rpcActualResult.get("value").asText();
}
protected ObjectNode sendRpcObserve(String method, String params, String deviceIdStr) throws Exception {
String sendRpcRequest;
if (params == null) {
sendRpcRequest = "{\"method\": \"" + method + "\"}";
} else {
sendRpcRequest = "{\"method\": \"" + method + "\", \"params\": {\"id\": \"" + params + "\"}}";
}
return testRestClient.postRpcLwm2mParams(deviceIdStr, sendRpcRequest);
}
protected ObjectNode sendRpcObserveComposite(String keys, String deviceIdStr) throws Exception {
String method = "ObserveComposite";
String sendRpcRequest = "{\"method\": \"" + method + "\", \"params\": {\"keys\":" + keys + "}}";
return testRestClient.postRpcLwm2mParams(deviceIdStr, sendRpcRequest);
}
public void checkLatestTelemetryUploaded(DeviceId deviceId, long tsBefore, String... keyNames) throws Exception {
WsClient wsClient = subscribeToWebSocket(deviceId, "LATEST_TELEMETRY", CmdsType.TS_SUB_CMDS);
WsTelemetryResponse actualLatestTelemetry = wsClient.getLastMessage();
wsClient.closeBlocking();
Assert.assertEquals(actualLatestTelemetry.getData().size(), 1);
String actualKeyName = actualLatestTelemetry.getLatestValues().keySet().toArray()[0].toString();
Assert.assertTrue(Sets.newHashSet(keyNames).contains(actualKeyName));
Assert.assertTrue((Long) actualLatestTelemetry.getDataValuesByKey(actualKeyName).get(0) > tsBefore);
}
}

35
msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/lwm2m/Lwm2mDevicesForTest.java

@ -0,0 +1,35 @@
/**
* Copyright © 2016-2024 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.msa.connectivity.lwm2m;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.DeviceProfile;
import org.thingsboard.server.msa.connectivity.lwm2m.client.LwM2MTestClient;
@Slf4j
@Data
public class Lwm2mDevicesForTest {
Device lwM2MDeviceTest;
LwM2MTestClient lwM2MTestClient;
DeviceProfile lwm2mDeviceProfile;
public Lwm2mDevicesForTest(DeviceProfile deviceProfile) {
this.lwm2mDeviceProfile = deviceProfile;
}
}

2
msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/lwm2m/FwLwM2MDevice.java → msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/lwm2m/client/FwLwM2MDevice.java

@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package org.thingsboard.server.msa.connectivity.lwm2m; package org.thingsboard.server.msa.connectivity.lwm2m.client;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.eclipse.leshan.client.resource.BaseInstanceEnabler; import org.eclipse.leshan.client.resource.BaseInstanceEnabler;

74
msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/lwm2m/LwM2MTestClient.java → msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/lwm2m/client/LwM2MTestClient.java

@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package org.thingsboard.server.msa.connectivity.lwm2m; package org.thingsboard.server.msa.connectivity.lwm2m.client;
import lombok.Data; import lombok.Data;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
@ -44,13 +44,14 @@ import org.eclipse.leshan.core.model.LwM2mModel;
import org.eclipse.leshan.core.model.ObjectLoader; import org.eclipse.leshan.core.model.ObjectLoader;
import org.eclipse.leshan.core.model.ObjectModel; import org.eclipse.leshan.core.model.ObjectModel;
import org.eclipse.leshan.core.model.StaticModel; import org.eclipse.leshan.core.model.StaticModel;
import org.eclipse.leshan.core.node.LwM2mPath;
import org.eclipse.leshan.core.node.codec.DefaultLwM2mDecoder; import org.eclipse.leshan.core.node.codec.DefaultLwM2mDecoder;
import org.eclipse.leshan.core.node.codec.DefaultLwM2mEncoder; import org.eclipse.leshan.core.node.codec.DefaultLwM2mEncoder;
import org.eclipse.leshan.core.request.BootstrapRequest; import org.eclipse.leshan.core.request.BootstrapRequest;
import org.eclipse.leshan.core.request.DeregisterRequest; import org.eclipse.leshan.core.request.DeregisterRequest;
import org.eclipse.leshan.core.request.RegisterRequest; import org.eclipse.leshan.core.request.RegisterRequest;
import org.eclipse.leshan.core.request.UpdateRequest; import org.eclipse.leshan.core.request.UpdateRequest;
import org.thingsboard.server.msa.connectivity.lwm2m.Lwm2mTestHelper.LwM2MClientState; import org.thingsboard.server.msa.connectivity.lwm2m.client.Lwm2mTestHelper.LwM2MClientState;
import java.io.IOException; import java.io.IOException;
import java.net.InetSocketAddress; import java.net.InetSocketAddress;
@ -60,6 +61,7 @@ import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
@ -70,33 +72,35 @@ import static org.eclipse.leshan.core.LwM2mId.DEVICE;
import static org.eclipse.leshan.core.LwM2mId.FIRMWARE; import static org.eclipse.leshan.core.LwM2mId.FIRMWARE;
import static org.eclipse.leshan.core.LwM2mId.SECURITY; import static org.eclipse.leshan.core.LwM2mId.SECURITY;
import static org.eclipse.leshan.core.LwM2mId.SERVER; import static org.eclipse.leshan.core.LwM2mId.SERVER;
import static org.thingsboard.server.msa.connectivity.lwm2m.Lwm2mTestHelper.LwM2MClientState.ON_BOOTSTRAP_FAILURE; import static org.thingsboard.server.msa.connectivity.lwm2m.client.Lwm2mTestHelper.BINARY_APP_DATA_CONTAINER;
import static org.thingsboard.server.msa.connectivity.lwm2m.Lwm2mTestHelper.LwM2MClientState.ON_BOOTSTRAP_STARTED; import static org.thingsboard.server.msa.connectivity.lwm2m.client.Lwm2mTestHelper.LwM2MClientState.ON_BOOTSTRAP_FAILURE;
import static org.thingsboard.server.msa.connectivity.lwm2m.Lwm2mTestHelper.LwM2MClientState.ON_BOOTSTRAP_SUCCESS; import static org.thingsboard.server.msa.connectivity.lwm2m.client.Lwm2mTestHelper.LwM2MClientState.ON_BOOTSTRAP_STARTED;
import static org.thingsboard.server.msa.connectivity.lwm2m.Lwm2mTestHelper.LwM2MClientState.ON_BOOTSTRAP_TIMEOUT; import static org.thingsboard.server.msa.connectivity.lwm2m.client.Lwm2mTestHelper.LwM2MClientState.ON_BOOTSTRAP_SUCCESS;
import static org.thingsboard.server.msa.connectivity.lwm2m.Lwm2mTestHelper.LwM2MClientState.ON_DEREGISTRATION_FAILURE; import static org.thingsboard.server.msa.connectivity.lwm2m.client.Lwm2mTestHelper.LwM2MClientState.ON_BOOTSTRAP_TIMEOUT;
import static org.thingsboard.server.msa.connectivity.lwm2m.Lwm2mTestHelper.LwM2MClientState.ON_DEREGISTRATION_STARTED; import static org.thingsboard.server.msa.connectivity.lwm2m.client.Lwm2mTestHelper.LwM2MClientState.ON_DEREGISTRATION_FAILURE;
import static org.thingsboard.server.msa.connectivity.lwm2m.Lwm2mTestHelper.LwM2MClientState.ON_DEREGISTRATION_SUCCESS; import static org.thingsboard.server.msa.connectivity.lwm2m.client.Lwm2mTestHelper.LwM2MClientState.ON_DEREGISTRATION_STARTED;
import static org.thingsboard.server.msa.connectivity.lwm2m.Lwm2mTestHelper.LwM2MClientState.ON_DEREGISTRATION_TIMEOUT; import static org.thingsboard.server.msa.connectivity.lwm2m.client.Lwm2mTestHelper.LwM2MClientState.ON_DEREGISTRATION_SUCCESS;
import static org.thingsboard.server.msa.connectivity.lwm2m.Lwm2mTestHelper.LwM2MClientState.ON_EXPECTED_ERROR; import static org.thingsboard.server.msa.connectivity.lwm2m.client.Lwm2mTestHelper.LwM2MClientState.ON_DEREGISTRATION_TIMEOUT;
import static org.thingsboard.server.msa.connectivity.lwm2m.Lwm2mTestHelper.LwM2MClientState.ON_INIT; import static org.thingsboard.server.msa.connectivity.lwm2m.client.Lwm2mTestHelper.LwM2MClientState.ON_EXPECTED_ERROR;
import static org.thingsboard.server.msa.connectivity.lwm2m.Lwm2mTestHelper.LwM2MClientState.ON_REGISTRATION_FAILURE; import static org.thingsboard.server.msa.connectivity.lwm2m.client.Lwm2mTestHelper.LwM2MClientState.ON_INIT;
import static org.thingsboard.server.msa.connectivity.lwm2m.Lwm2mTestHelper.LwM2MClientState.ON_REGISTRATION_STARTED; import static org.thingsboard.server.msa.connectivity.lwm2m.client.Lwm2mTestHelper.LwM2MClientState.ON_REGISTRATION_FAILURE;
import static org.thingsboard.server.msa.connectivity.lwm2m.Lwm2mTestHelper.LwM2MClientState.ON_REGISTRATION_SUCCESS; import static org.thingsboard.server.msa.connectivity.lwm2m.client.Lwm2mTestHelper.LwM2MClientState.ON_REGISTRATION_STARTED;
import static org.thingsboard.server.msa.connectivity.lwm2m.Lwm2mTestHelper.LwM2MClientState.ON_REGISTRATION_TIMEOUT; import static org.thingsboard.server.msa.connectivity.lwm2m.client.Lwm2mTestHelper.LwM2MClientState.ON_REGISTRATION_SUCCESS;
import static org.thingsboard.server.msa.connectivity.lwm2m.Lwm2mTestHelper.LwM2MClientState.ON_UPDATE_FAILURE; import static org.thingsboard.server.msa.connectivity.lwm2m.client.Lwm2mTestHelper.LwM2MClientState.ON_REGISTRATION_TIMEOUT;
import static org.thingsboard.server.msa.connectivity.lwm2m.Lwm2mTestHelper.LwM2MClientState.ON_UPDATE_STARTED; import static org.thingsboard.server.msa.connectivity.lwm2m.client.Lwm2mTestHelper.LwM2MClientState.ON_UPDATE_FAILURE;
import static org.thingsboard.server.msa.connectivity.lwm2m.Lwm2mTestHelper.LwM2MClientState.ON_UPDATE_SUCCESS; import static org.thingsboard.server.msa.connectivity.lwm2m.client.Lwm2mTestHelper.LwM2MClientState.ON_UPDATE_STARTED;
import static org.thingsboard.server.msa.connectivity.lwm2m.Lwm2mTestHelper.LwM2MClientState.ON_UPDATE_TIMEOUT; import static org.thingsboard.server.msa.connectivity.lwm2m.client.Lwm2mTestHelper.LwM2MClientState.ON_UPDATE_SUCCESS;
import static org.thingsboard.server.msa.connectivity.lwm2m.Lwm2mTestHelper.resources; import static org.thingsboard.server.msa.connectivity.lwm2m.client.Lwm2mTestHelper.LwM2MClientState.ON_UPDATE_TIMEOUT;
import static org.thingsboard.server.msa.connectivity.lwm2m.Lwm2mTestHelper.serverId; import static org.thingsboard.server.msa.connectivity.lwm2m.client.Lwm2mTestHelper.resources;
import static org.thingsboard.server.msa.connectivity.lwm2m.Lwm2mTestHelper.shortServerId; import static org.thingsboard.server.msa.connectivity.lwm2m.client.Lwm2mTestHelper.serverId;
import static org.thingsboard.server.msa.connectivity.lwm2m.client.Lwm2mTestHelper.shortServerId;
@Slf4j @Slf4j
@Data @Data
public class LwM2MTestClient { public class LwM2MTestClient {
private final ScheduledExecutorService executor;
private final String endpoint; private final String endpoint;
private LeshanClient leshanClient; private LeshanClient leshanClient;
private SimpleLwM2MDevice lwM2MDevice; private SimpleLwM2MDevice lwM2MDevice;
@ -104,8 +108,13 @@ public class LwM2MTestClient {
private Set<LwM2MClientState> clientStates; private Set<LwM2MClientState> clientStates;
private FwLwM2MDevice fwLwM2MDevice; private FwLwM2MDevice fwLwM2MDevice;
private LwM2mBinaryAppDataContainer lwM2MBinaryAppDataContainer;
private Map<LwM2MClientState, Integer> clientDtlsCid; private Map<LwM2MClientState, Integer> clientDtlsCid;
private int countUpdateRegistrationSuccess;
private int countReadObserveAfterUpdateRegistrationSuccess;
public void init(Security security, int clientPort) throws InvalidDDFFileException, IOException { public void init(Security security, int clientPort) throws InvalidDDFFileException, IOException {
assertThat(leshanClient).as("client already initialized").isNull(); assertThat(leshanClient).as("client already initialized").isNull();
@ -123,9 +132,12 @@ public class LwM2MTestClient {
lwm2mServer.setId(serverId); lwm2mServer.setId(serverId);
initializer.setInstancesForObject(SERVER, lwm2mServer); initializer.setInstancesForObject(SERVER, lwm2mServer);
initializer.setInstancesForObject(DEVICE, lwM2MDevice = new SimpleLwM2MDevice()); SimpleLwM2MDevice simpleLwM2MDevice = new SimpleLwM2MDevice(executor);
initializer.setInstancesForObject(DEVICE, lwM2MDevice = simpleLwM2MDevice);
initializer.setClassForObject(ACCESS_CONTROL, DummyInstanceEnabler.class); initializer.setClassForObject(ACCESS_CONTROL, DummyInstanceEnabler.class);
initializer.setInstancesForObject(FIRMWARE, fwLwM2MDevice = new FwLwM2MDevice()); initializer.setInstancesForObject(FIRMWARE, fwLwM2MDevice = new FwLwM2MDevice());
initializer.setInstancesForObject(BINARY_APP_DATA_CONTAINER, lwM2MBinaryAppDataContainer = new LwM2mBinaryAppDataContainer(executor, 0),
new LwM2mBinaryAppDataContainer(executor, 1));
List<LwM2mObjectEnabler> enablers = initializer.createAll(); List<LwM2mObjectEnabler> enablers = initializer.createAll();
@ -218,6 +230,7 @@ public class LwM2MTestClient {
clientDtlsCid = new HashMap<>(); clientDtlsCid = new HashMap<>();
clientStates.add(ON_INIT); clientStates.add(ON_INIT);
leshanClient = builder.build(); leshanClient = builder.build();
simpleLwM2MDevice.setLwM2MTestClient(this);
LwM2mClientObserver observer = new LwM2mClientObserver() { LwM2mClientObserver observer = new LwM2mClientObserver() {
@Override @Override
@ -248,7 +261,7 @@ public class LwM2MTestClient {
@Override @Override
public void onRegistrationSuccess(LwM2mServer server, RegisterRequest request, String registrationID) { public void onRegistrationSuccess(LwM2mServer server, RegisterRequest request, String registrationID) {
clientStates.add(ON_REGISTRATION_SUCCESS); clientStates.add(ON_REGISTRATION_SUCCESS);
} }
@Override @Override
public void onRegistrationFailure(LwM2mServer server, RegisterRequest request, ResponseCode responseCode, String errorMessage, Exception cause) { public void onRegistrationFailure(LwM2mServer server, RegisterRequest request, ResponseCode responseCode, String errorMessage, Exception cause) {
@ -268,6 +281,8 @@ public class LwM2MTestClient {
@Override @Override
public void onUpdateSuccess(LwM2mServer server, UpdateRequest request) { public void onUpdateSuccess(LwM2mServer server, UpdateRequest request) {
clientStates.add(ON_UPDATE_SUCCESS); clientStates.add(ON_UPDATE_SUCCESS);
countUpdateRegistrationSuccess++;
countReadObserveAfterUpdateRegistrationSuccess = 0;
} }
@Override @Override
@ -319,6 +334,12 @@ public class LwM2MTestClient {
public void objectAdded(LwM2mObjectEnabler object) { public void objectAdded(LwM2mObjectEnabler object) {
log.info("Object {} v{} enabled.", object.getId(), object.getObjectModel().version); log.info("Object {} v{} enabled.", object.getId(), object.getObjectModel().version);
} }
@Override
public void resourceChanged(LwM2mPath... paths) {
countReadObserveAfterUpdateRegistrationSuccess++;
log.trace("resourceChanged paths {} cntReadObserve {} cntUpdateSuccess {} .", paths, countReadObserveAfterUpdateRegistrationSuccess, countUpdateRegistrationSuccess);
}
}); });
leshanClient.start(); leshanClient.start();
@ -334,5 +355,8 @@ public class LwM2MTestClient {
if (fwLwM2MDevice != null) { if (fwLwM2MDevice != null) {
fwLwM2MDevice.destroy(); fwLwM2MDevice.destroy();
} }
if (lwM2MBinaryAppDataContainer != null) {
lwM2MBinaryAppDataContainer.destroy();
}
} }
} }

230
msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/lwm2m/client/LwM2mBinaryAppDataContainer.java

@ -0,0 +1,230 @@
/**
* Copyright © 2016-2024 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.msa.connectivity.lwm2m.client;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.leshan.client.resource.BaseInstanceEnabler;
import org.eclipse.leshan.client.servers.LwM2mServer;
import org.eclipse.leshan.core.model.ObjectModel;
import org.eclipse.leshan.core.model.ResourceModel;
import org.eclipse.leshan.core.node.LwM2mMultipleResource;
import org.eclipse.leshan.core.node.LwM2mResource;
import org.eclipse.leshan.core.response.ReadResponse;
import org.eclipse.leshan.core.response.WriteResponse;
import javax.security.auth.Destroyable;
import java.sql.Time;
import java.util.Arrays;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
@Slf4j
public class LwM2mBinaryAppDataContainer extends BaseInstanceEnabler implements Destroyable {
/**
* id = 0
* Multiple
* base64
*/
/**
* Example1:
* InNlcnZpY2VJZCI6Ik1ldGVyIiwNCiJzZXJ2aWNlRGF0YSI6ew0KImN1cnJlbnRSZWFka
* W5nIjoiNDYuMyIsDQoic2lnbmFsU3RyZW5ndGgiOjE2LA0KImRhaWx5QWN0aXZpdHlUaW1lIjo1NzA2DQo=
* "serviceId":"Meter",
* "serviceData":{
* "currentReading":"46.3",
* "signalStrength":16,
* "dailyActivityTime":5706
*/
/**
* Example2:
* InNlcnZpY2VJZCI6IldhdGVyTWV0ZXIiLA0KImNtZCI6IlNFVF9URU1QRVJBVFVSRV9SRUFEX
* 1BFUklPRCIsDQoicGFyYXMiOnsNCiJ2YWx1ZSI6NA0KICAgIH0sDQoNCg0K
* "serviceId":"WaterMeter",
* "cmd":"SET_TEMPERATURE_READ_PERIOD",
* "paras":{
* "value":4
* },
*/
Map<Integer, byte[]> data;
private Integer priority = 0;
private Time timestamp;
private String description;
private String dataFormat;
private Integer appID = -1;
private static final List<Integer> supportedResources = Arrays.asList(0, 1, 2, 3, 4, 5);
public LwM2mBinaryAppDataContainer() {
}
public LwM2mBinaryAppDataContainer(ScheduledExecutorService executorService, Integer id) {
try {
if (id != null) this.setId(id);
executorService.scheduleWithFixedDelay(() -> {
fireResourceChange(0);
fireResourceChange(2);
}
, 1800000, 1800000, TimeUnit.MILLISECONDS); // 30 MIN
} catch (Throwable e) {
log.error("[{}]Throwable", e.toString());
e.printStackTrace();
}
}
@Override
public ReadResponse read(LwM2mServer identity, int resourceId) {
try {
switch (resourceId) {
case 0:
ReadResponse response = ReadResponse.success(resourceId, getData(), ResourceModel.Type.OPAQUE);
return response;
case 1:
return ReadResponse.success(resourceId, getPriority());
case 2:
return ReadResponse.success(resourceId, getTimestamp());
case 3:
return ReadResponse.success(resourceId, getDescription());
case 4:
return ReadResponse.success(resourceId, getDataFormat());
case 5:
return ReadResponse.success(resourceId, getAppID());
default:
return super.read(identity, resourceId);
}
} catch (Exception e) {
return ReadResponse.badRequest(e.getMessage());
}
}
@Override
public WriteResponse write(LwM2mServer identity, boolean replace, int resourceId, LwM2mResource value) {
log.info("Write on Device resource /[{}]/[{}]/[{}]", getModel().id, getId(), resourceId);
switch (resourceId) {
case 0:
if (setData(value, replace)) {
return WriteResponse.success();
} else {
WriteResponse.badRequest("Invalidate value ...");
}
case 1:
setPriority((Integer) (value.getValue() instanceof Long ? ((Long) value.getValue()).intValue() : value.getValue()));
fireResourceChange(resourceId);
return WriteResponse.success();
case 2:
setTimestamp(((Date) value.getValue()).getTime());
fireResourceChange(resourceId);
return WriteResponse.success();
case 3:
setDescription((String) value.getValue());
fireResourceChange(resourceId);
return WriteResponse.success();
case 4:
setDataFormat((String) value.getValue());
fireResourceChange(resourceId);
return WriteResponse.success();
case 5:
setAppID((Integer) value.getValue());
fireResourceChange(resourceId);
return WriteResponse.success();
default:
return super.write(identity, replace, resourceId, value);
}
}
private Integer getAppID() {
return this.appID;
}
private void setAppID(Integer appId) {
this.appID = appId;
}
private void setDataFormat(String value) {
this.dataFormat = value;
}
private String getDataFormat() {
return this.dataFormat == null ? "OPAQUE" : this.dataFormat;
}
private void setDescription(String value) {
this.description = value;
}
private String getDescription() {
// return this.description == null ? "meter reading" : this.description;
return this.description;
}
private void setTimestamp(long time) {
this.timestamp = new Time(time);
}
private Time getTimestamp() {
return this.timestamp != null ? this.timestamp : new Time(new Date().getTime());
}
private boolean setData(LwM2mResource value, boolean replace) {
try {
if (value instanceof LwM2mMultipleResource) {
if (replace || this.data == null) {
this.data = new HashMap<>();
}
value.getInstances().values().forEach(v -> {
this.data.put(v.getId(), (byte[]) v.getValue());
});
return true;
} else {
return false;
}
} catch (Exception e) {
return false;
}
}
private Map<Integer, byte[]> getData() {
if (data == null) {
this.data = new HashMap<>();
this.data.put(0, new byte[]{(byte) 0xAC});
}
return data;
}
@Override
public List<Integer> getAvailableResourceIds(ObjectModel model) {
return supportedResources;
}
@Override
public void destroy() {
}
private int getPriority() {
return this.priority;
}
private void setPriority(int value) {
this.priority = value;
}
}

190
msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/lwm2m/client/LwM2mTemperatureSensor.java

@ -0,0 +1,190 @@
/**
* Copyright © 2016-2024 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.msa.connectivity.lwm2m.client;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.leshan.client.LeshanClient;
import org.eclipse.leshan.client.resource.BaseInstanceEnabler;
import org.eclipse.leshan.client.send.ManualDataSender;
import org.eclipse.leshan.client.servers.LwM2mServer;
import org.eclipse.leshan.core.model.ObjectModel;
import org.eclipse.leshan.core.node.LwM2mPath;
import org.eclipse.leshan.core.request.ContentFormat;
import org.eclipse.leshan.core.request.argument.Arguments;
import org.eclipse.leshan.core.response.ExecuteResponse;
import org.eclipse.leshan.core.response.ReadResponse;
import javax.security.auth.Destroyable;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Random;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
@Slf4j
public class LwM2mTemperatureSensor extends BaseInstanceEnabler implements Destroyable {
private static final String UNIT_CELSIUS = "cel";
private double currentTemp = 20d;
private double minMeasuredValue = currentTemp;
private double maxMeasuredValue = currentTemp;
private LeshanClient leshanClient;
private List<Double> containingValues;
protected static final Random RANDOM = new Random();
private static final List<Integer> supportedResources = Arrays.asList(5601, 5602, 5700, 5701);
public LwM2mTemperatureSensor() {
}
public LwM2mTemperatureSensor(ScheduledExecutorService executorService, Integer id) {
try {
if (id != null) this.setId(id);
executorService.scheduleWithFixedDelay(this::adjustTemperature, 2000, 2000, TimeUnit.MILLISECONDS);
} catch (Throwable e) {
log.error("[{}]Throwable", e.toString());
e.printStackTrace();
}
}
@Override
public synchronized ReadResponse read(LwM2mServer identity, int resourceId) {
log.info("Read on Temperature resource /[{}]/[{}]/[{}]", getModel().id, getId(), resourceId);
switch (resourceId) {
case 5601:
return ReadResponse.success(resourceId, getTwoDigitValue(minMeasuredValue));
case 5602:
return ReadResponse.success(resourceId, getTwoDigitValue(maxMeasuredValue));
case 5700:
if (identity == LwM2mServer.SYSTEM) {
setTemperature();
setData();
return ReadResponse.success(resourceId, getTwoDigitValue(currentTemp));
} else if (this.getId() == 12 && this.leshanClient != null) {
containingValues = new ArrayList<>();
sendCollected(5700);
return ReadResponse.success(resourceId, getData());
} else {
return ReadResponse.success(resourceId, getTwoDigitValue(currentTemp));
}
case 5701:
return ReadResponse.success(resourceId, UNIT_CELSIUS);
default:
return super.read(identity, resourceId);
}
}
@Override
public synchronized ExecuteResponse execute(LwM2mServer identity, int resourceId, Arguments arguments) {
log.info("Execute on Temperature resource /[{}]/[{}]/[{}]", getModel().id, getId(), resourceId);
switch (resourceId) {
case 5605:
resetMinMaxMeasuredValues();
return ExecuteResponse.success();
default:
return super.execute(identity, resourceId, arguments);
}
}
private double getTwoDigitValue(double value) {
BigDecimal toBeTruncated = BigDecimal.valueOf(value);
return toBeTruncated.setScale(2, RoundingMode.HALF_UP).doubleValue();
}
private void adjustTemperature() {
setTemperature();
Integer changedResource = adjustMinMaxMeasuredValue(currentTemp);
fireResourceChange(5700);
if (changedResource != null) {
fireResourceChange(changedResource);
}
}
private void setTemperature(){
float delta = (RANDOM.nextInt(20) - 10) / 10f;
currentTemp += delta;
}
private synchronized Integer adjustMinMaxMeasuredValue(double newTemperature) {
if (newTemperature > maxMeasuredValue) {
maxMeasuredValue = newTemperature;
return 5602;
} else if (newTemperature < minMeasuredValue) {
minMeasuredValue = newTemperature;
return 5601;
} else {
return null;
}
}
private void resetMinMaxMeasuredValues() {
minMeasuredValue = currentTemp;
maxMeasuredValue = currentTemp;
}
@Override
public List<Integer> getAvailableResourceIds(ObjectModel model) {
return supportedResources;
}
protected void setLeshanClient(LeshanClient leshanClient){
this.leshanClient = leshanClient;
}
@Override
public void destroy() {
}
private void sendCollected(int resourceId) {
try {
LwM2mServer registeredServer = this.leshanClient.getRegisteredServers().values().iterator().next();
ManualDataSender sender = this.leshanClient.getSendService().getDataSender(ManualDataSender.DEFAULT_NAME,
ManualDataSender.class);
sender.collectData(Arrays.asList(getPathForCollectedValue(resourceId)));
Thread.sleep(1000);
sender.collectData(Arrays.asList(getPathForCollectedValue(resourceId)));
sender.sendCollectedData(registeredServer, ContentFormat.SENML_JSON, 1000, false);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
private LwM2mPath getPathForCollectedValue(int resourceId) {
return new LwM2mPath(3303, this.getId(), resourceId);
}
private double getData() {
if (containingValues.size() > 1) {
Integer t0 = Math.toIntExact(Math.round(containingValues.get(0) * 100));
Integer t1 = Math.toIntExact(Math.round(containingValues.get(1) * 100));
long to_t1 = (((long) t0) << 32) | (t1 & 0xffffffffL);
return Double.longBitsToDouble(to_t1);
} else {
return currentTemp;
}
}
private void setData() {
if (containingValues == null){
containingValues = new ArrayList<>();
}
containingValues.add(getTwoDigitValue(currentTemp));
}
}

2
msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/lwm2m/LwM2mValueConverterImpl.java → msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/lwm2m/client/LwM2mValueConverterImpl.java

@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package org.thingsboard.server.msa.connectivity.lwm2m; package org.thingsboard.server.msa.connectivity.lwm2m.client;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.eclipse.leshan.core.model.ResourceModel.Type; import org.eclipse.leshan.core.model.ResourceModel.Type;

51
msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/lwm2m/Lwm2mTestHelper.java → msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/lwm2m/client/Lwm2mTestHelper.java

@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package org.thingsboard.server.msa.connectivity.lwm2m; package org.thingsboard.server.msa.connectivity.lwm2m.client;
import org.eclipse.californium.elements.config.Configuration; import org.eclipse.californium.elements.config.Configuration;
import org.eclipse.leshan.client.object.Security; import org.eclipse.leshan.client.object.Security;
@ -25,7 +25,7 @@ import static org.eclipse.leshan.client.object.Security.noSec;
public class Lwm2mTestHelper { public class Lwm2mTestHelper {
// Models // Models
public static final String[] resources = new String[]{ "0.xml", "1.xml", "2.xml", "3.xml", "5.xml"}; public static final String[] resources = new String[]{"0.xml", "1.xml", "2.xml", "3.xml", "5.xml", "19.xml"};
public static final int serverId = 1; public static final int serverId = 1;
public static final int port = 5685; public static final int port = 5685;
@ -44,16 +44,41 @@ public class Lwm2mTestHelper {
public static final String CLIENT_PSK_IDENTITY = "SOME_PSK_ID"; public static final String CLIENT_PSK_IDENTITY = "SOME_PSK_ID";
public static final String CLIENT_PSK_KEY = "73656372657450534b73656372657450"; public static final String CLIENT_PSK_KEY = "73656372657450534b73656372657450";
public static final int BINARY_APP_DATA_CONTAINER = 19;
public static final String RESOURCE_ID_NAME_3_9 = "batteryLevel";
public static final String RESOURCE_ID_NAME_3_14 = "UtfOffset";
public static final String RESOURCE_ID_NAME_19_0_0 = "dataRead";
public static final String RESOURCE_ID_NAME_19_0_2 = "dataCreationTime";
public static final String RESOURCE_ID_NAME_19_1_0 = "dataWrite";
public static String OBSERVE_ATTRIBUTES_WITH_PARAMS =
public static final String OBSERVE_ATTRIBUTES_WITHOUT_PARAMS =
" {\n" + " {\n" +
" \"keyName\": {},\n" + " \"keyName\": {\n" +
" \"observe\": [],\n" + " \"/3_1.2/0/9\": \"batteryLevel\",\n" +
" \"attribute\": [],\n" + " \"/3_1.2/0/14\": \"UtfOffset\",\n" +
" \"telemetry\": [],\n" + " \"/19_1.1/0/0\": \"dataRead\",\n" +
" \"/19_1.1/1/0\": \"dataWrite\",\n" +
" \"/19_1.1/0/2\": \"dataCreationTime\"\n" +
" },\n" +
" \"observe\": [\n" +
" \"/3_1.2/0/9\",\n" +
" \"/19_1.1/0/0\",\n" +
" \"/19_1.1/1/0\",\n" +
" \"/19_1.1/0/2\"\n" +
" ],\n" +
" \"attribute\": [\n" +
" \"/3_1.2/0/14\",\n" +
" \"/19_1.1/0/2\"\n" +
" ],\n" +
" \"telemetry\": [\n" +
" \"/3_1.2/0/9\",\n" +
" \"/19_1.1/0/0\",\n" +
" \"/19_1.1/1/0\"\n" +
" ],\n" +
" \"attributeLwm2m\": {}\n" + " \"attributeLwm2m\": {}\n" +
" }"; " }";
public static final String CLIENT_LWM2M_SETTINGS = public static final String CLIENT_LWM2M_SETTINGS =
" {\n" + " {\n" +
" \"edrxCycle\": null,\n" + " \"edrxCycle\": null,\n" +
@ -67,10 +92,6 @@ public class Lwm2mTestHelper {
" \"clientOnlyObserveAfterConnect\": 1\n" + " \"clientOnlyObserveAfterConnect\": 1\n" +
" }"; " }";
public static final int BINARY_APP_DATA_CONTAINER = 19;
public static final int OBJECT_INSTANCE_ID_0 = 0;
public static final int OBJECT_INSTANCE_ID_1 = 1;
public enum LwM2MClientState { public enum LwM2MClientState {
ON_INIT(0, "onInit"), ON_INIT(0, "onInit"),
@ -91,8 +112,8 @@ public class Lwm2mTestHelper {
ON_DEREGISTRATION_FAILURE(14, "onDeregistrationFailure"), ON_DEREGISTRATION_FAILURE(14, "onDeregistrationFailure"),
ON_DEREGISTRATION_TIMEOUT(15, "onDeregistrationTimeout"), ON_DEREGISTRATION_TIMEOUT(15, "onDeregistrationTimeout"),
ON_EXPECTED_ERROR(16, "onUnexpectedError"), ON_EXPECTED_ERROR(16, "onUnexpectedError"),
ON_READ_CONNECTION_ID (17, "onReadConnection"), ON_READ_CONNECTION_ID(17, "onReadConnection"),
ON_WRITE_CONNECTION_ID (18, "onWriteConnection"); ON_WRITE_CONNECTION_ID(18, "onWriteConnection");
public int code; public int code;
public String type; public String type;
@ -125,7 +146,7 @@ public class Lwm2mTestHelper {
serverCoapConfig.setTransient(DTLS_CONNECTION_ID_LENGTH); serverCoapConfig.setTransient(DTLS_CONNECTION_ID_LENGTH);
serverCoapConfig.setTransient(DTLS_CONNECTION_ID_NODE_ID); serverCoapConfig.setTransient(DTLS_CONNECTION_ID_NODE_ID);
serverCoapConfig.set(DTLS_CONNECTION_ID_LENGTH, cIdLength); serverCoapConfig.set(DTLS_CONNECTION_ID_LENGTH, cIdLength);
if ( cIdLength > 4) { if (cIdLength > 4) {
serverCoapConfig.set(DTLS_CONNECTION_ID_NODE_ID, 0); serverCoapConfig.set(DTLS_CONNECTION_ID_NODE_ID, 0);
} else { } else {
serverCoapConfig.set(DTLS_CONNECTION_ID_NODE_ID, null); serverCoapConfig.set(DTLS_CONNECTION_ID_NODE_ID, null);

38
msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/lwm2m/SimpleLwM2MDevice.java → msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/lwm2m/client/SimpleLwM2MDevice.java

@ -13,8 +13,9 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package org.thingsboard.server.msa.connectivity.lwm2m; package org.thingsboard.server.msa.connectivity.lwm2m.client;
import lombok.Data;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.eclipse.leshan.client.resource.BaseInstanceEnabler; import org.eclipse.leshan.client.resource.BaseInstanceEnabler;
import org.eclipse.leshan.client.servers.LwM2mServer; import org.eclipse.leshan.client.servers.LwM2mServer;
@ -36,8 +37,11 @@ import java.util.Map;
import java.util.PrimitiveIterator; import java.util.PrimitiveIterator;
import java.util.Random; import java.util.Random;
import java.util.TimeZone; import java.util.TimeZone;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
@Slf4j @Slf4j
@Data
public class SimpleLwM2MDevice extends BaseInstanceEnabler implements Destroyable { public class SimpleLwM2MDevice extends BaseInstanceEnabler implements Destroyable {
@ -46,17 +50,30 @@ public class SimpleLwM2MDevice extends BaseInstanceEnabler implements Destroyabl
private static final int max = 50; private static final int max = 50;
private static final PrimitiveIterator.OfInt randomIterator = new Random().ints(min,max + 1).iterator(); private static final PrimitiveIterator.OfInt randomIterator = new Random().ints(min,max + 1).iterator();
private static final List<Integer> supportedResources = Arrays.asList(0, 1, 2, 3, 6, 7, 8, 9, 10, 11, 13, 14, 15, 16, 17, 18, 19, 20, 21); private static final List<Integer> supportedResources = Arrays.asList(0, 1, 2, 3, 6, 7, 8, 9, 10, 11, 13, 14, 15, 16, 17, 18, 19, 20, 21);
private int countReadObserveAfterUpdateRegistrationSuccess_3_0_9;
private int countUpdateRegistrationSuccessLast;
private LwM2MTestClient lwM2MTestClient;
public SimpleLwM2MDevice() { public SimpleLwM2MDevice() {
} }
public SimpleLwM2MDevice(ScheduledExecutorService executorService) {
try {
executorService.scheduleWithFixedDelay(() -> {
fireResourceChange(9);
}
, 1, 1, TimeUnit.SECONDS); // 30 MIN
// , 1800000, 1800000, TimeUnit.MILLISECONDS); // 30 MIN
} catch (Throwable e) {
log.error("[{}]Throwable", e.toString());
e.printStackTrace();
}
}
@Override @Override
public ReadResponse read(LwM2mServer identity, int resourceId) { public ReadResponse read(LwM2mServer identity, int resourceId) {
if (!identity.isSystem()) if (!identity.isSystem())
log.info("Read on Device resource /{}/{}/{}", getModel().id, getId(), resourceId); log.trace("Read on Device resource /{}/{}/{}", getModel().id, getId(), resourceId);
switch (resourceId) { switch (resourceId) {
case 0: case 0:
return ReadResponse.success(resourceId, getManufacturer()); return ReadResponse.success(resourceId, getManufacturer());
@ -155,8 +172,15 @@ public class SimpleLwM2MDevice extends BaseInstanceEnabler implements Destroyabl
} }
private int getBatteryLevel() { private int getBatteryLevel() {
int batteryLevel = randomIterator.nextInt();
if (countUpdateRegistrationSuccessLast != this.lwM2MTestClient.getCountUpdateRegistrationSuccess()) {
countUpdateRegistrationSuccessLast = this.lwM2MTestClient.getCountUpdateRegistrationSuccess();
countReadObserveAfterUpdateRegistrationSuccess_3_0_9 = 0;
}
countReadObserveAfterUpdateRegistrationSuccess_3_0_9++;
this.lwM2MTestClient.setCountReadObserveAfterUpdateRegistrationSuccess(countReadObserveAfterUpdateRegistrationSuccess_3_0_9);
log.info("Read on Device resource /{}/{}/9 batteryLevel = {}, cntReadAfterUpdateReg [{}] ", getModel().id, getId(), batteryLevel, countReadObserveAfterUpdateRegistrationSuccess_3_0_9);
return randomIterator.nextInt(); return randomIterator.nextInt();
// return 42;
} }
private long getMemoryFree() { private long getMemoryFree() {
@ -212,6 +236,10 @@ public class SimpleLwM2MDevice extends BaseInstanceEnabler implements Destroyabl
return supportedResources; return supportedResources;
} }
protected void setLwM2MTestClient(LwM2MTestClient lwM2MTestClient){
this.lwM2MTestClient = lwM2MTestClient;
}
@Override @Override
public void destroy() { public void destroy() {
} }

52
msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/lwm2m/rpc/Lwm2mObserveCompositeTest.java

@ -0,0 +1,52 @@
/**
* Copyright © 2016-2024 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.msa.connectivity.lwm2m.rpc;
import org.testcontainers.shaded.org.apache.commons.lang3.RandomStringUtils;
import org.testng.annotations.AfterMethod;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;
import org.thingsboard.server.msa.DisableUIListeners;
import org.thingsboard.server.msa.connectivity.lwm2m.AbstractLwm2mClientTest;
import org.thingsboard.server.msa.connectivity.lwm2m.Lwm2mDevicesForTest;
import static org.thingsboard.server.msa.ui.utils.Const.TENANT_EMAIL;
import static org.thingsboard.server.msa.ui.utils.Const.TENANT_PASSWORD;
@DisableUIListeners
public class Lwm2mObserveCompositeTest extends AbstractLwm2mClientTest {
private Lwm2mDevicesForTest lwm2mDevicesForTest;
private final static String name = "lwm2m-NoSec-ObserveComposite";
@BeforeMethod
public void setUp() throws Exception {
testRestClient.login(TENANT_EMAIL, TENANT_PASSWORD);
this.lwm2mDevicesForTest = new Lwm2mDevicesForTest(initTest(name + "-profile" + RandomStringUtils.randomAlphanumeric(7)));
}
@AfterMethod
public void tearDown() {
destroyAfter(this.lwm2mDevicesForTest);
}
@Test
public void testObserveResource_Update_AfterUpdateRegistration() throws Exception {
createLwm2mDevicesForConnectNoSec( name + "-" + RandomStringUtils.randomAlphanumeric(7), this.lwm2mDevicesForTest );
observeCompositeResource_Update_AfterUpdateRegistration_test(this.lwm2mDevicesForTest.getLwM2MTestClient(), this.lwm2mDevicesForTest.getLwM2MDeviceTest().getId());
}
}

52
msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/lwm2m/rpc/Lwm2mObserveTest.java

@ -0,0 +1,52 @@
/**
* Copyright © 2016-2024 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.msa.connectivity.lwm2m.rpc;
import org.testcontainers.shaded.org.apache.commons.lang3.RandomStringUtils;
import org.testng.annotations.AfterMethod;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;
import org.thingsboard.server.msa.connectivity.lwm2m.AbstractLwm2mClientTest;
import org.thingsboard.server.msa.DisableUIListeners;
import org.thingsboard.server.msa.connectivity.lwm2m.Lwm2mDevicesForTest;
import static org.thingsboard.server.msa.ui.utils.Const.TENANT_EMAIL;
import static org.thingsboard.server.msa.ui.utils.Const.TENANT_PASSWORD;
@DisableUIListeners
public class Lwm2mObserveTest extends AbstractLwm2mClientTest {
private Lwm2mDevicesForTest lwm2mDevicesForTest;
private final static String name = "lwm2m-NoSec-Observe";
@BeforeMethod
public void setUp() throws Exception {
testRestClient.login(TENANT_EMAIL, TENANT_PASSWORD);
this.lwm2mDevicesForTest = new Lwm2mDevicesForTest(initTest(name + "-profile" + RandomStringUtils.randomAlphanumeric(7)));
}
@AfterMethod
public void tearDown() {
destroyAfter(this.lwm2mDevicesForTest);
}
@Test
public void testObserveResource_Update_AfterUpdateRegistration() throws Exception {
createLwm2mDevicesForConnectNoSec( name + "-" + RandomStringUtils.randomAlphanumeric(7), this.lwm2mDevicesForTest );
observeResource_Update_AfterUpdateRegistration_test(this.lwm2mDevicesForTest.getLwM2MTestClient(), this.lwm2mDevicesForTest.getLwM2MDeviceTest().getId());
}
}

15
msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/Lwm2mClientNoSecTest.java → msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/lwm2m/security/Lwm2mClientNoSecTest.java

@ -13,33 +13,38 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package org.thingsboard.server.msa.connectivity; package org.thingsboard.server.msa.connectivity.lwm2m.security;
import org.testcontainers.shaded.org.apache.commons.lang3.RandomStringUtils;
import org.testng.annotations.AfterMethod; import org.testng.annotations.AfterMethod;
import org.testng.annotations.BeforeMethod; import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test; import org.testng.annotations.Test;
import org.thingsboard.server.msa.AbstractLwm2mClientTest; import org.thingsboard.server.msa.connectivity.lwm2m.AbstractLwm2mClientTest;
import org.thingsboard.server.msa.DisableUIListeners; import org.thingsboard.server.msa.DisableUIListeners;
import org.thingsboard.server.msa.connectivity.lwm2m.Lwm2mDevicesForTest;
import static org.thingsboard.server.msa.connectivity.lwm2m.client.Lwm2mTestHelper.CLIENT_ENDPOINT_NO_SEC;
import static org.thingsboard.server.msa.ui.utils.Const.TENANT_EMAIL; import static org.thingsboard.server.msa.ui.utils.Const.TENANT_EMAIL;
import static org.thingsboard.server.msa.ui.utils.Const.TENANT_PASSWORD; import static org.thingsboard.server.msa.ui.utils.Const.TENANT_PASSWORD;
@DisableUIListeners @DisableUIListeners
public class Lwm2mClientNoSecTest extends AbstractLwm2mClientTest { public class Lwm2mClientNoSecTest extends AbstractLwm2mClientTest {
private Lwm2mDevicesForTest lwm2mDevicesForTest;
@BeforeMethod @BeforeMethod
public void setUp() throws Exception { public void setUp() throws Exception {
testRestClient.login(TENANT_EMAIL, TENANT_PASSWORD); testRestClient.login(TENANT_EMAIL, TENANT_PASSWORD);
initTest("lwm2m-NoSec"); this.lwm2mDevicesForTest = new Lwm2mDevicesForTest(initTest("lwm2m-NoSec-profile" + "-" + RandomStringUtils.randomAlphanumeric(7)));
} }
@AfterMethod @AfterMethod
public void tearDown() { public void tearDown() {
destroyAfter(); destroyAfter(this.lwm2mDevicesForTest);
} }
@Test @Test
public void connectLwm2mClientNoSecWithLwm2mServer() throws Exception { public void connectLwm2mClientNoSecWithLwm2mServer() throws Exception {
connectLwm2mClientNoSec(); createLwm2mDevicesForConnectNoSec(CLIENT_ENDPOINT_NO_SEC, this.lwm2mDevicesForTest);
basicTestConnection(this.lwm2mDevicesForTest.getLwM2MTestClient(), "TestConnection Lwm2m NoSec (msa)");
} }
} }

14
msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/Lwm2mClientPskTest.java → msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/lwm2m/security/Lwm2mClientPskTest.java

@ -13,13 +13,15 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package org.thingsboard.server.msa.connectivity; package org.thingsboard.server.msa.connectivity.lwm2m.security;
import org.testcontainers.shaded.org.apache.commons.lang3.RandomStringUtils;
import org.testng.annotations.AfterMethod; import org.testng.annotations.AfterMethod;
import org.testng.annotations.BeforeMethod; import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test; import org.testng.annotations.Test;
import org.thingsboard.server.msa.AbstractLwm2mClientTest; import org.thingsboard.server.msa.connectivity.lwm2m.AbstractLwm2mClientTest;
import org.thingsboard.server.msa.DisableUIListeners; import org.thingsboard.server.msa.DisableUIListeners;
import org.thingsboard.server.msa.connectivity.lwm2m.Lwm2mDevicesForTest;
import static org.thingsboard.server.msa.ui.utils.Const.TENANT_EMAIL; import static org.thingsboard.server.msa.ui.utils.Const.TENANT_EMAIL;
import static org.thingsboard.server.msa.ui.utils.Const.TENANT_PASSWORD; import static org.thingsboard.server.msa.ui.utils.Const.TENANT_PASSWORD;
@ -27,19 +29,21 @@ import static org.thingsboard.server.msa.ui.utils.Const.TENANT_PASSWORD;
@DisableUIListeners @DisableUIListeners
public class Lwm2mClientPskTest extends AbstractLwm2mClientTest { public class Lwm2mClientPskTest extends AbstractLwm2mClientTest {
private Lwm2mDevicesForTest lwm2mDevicesForTest;
@BeforeMethod @BeforeMethod
public void setUp() throws Exception { public void setUp() throws Exception {
testRestClient.login(TENANT_EMAIL, TENANT_PASSWORD); testRestClient.login(TENANT_EMAIL, TENANT_PASSWORD);
initTest("lwm2m-Psk"); this.lwm2mDevicesForTest = new Lwm2mDevicesForTest(initTest("lwm2m-Psk-profile" + "-" + RandomStringUtils.randomAlphanumeric(7)));
} }
@AfterMethod @AfterMethod
public void tearDown() { public void tearDown() {
destroyAfter(); destroyAfter(this.lwm2mDevicesForTest);
} }
@Test @Test
public void connectLwm2mClientPskWithLwm2mServer() throws Exception { public void connectLwm2mClientPskWithLwm2mServer() throws Exception {
connectLwm2mClientPsk(); createLwm2mDevicesForConnectPsk(this.lwm2mDevicesForTest);
basicTestConnection(this.lwm2mDevicesForTest.getLwM2MTestClient(), "TestConnection Lwm2m Rpc (msa)");
} }
} }

144
msa/black-box-tests/src/test/resources/lwm2m-registry/19.xml

@ -0,0 +1,144 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
FILE INFORMATION
OMA Permanent Document
File: OMA-SUP-LwM2M_BinaryAppDataContainer-V1_0_1-20190221-A
Type: xml
Public Reachable Information
Path: http://www.openmobilealliance.org/tech/profiles
Name: LwM2M_BinaryAppDataContainer-v1_0_1.xml
NORMATIVE INFORMATION
Information about this file can be found in the latest revision of
OMA-TS-LWM2M_BinaryAppDataContainer-V1_0_1
This is available at http://www.openmobilealliance.org/
Send comments to https://github.com/OpenMobileAlliance/OMA_LwM2M_for_Developers/issues
CHANGE HISTORY
15062018 Status changed to Approved by DM, Doc Ref # OMA-DM&SE-2018-0061-INP_LWM2M_APPDATA_V1_0_ERP_for_final_Approval
21022019 Status changed to Approved by IPSO, Doc Ref # OMA-IPSO-2019-0025-INP_LwM2M_Object_App_Data_Container_1_0_1_for_Final_Approval
LEGAL DISCLAIMER
Copyright 2019 Open Mobile Alliance.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions
are met:
1. Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived
from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
POSSIBILITY OF SUCH DAMAGE.
The above license is used as a license under copyright only. Please
reference the OMA IPR Policy for patent licensing terms:
https://www.omaspecworks.org/about/intellectual-property-rights/
-->
<LWM2M xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="http://www.openmobilealliance.org/tech/profiles/LWM2M-v1_1.xsd">
<Object ObjectType="MODefinition">
<Name>BinaryAppDataContainer</Name>
<Description1><![CDATA[This LwM2M Objects provides the application service data related to a LwM2M Server, eg. Water meter data.
There are several methods to create instance to indicate the message direction based on the negotiation between Application and LwM2M. The Client and Server should negotiate the instance(s) used to exchange the data. For example:
- Using a single instance for both directions communication, from Client to Server and from Server to Client.
- Using an instance for communication from Client to Server and another one for communication from Server to Client
- Using several instances
]]></Description1>
<ObjectID>19</ObjectID>
<ObjectURN>urn:oma:lwm2m:oma:19:1.1</ObjectURN>
<LWM2MVersion>1.1</LWM2MVersion>
<ObjectVersion>1.1</ObjectVersion>
<MultipleInstances>Multiple</MultipleInstances>
<Mandatory>Optional</Mandatory>
<Resources>
<Item ID="0"><Name>Data</Name>
<Operations>RW</Operations>
<MultipleInstances>Multiple</MultipleInstances>
<Mandatory>Mandatory</Mandatory>
<Type>Opaque</Type>
<RangeEnumeration />
<Units />
<Description><![CDATA[Indicates the application data content.]]></Description>
</Item>
<Item ID="1"><Name>Data Priority</Name>
<Operations>RW</Operations>
<MultipleInstances>Single</MultipleInstances>
<Mandatory>Optional</Mandatory>
<Type>Integer</Type>
<RangeEnumeration>1 bytes</RangeEnumeration>
<Units />
<Description><![CDATA[Indicates the Application data priority:
0:Immediate
1:BestEffort
2:Latest
3-100: Reserved for future use.
101-254: Proprietary mode.]]></Description>
</Item>
<Item ID="2"><Name>Data Creation Time</Name>
<Operations>RW</Operations>
<MultipleInstances>Single</MultipleInstances>
<Mandatory>Optional</Mandatory>
<Type>Time</Type>
<RangeEnumeration />
<Units />
<Description><![CDATA[Indicates the Data instance creation timestamp.]]></Description>
</Item>
<Item ID="3"><Name>Data Description</Name>
<Operations>RW</Operations>
<MultipleInstances>Single</MultipleInstances>
<Mandatory>Optional</Mandatory>
<Type>String</Type>
<RangeEnumeration>32 bytes</RangeEnumeration>
<Units />
<Description><![CDATA[Indicates the data description.
e.g. "meter reading".]]></Description>
</Item>
<Item ID="4"><Name>Data Format</Name>
<Operations>RW</Operations>
<MultipleInstances>Single</MultipleInstances>
<Mandatory>Optional</Mandatory>
<Type>String</Type>
<RangeEnumeration>32 bytes</RangeEnumeration>
<Units />
<Description><![CDATA[Indicates the format of the Application Data.
e.g. YG-Meter-Water-Reading
UTF8-string
]]></Description>
</Item>
<Item ID="5"><Name>App ID</Name>
<Operations>RW</Operations>
<MultipleInstances>Single</MultipleInstances>
<Mandatory>Optional</Mandatory>
<Type>Integer</Type>
<RangeEnumeration>2 bytes</RangeEnumeration>
<Units />
<Description><![CDATA[Indicates the destination Application ID.]]></Description>
</Item></Resources>
<Description2><![CDATA[]]></Description2>
</Object>
</LWM2M>

103
msa/black-box-tests/src/test/resources/lwm2m-registry/3303.xml

@ -0,0 +1,103 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Copyright © 2016-2018 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.
-->
<LWM2M xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="http://openmobilealliance.org/tech/profiles/LWM2M.xsd">
<Object ObjectType="MODefinition">
<Name>Temperature</Name>
<Description1>This IPSO object should be used with a temperature sensor to report a temperature measurement. It also provides resources for minimum/maximum measured values and the minimum/maximum range that can be measured by the temperature sensor. An example measurement unit is degrees Celsius.</Description1>
<ObjectID>3303</ObjectID>
<ObjectURN>urn:oma:lwm2m:ext:3303</ObjectURN>
<LWM2MVersion>1.0</LWM2MVersion>
<ObjectVersion>1.0</ObjectVersion>
<MultipleInstances>Multiple</MultipleInstances>
<Mandatory>Optional</Mandatory>
<Resources>
<Item ID="5700">
<Name>Sensor Value</Name>
<Operations>R</Operations>
<MultipleInstances>Single</MultipleInstances>
<Mandatory>Mandatory</Mandatory>
<Type>Float</Type>
<RangeEnumeration></RangeEnumeration>
<Units></Units>
<Description>Last or Current Measured Value from the Sensor</Description>
</Item>
<Item ID="5601">
<Name>Min Measured Value</Name>
<Operations>R</Operations>
<MultipleInstances>Single</MultipleInstances>
<Mandatory>Optional</Mandatory>
<Type>Float</Type>
<RangeEnumeration></RangeEnumeration>
<Units></Units>
<Description>The minimum value measured by the sensor since power ON or reset</Description>
</Item>
<Item ID="5602">
<Name>Max Measured Value</Name>
<Operations>R</Operations>
<MultipleInstances>Single</MultipleInstances>
<Mandatory>Optional</Mandatory>
<Type>Float</Type>
<RangeEnumeration></RangeEnumeration>
<Units></Units>
<Description>The maximum value measured by the sensor since power ON or reset</Description>
</Item>
<Item ID="5603">
<Name>Min Range Value</Name>
<Operations>R</Operations>
<MultipleInstances>Single</MultipleInstances>
<Mandatory>Optional</Mandatory>
<Type>Float</Type>
<RangeEnumeration></RangeEnumeration>
<Units></Units>
<Description>The minimum value that can be measured by the sensor</Description>
</Item>
<Item ID="5604">
<Name>Max Range Value</Name>
<Operations>R</Operations>
<MultipleInstances>Single</MultipleInstances>
<Mandatory>Optional</Mandatory>
<Type>Float</Type>
<RangeEnumeration></RangeEnumeration>
<Units></Units>
<Description>The maximum value that can be measured by the sensor</Description>
</Item>
<Item ID="5701">
<Name>Sensor Units</Name>
<Operations>R</Operations>
<MultipleInstances>Single</MultipleInstances>
<Mandatory>Optional</Mandatory>
<Type>String</Type>
<RangeEnumeration></RangeEnumeration>
<Units></Units>
<Description>Measurement Units Definition.</Description>
</Item>
<Item ID="5605">
<Name>Reset Min and Max Measured Values</Name>
<Operations>E</Operations>
<MultipleInstances>Single</MultipleInstances>
<Mandatory>Optional</Mandatory>
<Type></Type>
<RangeEnumeration></RangeEnumeration>
<Units></Units>
<Description>Reset the Min and Max Measured Values to Current Value</Description>
</Item>
</Resources>
<Description2></Description2>
</Object>
</LWM2M>

22
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/FetchMode.java

@ -0,0 +1,22 @@
/**
* Copyright © 2016-2024 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.rule.engine.metadata;
public enum FetchMode {
FIRST, ALL, LAST
}

149
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetTelemetryNode.java

@ -15,6 +15,7 @@
*/ */
package org.thingsboard.rule.engine.metadata; package org.thingsboard.rule.engine.metadata;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode; import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListenableFuture;
@ -30,17 +31,17 @@ import org.thingsboard.rule.engine.api.TbNode;
import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeConfiguration;
import org.thingsboard.rule.engine.api.TbNodeException; import org.thingsboard.rule.engine.api.TbNodeException;
import org.thingsboard.rule.engine.api.util.TbNodeUtils; import org.thingsboard.rule.engine.api.util.TbNodeUtils;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.kv.Aggregation; import org.thingsboard.server.common.data.kv.Aggregation;
import org.thingsboard.server.common.data.kv.BaseReadTsKvQuery; import org.thingsboard.server.common.data.kv.BaseReadTsKvQuery;
import org.thingsboard.server.common.data.kv.ReadTsKvQuery; import org.thingsboard.server.common.data.kv.ReadTsKvQuery;
import org.thingsboard.server.common.data.kv.TsKvEntry; import org.thingsboard.server.common.data.kv.TsKvEntry;
import org.thingsboard.server.common.data.page.SortOrder.Direction;
import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.common.data.plugin.ComponentType;
import org.thingsboard.server.common.data.util.TbPair;
import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.common.msg.TbMsgMetaData;
import java.util.List; import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@ -51,6 +52,7 @@ import java.util.stream.Collectors;
@RuleNode(type = ComponentType.ENRICHMENT, @RuleNode(type = ComponentType.ENRICHMENT,
name = "originator telemetry", name = "originator telemetry",
configClazz = TbGetTelemetryNodeConfiguration.class, configClazz = TbGetTelemetryNodeConfiguration.class,
version = 1,
nodeDescription = "Adds message originator telemetry for selected time range into message metadata", nodeDescription = "Adds message originator telemetry for selected time range into message metadata",
nodeDetails = "Useful when you need to get telemetry data set from the message originator for a specific time range " + nodeDetails = "Useful when you need to get telemetry data set from the message originator for a specific time range " +
"instead of fetching just the latest telemetry or if you need to get the closest telemetry to the fetch interval start or end. " + "instead of fetching just the latest telemetry or if you need to get the closest telemetry to the fetch interval start or end. " +
@ -60,53 +62,61 @@ import java.util.stream.Collectors;
configDirective = "tbEnrichmentNodeGetTelemetryFromDatabase") configDirective = "tbEnrichmentNodeGetTelemetryFromDatabase")
public class TbGetTelemetryNode implements TbNode { public class TbGetTelemetryNode implements TbNode {
private static final String DESC_ORDER = "DESC";
private static final String ASC_ORDER = "ASC";
private TbGetTelemetryNodeConfiguration config; private TbGetTelemetryNodeConfiguration config;
private List<String> tsKeyNames; private List<String> tsKeyNames;
private int limit; private int limit;
private String fetchMode; private FetchMode fetchMode;
private String orderByFetchAll; private Direction orderBy;
private Aggregation aggregation; private Aggregation aggregation;
@Override @Override
public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException { public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
this.config = TbNodeUtils.convert(configuration, TbGetTelemetryNodeConfiguration.class); this.config = TbNodeUtils.convert(configuration, TbGetTelemetryNodeConfiguration.class);
tsKeyNames = config.getLatestTsKeyNames(); tsKeyNames = config.getLatestTsKeyNames();
limit = config.getFetchMode().equals(TbGetTelemetryNodeConfiguration.FETCH_MODE_ALL) ? validateLimit(config.getLimit()) : 1; if (tsKeyNames.isEmpty()) {
throw new TbNodeException("Telemetry should be specified!", true);
}
fetchMode = config.getFetchMode(); fetchMode = config.getFetchMode();
orderByFetchAll = config.getOrderBy(); if (fetchMode == null) {
if (StringUtils.isEmpty(orderByFetchAll)) { throw new TbNodeException("FetchMode should be specified!", true);
orderByFetchAll = ASC_ORDER;
} }
aggregation = parseAggregationConfig(config.getAggregation()); switch (fetchMode) {
} case ALL:
limit = validateLimit(config.getLimit());
Aggregation parseAggregationConfig(String aggName) { if (config.getOrderBy() == null) {
if (StringUtils.isEmpty(aggName) || !fetchMode.equals(TbGetTelemetryNodeConfiguration.FETCH_MODE_ALL)) { throw new TbNodeException("OrderBy should be specified!", true);
return Aggregation.NONE; }
orderBy = config.getOrderBy();
if (config.getAggregation() == null) {
throw new TbNodeException("Aggregation should be specified!", true);
}
aggregation = config.getAggregation();
break;
case FIRST:
limit = 1;
orderBy = Direction.ASC;
aggregation = Aggregation.NONE;
break;
case LAST:
limit = 1;
orderBy = Direction.DESC;
aggregation = Aggregation.NONE;
break;
} }
return Aggregation.valueOf(aggName);
} }
@Override @Override
public void onMsg(TbContext ctx, TbMsg msg) throws ExecutionException, InterruptedException, TbNodeException { public void onMsg(TbContext ctx, TbMsg msg) {
if (tsKeyNames.isEmpty()) { Interval interval = getInterval(msg);
ctx.tellFailure(msg, new IllegalStateException("Telemetry is not selected!")); if (interval.getStartTs() > interval.getEndTs()) {
} else { throw new RuntimeException("Interval start should be less than Interval end");
try {
Interval interval = getInterval(msg);
List<String> keys = TbNodeUtils.processPatterns(tsKeyNames, msg);
ListenableFuture<List<TsKvEntry>> list = ctx.getTimeseriesService().findAll(ctx.getTenantId(), msg.getOriginator(), buildQueries(interval, keys));
DonAsynchron.withCallback(list, data -> {
var metaData = updateMetadata(data, msg, keys);
ctx.tellSuccess(TbMsg.transformMsgMetadata(msg, metaData));
}, error -> ctx.tellFailure(msg, error), ctx.getDbCallbackExecutor());
} catch (Exception e) {
ctx.tellFailure(msg, e);
}
} }
List<String> keys = TbNodeUtils.processPatterns(tsKeyNames, msg);
ListenableFuture<List<TsKvEntry>> list = ctx.getTimeseriesService().findAll(ctx.getTenantId(), msg.getOriginator(), buildQueries(interval, keys));
DonAsynchron.withCallback(list, data -> {
var metaData = updateMetadata(data, msg, keys);
ctx.tellSuccess(TbMsg.transformMsgMetadata(msg, metaData));
}, error -> ctx.tellFailure(msg, error), ctx.getDbCallbackExecutor());
} }
private List<ReadTsKvQuery> buildQueries(Interval interval, List<String> keys) { private List<ReadTsKvQuery> buildQueries(Interval interval, List<String> keys) {
@ -116,24 +126,13 @@ public class TbGetTelemetryNode implements TbNode {
interval.getEndTs() - interval.getStartTs(); interval.getEndTs() - interval.getStartTs();
return keys.stream() return keys.stream()
.map(key -> new BaseReadTsKvQuery(key, interval.getStartTs(), interval.getEndTs(), aggIntervalStep, limit, aggregation, getOrderBy())) .map(key -> new BaseReadTsKvQuery(key, interval.getStartTs(), interval.getEndTs(), aggIntervalStep, limit, aggregation, orderBy.name()))
.collect(Collectors.toList()); .collect(Collectors.toList());
} }
private String getOrderBy() {
switch (fetchMode) {
case TbGetTelemetryNodeConfiguration.FETCH_MODE_ALL:
return orderByFetchAll;
case TbGetTelemetryNodeConfiguration.FETCH_MODE_FIRST:
return ASC_ORDER;
default:
return DESC_ORDER;
}
}
private TbMsgMetaData updateMetadata(List<TsKvEntry> entries, TbMsg msg, List<String> keys) { private TbMsgMetaData updateMetadata(List<TsKvEntry> entries, TbMsg msg, List<String> keys) {
ObjectNode resultNode = JacksonUtil.newObjectNode(JacksonUtil.ALLOW_UNQUOTED_FIELD_NAMES_MAPPER); ObjectNode resultNode = JacksonUtil.newObjectNode(JacksonUtil.ALLOW_UNQUOTED_FIELD_NAMES_MAPPER);
if (TbGetTelemetryNodeConfiguration.FETCH_MODE_ALL.equals(fetchMode)) { if (FetchMode.ALL.equals(fetchMode)) {
entries.forEach(entry -> processArray(resultNode, entry)); entries.forEach(entry -> processArray(resultNode, entry));
} else { } else {
entries.forEach(entry -> processSingle(resultNode, entry)); entries.forEach(entry -> processSingle(resultNode, entry));
@ -174,7 +173,7 @@ public class TbGetTelemetryNode implements TbNode {
return getIntervalFromPatterns(msg); return getIntervalFromPatterns(msg);
} else { } else {
Interval interval = new Interval(); Interval interval = new Interval();
long ts = System.currentTimeMillis(); long ts = getCurrentTimeMillis();
interval.setStartTs(ts - TimeUnit.valueOf(config.getStartIntervalTimeUnit()).toMillis(config.getStartInterval())); interval.setStartTs(ts - TimeUnit.valueOf(config.getStartIntervalTimeUnit()).toMillis(config.getStartInterval()));
interval.setEndTs(ts - TimeUnit.valueOf(config.getEndIntervalTimeUnit()).toMillis(config.getEndInterval())); interval.setEndTs(ts - TimeUnit.valueOf(config.getEndIntervalTimeUnit()).toMillis(config.getEndInterval()));
return interval; return interval;
@ -211,12 +210,15 @@ public class TbGetTelemetryNode implements TbNode {
return pattern.replaceAll("[$\\[{}\\]]", ""); return pattern.replaceAll("[$\\[{}\\]]", "");
} }
private int validateLimit(int limit) { private int validateLimit(int limit) throws TbNodeException {
if (limit != 0) { if (limit < 2 || limit > TbGetTelemetryNodeConfiguration.MAX_FETCH_SIZE) {
return limit; throw new TbNodeException("Limit should be in a range from 2 to 1000.", true);
} else {
return TbGetTelemetryNodeConfiguration.MAX_FETCH_SIZE;
} }
return limit;
}
long getCurrentTimeMillis() {
return System.currentTimeMillis();
} }
@Data @Data
@ -226,4 +228,47 @@ public class TbGetTelemetryNode implements TbNode {
private Long endTs; private Long endTs;
} }
@Override
public TbPair<Boolean, JsonNode> upgrade(int fromVersion, JsonNode oldConfiguration) throws TbNodeException {
boolean hasChanges = false;
switch (fromVersion) {
case 0 -> {
if (oldConfiguration.hasNonNull("fetchMode")) {
String fetchMode = oldConfiguration.get("fetchMode").asText();
switch (fetchMode) {
case "FIRST":
((ObjectNode) oldConfiguration).put("orderBy", Direction.ASC.name());
((ObjectNode) oldConfiguration).put("aggregation", Aggregation.NONE.name());
hasChanges = true;
break;
case "LAST":
((ObjectNode) oldConfiguration).put("orderBy", Direction.DESC.name());
((ObjectNode) oldConfiguration).put("aggregation", Aggregation.NONE.name());
hasChanges = true;
break;
case "ALL":
if (oldConfiguration.has("orderBy") &&
(oldConfiguration.get("orderBy").isNull() || oldConfiguration.get("orderBy").asText().isEmpty())) {
((ObjectNode) oldConfiguration).put("orderBy", Direction.ASC.name());
hasChanges = true;
}
if (oldConfiguration.has("aggregation") &&
(oldConfiguration.get("aggregation").isNull() || oldConfiguration.get("aggregation").asText().isEmpty())) {
((ObjectNode) oldConfiguration).put("aggregation", Aggregation.NONE.name());
hasChanges = true;
}
break;
default:
((ObjectNode) oldConfiguration).put("fetchMode", FetchMode.LAST.name());
((ObjectNode) oldConfiguration).put("orderBy", Direction.DESC.name());
((ObjectNode) oldConfiguration).put("aggregation", Aggregation.NONE.name());
hasChanges = true;
break;
}
}
}
}
return new TbPair<>(hasChanges, oldConfiguration);
}
} }

17
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetTelemetryNodeConfiguration.java

@ -18,6 +18,7 @@ package org.thingsboard.rule.engine.metadata;
import lombok.Data; import lombok.Data;
import org.thingsboard.rule.engine.api.NodeConfiguration; import org.thingsboard.rule.engine.api.NodeConfiguration;
import org.thingsboard.server.common.data.kv.Aggregation; import org.thingsboard.server.common.data.kv.Aggregation;
import org.thingsboard.server.common.data.page.SortOrder.Direction;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
@ -29,10 +30,6 @@ import java.util.concurrent.TimeUnit;
@Data @Data
public class TbGetTelemetryNodeConfiguration implements NodeConfiguration<TbGetTelemetryNodeConfiguration> { public class TbGetTelemetryNodeConfiguration implements NodeConfiguration<TbGetTelemetryNodeConfiguration> {
public static final String FETCH_MODE_FIRST = "FIRST";
public static final String FETCH_MODE_LAST = "LAST";
public static final String FETCH_MODE_ALL = "ALL";
public static final int MAX_FETCH_SIZE = 1000; public static final int MAX_FETCH_SIZE = 1000;
private int startInterval; private int startInterval;
@ -45,9 +42,9 @@ public class TbGetTelemetryNodeConfiguration implements NodeConfiguration<TbGetT
private String startIntervalTimeUnit; private String startIntervalTimeUnit;
private String endIntervalTimeUnit; private String endIntervalTimeUnit;
private String fetchMode; //FIRST, LAST, ALL private FetchMode fetchMode; //FIRST, LAST, ALL
private String orderBy; //ASC, DESC private Direction orderBy; //ASC, DESC
private String aggregation; //MIN, MAX, AVG, SUM, COUNT, NONE; private Aggregation aggregation; //MIN, MAX, AVG, SUM, COUNT, NONE;
private int limit; private int limit;
private List<String> latestTsKeyNames; private List<String> latestTsKeyNames;
@ -56,7 +53,7 @@ public class TbGetTelemetryNodeConfiguration implements NodeConfiguration<TbGetT
public TbGetTelemetryNodeConfiguration defaultConfiguration() { public TbGetTelemetryNodeConfiguration defaultConfiguration() {
TbGetTelemetryNodeConfiguration configuration = new TbGetTelemetryNodeConfiguration(); TbGetTelemetryNodeConfiguration configuration = new TbGetTelemetryNodeConfiguration();
configuration.setLatestTsKeyNames(Collections.emptyList()); configuration.setLatestTsKeyNames(Collections.emptyList());
configuration.setFetchMode("FIRST"); configuration.setFetchMode(FetchMode.FIRST);
configuration.setStartIntervalTimeUnit(TimeUnit.MINUTES.name()); configuration.setStartIntervalTimeUnit(TimeUnit.MINUTES.name());
configuration.setStartInterval(2); configuration.setStartInterval(2);
configuration.setEndIntervalTimeUnit(TimeUnit.MINUTES.name()); configuration.setEndIntervalTimeUnit(TimeUnit.MINUTES.name());
@ -64,8 +61,8 @@ public class TbGetTelemetryNodeConfiguration implements NodeConfiguration<TbGetT
configuration.setUseMetadataIntervalPatterns(false); configuration.setUseMetadataIntervalPatterns(false);
configuration.setStartIntervalPattern(""); configuration.setStartIntervalPattern("");
configuration.setEndIntervalPattern(""); configuration.setEndIntervalPattern("");
configuration.setOrderBy("ASC"); configuration.setOrderBy(Direction.ASC);
configuration.setAggregation(Aggregation.NONE.name()); configuration.setAggregation(Aggregation.NONE);
configuration.setLimit(MAX_FETCH_SIZE); configuration.setLimit(MAX_FETCH_SIZE);
return configuration; return configuration;
} }

644
rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/TbGetTelemetryNodeTest.java

@ -15,72 +15,634 @@
*/ */
package org.thingsboard.rule.engine.metadata; package org.thingsboard.rule.engine.metadata;
import org.junit.jupiter.api.Assertions; import com.google.common.util.concurrent.Futures;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.junit.jupiter.params.provider.ValueSource;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.common.util.ListeningExecutor;
import org.thingsboard.rule.engine.AbstractRuleNodeUpgradeTest;
import org.thingsboard.rule.engine.TestDbCallbackExecutor;
import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbContext;
import org.thingsboard.rule.engine.api.TbNode;
import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeConfiguration;
import org.thingsboard.rule.engine.api.TbNodeException;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.kv.Aggregation; import org.thingsboard.server.common.data.kv.Aggregation;
import org.thingsboard.server.common.data.kv.BasicTsKvEntry;
import org.thingsboard.server.common.data.kv.DoubleDataEntry;
import org.thingsboard.server.common.data.kv.ReadTsKvQuery;
import org.thingsboard.server.common.data.kv.TsKvEntry;
import org.thingsboard.server.common.data.kv.TsKvQuery;
import org.thingsboard.server.common.data.msg.TbMsgType;
import org.thingsboard.server.common.data.page.SortOrder.Direction;
import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.common.msg.TbMsgMetaData;
import org.thingsboard.server.dao.timeseries.TimeseriesService;
import static org.hamcrest.CoreMatchers.is; import java.util.Collections;
import static org.hamcrest.MatcherAssert.assertThat; import java.util.List;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
import java.util.stream.Stream;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.willCallRealMethod; import static org.mockito.ArgumentMatchers.anyList;
import static org.mockito.Mockito.mock; import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.spy; import static org.mockito.BDDMockito.given;
import static org.mockito.BDDMockito.spy;
import static org.mockito.BDDMockito.then;
import static org.mockito.BDDMockito.willReturn;
@ExtendWith(MockitoExtension.class)
public class TbGetTelemetryNodeTest extends AbstractRuleNodeUpgradeTest {
private final TenantId TENANT_ID = TenantId.fromUUID(UUID.fromString("5738401b-9dba-422b-b656-a62fe7431917"));
private final DeviceId DEVICE_ID = new DeviceId(UUID.fromString("8a8fd749-b2ec-488b-a6c6-fc66614d8686"));
public class TbGetTelemetryNodeTest { private final ListeningExecutor executor = new TestDbCallbackExecutor();
TbGetTelemetryNode node; private TbGetTelemetryNode node;
TbGetTelemetryNodeConfiguration config; private TbGetTelemetryNodeConfiguration config;
TbNodeConfiguration nodeConfiguration;
TbContext ctx; @Mock
private TbContext ctxMock;
@Mock
private TimeseriesService timeseriesServiceMock;
@BeforeEach @BeforeEach
public void setUp() throws Exception { public void setUp() {
ctx = mock(TbContext.class);
node = spy(new TbGetTelemetryNode()); node = spy(new TbGetTelemetryNode());
config = new TbGetTelemetryNodeConfiguration(); config = new TbGetTelemetryNodeConfiguration().defaultConfiguration();
config.setFetchMode("ALL"); config.setLatestTsKeyNames(List.of("temperature"));
nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config)); }
node.init(ctx, nodeConfiguration);
@Test
public void verifyDefaultConfig() {
config = new TbGetTelemetryNodeConfiguration().defaultConfiguration();
assertThat(config.getStartInterval()).isEqualTo(2);
assertThat(config.getEndInterval()).isEqualTo(1);
assertThat(config.getStartIntervalPattern()).isEqualTo("");
assertThat(config.getEndIntervalPattern()).isEqualTo("");
assertThat(config.isUseMetadataIntervalPatterns()).isFalse();
assertThat(config.getStartIntervalTimeUnit()).isEqualTo(TimeUnit.MINUTES.name());
assertThat(config.getEndIntervalTimeUnit()).isEqualTo(TimeUnit.MINUTES.name());
assertThat(config.getFetchMode()).isEqualTo(FetchMode.FIRST);
assertThat(config.getOrderBy()).isEqualTo(Direction.ASC);
assertThat(config.getAggregation()).isEqualTo(Aggregation.NONE);
assertThat(config.getLimit()).isEqualTo(1000);
assertThat(config.getLatestTsKeyNames()).isEmpty();
}
@Test
public void givenEmptyTsKeyNames_whenInit_thenThrowsException() {
// GIVEN
config.setLatestTsKeyNames(Collections.emptyList());
// WHEN-THEN
assertThatThrownBy(() -> node.init(ctxMock, new TbNodeConfiguration(JacksonUtil.valueToTree(config))))
.isInstanceOf(TbNodeException.class)
.hasMessage("Telemetry should be specified!")
.extracting(e -> ((TbNodeException) e).isUnrecoverable())
.isEqualTo(true);
}
@Test
public void givenFetchModeIsNull_whenInit_thenThrowsException() {
// GIVEN
config.setFetchMode(null);
// WHEN-THEN
assertThatThrownBy(() -> node.init(ctxMock, new TbNodeConfiguration(JacksonUtil.valueToTree(config))))
.isInstanceOf(TbNodeException.class)
.hasMessage("FetchMode should be specified!")
.extracting(e -> ((TbNodeException) e).isUnrecoverable())
.isEqualTo(true);
}
@Test
public void givenFetchModeAllAndOrderByIsNull_whenInit_thenThrowsException() {
// GIVEN
config.setFetchMode(FetchMode.ALL);
config.setOrderBy(null);
// WHEN-THEN
assertThatThrownBy(() -> node.init(ctxMock, new TbNodeConfiguration(JacksonUtil.valueToTree(config))))
.isInstanceOf(TbNodeException.class)
.hasMessage("OrderBy should be specified!")
.extracting(e -> ((TbNodeException) e).isUnrecoverable())
.isEqualTo(true);
}
@ParameterizedTest
@ValueSource(ints = {-1, 0, 1, 1001, 2000})
public void givenFetchModeAllAndLimitIsOutOfRange_whenInit_thenThrowsException(int limit) {
// GIVEN
config.setFetchMode(FetchMode.ALL);
config.setLimit(limit);
willCallRealMethod().given(node).parseAggregationConfig(any()); // WHEN-THEN
assertThatThrownBy(() -> node.init(ctxMock, new TbNodeConfiguration(JacksonUtil.valueToTree(config))))
.isInstanceOf(TbNodeException.class)
.hasMessage("Limit should be in a range from 2 to 1000.")
.extracting(e -> ((TbNodeException) e).isUnrecoverable())
.isEqualTo(true);
} }
@Test @Test
public void givenAggregationAsString_whenParseAggregation_thenReturnEnum() { public void givenFetchModeIsAllAndAggregationIsNull_whenInit_thenThrowsException() {
//compatibility with old configs without "aggregation" parameter // GIVEN
assertThat(node.parseAggregationConfig(null), is(Aggregation.NONE)); config.setFetchMode(FetchMode.ALL);
assertThat(node.parseAggregationConfig(""), is(Aggregation.NONE)); config.setAggregation(null);
// WHEN-THEN
assertThatThrownBy(() -> node.init(ctxMock, new TbNodeConfiguration(JacksonUtil.valueToTree(config))))
.isInstanceOf(TbNodeException.class)
.hasMessage("Aggregation should be specified!")
.extracting(e -> ((TbNodeException) e).isUnrecoverable())
.isEqualTo(true);
}
//common values @Test
assertThat(node.parseAggregationConfig("MIN"), is(Aggregation.MIN)); public void givenIntervalStartIsGreaterThanIntervalEnd_whenOnMsg_thenThrowsException() throws TbNodeException {
assertThat(node.parseAggregationConfig("MAX"), is(Aggregation.MAX)); // GIVEN
assertThat(node.parseAggregationConfig("AVG"), is(Aggregation.AVG)); config.setStartInterval(1);
assertThat(node.parseAggregationConfig("SUM"), is(Aggregation.SUM)); config.setEndInterval(2);
assertThat(node.parseAggregationConfig("COUNT"), is(Aggregation.COUNT));
assertThat(node.parseAggregationConfig("NONE"), is(Aggregation.NONE));
//all possible values in future // WHEN-THEN
for (Aggregation aggEnum : Aggregation.values()) { node.init(ctxMock, new TbNodeConfiguration(JacksonUtil.valueToTree(config)));
assertThat(node.parseAggregationConfig(aggEnum.name()), is(aggEnum)); TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, DEVICE_ID, TbMsgMetaData.EMPTY, TbMsg.EMPTY_JSON_OBJECT);
} assertThatThrownBy(() -> node.onMsg(ctxMock, msg))
.isInstanceOf(RuntimeException.class)
.hasMessage("Interval start should be less than Interval end");
} }
@Test @Test
public void givenAggregationWhiteSpace_whenParseAggregation_thenException() { public void givenUseMetadataIntervalPatternsIsTrue_whenOnMsg_thenVerifyStartAndEndTsInQuery() throws TbNodeException {
Assertions.assertThrows(IllegalArgumentException.class, () -> { // GIVEN
node.parseAggregationConfig(" "); config.setUseMetadataIntervalPatterns(true);
}); config.setStartIntervalPattern("${mdStartInterval}");
config.setEndIntervalPattern("$[msgEndInterval]");
node.init(ctxMock, new TbNodeConfiguration(JacksonUtil.valueToTree(config)));
mockTimeseriesService();
given(timeseriesServiceMock.findAll(any(TenantId.class), any(EntityId.class), anyList())).willReturn(Futures.immediateFuture(Collections.emptyList()));
// WHEN
long startTs = 1719220350000L;
long endTs = 1719220353000L;
TbMsgMetaData metaData = new TbMsgMetaData();
metaData.putValue("mdStartInterval", String.valueOf(startTs));
TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, DEVICE_ID, metaData, "{\"msgEndInterval\":\"" + endTs + "\"}");
node.onMsg(ctxMock, msg);
// THEN
ArgumentCaptor<List<ReadTsKvQuery>> actualReadTsKvQueryList = ArgumentCaptor.forClass(List.class);
then(timeseriesServiceMock).should().findAll(eq(TENANT_ID), eq(DEVICE_ID), actualReadTsKvQueryList.capture());
ReadTsKvQuery actualReadTsKvQuery = actualReadTsKvQueryList.getValue().get(0);
assertThat(actualReadTsKvQuery.getStartTs()).isEqualTo(startTs);
assertThat(actualReadTsKvQuery.getEndTs()).isEqualTo(endTs);
}
@Test
public void givenUseMetadataIntervalPatternsIsFalse_whenOnMsg_thenVerifyStartAndEndTsInQuery() throws TbNodeException {
// GIVEN
node.init(ctxMock, new TbNodeConfiguration(JacksonUtil.valueToTree(config)));
long ts = System.currentTimeMillis();
willReturn(ts).given(node).getCurrentTimeMillis();
mockTimeseriesService();
given(timeseriesServiceMock.findAll(any(TenantId.class), any(EntityId.class), anyList())).willReturn(Futures.immediateFuture(Collections.emptyList()));
// WHEN
TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, DEVICE_ID, TbMsgMetaData.EMPTY, TbMsg.EMPTY_JSON_OBJECT);
node.onMsg(ctxMock, msg);
// THEN
ArgumentCaptor<List<ReadTsKvQuery>> actualReadTsKvQueryList = ArgumentCaptor.forClass(List.class);
then(timeseriesServiceMock).should().findAll(eq(TENANT_ID), eq(DEVICE_ID), actualReadTsKvQueryList.capture());
ReadTsKvQuery actualReadTsKvQuery = actualReadTsKvQueryList.getValue().get(0);
assertThat(actualReadTsKvQuery.getStartTs()).isEqualTo(ts - TimeUnit.MINUTES.toMillis(config.getStartInterval()));
assertThat(actualReadTsKvQuery.getEndTs()).isEqualTo(ts - TimeUnit.MINUTES.toMillis(config.getEndInterval()));
}
@Test
public void givenTsKeyNamesPatterns_whenOnMsg_thenVerifyTsKeyNamesInQuery() throws TbNodeException {
// GIVEN
config.setLatestTsKeyNames(List.of("temperature", "${mdTsKey}", "$[msgTsKey]"));
node.init(ctxMock, new TbNodeConfiguration(JacksonUtil.valueToTree(config)));
mockTimeseriesService();
given(timeseriesServiceMock.findAll(any(TenantId.class), any(EntityId.class), anyList())).willReturn(Futures.immediateFuture(Collections.emptyList()));
// WHEN
TbMsgMetaData metaData = new TbMsgMetaData();
metaData.putValue("mdTsKey", "humidity");
TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, DEVICE_ID, metaData, "{\"msgTsKey\":\"pressure\"}");
node.onMsg(ctxMock, msg);
// THEN
ArgumentCaptor<List<ReadTsKvQuery>> actualReadTsKvQueryList = ArgumentCaptor.forClass(List.class);
then(timeseriesServiceMock).should().findAll(eq(TENANT_ID), eq(DEVICE_ID), actualReadTsKvQueryList.capture());
List<String> actualKeys = actualReadTsKvQueryList.getValue().stream().map(TsKvQuery::getKey).toList();
assertThat(actualKeys).containsExactlyInAnyOrder("temperature", "humidity", "pressure");
}
@ParameterizedTest
@MethodSource
public void givenAggregation_whenOnMsg_thenVerifyAggregationStepInQuery(Aggregation aggregation, Consumer<ReadTsKvQuery> aggregationStepVerifier) throws TbNodeException {
// GIVEN
config.setStartInterval(5);
config.setEndInterval(1);
config.setFetchMode(FetchMode.ALL);
config.setAggregation(aggregation);
node.init(ctxMock, new TbNodeConfiguration(JacksonUtil.valueToTree(config)));
mockTimeseriesService();
given(timeseriesServiceMock.findAll(any(TenantId.class), any(EntityId.class), anyList())).willReturn(Futures.immediateFuture(Collections.emptyList()));
// WHEN
TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, DEVICE_ID, TbMsgMetaData.EMPTY, TbMsg.EMPTY_JSON_OBJECT);
node.onMsg(ctxMock, msg);
// THEN
ArgumentCaptor<List<ReadTsKvQuery>> actualReadTsKvQueryList = ArgumentCaptor.forClass(List.class);
then(timeseriesServiceMock).should().findAll(eq(TENANT_ID), eq(DEVICE_ID), actualReadTsKvQueryList.capture());
ReadTsKvQuery actualReadTsKvQuery = actualReadTsKvQueryList.getValue().get(0);
aggregationStepVerifier.accept(actualReadTsKvQuery);
}
private static Stream<Arguments> givenAggregation_whenOnMsg_thenVerifyAggregationStepInQuery() {
return Stream.of(
Arguments.of(Aggregation.NONE, (Consumer<ReadTsKvQuery>) query -> assertThat(query.getInterval()).isEqualTo(1)),
Arguments.of(Aggregation.AVG, (Consumer<ReadTsKvQuery>) query -> assertThat(query.getInterval()).isEqualTo(query.getEndTs() - query.getStartTs()))
);
}
@ParameterizedTest
@MethodSource
public void givenFetchModeAndLimit_whenOnMsg_thenVerifyLimitInQuery(FetchMode fetchMode, int limit, Consumer<ReadTsKvQuery> limitInQueryVerifier) throws TbNodeException {
// GIVEN
config.setFetchMode(fetchMode);
config.setLimit(limit);
node.init(ctxMock, new TbNodeConfiguration(JacksonUtil.valueToTree(config)));
mockTimeseriesService();
given(timeseriesServiceMock.findAll(any(TenantId.class), any(EntityId.class), anyList())).willReturn(Futures.immediateFuture(Collections.emptyList()));
// WHEN
TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, DEVICE_ID, TbMsgMetaData.EMPTY, TbMsg.EMPTY_JSON_OBJECT);
node.onMsg(ctxMock, msg);
// THEN
ArgumentCaptor<List<ReadTsKvQuery>> actualReadTsKvQueryList = ArgumentCaptor.forClass(List.class);
then(timeseriesServiceMock).should().findAll(eq(TENANT_ID), eq(DEVICE_ID), actualReadTsKvQueryList.capture());
ReadTsKvQuery actualReadTsKvQuery = actualReadTsKvQueryList.getValue().get(0);
limitInQueryVerifier.accept(actualReadTsKvQuery);
}
private static Stream<Arguments> givenFetchModeAndLimit_whenOnMsg_thenVerifyLimitInQuery() {
return Stream.of(
Arguments.of(
FetchMode.ALL,
5,
(Consumer<ReadTsKvQuery>) query -> assertThat(query.getLimit()).isEqualTo(5)),
Arguments.of(
FetchMode.FIRST,
TbGetTelemetryNodeConfiguration.MAX_FETCH_SIZE,
(Consumer<ReadTsKvQuery>) query -> assertThat(query.getLimit()).isEqualTo(1)),
Arguments.of(
FetchMode.LAST,
10,
(Consumer<ReadTsKvQuery>) query -> assertThat(query.getLimit()).isEqualTo(1))
);
}
@ParameterizedTest
@MethodSource
public void givenFetchModeAndOrder_whenOnMsg_thenVerifyOrderInQuery(FetchMode fetchMode, Direction orderBy, Consumer<ReadTsKvQuery> orderInQueryVerifier) throws TbNodeException {
// GIVEN
config.setFetchMode(fetchMode);
config.setOrderBy(orderBy);
node.init(ctxMock, new TbNodeConfiguration(JacksonUtil.valueToTree(config)));
mockTimeseriesService();
given(timeseriesServiceMock.findAll(any(TenantId.class), any(EntityId.class), anyList())).willReturn(Futures.immediateFuture(Collections.emptyList()));
// WHEN
TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, DEVICE_ID, TbMsgMetaData.EMPTY, TbMsg.EMPTY_JSON_OBJECT);
node.onMsg(ctxMock, msg);
// THEN
ArgumentCaptor<List<ReadTsKvQuery>> actualReadTsKvQueryList = ArgumentCaptor.forClass(List.class);
then(timeseriesServiceMock).should().findAll(eq(TENANT_ID), eq(DEVICE_ID), actualReadTsKvQueryList.capture());
ReadTsKvQuery actualReadTsKvQuery = actualReadTsKvQueryList.getValue().get(0);
orderInQueryVerifier.accept(actualReadTsKvQuery);
}
private static Stream<Arguments> givenFetchModeAndOrder_whenOnMsg_thenVerifyOrderInQuery() {
return Stream.of(
Arguments.of(
FetchMode.ALL,
Direction.DESC,
(Consumer<ReadTsKvQuery>) query -> assertThat(query.getOrder()).isEqualTo("DESC")),
Arguments.of(
FetchMode.FIRST,
Direction.ASC,
(Consumer<ReadTsKvQuery>) query -> assertThat(query.getOrder()).isEqualTo("ASC")),
Arguments.of(
FetchMode.LAST,
Direction.ASC,
(Consumer<ReadTsKvQuery>) query -> assertThat(query.getOrder()).isEqualTo("DESC"))
);
}
@ParameterizedTest
@MethodSource
public void givenInvalidIntervalPatterns_whenOnMsg_thenThrowsException(String startIntervalPattern, String errorMsg) throws TbNodeException {
// GIVEN
config.setUseMetadataIntervalPatterns(true);
config.setStartIntervalPattern(startIntervalPattern);
node.init(ctxMock, new TbNodeConfiguration(JacksonUtil.valueToTree(config)));
// WHEN-THEN
TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, DEVICE_ID, TbMsgMetaData.EMPTY, "{\"msgStartInterval\":\"start\"}");
assertThatThrownBy(() -> node.onMsg(ctxMock, msg)).isInstanceOf(IllegalArgumentException.class).hasMessage(errorMsg);
}
private static Stream<Arguments> givenInvalidIntervalPatterns_whenOnMsg_thenThrowsException() {
return Stream.of(
Arguments.of("${mdStartInterval}", "Message value: 'mdStartInterval' is undefined"),
Arguments.of("$[msgStartInterval]", "Message value: 'msgStartInterval' has invalid format")
);
} }
@Test @Test
public void givenAggregationIncorrect_whenParseAggregation_thenException() { public void givenFetchModeAll_whenOnMsg_thenTellSuccessAndVerifyMsg() throws TbNodeException {
Assertions.assertThrows(IllegalArgumentException.class, () -> { // GIVEN
node.parseAggregationConfig("TOP"); config.setLatestTsKeyNames(List.of("temperature", "humidity"));
}); config.setFetchMode(FetchMode.ALL);
node.init(ctxMock, new TbNodeConfiguration(JacksonUtil.valueToTree(config)));
mockTimeseriesService();
long ts = System.currentTimeMillis();
List<TsKvEntry> tsKvEntries = List.of(
new BasicTsKvEntry(ts - 5, new DoubleDataEntry("temperature", 23.1)),
new BasicTsKvEntry(ts - 4, new DoubleDataEntry("temperature", 22.4)),
new BasicTsKvEntry(ts - 4, new DoubleDataEntry("humidity", 55.5))
);
given(timeseriesServiceMock.findAll(any(TenantId.class), any(EntityId.class), anyList())).willReturn(Futures.immediateFuture(tsKvEntries));
// WHEN
TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, DEVICE_ID, TbMsgMetaData.EMPTY, TbMsg.EMPTY_JSON_OBJECT);
node.onMsg(ctxMock, msg);
// THEN
ArgumentCaptor<TbMsg> actualMsg = ArgumentCaptor.forClass(TbMsg.class);
then(ctxMock).should().tellSuccess(actualMsg.capture());
TbMsgMetaData metaData = new TbMsgMetaData();
metaData.putValue("temperature", "[{\"ts\":" + (ts - 5) + ",\"value\":23.1},{\"ts\":" + (ts - 4) + ",\"value\":22.4}]");
metaData.putValue("humidity", "[{\"ts\":" + (ts - 4) + ",\"value\":55.5}]");
TbMsg expectedMsg = TbMsg.transformMsgMetadata(msg, metaData);
assertThat(actualMsg.getValue()).usingRecursiveComparison().ignoringFields("ctx").isEqualTo(expectedMsg);
} }
@ParameterizedTest
@ValueSource(strings = {"FIRST", "LAST"})
public void givenFetchMode_whenOnMsg_thenTellSuccessAndVerifyMsg(String fetchMode) throws TbNodeException {
// GIVEN
config.setFetchMode(FetchMode.valueOf(fetchMode));
config.setLatestTsKeyNames(List.of("temperature", "humidity"));
node.init(ctxMock, new TbNodeConfiguration(JacksonUtil.valueToTree(config)));
mockTimeseriesService();
long ts = System.currentTimeMillis();
List<TsKvEntry> tsKvEntries = List.of(
new BasicTsKvEntry(ts - 4, new DoubleDataEntry("temperature", 22.4)),
new BasicTsKvEntry(ts - 4, new DoubleDataEntry("humidity", 55.5))
);
given(timeseriesServiceMock.findAll(any(TenantId.class), any(EntityId.class), anyList())).willReturn(Futures.immediateFuture(tsKvEntries));
// WHEN
TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, DEVICE_ID, TbMsgMetaData.EMPTY, TbMsg.EMPTY_JSON_OBJECT);
node.onMsg(ctxMock, msg);
// THEN
ArgumentCaptor<TbMsg> actualMsg = ArgumentCaptor.forClass(TbMsg.class);
then(ctxMock).should().tellSuccess(actualMsg.capture());
TbMsgMetaData metaData = new TbMsgMetaData();
metaData.putValue("temperature", "\"22.4\"");
metaData.putValue("humidity", "\"55.5\"");
TbMsg expectedMsg = TbMsg.transformMsgMetadata(msg, metaData);
assertThat(actualMsg.getValue()).usingRecursiveComparison().ignoringFields("ctx").isEqualTo(expectedMsg);
}
private void mockTimeseriesService() {
given(ctxMock.getTimeseriesService()).willReturn(timeseriesServiceMock);
given(ctxMock.getTenantId()).willReturn(TENANT_ID);
given(ctxMock.getDbCallbackExecutor()).willReturn(executor);
}
private static Stream<Arguments> givenFromVersionAndConfig_whenUpgrade_thenVerifyHasChangesAndConfig() {
return Stream.of(
// config for version 0 (fetchMode is 'FIRST' and orderBy is 'INVALID_ORDER_BY' and aggregation is 'SUM')
Arguments.of(0,
"""
{
"latestTsKeyNames": [],
"aggregation": "SUM",
"fetchMode": "FIRST",
"orderBy": "INVALID_ORDER_BY",
"limit": 1000,
"useMetadataIntervalPatterns": false,
"startIntervalPattern": "",
"endIntervalPattern": "",
"startInterval": 2,
"startIntervalTimeUnit": "MINUTES",
"endInterval": 1,
"endIntervalTimeUnit": "MINUTES"
}
""",
true,
"""
{
"latestTsKeyNames": [],
"aggregation": "NONE",
"fetchMode": "FIRST",
"orderBy": "ASC",
"limit": 1000,
"useMetadataIntervalPatterns": false,
"startIntervalPattern": "",
"endIntervalPattern": "",
"startInterval": 2,
"startIntervalTimeUnit": "MINUTES",
"endInterval": 1,
"endIntervalTimeUnit": "MINUTES"
}
"""),
// config for version 0 (fetchMode is 'LAST' and orderBy is 'ASC' and aggregation is 'AVG')
Arguments.of(0,
"""
{
"latestTsKeyNames": [],
"aggregation": "AVG",
"fetchMode": "LAST",
"orderBy": "ASC",
"limit": 1000,
"useMetadataIntervalPatterns": false,
"startIntervalPattern": "",
"endIntervalPattern": "",
"startInterval": 2,
"startIntervalTimeUnit": "MINUTES",
"endInterval": 1,
"endIntervalTimeUnit": "MINUTES"
}
""",
true,
"""
{
"latestTsKeyNames": [],
"aggregation": "NONE",
"fetchMode": "LAST",
"orderBy": "DESC",
"limit": 1000,
"useMetadataIntervalPatterns": false,
"startIntervalPattern": "",
"endIntervalPattern": "",
"startInterval": 2,
"startIntervalTimeUnit": "MINUTES",
"endInterval": 1,
"endIntervalTimeUnit": "MINUTES"
}
"""),
// config for version 0 (fetchMode is 'ALL' and orderBy is empty and aggregation is null)
Arguments.of(0,
"""
{
"latestTsKeyNames": [],
"aggregation": null,
"fetchMode": "ALL",
"orderBy": "",
"limit": 1000,
"useMetadataIntervalPatterns": false,
"startIntervalPattern": "",
"endIntervalPattern": "",
"startInterval": 2,
"startIntervalTimeUnit": "MINUTES",
"endInterval": 1,
"endIntervalTimeUnit": "MINUTES"
}
""",
true,
"""
{
"latestTsKeyNames": [],
"aggregation": "NONE",
"fetchMode": "ALL",
"orderBy": "ASC",
"limit": 1000,
"useMetadataIntervalPatterns": false,
"startIntervalPattern": "",
"endIntervalPattern": "",
"startInterval": 2,
"startIntervalTimeUnit": "MINUTES",
"endInterval": 1,
"endIntervalTimeUnit": "MINUTES"
}
"""),
// config for version 0 (fetchMode is 'ALL' and orderBy is 'DESC' and aggregation is 'SUM')
Arguments.of(0,
"""
{
"latestTsKeyNames": [],
"aggregation": "SUM",
"fetchMode": "ALL",
"orderBy": "DESC",
"limit": 1000,
"useMetadataIntervalPatterns": false,
"startIntervalPattern": "",
"endIntervalPattern": "",
"startInterval": 2,
"startIntervalTimeUnit": "MINUTES",
"endInterval": 1,
"endIntervalTimeUnit": "MINUTES"
}
""",
false,
"""
{
"latestTsKeyNames": [],
"aggregation": "SUM",
"fetchMode": "ALL",
"orderBy": "DESC",
"limit": 1000,
"useMetadataIntervalPatterns": false,
"startIntervalPattern": "",
"endIntervalPattern": "",
"startInterval": 2,
"startIntervalTimeUnit": "MINUTES",
"endInterval": 1,
"endIntervalTimeUnit": "MINUTES"
}
"""),
// config for version 0 (fetchMode is 'INVALID_MODE' and orderBy is 'INVALID_ORDER_BY' and aggregation is 'INVALID_AGGREGATION')
Arguments.of(0,
"""
{
"latestTsKeyNames": [],
"aggregation": "INVALID_AGGREGATION",
"fetchMode": "INVALID_MODE",
"orderBy": "INVALID_ORDER_BY",
"limit": 1000,
"useMetadataIntervalPatterns": false,
"startIntervalPattern": "",
"endIntervalPattern": "",
"startInterval": 2,
"startIntervalTimeUnit": "MINUTES",
"endInterval": 1,
"endIntervalTimeUnit": "MINUTES"
}
""",
true,
"""
{
"latestTsKeyNames": [],
"aggregation": "NONE",
"fetchMode": "LAST",
"orderBy": "DESC",
"limit": 1000,
"useMetadataIntervalPatterns": false,
"startIntervalPattern": "",
"endIntervalPattern": "",
"startInterval": 2,
"startIntervalTimeUnit": "MINUTES",
"endInterval": 1,
"endIntervalTimeUnit": "MINUTES"
}
""")
);
}
@Override
protected TbNode getTestNode() {
return node;
}
} }

9
ui-ngx/src/app/core/utils.ts

@ -877,6 +877,15 @@ export const getOS = (): string => {
return os; return os;
}; };
export const isSafari = (): boolean => {
const userAgent = window.navigator.userAgent.toLowerCase();
return /^((?!chrome|android).)*safari/i.test(userAgent);
};
export const isFirefox = (): boolean => {
const userAgent = window.navigator.userAgent.toLowerCase();
return /^((?!seamonkey).)*firefox/i.test(userAgent);
};
export const camelCase = (str: string): string => { export const camelCase = (str: string): string => {
return _.camelCase(str); return _.camelCase(str);

23
ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.ts

@ -169,6 +169,8 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
LayoutType = LayoutType; LayoutType = LayoutType;
private destroyed = false;
private forcePristine = false; private forcePristine = false;
get isDirty(): boolean { get isDirty(): boolean {
@ -594,6 +596,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
} }
ngOnDestroy(): void { ngOnDestroy(): void {
this.destroyed = true;
this.cleanupDashboardCss(); this.cleanupDashboardCss();
if (this.isMobileApp && this.syncStateWithQueryParam) { if (this.isMobileApp && this.syncStateWithQueryParam) {
this.mobileService.unregisterToggleLayoutFunction(); this.mobileService.unregisterToggleLayoutFunction();
@ -1106,16 +1109,18 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
} }
public openDashboardState(state: string, openRightLayout?: boolean) { public openDashboardState(state: string, openRightLayout?: boolean) {
const layoutsData = this.dashboardUtils.getStateLayoutsData(this.dashboard, state); if (!this.destroyed) {
if (layoutsData) { const layoutsData = this.dashboardUtils.getStateLayoutsData(this.dashboard, state);
this.dashboardCtx.state = state; if (layoutsData) {
this.dashboardCtx.aliasController.dashboardStateChanged(); this.dashboardCtx.state = state;
this.isRightLayoutOpened = openRightLayout ? true : false; this.dashboardCtx.aliasController.dashboardStateChanged();
this.updateLayouts(layoutsData); this.isRightLayoutOpened = openRightLayout ? true : false;
this.updateLayouts(layoutsData);
}
setTimeout(() => {
this.mobileService.onDashboardLoaded(this.layouts.right.show, this.isRightLayoutOpened);
});
} }
setTimeout(() => {
this.mobileService.onDashboardLoaded(this.layouts.right.show, this.isRightLayoutOpened);
});
} }
private updateLayouts(layoutsData?: DashboardLayoutsInfo) { private updateLayouts(layoutsData?: DashboardLayoutsInfo) {

11
ui-ngx/src/app/modules/home/components/dashboard/dashboard.component.ts

@ -248,8 +248,15 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo
defaultItemCols: 8, defaultItemCols: 8,
defaultItemRows: 6, defaultItemRows: 6,
displayGrid: this.displayGrid, displayGrid: this.displayGrid,
resizable: {enabled: this.isEdit && !this.isEditingWidget, delayStart: 50}, resizable: {
draggable: {enabled: this.isEdit && !this.isEditingWidget}, enabled: this.isEdit && !this.isEditingWidget,
delayStart: 50,
stop: (_, itemComponent) => {(itemComponent.item as DashboardWidget).updatePosition(itemComponent.$item.x, itemComponent.$item.y);}
},
draggable: {
enabled: this.isEdit && !this.isEditingWidget,
stop: (_, itemComponent) => {(itemComponent.item as DashboardWidget).updatePosition(itemComponent.$item.x, itemComponent.$item.y);}
},
itemChangeCallback: () => this.dashboardWidgets.sortWidgets(), itemChangeCallback: () => this.dashboardWidgets.sortWidgets(),
itemInitCallback: (_, itemComponent) => { itemInitCallback: (_, itemComponent) => {
(itemComponent.item as DashboardWidget).gridsterItemComponent = itemComponent; (itemComponent.item as DashboardWidget).gridsterItemComponent = itemComponent;

4
ui-ngx/src/app/modules/home/components/widget/lib/scada/scada-symbol-widget.component.ts

@ -41,6 +41,7 @@ import { isDefinedAndNotNull, mergeDeep } from '@core/utils';
import { WidgetContext } from '@home/models/widget-component.models'; import { WidgetContext } from '@home/models/widget-component.models';
import { catchError, share } from 'rxjs/operators'; import { catchError, share } from 'rxjs/operators';
import { MatIconRegistry } from '@angular/material/icon'; import { MatIconRegistry } from '@angular/material/icon';
import { RafService } from '@core/services/raf.service';
@Component({ @Component({
selector: 'tb-scada-symbol-widget', selector: 'tb-scada-symbol-widget',
@ -77,6 +78,7 @@ export class ScadaSymbolWidgetComponent implements OnInit, AfterViewInit, OnDest
protected sanitizer: DomSanitizer, protected sanitizer: DomSanitizer,
private imageService: ImageService, private imageService: ImageService,
private iconRegistry: MatIconRegistry, private iconRegistry: MatIconRegistry,
private raf: RafService,
protected cd: ChangeDetectorRef) { protected cd: ChangeDetectorRef) {
} }
@ -141,7 +143,7 @@ export class ScadaSymbolWidgetComponent implements OnInit, AfterViewInit, OnDest
this.noScadaSymbol = true; this.noScadaSymbol = true;
this.cd.markForCheck(); this.cd.markForCheck();
} else { } else {
this.scadaSymbolObject = new ScadaSymbolObject(rootElement, this.ctx, this.iconRegistry, this.scadaSymbolObject = new ScadaSymbolObject(rootElement, this.ctx, this.iconRegistry, this.raf,
content, content,
this.settings.scadaSymbolObjectSettings, this, simulated); this.settings.scadaSymbolObjectSettings, this, simulated);
} }

738
ui-ngx/src/app/modules/home/components/widget/lib/scada/scada-symbol.models.ts

@ -15,7 +15,22 @@
/// ///
import { ValueType } from '@shared/models/constants'; import { ValueType } from '@shared/models/constants';
import { Box, Element, Runner, Style, SVG, Svg, Text, Timeline } from '@svgdotjs/svg.js'; import {
Box,
EasingLiteral,
Element,
Matrix,
MatrixExtract,
MatrixTransformParam,
Runner,
Style,
SVG,
Svg,
Text,
Timeline,
TimesParam,
TransformData
} from '@svgdotjs/svg.js';
import '@svgdotjs/svg.panzoom.js'; import '@svgdotjs/svg.panzoom.js';
import { import {
DataToValueType, DataToValueType,
@ -27,9 +42,14 @@ import {
} from '@shared/models/action-widget-settings.models'; } from '@shared/models/action-widget-settings.models';
import { import {
createLabelFromSubscriptionEntityInfo, createLabelFromSubscriptionEntityInfo,
deepClone,
formatValue, formatValue,
guid, guid,
isDefinedAndNotNull, isDefinedAndNotNull,
isFirefox,
isNumeric,
isSafari,
isUndefined,
isUndefinedOrNull, isUndefinedOrNull,
mergeDeep, mergeDeep,
parseFunction parseFunction
@ -44,6 +64,7 @@ import { WidgetAction, WidgetActionType, widgetActionTypeTranslationMap } from '
import { catchError, map, take, takeUntil } from 'rxjs/operators'; import { catchError, map, take, takeUntil } from 'rxjs/operators';
import { isSvgIcon, splitIconName } from '@shared/models/icon.models'; import { isSvgIcon, splitIconName } from '@shared/models/icon.models';
import { MatIconRegistry } from '@angular/material/icon'; import { MatIconRegistry } from '@angular/material/icon';
import { RafService } from '@core/services/raf.service';
export interface ScadaSymbolApi { export interface ScadaSymbolApi {
generateElementId: () => string; generateElementId: () => string;
@ -54,11 +75,14 @@ export interface ScadaSymbolApi {
animate: (element: Element, duration: number) => Runner; animate: (element: Element, duration: number) => Runner;
resetAnimation: (element: Element) => void; resetAnimation: (element: Element) => void;
finishAnimation: (element: Element) => void; finishAnimation: (element: Element) => void;
cssAnimate: (element: Element, duration: number) => ScadaSymbolAnimation;
cssAnimation: (element: Element) => ScadaSymbolAnimation | undefined;
resetCssAnimation: (element: Element) => void;
finishCssAnimation: (element: Element) => void;
disable: (element: Element | Element[]) => void; disable: (element: Element | Element[]) => void;
enable: (element: Element | Element[]) => void; enable: (element: Element | Element[]) => void;
callAction: (event: Event, behaviorId: string, value?: any, observer?: Partial<Observer<void>>) => void; callAction: (event: Event, behaviorId: string, value?: any, observer?: Partial<Observer<void>>) => void;
setValue: (valueId: string, value: any) => void; setValue: (valueId: string, value: any) => void;
cssAnimate: CssScadaSymbolAnimations;
} }
export interface ScadaSymbolContext { export interface ScadaSymbolContext {
@ -73,6 +97,7 @@ export type ScadaSymbolStateRenderFunction = (ctx: ScadaSymbolContext, svg: Svg)
export type ScadaSymbolTagStateRenderFunction = (ctx: ScadaSymbolContext, element: Element) => void; export type ScadaSymbolTagStateRenderFunction = (ctx: ScadaSymbolContext, element: Element) => void;
// noinspection JSUnusedGlobalSymbols
export type ScadaSymbolActionTrigger = 'click'; export type ScadaSymbolActionTrigger = 'click';
export type ScadaSymbolActionFunction = (ctx: ScadaSymbolContext, element: Element, event: Event) => void; export type ScadaSymbolActionFunction = (ctx: ScadaSymbolContext, element: Element, event: Event) => void;
@ -263,10 +288,10 @@ const parseScadaSymbolMetadataFromDom = (svgDoc: Document): ScadaSymbolMetadata
const svg = svgDoc.getElementsByTagName('svg')[0]; const svg = svgDoc.getElementsByTagName('svg')[0];
let width = null; let width = null;
let height = null; let height = null;
if (svg.viewBox.baseVal.width && svg.viewBox.baseVal.height) { if (svg.viewBox?.baseVal?.width && svg.viewBox?.baseVal?.height) {
width = svg.viewBox.baseVal.width; width = svg.viewBox.baseVal.width;
height = svg.viewBox.baseVal.height; height = svg.viewBox.baseVal.height;
} else if (svg.width.baseVal.value && svg.height.baseVal.value) { } else if (svg.width?.baseVal?.value && svg.height?.baseVal?.value) {
width = svg.width.baseVal.value; width = svg.width.baseVal.value;
height = svg.height.baseVal.value; height = svg.height.baseVal.value;
} }
@ -489,9 +514,10 @@ export interface ScadaSymbolObjectCallbacks {
export class ScadaSymbolObject { export class ScadaSymbolObject {
private metadata: ScadaSymbolMetadata; private readonly metadata: ScadaSymbolMetadata;
private settings: ScadaSymbolObjectSettings; private settings: ScadaSymbolObjectSettings;
private context: ScadaSymbolContext; private context: ScadaSymbolContext;
private cssAnimations: CssScadaSymbolAnimations;
private svgShape: Svg; private svgShape: Svg;
private box: Box; private box: Box;
@ -512,7 +538,8 @@ export class ScadaSymbolObject {
constructor(private rootElement: HTMLElement, constructor(private rootElement: HTMLElement,
private ctx: WidgetContext, private ctx: WidgetContext,
private iconRegistry: MatIconRegistry, private iconRegistry: MatIconRegistry,
private svgContent: string, private raf: RafService,
private readonly svgContent: string,
private inputSettings: ScadaSymbolObjectSettings, private inputSettings: ScadaSymbolObjectSettings,
private callbacks: ScadaSymbolObjectCallbacks, private callbacks: ScadaSymbolObjectCallbacks,
private simulated: boolean) { private simulated: boolean) {
@ -609,6 +636,7 @@ export class ScadaSymbolObject {
} }
private init() { private init() {
this.cssAnimations = new CssScadaSymbolAnimations(this.svgShape, this.raf);
this.context = { this.context = {
api: { api: {
generateElementId: () => generateElementId(), generateElementId: () => generateElementId(),
@ -619,11 +647,14 @@ export class ScadaSymbolObject {
animate: this.animate.bind(this), animate: this.animate.bind(this),
resetAnimation: this.resetAnimation.bind(this), resetAnimation: this.resetAnimation.bind(this),
finishAnimation: this.finishAnimation.bind(this), finishAnimation: this.finishAnimation.bind(this),
cssAnimate: this.cssAnimate.bind(this),
cssAnimation: this.cssAnimation.bind(this),
resetCssAnimation: this.resetCssAnimation.bind(this),
finishCssAnimation: this.finishCssAnimation.bind(this),
disable: this.disableElement.bind(this), disable: this.disableElement.bind(this),
enable: this.enableElement.bind(this), enable: this.enableElement.bind(this),
callAction: this.callAction.bind(this), callAction: this.callAction.bind(this),
setValue: this.setValue.bind(this), setValue: this.setValue.bind(this),
cssAnimate: new CssScadaSymbolAnimations(this.svgShape)
}, },
tags: {}, tags: {},
properties: {}, properties: {},
@ -960,6 +991,22 @@ export class ScadaSymbolObject {
element.timeline(new Timeline()); element.timeline(new Timeline());
} }
private cssAnimate(element: Element, duration: number): ScadaSymbolAnimation {
return this.cssAnimations.animate(element, duration);
}
private cssAnimation(element: Element): ScadaSymbolAnimation | undefined {
return this.cssAnimations.animation(element);
}
private resetCssAnimation(element: Element) {
this.cssAnimations.resetAnimation(element);
}
private finishCssAnimation(element: Element) {
this.cssAnimations.finishAnimation(element);
}
private disableElement(e: Element | Element[]) { private disableElement(e: Element | Element[]) {
this.elements(e).forEach(element => { this.elements(e).forEach(element => {
element.attr({'pointer-events': 'none'}); element.attr({'pointer-events': 'none'});
@ -1013,49 +1060,83 @@ export class ScadaSymbolObject {
const scadaSymbolAnimationId = 'scadaSymbolAnimation'; const scadaSymbolAnimationId = 'scadaSymbolAnimation';
class CssScadaSymbolAnimations { interface ScadaSymbolAnimation {
constructor(private svgShape: Svg) {}
public rotate(element: Element, rotation: number, duration = 1000, loop = false): CssScadaSymbolAnimation { running(): boolean;
this.checkOldAnimation(element);
return this.setupAnimation(element, play(): void;
new RotateCssScadaSymbolAnimation(this.svgShape, element, loop, rotation || 0)).duration(duration); pause(): void;
} stop(): void;
finish(): void;
speed(speed: number): ScadaSymbolAnimation;
ease(easing: string): ScadaSymbolAnimation;
loop(times?: number, swing?: boolean): ScadaSymbolAnimation;
public move(element: Element, deltaX: number, deltaY: number, duration = 1000, loop = false): CssScadaSymbolAnimation { transform(transform: MatrixTransformParam, relative?: boolean): ScadaSymbolAnimation;
rotate(r: number, cx?: number, cy?: number): ScadaSymbolAnimation;
x(x: number): ScadaSymbolAnimation;
y(y: number): ScadaSymbolAnimation;
size(width: number, height: number): ScadaSymbolAnimation;
width(width: number): ScadaSymbolAnimation;
height(height: number): ScadaSymbolAnimation;
move(x: number, y: number): ScadaSymbolAnimation;
dmove(dx: number, dy: number): ScadaSymbolAnimation;
relative(x: number, y: number): ScadaSymbolAnimation;
scale(x: number, y?: number, cx?: number, cy?: number): ScadaSymbolAnimation;
attr(attr: string | object, value?: any): ScadaSymbolAnimation;
}
class CssScadaSymbolAnimations {
constructor(private svgShape: Svg,
private raf: RafService) {}
public animate(element: Element, duration = 1000): ScadaSymbolAnimation {
this.checkOldAnimation(element); this.checkOldAnimation(element);
return this.setupAnimation(element, return this.setupAnimation(element, this.createAnimation(element, duration));
new MoveCssScadaSymbolAnimation(this.svgShape, element, loop, deltaX, deltaY).duration(duration));
} }
public attr(element: Element, attrName: string, value: any, duration = 1000, loop = false): CssScadaSymbolAnimation { public animation(element: Element): ScadaSymbolAnimation | undefined {
this.checkOldAnimation(element); return element.remember(scadaSymbolAnimationId);
return this.setupAnimation(element,
new AttrsCssScadaSymbolAnimation(this.svgShape, element, loop, {[attrName]: value}).duration(duration));
} }
public attrs(element: Element, attr: any, duration = 1000, loop = false): CssScadaSymbolAnimation { public resetAnimation(element: Element) {
this.checkOldAnimation(element); const animation: ScadaSymbolAnimation = element.remember(scadaSymbolAnimationId);
return this.setupAnimation(element, if (animation) {
new AttrsCssScadaSymbolAnimation(this.svgShape, element, loop, attr).duration(duration)); animation.stop();
element.remember(scadaSymbolAnimationId, null);
}
} }
public animation(element: Element): CssScadaSymbolAnimation | undefined { public finishAnimation(element: Element) {
return element.remember(scadaSymbolAnimationId); const animation: ScadaSymbolAnimation = element.remember(scadaSymbolAnimationId);
if (animation) {
animation.finish();
element.remember(scadaSymbolAnimationId, null);
}
} }
private checkOldAnimation(element: Element) { private checkOldAnimation(element: Element) {
const previousAnimation: CssScadaSymbolAnimation = element.remember(scadaSymbolAnimationId); const previousAnimation: ScadaSymbolAnimation = element.remember(scadaSymbolAnimationId);
if (previousAnimation) { if (previousAnimation) {
previousAnimation.destroy(); previousAnimation.finish();
} }
} }
private setupAnimation(element: Element, animation: CssScadaSymbolAnimation): CssScadaSymbolAnimation { private setupAnimation(element: Element, animation: ScadaSymbolAnimation): ScadaSymbolAnimation {
animation.init();
element.remember(scadaSymbolAnimationId, animation); element.remember(scadaSymbolAnimationId, animation);
return animation; return animation;
} }
private createAnimation(element: Element, duration: number): ScadaSymbolAnimation {
const fallbackToJs = (isSafari() || isFirefox()) && element.type === 'pattern';
if (fallbackToJs) {
return new JsScadaSymbolAnimation(element, duration);
} else {
return new CssScadaSymbolAnimation(this.svgShape, this.raf, element, duration);
}
}
} }
interface ScadaSymbolAnimationKeyframe { interface ScadaSymbolAnimationKeyframe {
@ -1063,113 +1144,239 @@ interface ScadaSymbolAnimationKeyframe {
style: any; style: any;
} }
abstract class CssScadaSymbolAnimation { class CssScadaSymbolAnimation implements ScadaSymbolAnimation {
private _animationName: string; private _animationName: string;
private _animationStyle: Style; private _animationStyle: Style;
private _active = false;
private _running = true; private _running = true;
private _speed = 1; private _speed = 1;
private _duration = 1000; private readonly _duration: number = 1000;
private _easing = 'linear'; private _easing = 'linear';
private _times = 1;
protected constructor(protected svgShape: Svg, private _swing = false;
protected element: Element,
protected loop: boolean) { private _hasAnimations = false;
private _transform: MatrixTransformParam;
private _relative: boolean;
private _initialTransform: MatrixExtract;
private _transformOriginX: number = null;
private _transformOriginY: number = null;
private _attrs: any;
private _startAttrs: any;
private _endAttrs: any;
private _caf = null;
constructor(private svgShape: Svg,
private raf: RafService,
private element: Element,
duration = 1000) {
this._duration = duration;
} }
public init() { public running(): boolean {
this.prepareAnimation(); return this._active && this._running;
} }
public start() { public play() {
if (!this._running) { if (!this._running) {
this.updateAnimationStyle('animation-play-state', 'running');
this._running = true; this._running = true;
this.updateAnimationStyle('animation-play-state', this.playStateStyle());
} }
} }
public pause() { public pause() {
if (this._running) { if (this._running) {
this.updateAnimationStyle('animation-play-state', 'paused');
this._running = false; this._running = false;
this.updateAnimationStyle('animation-play-state', this.playStateStyle());
} }
} }
public running(): boolean { public stop() {
return this._running; this._running = false;
if (this._hasAnimations) {
this.destroy();
this.applyStartAttrs();
}
} }
public destroy() { public finish() {
if (this._animationStyle) { this._running = false;
this._animationStyle.remove(); if (this._hasAnimations) {
this.element.removeClass(this._animationName); this.destroy();
this._animationStyle = null;
this._animationName = null;
} }
} }
public duration(duration: number): CssScadaSymbolAnimation { public speed(speed: number): this {
this._duration = duration; this._speed = speed;
this.updateAnimationStyle(this.loop ? 'animation-duration' : 'transition-duration', this.updateAnimationStyle('animation-duration',
Math.round(this._duration / this._speed) + 'ms'); this.durationStyle());
this.updateAnimationStyle('animation-play-state', this.playStateStyle());
return this; return this;
} }
public speed(speed: number): CssScadaSymbolAnimation { public ease(easing: string): this {
this._speed = speed; this._easing = easing;
this.updateAnimationStyle(this.loop ? 'animation-duration' : 'transition-duration', this.updateAnimationStyle('animation-timing-function', this._easing);
Math.round(this._duration / this._speed) + 'ms');
return this; return this;
} }
public easing(easing: string): CssScadaSymbolAnimation { public loop(times = 0, swing = false): this {
this._easing = easing; this._times = times;
this.updateAnimationStyle(this.loop ? 'animation-timing-function' : 'transition-timing-function', this._easing); this._swing = swing;
if (this._animationStyle) {
this.createOrUpdateAnimation();
}
return this;
}
public transform(transform: MatrixTransformParam, relative = false): this {
this._hasAnimations = true;
for (const key of Object.keys(transform)) {
const val = transform[key];
if (!isFinite(val) && !Array.isArray(val)) {
delete transform[key];
}
}
if (this._transform) {
this._transform = Object.assign(this._transform, transform);
} else {
this._transform = deepClone(transform);
}
this._relative = relative;
this.createOrUpdateAnimation();
return this; return this;
} }
public rotate(r: number, cx?: number, cy?: number): this {
return this.transform({rotate: r, ox: cx, oy: cy}, true);
}
public x(x: number): this {
return this.transform({translateX: x});
}
public y(y: number): this {
return this.transform({translateY: y});
}
public size(width: number, height: number): this {
const box = this.element.bbox();
if (width == null || height == null) {
if (width == null) {
width = box.width / box.height * height;
} else if (height == null) {
height = box.height / box.width * width;
}
}
const scaleX = width / box.width;
const scaleY = height / box.height;
return this.scale(scaleX, scaleY);
}
public width(width: number): this {
return this.size(width, this.element.bbox().height);
}
public height(height: number): this {
return this.size(this.element.bbox().width, height);
}
public move(x: number, y: number): this {
const box = this.element.bbox();
const dx = x - box.x;
const dy = y - box.y;
return this.dmove(dx, dy);
}
public dmove(dx: number, dy: number): this {
return this.transform({translateX: dx, translateY: dy}, true);
}
public relative(x: number, y: number): this {
return this.transform({translateX: x, translateY: y}, true);
}
public scale(x: number, y?: number, cx?: number, cy?: number): this {
return this.transform({scaleX: x, scaleY: isUndefined(y) ? x : y, ox: cx, oy: cy}, true);
}
public attr(attr: string | object, value?: any): this {
this._hasAnimations = true;
if (!this._attrs) {
this._attrs = {};
}
if (typeof attr === 'object') {
for (const key of Object.keys(attr)) {
this._attrs[key] = attr[key];
}
} else {
this._attrs[attr] = value;
}
this.createOrUpdateAnimation();
return this;
}
private createOrUpdateAnimation() {
this.destroy();
this._caf = this.raf.raf(() => this.prepareAnimation());
}
private prepareAnimation() { private prepareAnimation() {
this._active = true;
this.prepareTransform();
this.prepareStartEndAttrs();
this._animationName = 'animation_' + generateElementId(); this._animationName = 'animation_' + generateElementId();
this.element.on('animationend', (evt) => {
if ((evt as any).animationName === this._animationName) {
this.destroy();
}
});
this._animationStyle = this.svgShape.style(); this._animationStyle = this.svgShape.style();
let styles: any; const styles = {
if (this.loop) {
styles = {
'animation-name': this._animationName, 'animation-name': this._animationName,
'animation-duration': this._duration + 'ms', 'animation-duration': this.durationStyle(),
'animation-timing-function': this._easing, 'animation-timing-function': this._easing,
'animation-iteration-count': 'infinite', 'animation-iteration-count': this._times === 0 ? 'infinite' : this._times,
'animation-fill-mode': 'forwards', 'animation-play-state': this.playStateStyle()
'animation-play-state': 'running', };
...this.animationStyles()
};
} else {
styles = {
'transition-property': this.transitionProperties(),
'transition-duration': this._duration + 'ms',
'transition-timing-function': this._easing,
...this.animationStyles()
};
}
this._animationStyle.rule('.' + this._animationName, styles); this._animationStyle.rule('.' + this._animationName, styles);
if (this.loop) { const keyframes = this.animationKeyframes();
const keyframes = this.animationKeyframes(); let keyframesCss = `\n@keyframes ${this._animationName} {\n`;
let keyframesCss = `\n@keyframes ${this._animationName} {\n`; for (const keyframe of keyframes) {
for (const keyframe of keyframes) { let keyframeCss = ` ${keyframe.stop} {\n`;
let keyframeCss = ` ${keyframe.stop} {\n`; for (const i of Object.keys(keyframe.style)) {
for (const i of Object.keys(keyframe.style)) { keyframeCss += ' ' + i + ':' + keyframe.style[i] + ';\n';
keyframeCss += ' ' + i + ':' + keyframe.style[i] + ';\n'; }
} keyframeCss += ' }\n';
keyframeCss += ' }\n'; keyframesCss += keyframeCss;
keyframesCss += keyframeCss; }
keyframesCss += '}';
this._animationStyle.addText(keyframesCss);
setTimeout(() => {
this.element.addClass(this._animationName);
if (!this._swing) {
this.applyEndAttrs();
} }
keyframesCss += '}'; }, 0);
this._animationStyle.addText(keyframesCss); }
private destroy() {
this.element.off('animationend');
this._active = false;
if (this._caf) {
this._caf();
this._caf = null;
} }
this.element.addClass(this._animationName); if (this._animationStyle) {
if (!this.loop) { this._animationStyle.remove();
this.doTransform(); this.element.removeClass(this._animationName);
this._animationStyle = null;
this._animationName = null;
} }
} }
@ -1181,130 +1388,313 @@ abstract class CssScadaSymbolAnimation {
} }
} }
protected animationStyles(): any { private durationStyle(): string {
return {}; return (this._speed > 0 && this._duration > 0) ? Math.round(
(this._duration / this._speed) * (this._swing ? 2 : 1)
) + 'ms' : '1000ms';
} }
protected abstract animationKeyframes(): ScadaSymbolAnimationKeyframe[]; private playStateStyle(): string {
return (this._running && this._speed > 0) ? 'running' : 'paused';
}
protected abstract transitionProperties(): string; private animationKeyframes(): ScadaSymbolAnimationKeyframe[] {
const keyframes: ScadaSymbolAnimationKeyframe[] = [];
let startStyle: any = {};
let endStyle: any = {};
if (this._transform) {
const transformed = this.transformedData();
startStyle = this.cssTransform();
endStyle = this.cssTransform(transformed);
}
if (this._attrs) {
startStyle = {...startStyle, ...this.currentCssAttrs()};
endStyle = {...endStyle, ...this.toCssAttrs(this._attrs)};
}
keyframes.push({
stop: '0%',
style: startStyle
});
if (this._swing) {
keyframes.push(...[
{
stop: '50%',
style: endStyle
},
{
stop: '100%',
style: startStyle
}]
);
} else {
keyframes.push({
stop: '100%',
style: endStyle
});
}
return keyframes;
}
protected doTransform() { private prepareStartEndAttrs() {
if (this._attrs) {
this._startAttrs = {...this._startAttrs, ...this.currentSvgAttrs()};
this._endAttrs = {...this._endAttrs, ...this._attrs};
}
} }
} private applyStartAttrs() {
if (this._initialTransform) {
this.element.transform(this._initialTransform);
}
if (this._startAttrs) {
this.element.attr(this._startAttrs);
}
}
class RotateCssScadaSymbolAnimation extends CssScadaSymbolAnimation { private applyEndAttrs() {
if (this._transform) {
this.element.transform(this._transform, this._relative);
}
if (this._endAttrs) {
this.element.attr(this._endAttrs);
}
}
private prepareTransform() {
if (this._transform) {
this._transformOriginX = this.element.cx();
this._transformOriginY = this.element.cy();
if (isDefinedAndNotNull(this._transform.originX)) {
this._transformOriginX = this._transform.originX;
} else if (isDefinedAndNotNull(this._transform.ox)) {
this._transformOriginX = this._transform.ox;
}
if (isDefinedAndNotNull(this._transform.originY)) {
this._transformOriginY = this._transform.originY;
} else if (isDefinedAndNotNull(this._transform.oy)) {
this._transformOriginX = this._transform.oy;
}
constructor(protected svgShape: Svg, this._transformOriginX = this.normFloat(this._transformOriginX);
protected element: Element, this._transformOriginY = this.normFloat(this._transformOriginY);
protected loop: boolean,
private rotation: number) { const transformValue: string = this.element.attr('transform');
super(svgShape, element, loop); const hasMatrixTransform = transformValue && transformValue.startsWith('matrix');
if (hasMatrixTransform) {
const matrix = new Matrix(this.element);
this._initialTransform = matrix.decompose(this._transformOriginX, this._transformOriginY);
} else {
this._initialTransform = this.element.transform();
}
this._initialTransform.originX = this._transformOriginX;
this._initialTransform.originY = this._transformOriginY;
for (const key of ['translateX', 'translateY', 'scaleX', 'scaleY', 'rotate']) {
this._initialTransform[key] = this.normFloat(this._initialTransform[key]);
}
for (const key of ['b', 'c']) {
this._initialTransform[key] = this.normFloat(this._initialTransform[key], 0);
}
}
} }
protected animationStyles(): any { private transformedData(): TransformData {
return { const transformed: TransformData = {};
'transform-origin': `${this.element.cx()}px ${this.element.cy()}px` const transform = this._initialTransform;
}; for (const key of Object.keys(this._transform)) {
if (this._relative) {
transformed[key] = this.normFloat(transform[key] + this._transform[key]);
} else {
transformed[key] = this.normFloat(this._transform[key]);
}
}
return transformed;
} }
protected animationKeyframes(): ScadaSymbolAnimationKeyframe[] { private currentCssAttrs(): any {
const transform = this.element.transform(); const cssAttrs = {};
return [ const computed = getComputedStyle(this.element.node);
{ for (const key of Object.keys(this._attrs)) {
stop: '0%', const value = computed.getPropertyValue(key);
style: { if (isDefinedAndNotNull(value)) {
transform: `translate(${transform.translateX}px, ${transform.translateY}px) rotate(${transform.rotate}deg)` cssAttrs[key] = value;
} }
}, }
{ return cssAttrs;
stop: '100%', }
style: {
transform: `translate(${transform.translateX}px, ${transform.translateY}px) rotate(${this.rotation}deg)` private currentSvgAttrs(): any {
const svgAttrs = {};
for (const key of Object.keys(this._attrs)) {
const value = this.element.attr(key);
if (isDefinedAndNotNull(value)) {
svgAttrs[key] = value;
}
}
return svgAttrs;
}
private toCssAttrs(attrs: any): any {
const cssAttrs: any = {};
for (const key of Object.keys(attrs)) {
let val = attrs[key];
if (['x', 'y', 'width', 'height'].includes(key)) {
if (isNumeric(val)) {
val += 'px';
} }
} }
]; cssAttrs[key] = val;
}
return cssAttrs;
} }
protected transitionProperties(): string { private cssTransform(inputTransform?: TransformData): any {
return 'transform'; let transform = this._initialTransform || this.element.transform();
if (inputTransform) {
transform = deepClone(transform);
Object.assign(transform, inputTransform);
}
return {
'transform-origin': `${transform.originX}px ${transform.originY}px`,
transform: `translate(${transform.translateX}px, ${transform.translateY}px) ` +
`scale(${transform.scaleX}, ${transform.scaleY}) ` +
`rotate(${transform.rotate}deg)`};
} }
protected doTransform() { private normFloat(num: number, digits = 2): number {
const transform = this.element.transform(); const factor = Math.pow(10, digits);
this.element.attr({transform: `translate(${transform.translateX} ${transform.translateY}) rotate(${this.rotation})`}); return Math.round((num + Number.EPSILON) * factor) / factor;
} }
} }
class MoveCssScadaSymbolAnimation extends CssScadaSymbolAnimation { class JsScadaSymbolAnimation implements ScadaSymbolAnimation {
constructor(protected svgShape: Svg, private readonly _runner: Runner;
protected element: Element, private _timeline: Timeline;
protected loop: boolean, private _running = true;
private deltaX: number,
private deltaY: number) { constructor(private element: Element,
super(svgShape, element, loop); duration = 1000) {
this._timeline = this.element.timeline();
this._runner = this.element.animate(duration, 0, 'now').ease('-');
} }
protected animationStyles(): any { public runner(): Runner {
return { return this._runner;
'transform-origin': `${this.element.cx()}px ${this.element.cy()}px`
};
} }
protected animationKeyframes(): ScadaSymbolAnimationKeyframe[] { public running(): boolean {
const transform = this.element.transform(); return this._running;
return [
{
stop: '0%',
style: {
transform: `translate(${transform.translateX}px, ${transform.translateY}px)`
}
},
{
stop: '100%',
style: {
transform: `translate(${transform.translateX+this.deltaX}px, ${transform.translateY+this.deltaY}px)`
}
}
];
} }
protected transitionProperties(): string { public play() {
return 'transform'; if (!this._running) {
this._timeline.play();
this._running = true;
}
} }
protected doTransform() { public pause() {
this.element.relative(this.deltaX, this.deltaY); if (this._running) {
this._timeline.pause();
this._running = false;
}
} }
} public stop() {
this._running = false;
this._timeline.stop();
this._timeline = new Timeline();
this.element.timeline(this._timeline);
}
public finish() {
this._running = false;
this._timeline.finish();
this._timeline = new Timeline();
this.element.timeline(this._timeline);
}
public speed(speed: number): this {
this._timeline.speed(speed);
return this;
}
// Runner methods
public ease(easing: string): this {
this._runner.ease(easing as EasingLiteral);
return this;
}
public loop(times: number | TimesParam, swing?: boolean, wait?: number): this {
if (typeof times === 'object') {
this._runner.loop(times);
} else {
this._runner.loop(times, swing, wait);
}
return this;
}
public transform(transform: MatrixTransformParam, relative?: boolean): this {
this._runner.transform(transform, relative);
return this;
}
public rotate(_r: number, _cx?: number, _cy?: number): this {
(this._runner as any).rotate(...arguments);
return this;
}
public x(x: number): this {
this._runner.x(x);
return this;
}
public y(y: number): this {
this._runner.y(y);
return this;
}
class AttrsCssScadaSymbolAnimation extends CssScadaSymbolAnimation { public size(width: number, height: number): this {
this._runner.size(width, height);
return this;
}
constructor(protected svgShape: Svg, public width(width: number): this {
protected element: Element, this._runner.width(width);
protected loop: boolean, return this;
private attr: any) {
super(svgShape, element, loop);
} }
protected animationStyles(): any { public height(height: number): this {
return {}; this._runner.height(height);
return this;
} }
protected animationKeyframes(): ScadaSymbolAnimationKeyframe[] { public move(x: number, y: number): this {
return []; this._runner.move(x, y);
return this;
} }
protected transitionProperties(): string { public dmove(dx: number, dy: number): this {
return Object.keys(this.attr).join(' '); this._runner.dmove(dx, dy);
return this;
} }
protected doTransform() { public relative(_x: number, _y: number): this {
this.element.attr(this.attr); (this._runner as any).relative(...arguments);
return this;
}
public scale(_x: number, _y?: number, _cx?: number, _cy?: number): this {
(this._runner as any).scale(...arguments);
return this;
}
public attr(a: string | object, v?: string): this {
this._runner.attr(a, v);
return this;
} }
} }

46
ui-ngx/src/app/modules/home/components/widget/widget-container.component.ts

@ -18,11 +18,11 @@ import {
AfterViewInit, AfterViewInit,
ChangeDetectionStrategy, ChangeDetectionStrategy,
ChangeDetectorRef, ChangeDetectorRef,
Component, Component, ComponentRef,
ElementRef, ElementRef,
EventEmitter, EventEmitter,
HostBinding, HostBinding,
Input, OnChanges, Input, NgZone, OnChanges,
OnDestroy, OnDestroy,
OnInit, OnInit,
Output, Output,
@ -134,7 +134,8 @@ export class WidgetContainerComponent extends PageComponent implements OnInit, O
private renderer: Renderer2, private renderer: Renderer2,
private container: ViewContainerRef, private container: ViewContainerRef,
private dashboardUtils: DashboardUtilsService, private dashboardUtils: DashboardUtilsService,
private utils: UtilsService) { private utils: UtilsService,
private zone: NgZone) {
super(store); super(store);
} }
@ -173,7 +174,7 @@ export class WidgetContainerComponent extends PageComponent implements OnInit, O
if (this.cssClass) { if (this.cssClass) {
this.utils.clearCssElement(this.renderer, this.cssClass); this.utils.clearCssElement(this.renderer, this.cssClass);
} }
if (this.editWidgetActionsTooltip) { if (this.editWidgetActionsTooltip && !this.editWidgetActionsTooltip.status().destroyed) {
this.editWidgetActionsTooltip.destroy(); this.editWidgetActionsTooltip.destroy();
} }
} }
@ -255,6 +256,7 @@ export class WidgetContainerComponent extends PageComponent implements OnInit, O
} }
private initEditWidgetActionTooltip(parent: HTMLElement) { private initEditWidgetActionTooltip(parent: HTMLElement) {
let componentRef: ComponentRef<EditWidgetActionsTooltipComponent>;
from(import('tooltipster')).subscribe(() => { from(import('tooltipster')).subscribe(() => {
$(this.gridsterItem.el).tooltipster({ $(this.gridsterItem.el).tooltipster({
parent: $(parent), parent: $(parent),
@ -305,24 +307,26 @@ export class WidgetContainerComponent extends PageComponent implements OnInit, O
} }
}); });
this.editWidgetActionsTooltip = $(this.gridsterItem.el).tooltipster('instance'); this.editWidgetActionsTooltip = $(this.gridsterItem.el).tooltipster('instance');
const componentRef = this.container.createComponent(EditWidgetActionsTooltipComponent); this.zone.run(() => {
componentRef.instance.container = this; componentRef = this.container.createComponent(EditWidgetActionsTooltipComponent);
componentRef.instance.viewInited.subscribe(() => { componentRef.instance.container = this;
if (this.editWidgetActionsTooltip.status().open) { componentRef.instance.viewInited.subscribe(() => {
this.editWidgetActionsTooltip.reposition(); if (this.editWidgetActionsTooltip.status().open) {
} this.editWidgetActionsTooltip.reposition();
}); }
this.editWidgetActionsTooltip.on('destroyed', () => { });
componentRef.destroy(); this.editWidgetActionsTooltip.on('destroyed', () => {
componentRef.destroy();
});
const parentElement = componentRef.instance.element.nativeElement;
const content = parentElement.firstChild;
parentElement.removeChild(content);
parentElement.style.display = 'none';
this.editWidgetActionsTooltip.content(content);
this.updateEditWidgetActionsTooltipState();
this.widget.onSelected((selected) =>
this.updateEditWidgetActionsTooltipSelectedState(selected));
}); });
const parentElement = componentRef.instance.element.nativeElement;
const content = parentElement.firstChild;
parentElement.removeChild(content);
parentElement.style.display = 'none';
this.editWidgetActionsTooltip.content(content);
this.updateEditWidgetActionsTooltipState();
this.widget.onSelected((selected) =>
this.updateEditWidgetActionsTooltipSelectedState(selected));
}); });
} }

30
ui-ngx/src/app/modules/home/models/dashboard-component.models.ts

@ -706,15 +706,7 @@ export class DashboardWidget implements GridsterItem, IDashboardWidget {
return Math.floor(res); return Math.floor(res);
} }
set x(x: number) { set x(_: number) {}
if (!this.dashboard.isMobileSize) {
if (this.widgetLayout) {
this.widgetLayout.col = x;
} else {
this.widget.col = x;
}
}
}
@enumerable(true) @enumerable(true)
get y(): number { get y(): number {
@ -727,15 +719,7 @@ export class DashboardWidget implements GridsterItem, IDashboardWidget {
return Math.floor(res); return Math.floor(res);
} }
set y(y: number) { set y(_: number) {}
if (!this.dashboard.isMobileSize) {
if (this.widgetLayout) {
this.widgetLayout.row = y;
} else {
this.widget.row = y;
}
}
}
get preserveAspectRatio(): boolean { get preserveAspectRatio(): boolean {
if (!this.dashboard.isMobileSize && this.widgetLayout) { if (!this.dashboard.isMobileSize && this.widgetLayout) {
@ -833,4 +817,14 @@ export class DashboardWidget implements GridsterItem, IDashboardWidget {
} }
return order; return order;
} }
updatePosition(x: number, y: number) {
if (this.widgetLayout) {
this.widgetLayout.col = x;
this.widgetLayout.row = y;
} else {
this.widget.col = x;
this.widget.row = y;
}
}
} }

7
ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol-editor.component.ts

@ -20,7 +20,7 @@ import {
Component, Component,
ElementRef, ElementRef,
EventEmitter, EventEmitter,
Input, Input, NgZone,
OnChanges, OnChanges,
OnDestroy, OnDestroy,
OnInit, OnInit,
@ -102,7 +102,8 @@ export class ScadaSymbolEditorComponent implements OnInit, OnDestroy, AfterViewI
this.updateEditorMode(value); this.updateEditorMode(value);
} }
constructor(private cd: ChangeDetectorRef) { constructor(private cd: ChangeDetectorRef,
private zone: NgZone) {
} }
ngOnInit(): void { ngOnInit(): void {
@ -128,7 +129,7 @@ export class ScadaSymbolEditorComponent implements OnInit, OnDestroy, AfterViewI
}; };
this.scadaSymbolEditObject = new ScadaSymbolEditObject(this.scadaSymbolShape.nativeElement, this.scadaSymbolEditObject = new ScadaSymbolEditObject(this.scadaSymbolShape.nativeElement,
this.tooltipsContainer.nativeElement, this.tooltipsContainer.nativeElement,
this.tooltipsContainerComponent.viewContainerRef, this.editObjectCallbacks, this.readonly); this.tooltipsContainerComponent.viewContainerRef, this.zone, this.editObjectCallbacks, this.readonly);
if (this.data) { if (this.data) {
this.updateContent(this.data.scadaSymbolContent); this.updateContent(this.data.scadaSymbolContent);
} }

80
ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol-editor.models.ts

@ -17,7 +17,7 @@
import { ImageResourceInfo } from '@shared/models/resource.models'; import { ImageResourceInfo } from '@shared/models/resource.models';
import * as svgjs from '@svgdotjs/svg.js'; import * as svgjs from '@svgdotjs/svg.js';
import { Box, Element, Rect, Style, SVG, Svg, Timeline } from '@svgdotjs/svg.js'; import { Box, Element, Rect, Style, SVG, Svg, Timeline } from '@svgdotjs/svg.js';
import { ViewContainerRef } from '@angular/core'; import { NgZone, ViewContainerRef } from '@angular/core';
import { forkJoin, from } from 'rxjs'; import { forkJoin, from } from 'rxjs';
import { import {
setupAddTagPanelTooltip, setupAddTagPanelTooltip,
@ -79,6 +79,7 @@ export class ScadaSymbolEditObject {
constructor(private rootElement: HTMLElement, constructor(private rootElement: HTMLElement,
public tooltipsContainer: HTMLElement, public tooltipsContainer: HTMLElement,
public viewContainerRef: ViewContainerRef, public viewContainerRef: ViewContainerRef,
public zone: NgZone,
private callbacks: ScadaSymbolEditObjectCallbacks, private callbacks: ScadaSymbolEditObjectCallbacks,
public readonly: boolean) { public readonly: boolean) {
this.shapeResize$ = new ResizeObserver(() => { this.shapeResize$ = new ResizeObserver(() => {
@ -758,7 +759,9 @@ export class ScadaSymbolElement {
} }
private setupTagPanel() { private setupTagPanel() {
setupTagPanelTooltip(this, this.editObject.viewContainerRef); this.editObject.zone.run(() => {
setupTagPanelTooltip(this, this.editObject.viewContainerRef);
});
} }
private createAddTagTooltip() { private createAddTagTooltip() {
@ -806,7 +809,9 @@ export class ScadaSymbolElement {
} }
private setupAddTagPanel() { private setupAddTagPanel() {
setupAddTagPanelTooltip(this, this.editObject.viewContainerRef); this.editObject.zone.run(() => {
setupAddTagPanelTooltip(this, this.editObject.viewContainerRef);
});
} }
private innerTagTooltipPosition(_instance: ITooltipsterInstance, helper: ITooltipsterHelper, private innerTagTooltipPosition(_instance: ITooltipsterInstance, helper: ITooltipsterHelper,
@ -826,6 +831,7 @@ export class ScadaSymbolElement {
private innerAddTagTooltipPosition(_instance: ITooltipsterInstance, private innerAddTagTooltipPosition(_instance: ITooltipsterInstance,
_helper: ITooltipsterHelper, position: ITooltipPosition): ITooltipPosition { _helper: ITooltipsterHelper, position: ITooltipPosition): ITooltipPosition {
const distance = 10; const distance = 10;
const parentRect = this.tooltipContainer[0].getBoundingClientRect();
switch (position.side) { switch (position.side) {
case 'right': case 'right':
position.coord.top = this.tooltipMouseY - position.size.height / 2; position.coord.top = this.tooltipMouseY - position.size.height / 2;
@ -851,6 +857,14 @@ export class ScadaSymbolElement {
position.target = this.tooltipMouseX; position.target = this.tooltipMouseX;
break; break;
} }
const rightOverflow = parentRect.right - (position.coord.left + position.size.width);
if (rightOverflow < 0) {
position.coord.left += rightOverflow;
}
const leftOverflow = parentRect.left - position.coord.left;
if (leftOverflow > 0) {
position.coord.left += leftOverflow;
}
return position; return position;
} }
@ -1159,6 +1173,66 @@ export const scadaSymbolContextCompletion = (metadata: ScadaSymbolMetadata, tags
type: 'ScadaSymbolApi', type: 'ScadaSymbolApi',
description: 'SCADA symbol API', description: 'SCADA symbol API',
children: { children: {
cssAnimate: {
meta: 'function',
description: 'Finishes any previous CSS animation and starts new CSS animation for SVG element.',
args: [
{
name: 'element',
description: 'SVG element',
type: 'Element'
},
{
name: 'duration',
description: 'Animation duration in milliseconds',
type: 'number'
}
],
return: {
description: 'Instance of ScadaSymbolAnimation which has generally similar methods as ' +
'<a href="https://svgjs.dev/docs/3.2/animating/#svg-runner">SVG.Runner</a> to control the animation.',
type: 'ScadaSymbolAnimation'
}
},
cssAnimation: {
meta: 'function',
description: 'Get the current CSS animation applied for the SVG element.',
args: [
{
name: 'element',
description: 'SVG element',
type: 'Element'
}
],
return: {
description: 'Instance of ScadaSymbolAnimation which has generally similar methods as ' +
'<a href="https://svgjs.dev/docs/3.2/animating/#svg-runner">SVG.Runner</a> to control the animation.',
type: 'ScadaSymbolAnimation'
}
},
resetCssAnimation: {
meta: 'function',
description: 'Stops CSS animation if any and restore SVG element initial state, removes CSS animation instance.',
args: [
{
name: 'element',
description: 'SVG element',
type: 'Element'
},
]
},
finishCssAnimation: {
meta: 'function',
description: 'Finishes CSS animation if any, SVG element state updated according to the end animation values, ' +
'removes CSS animation instance.',
args: [
{
name: 'element',
description: 'SVG element',
type: 'Element'
},
]
},
animate: { animate: {
meta: 'function', meta: 'function',
description: 'Finishes any previous animation and starts new animation for SVG element.', description: 'Finishes any previous animation and starts new animation for SVG element.',

Loading…
Cancel
Save