@ -18,6 +18,8 @@ package org.thingsboard.server.controller;
import com.fasterxml.jackson.core.type.TypeReference ;
import com.fasterxml.jackson.core.type.TypeReference ;
import com.fasterxml.jackson.databind.JsonNode ;
import com.fasterxml.jackson.databind.JsonNode ;
import com.fasterxml.jackson.databind.node.ObjectNode ;
import com.fasterxml.jackson.databind.node.ObjectNode ;
import com.google.common.util.concurrent.FutureCallback ;
import org.checkerframework.checker.nullness.qual.Nullable ;
import org.junit.After ;
import org.junit.After ;
import org.junit.Assert ;
import org.junit.Assert ;
import org.junit.Before ;
import org.junit.Before ;
@ -27,6 +29,7 @@ import org.springframework.test.context.TestPropertySource;
import org.springframework.test.web.servlet.ResultActions ;
import org.springframework.test.web.servlet.ResultActions ;
import org.testcontainers.shaded.org.apache.commons.lang3.RandomStringUtils ;
import org.testcontainers.shaded.org.apache.commons.lang3.RandomStringUtils ;
import org.thingsboard.common.util.JacksonUtil ;
import org.thingsboard.common.util.JacksonUtil ;
import org.thingsboard.rule.engine.api.TimeseriesSaveRequest ;
import org.thingsboard.server.common.data.AttributeScope ;
import org.thingsboard.server.common.data.AttributeScope ;
import org.thingsboard.server.common.data.Customer ;
import org.thingsboard.server.common.data.Customer ;
import org.thingsboard.server.common.data.Dashboard ;
import org.thingsboard.server.common.data.Dashboard ;
@ -42,6 +45,10 @@ import org.thingsboard.server.common.data.alarm.AlarmSeverity;
import org.thingsboard.server.common.data.asset.Asset ;
import org.thingsboard.server.common.data.asset.Asset ;
import org.thingsboard.server.common.data.id.DeviceId ;
import org.thingsboard.server.common.data.id.DeviceId ;
import org.thingsboard.server.common.data.id.EntityId ;
import org.thingsboard.server.common.data.id.EntityId ;
import org.thingsboard.server.common.data.kv.Aggregation ;
import org.thingsboard.server.common.data.kv.BasicTsKvEntry ;
import org.thingsboard.server.common.data.kv.LongDataEntry ;
import org.thingsboard.server.common.data.kv.TsKvEntry ;
import org.thingsboard.server.common.data.page.PageData ;
import org.thingsboard.server.common.data.page.PageData ;
import org.thingsboard.server.common.data.query.AlarmCountQuery ;
import org.thingsboard.server.common.data.query.AlarmCountQuery ;
import org.thingsboard.server.common.data.query.AlarmData ;
import org.thingsboard.server.common.data.query.AlarmData ;
@ -75,14 +82,20 @@ import org.thingsboard.server.common.data.relation.RelationEntityTypeFilter;
import org.thingsboard.server.common.data.security.Authority ;
import org.thingsboard.server.common.data.security.Authority ;
import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration ;
import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration ;
import org.thingsboard.server.common.data.tenant.profile.TenantProfileData ;
import org.thingsboard.server.common.data.tenant.profile.TenantProfileData ;
import org.thingsboard.server.common.data.query.ComparisonTsValue ;
import org.thingsboard.server.dao.queue.QueueStatsService ;
import org.thingsboard.server.dao.queue.QueueStatsService ;
import org.thingsboard.server.dao.service.DaoSqlTest ;
import org.thingsboard.server.dao.service.DaoSqlTest ;
import org.thingsboard.server.service.telemetry.TelemetrySubscriptionService ;
import org.thingsboard.server.service.ws.telemetry.cmd.v2.AggHistoryCmd ;
import org.thingsboard.server.service.ws.telemetry.cmd.v2.AggKey ;
import java.util.ArrayList ;
import java.util.ArrayList ;
import java.util.Arrays ;
import java.util.Arrays ;
import java.util.Collections ;
import java.util.Collections ;
import java.util.List ;
import java.util.List ;
import java.util.concurrent.CountDownLatch ;
import java.util.concurrent.TimeUnit ;
import java.util.concurrent.TimeUnit ;
import java.util.concurrent.atomic.AtomicReference ;
import java.util.stream.Collectors ;
import java.util.stream.Collectors ;
import static org.assertj.core.api.Assertions.assertThat ;
import static org.assertj.core.api.Assertions.assertThat ;
@ -107,6 +120,9 @@ public class EntityQueryControllerTest extends AbstractControllerTest {
@Autowired
@Autowired
private QueueStatsService queueStatsService ;
private QueueStatsService queueStatsService ;
@Autowired
private TelemetrySubscriptionService tsService ;
@Before
@Before
public void beforeTest ( ) throws Exception {
public void beforeTest ( ) throws Exception {
loginSysAdmin ( ) ;
loginSysAdmin ( ) ;
@ -1329,6 +1345,212 @@ public class EntityQueryControllerTest extends AbstractControllerTest {
return numericFilter ;
return numericFilter ;
}
}
@Test
public void testFindEntityDataAggHistoryByQuery ( ) throws Exception {
Device device = new Device ( ) ;
device . setName ( "AggHistoryDevice" ) ;
device . setType ( "default" ) ;
device = doPost ( "/api/device" , device , Device . class ) ;
long now = System . currentTimeMillis ( ) ;
long startTs = now - TimeUnit . MINUTES . toMillis ( 30 ) ;
long endTs = now ;
long previousStartTs = now - TimeUnit . MINUTES . toMillis ( 60 ) ;
long previousEndTs = startTs - 1 ;
List < TsKvEntry > currentData = new ArrayList < > ( ) ;
for ( int i = 0 ; i < 20 ; i + + ) {
long ts = startTs + i * TimeUnit . MINUTES . toMillis ( 1 ) ;
currentData . add ( new BasicTsKvEntry ( ts , new LongDataEntry ( "temperature" , 30L ) ) ) ;
}
List < TsKvEntry > previousData = new ArrayList < > ( ) ;
for ( int i = 0 ; i < 10 ; i + + ) {
long ts = previousStartTs + i * TimeUnit . MINUTES . toMillis ( 1 ) ;
previousData . add ( new BasicTsKvEntry ( ts , new LongDataEntry ( "temperature" , 30L ) ) ) ;
}
sendTelemetry ( device , currentData ) ;
sendTelemetry ( device , previousData ) ;
DeviceTypeFilter filter = new DeviceTypeFilter ( List . of ( "default" ) , "AggHistoryDevice" ) ;
EntityDataPageLink pageLink = new EntityDataPageLink ( 10 , 0 , null , null ) ;
EntityDataQuery query = new EntityDataQuery ( filter , pageLink , Collections . emptyList ( ) , Collections . emptyList ( ) , null ) ;
AggKey key = new AggKey ( ) ;
key . setId ( 1 ) ;
key . setKey ( "temperature" ) ;
key . setAgg ( Aggregation . SUM ) ;
key . setPreviousStartTs ( previousStartTs ) ;
key . setPreviousEndTs ( previousEndTs ) ;
AggHistoryCmd cmd = new AggHistoryCmd ( ) ;
cmd . setKeys ( List . of ( key ) ) ;
cmd . setStartTs ( startTs ) ;
cmd . setEndTs ( endTs ) ;
ObjectNode request = JacksonUtil . newObjectNode ( ) ;
request . set ( "query" , JacksonUtil . valueToTree ( query ) ) ;
request . set ( "aggHistoryCmd" , JacksonUtil . valueToTree ( cmd ) ) ;
PageData < EntityData > result = doPostAsyncWithTypedResponse ( "/api/entitiesQuery/find/aggHistory" ,
request , new TypeReference < > ( ) { } , status ( ) . isOk ( ) ) ;
assertThat ( result . getData ( ) ) . hasSize ( 1 ) ;
EntityData entityData = result . getData ( ) . get ( 0 ) ;
assertThat ( entityData . getEntityId ( ) ) . isEqualTo ( device . getId ( ) ) ;
ComparisonTsValue agg = entityData . getAggLatest ( ) . get ( 1 ) ;
assertThat ( agg ) . isNotNull ( ) ;
assertThat ( agg . getCurrent ( ) . getValue ( ) ) . isEqualTo ( "600" ) ;
assertThat ( agg . getPrevious ( ) . getValue ( ) ) . isEqualTo ( "300" ) ;
}
@Test
public void testFindEntityDataAggHistoryByQuery_previousValueOnly ( ) throws Exception {
Device device = new Device ( ) ;
device . setName ( "AggPrevOnlyDevice" ) ;
device . setType ( "default" ) ;
device = doPost ( "/api/device" , device , Device . class ) ;
long now = System . currentTimeMillis ( ) ;
long startTs = now - TimeUnit . MINUTES . toMillis ( 30 ) ;
long endTs = now ;
long previousStartTs = now - TimeUnit . MINUTES . toMillis ( 60 ) ;
long previousEndTs = startTs - 1 ;
List < TsKvEntry > currentData = new ArrayList < > ( ) ;
for ( int i = 0 ; i < 5 ; i + + ) {
currentData . add ( new BasicTsKvEntry ( startTs + i * TimeUnit . MINUTES . toMillis ( 1 ) ,
new LongDataEntry ( "temperature" , 100L ) ) ) ;
}
List < TsKvEntry > previousData = new ArrayList < > ( ) ;
for ( int i = 0 ; i < 3 ; i + + ) {
previousData . add ( new BasicTsKvEntry ( previousStartTs + i * TimeUnit . MINUTES . toMillis ( 1 ) ,
new LongDataEntry ( "temperature" , 10L ) ) ) ;
}
sendTelemetry ( device , currentData ) ;
sendTelemetry ( device , previousData ) ;
AggKey key = new AggKey ( ) ;
key . setId ( 7 ) ;
key . setKey ( "temperature" ) ;
key . setAgg ( Aggregation . SUM ) ;
key . setPreviousStartTs ( previousStartTs ) ;
key . setPreviousEndTs ( previousEndTs ) ;
key . setPreviousValueOnly ( true ) ;
PageData < EntityData > result = postAggHistory ( device , startTs , endTs , List . of ( key ) ) ;
ComparisonTsValue agg = result . getData ( ) . get ( 0 ) . getAggLatest ( ) . get ( 7 ) ;
assertThat ( agg . getPrevious ( ) . getValue ( ) ) . isEqualTo ( "30" ) ;
assertThat ( agg . getCurrent ( ) ) . isNull ( ) ;
}
@Test
public void testFindEntityDataAggHistoryByQuery_noTelemetry ( ) throws Exception {
Device device = new Device ( ) ;
device . setName ( "AggEmptyDevice" ) ;
device . setType ( "default" ) ;
device = doPost ( "/api/device" , device , Device . class ) ;
long now = System . currentTimeMillis ( ) ;
AggKey key = new AggKey ( ) ;
key . setId ( 1 ) ;
key . setKey ( "temperature" ) ;
key . setAgg ( Aggregation . SUM ) ;
PageData < EntityData > result = postAggHistory ( device , now - TimeUnit . HOURS . toMillis ( 1 ) , now , List . of ( key ) ) ;
ComparisonTsValue agg = result . getData ( ) . get ( 0 ) . getAggLatest ( ) . get ( 1 ) ;
assertThat ( agg ) . isNotNull ( ) ;
assertThat ( agg . getCurrent ( ) . getValue ( ) ) . isEqualTo ( "0" ) ;
assertThat ( agg . getPrevious ( ) ) . isNull ( ) ;
}
@Test
public void testFindEntityDataAggHistoryByQuery_multipleKeysMixedAggregation ( ) throws Exception {
Device device = new Device ( ) ;
device . setName ( "AggMultiKeyDevice" ) ;
device . setType ( "default" ) ;
device = doPost ( "/api/device" , device , Device . class ) ;
long now = System . currentTimeMillis ( ) ;
long startTs = now - TimeUnit . MINUTES . toMillis ( 30 ) ;
long endTs = now ;
List < TsKvEntry > data = new ArrayList < > ( ) ;
long [ ] values = { 10L , 20L , 30L , 40L , 50L } ;
for ( int i = 0 ; i < values . length ; i + + ) {
data . add ( new BasicTsKvEntry ( startTs + i * TimeUnit . MINUTES . toMillis ( 1 ) ,
new LongDataEntry ( "temperature" , values [ i ] ) ) ) ;
}
sendTelemetry ( device , data ) ;
AggKey minKey = new AggKey ( ) ;
minKey . setId ( 1 ) ;
minKey . setKey ( "temperature" ) ;
minKey . setAgg ( Aggregation . MIN ) ;
AggKey maxKey = new AggKey ( ) ;
maxKey . setId ( 2 ) ;
maxKey . setKey ( "temperature" ) ;
maxKey . setAgg ( Aggregation . MAX ) ;
AggKey avgKey = new AggKey ( ) ;
avgKey . setId ( 3 ) ;
avgKey . setKey ( "temperature" ) ;
avgKey . setAgg ( Aggregation . AVG ) ;
PageData < EntityData > result = postAggHistory ( device , startTs , endTs , List . of ( minKey , maxKey , avgKey ) ) ;
var aggLatest = result . getData ( ) . get ( 0 ) . getAggLatest ( ) ;
assertThat ( aggLatest . get ( 1 ) . getCurrent ( ) . getValue ( ) ) . isEqualTo ( "10" ) ;
assertThat ( aggLatest . get ( 2 ) . getCurrent ( ) . getValue ( ) ) . isEqualTo ( "50" ) ;
assertThat ( Double . parseDouble ( aggLatest . get ( 3 ) . getCurrent ( ) . getValue ( ) ) ) . isEqualTo ( 30 . 0 ) ;
}
private PageData < EntityData > postAggHistory ( Device device , long startTs , long endTs , List < AggKey > keys ) throws Exception {
DeviceTypeFilter filter = new DeviceTypeFilter ( List . of ( "default" ) , device . getName ( ) ) ;
EntityDataPageLink pageLink = new EntityDataPageLink ( 10 , 0 , null , null ) ;
EntityDataQuery query = new EntityDataQuery ( filter , pageLink , Collections . emptyList ( ) , Collections . emptyList ( ) , null ) ;
AggHistoryCmd cmd = new AggHistoryCmd ( ) ;
cmd . setKeys ( keys ) ;
cmd . setStartTs ( startTs ) ;
cmd . setEndTs ( endTs ) ;
ObjectNode request = JacksonUtil . newObjectNode ( ) ;
request . set ( "query" , JacksonUtil . valueToTree ( query ) ) ;
request . set ( "aggHistoryCmd" , JacksonUtil . valueToTree ( cmd ) ) ;
return doPostAsyncWithTypedResponse ( "/api/entitiesQuery/find/aggHistory" ,
request , new TypeReference < > ( ) { } , status ( ) . isOk ( ) ) ;
}
private void sendTelemetry ( Device device , List < TsKvEntry > tsData ) throws InterruptedException {
CountDownLatch latch = new CountDownLatch ( 1 ) ;
AtomicReference < Throwable > error = new AtomicReference < > ( ) ;
tsService . saveTimeseries ( TimeseriesSaveRequest . builder ( )
. tenantId ( device . getTenantId ( ) )
. entityId ( device . getId ( ) )
. entries ( tsData )
. callback ( new FutureCallback < > ( ) {
@Override
public void onSuccess ( @Nullable Void result ) {
latch . countDown ( ) ;
}
@Override
public void onFailure ( Throwable t ) {
error . set ( t ) ;
latch . countDown ( ) ;
}
} )
. build ( ) ) ;
assertThat ( latch . await ( TIMEOUT , TimeUnit . SECONDS ) ) . isTrue ( ) ;
if ( error . get ( ) ! = null ) {
throw new AssertionError ( "Failed to save telemetry" , error . get ( ) ) ;
}
}
private KeyFilter buildStringKeyFilter ( EntityKeyType entityKeyType , String name , StringFilterPredicate . StringOperation operation , String value ) {
private KeyFilter buildStringKeyFilter ( EntityKeyType entityKeyType , String name , StringFilterPredicate . StringOperation operation , String value ) {
KeyFilter nameFilter = new KeyFilter ( ) ;
KeyFilter nameFilter = new KeyFilter ( ) ;
nameFilter . setKey ( new EntityKey ( entityKeyType , name ) ) ;
nameFilter . setKey ( new EntityKey ( entityKeyType , name ) ) ;