Browse Source

state and argument refactoring

pull/12547/head
IrynaMatveieva 1 year ago
parent
commit
5e16db275c
  1. 11
      application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldExecutionService.java
  2. 2
      application/src/main/java/org/thingsboard/server/service/cf/ctx/state/ArgumentEntry.java
  3. 38
      application/src/main/java/org/thingsboard/server/service/cf/ctx/state/BaseCalculatedFieldState.java
  4. 5
      application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java
  5. 1
      application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldState.java
  6. 9
      application/src/main/java/org/thingsboard/server/service/cf/ctx/state/ScriptCalculatedFieldState.java
  7. 26
      application/src/main/java/org/thingsboard/server/service/cf/ctx/state/SimpleCalculatedFieldState.java
  8. 26
      application/src/main/java/org/thingsboard/server/service/cf/ctx/state/SingleValueArgumentEntry.java
  9. 48
      application/src/main/java/org/thingsboard/server/service/cf/ctx/state/TsRollingArgumentEntry.java
  10. 238
      application/src/test/java/org/thingsboard/server/service/cf/ctx/state/ScriptCalculatedFieldStateTest.java
  11. 213
      application/src/test/java/org/thingsboard/server/service/cf/ctx/state/SimpleCalculatedFieldStateTest.java
  12. 71
      application/src/test/java/org/thingsboard/server/service/cf/ctx/state/SingleValueArgumentEntryTest.java
  13. 93
      application/src/test/java/org/thingsboard/server/service/cf/ctx/state/TsRollingArgumentEntryTest.java

11
application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldExecutionService.java

@ -39,7 +39,6 @@ import org.thingsboard.server.common.data.AttributeScope;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.cf.CalculatedField;
import org.thingsboard.server.common.data.cf.CalculatedFieldLink;
import org.thingsboard.server.common.data.cf.CalculatedFieldType;
import org.thingsboard.server.common.data.cf.configuration.Argument;
import org.thingsboard.server.common.data.cf.configuration.OutputType;
import org.thingsboard.server.common.data.id.AssetId;
@ -248,7 +247,7 @@ public class DefaultCalculatedFieldExecutionService extends AbstractPartitionBas
argFutures.put(entry.getKey(), argValueFuture);
}
return Futures.whenAllComplete(argFutures.values()).call(() -> {
var result = createStateByType(ctx.getCfType());
var result = createStateByType(ctx);
result.updateState(argFutures.entrySet().stream()
.collect(Collectors.toMap(
Entry::getKey, // Keep the key as is
@ -798,10 +797,10 @@ public class DefaultCalculatedFieldExecutionService extends AbstractPartitionBas
return payload;
}
private CalculatedFieldState createStateByType(CalculatedFieldType calculatedFieldType) {
return switch (calculatedFieldType) {
case SIMPLE -> new SimpleCalculatedFieldState();
case SCRIPT -> new ScriptCalculatedFieldState();
private CalculatedFieldState createStateByType(CalculatedFieldCtx ctx) {
return switch (ctx.getCfType()) {
case SIMPLE -> new SimpleCalculatedFieldState(ctx.getArgNames());
case SCRIPT -> new ScriptCalculatedFieldState(ctx.getArgNames());
};
}

2
application/src/main/java/org/thingsboard/server/service/cf/ctx/state/ArgumentEntry.java

@ -39,7 +39,7 @@ public interface ArgumentEntry {
Object getValue();
boolean hasUpdatedValue(ArgumentEntry entry);
boolean updateEntry(ArgumentEntry entry);
static ArgumentEntry createSingleValueArgument(KvEntry kvEntry) {
return new SingleValueArgumentEntry(kvEntry);

38
application/src/main/java/org/thingsboard/server/service/cf/ctx/state/BaseCalculatedFieldState.java

@ -16,14 +16,21 @@
package org.thingsboard.server.service.cf.ctx.state;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public abstract class BaseCalculatedFieldState implements CalculatedFieldState {
protected List<String> requiredArguments;
protected Map<String, ArgumentEntry> arguments;
public BaseCalculatedFieldState() {
arguments = new HashMap<>();
this.arguments = new HashMap<>();
}
public BaseCalculatedFieldState(List<String> requiredArguments) {
this.requiredArguments = requiredArguments;
this.arguments = new HashMap<>();
}
@Override
@ -44,22 +51,12 @@ public abstract class BaseCalculatedFieldState implements CalculatedFieldState {
ArgumentEntry newEntry = entry.getValue();
ArgumentEntry existingEntry = arguments.get(key);
if (existingEntry == null || existingEntry.hasUpdatedValue(newEntry)) {
if (existingEntry instanceof TsRollingArgumentEntry existingTsRollingEntry && newEntry instanceof TsRollingArgumentEntry newTsRollingEntry) {
existingTsRollingEntry.addAllTsRecords(newTsRollingEntry.getTsRecords());
} else if (existingEntry instanceof TsRollingArgumentEntry existingTsRollingEntry && newEntry instanceof SingleValueArgumentEntry singleValueEntry) {
existingTsRollingEntry.addTsRecord(singleValueEntry.getTs(), singleValueEntry.getValue());
} else if (existingEntry instanceof SingleValueArgumentEntry existingSingleValueEntry && newEntry instanceof SingleValueArgumentEntry singleValueEntry) {
// Long existingVersion = existingSingleValueEntry.getVersion();
// Long newVersion = singleValueEntry.getVersion();
// if (newVersion != null && (existingVersion == null || newVersion > existingVersion)) {
// arguments.put(key, newEntry.copy());
// }
arguments.put(key, newEntry.copy());
} else {
arguments.put(key, newEntry.copy());
}
if (existingEntry == null) {
validateNewEntry(newEntry);
arguments.put(key, newEntry.copy());
stateUpdated = true;
} else {
stateUpdated = existingEntry.updateEntry(newEntry);
}
}
@ -68,7 +65,12 @@ public abstract class BaseCalculatedFieldState implements CalculatedFieldState {
@Override
public boolean isReady() {
//TODO: IM
return true;
return arguments.keySet().containsAll(requiredArguments) &&
!arguments.containsValue(SingleValueArgumentEntry.EMPTY) &&
!arguments.containsValue(TsRollingArgumentEntry.EMPTY);
}
protected void validateNewEntry(ArgumentEntry newEntry) {
}
}

5
application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java

@ -16,6 +16,7 @@
package org.thingsboard.server.service.cf.ctx.state;
import lombok.Data;
import net.objecthunter.exp4j.Expression;
import org.thingsboard.script.api.tbel.TbelInvokeService;
import org.thingsboard.server.common.data.AttributeScope;
import org.thingsboard.server.common.data.cf.CalculatedField;
@ -31,7 +32,6 @@ import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.kv.AttributeKvEntry;
import org.thingsboard.server.common.data.kv.TsKvEntry;
import org.thingsboard.server.common.data.util.TbPair;
import org.thingsboard.server.gen.transport.TransportProtos.TsKvProto;
import java.util.ArrayList;
import java.util.HashMap;
@ -56,6 +56,7 @@ public class CalculatedFieldCtx {
private String expression;
private TbelInvokeService tbelInvokeService;
private CalculatedFieldScriptEngine calculatedFieldScriptEngine;
private ThreadLocal<Expression> customExpression;
public CalculatedFieldCtx(CalculatedField calculatedField, TbelInvokeService tbelInvokeService) {
this.cfId = calculatedField.getId();
@ -86,6 +87,8 @@ public class CalculatedFieldCtx {
this.tbelInvokeService = tbelInvokeService;
if (CalculatedFieldType.SCRIPT.equals(calculatedField.getType())) {
this.calculatedFieldScriptEngine = initEngine(tenantId, expression, tbelInvokeService);
} else {
this.customExpression = new ThreadLocal<>();
}
}

1
application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldState.java

@ -44,6 +44,5 @@ public interface CalculatedFieldState {
ListenableFuture<CalculatedFieldResult> performCalculation(CalculatedFieldCtx ctx);
@JsonIgnore
boolean isReady();
}

9
application/src/main/java/org/thingsboard/server/service/cf/ctx/state/ScriptCalculatedFieldState.java

@ -25,6 +25,7 @@ import org.thingsboard.server.common.data.cf.configuration.Argument;
import org.thingsboard.server.common.data.cf.configuration.Output;
import org.thingsboard.server.service.cf.CalculatedFieldResult;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
@ -32,6 +33,10 @@ import java.util.TreeMap;
@Slf4j
public class ScriptCalculatedFieldState extends BaseCalculatedFieldState {
public ScriptCalculatedFieldState(List<String> requiredArguments) {
super(requiredArguments);
}
@Override
public CalculatedFieldType getType() {
return CalculatedFieldType.SCRIPT;
@ -40,9 +45,9 @@ public class ScriptCalculatedFieldState extends BaseCalculatedFieldState {
@Override
public ListenableFuture<CalculatedFieldResult> performCalculation(CalculatedFieldCtx ctx) {
arguments.forEach((key, argumentEntry) -> {
if (argumentEntry instanceof TsRollingArgumentEntry) {
if (argumentEntry instanceof TsRollingArgumentEntry tsRollingEntry) {
Argument argument = ctx.getArguments().get(key);
TreeMap<Long, Object> tsRecords = ((TsRollingArgumentEntry) argumentEntry).getTsRecords();
TreeMap<Long, Object> tsRecords = tsRollingEntry.getTsRecords();
if (tsRecords.size() > argument.getLimit()) {
tsRecords.pollFirstEntry();
}

26
application/src/main/java/org/thingsboard/server/service/cf/ctx/state/SimpleCalculatedFieldState.java

@ -24,21 +24,32 @@ import org.thingsboard.server.common.data.cf.CalculatedFieldType;
import org.thingsboard.server.common.data.cf.configuration.Output;
import org.thingsboard.server.service.cf.CalculatedFieldResult;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@Data
public class SimpleCalculatedFieldState extends BaseCalculatedFieldState {
public SimpleCalculatedFieldState(List<String> requiredArguments) {
super(requiredArguments);
}
@Override
public CalculatedFieldType getType() {
return CalculatedFieldType.SIMPLE;
}
@Override
protected void validateNewEntry(ArgumentEntry newEntry) {
if (newEntry instanceof TsRollingArgumentEntry) {
throw new IllegalArgumentException("Rolling argument entry is not supported for simple calculated fields.");
}
}
@Override
public ListenableFuture<CalculatedFieldResult> performCalculation(CalculatedFieldCtx ctx) {
String expression = ctx.getExpression();
ThreadLocal<Expression> customExpression = new ThreadLocal<>();
ThreadLocal<Expression> customExpression = ctx.getCustomExpression();
var expr = customExpression.get();
if (expr == null) {
expr = new ExpressionBuilder(expression)
@ -47,9 +58,14 @@ public class SimpleCalculatedFieldState extends BaseCalculatedFieldState {
.build();
customExpression.set(expr);
}
Map<String, Double> variables = new HashMap<>();
this.arguments.forEach((k, v) -> variables.put(k, Double.parseDouble(v.getValue().toString())));
expr.setVariables(variables);
for (Map.Entry<String, ArgumentEntry> entry : this.arguments.entrySet()) {
try {
expr.setVariable(entry.getKey(), Double.parseDouble(entry.getValue().getValue().toString()));
} catch (NumberFormatException e) {
throw new IllegalArgumentException("Argument '" + entry.getKey() + "' is not a number.");
}
}
double expressionResult = expr.evaluate();

26
application/src/main/java/org/thingsboard/server/service/cf/ctx/state/SingleValueArgumentEntry.java

@ -21,7 +21,6 @@ import lombok.NoArgsConstructor;
import org.thingsboard.server.common.data.kv.AttributeKvEntry;
import org.thingsboard.server.common.data.kv.KvEntry;
import org.thingsboard.server.common.data.kv.TsKvEntry;
import org.thingsboard.server.common.util.KvProtoUtil;
import org.thingsboard.server.common.util.ProtoUtils;
import org.thingsboard.server.gen.transport.TransportProtos.AttributeValueProto;
import org.thingsboard.server.gen.transport.TransportProtos.TsKvProto;
@ -35,7 +34,6 @@ public class SingleValueArgumentEntry implements ArgumentEntry {
private long ts;
private Object value;
private Long version;
public SingleValueArgumentEntry(TsKvProto entry) {
@ -79,14 +77,28 @@ public class SingleValueArgumentEntry implements ArgumentEntry {
return value;
}
@Override
public boolean hasUpdatedValue(ArgumentEntry entry) {
return this.ts != ((SingleValueArgumentEntry) entry).getTs();
}
@Override
public ArgumentEntry copy() {
return new SingleValueArgumentEntry(this.ts, this.value, this.version);
}
@Override
public boolean updateEntry(ArgumentEntry entry) {
if (entry instanceof SingleValueArgumentEntry singleValueEntry) {
if (singleValueEntry.getTs() == this.ts) {
return false;
}
Long newVersion = singleValueEntry.getVersion();
if (newVersion == null || this.version == null || newVersion > this.version) {
this.ts = singleValueEntry.getTs();
this.value = singleValueEntry.getValue();
this.version = newVersion;
return true;
}
} else {
throw new IllegalArgumentException("Unsupported argument entry type for single value argument entry: " + entry.getType());
}
return false;
}
}

48
application/src/main/java/org/thingsboard/server/service/cf/ctx/state/TsRollingArgumentEntry.java

@ -62,31 +62,49 @@ public class TsRollingArgumentEntry implements ArgumentEntry {
}
@Override
public boolean hasUpdatedValue(ArgumentEntry entry) {
return entry instanceof SingleValueArgumentEntry ?
!tsRecords.containsKey(((SingleValueArgumentEntry) entry).getTs()) :
!tsRecords.keySet().containsAll(((TsRollingArgumentEntry) entry).getTsRecords().keySet());
public ArgumentEntry copy() {
return new TsRollingArgumentEntry(new TreeMap<>(tsRecords));
}
@Override
public ArgumentEntry copy() {
return new TsRollingArgumentEntry(new TreeMap<>(tsRecords));
public boolean updateEntry(ArgumentEntry entry) {
if (entry instanceof TsRollingArgumentEntry tsRollingEntry) {
return updateTsRollingEntry(tsRollingEntry);
} else if (entry instanceof SingleValueArgumentEntry singleValueEntry) {
return updateSingleValueEntry(singleValueEntry);
} else {
throw new IllegalArgumentException("Unsupported argument entry type for rolling argument entry: " + entry.getType());
}
}
private boolean updateTsRollingEntry(TsRollingArgumentEntry tsRollingEntry) {
boolean updated = false;
for (Map.Entry<Long, Object> tsRecordEntry : tsRollingEntry.getTsRecords().entrySet()) {
updated |= addTsRecordIfAbsent(tsRecordEntry.getKey(), tsRecordEntry.getValue());
}
return updated;
}
public void addTsRecord(Long key, Object value) {
private boolean updateSingleValueEntry(SingleValueArgumentEntry singleValueEntry) {
return addTsRecordIfAbsent(singleValueEntry.getTs(), singleValueEntry.getValue());
}
private boolean addTsRecordIfAbsent(Long ts, Object value) {
if (!tsRecords.containsKey(ts)) {
addTsRecord(ts, value);
return true;
}
return false;
}
private void addTsRecord(Long ts, Object value) {
if (NumberUtils.isParsable(value.toString())) {
tsRecords.put(key, value);
tsRecords.put(ts, value);
if (tsRecords.size() > MAX_ROLLING_ARGUMENT_ENTRY_SIZE) {
tsRecords.pollFirstEntry();
}
} else {
log.warn("Argument type 'TS_ROLLING' only supports numeric values.");
}
}
public void addAllTsRecords(Map<Long, Object> newRecords) {
for (Map.Entry<Long, Object> entry : newRecords.entrySet()) {
addTsRecord(entry.getKey(), entry.getValue());
throw new IllegalArgumentException("Argument type " + getType() + " only supports numeric values.");
}
}

238
application/src/test/java/org/thingsboard/server/service/cf/ctx/state/ScriptCalculatedFieldStateTest.java

@ -0,0 +1,238 @@
/**
* 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.service.cf.ctx.state;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.thingsboard.script.api.tbel.DefaultTbelInvokeService;
import org.thingsboard.script.api.tbel.TbelInvokeService;
import org.thingsboard.server.common.data.AttributeScope;
import org.thingsboard.server.common.data.cf.CalculatedField;
import org.thingsboard.server.common.data.cf.CalculatedFieldType;
import org.thingsboard.server.common.data.cf.configuration.Argument;
import org.thingsboard.server.common.data.cf.configuration.ArgumentType;
import org.thingsboard.server.common.data.cf.configuration.CalculatedFieldConfiguration;
import org.thingsboard.server.common.data.cf.configuration.Output;
import org.thingsboard.server.common.data.cf.configuration.OutputType;
import org.thingsboard.server.common.data.cf.configuration.ReferencedEntityKey;
import org.thingsboard.server.common.data.cf.configuration.SimpleCalculatedFieldConfiguration;
import org.thingsboard.server.common.data.id.AssetId;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.service.cf.CalculatedFieldResult;
import java.util.HashMap;
import java.util.Map;
import java.util.TreeMap;
import java.util.UUID;
import java.util.concurrent.ExecutionException;
import static org.assertj.core.api.Assertions.assertThat;
@SpringBootTest(classes = DefaultTbelInvokeService.class)
public class ScriptCalculatedFieldStateTest {
private final TenantId TENANT_ID = TenantId.fromUUID(UUID.fromString("5b18e321-3327-4290-b996-d72a65e90382"));
private final DeviceId DEVICE_ID = new DeviceId(UUID.fromString("5512071d-5abc-411d-a907-4cdb6539c2eb"));
private final AssetId ASSET_ID = new AssetId(UUID.fromString("5bc010ae-bcfd-46c8-98b9-8ee8c8955a76"));
private final SingleValueArgumentEntry assetHumidityArgEntry = new SingleValueArgumentEntry(System.currentTimeMillis() - 10, 43, 122L);
private final TsRollingArgumentEntry deviceTemperatureArgEntry = createRollingArgEntry();
private final long ts = System.currentTimeMillis();
private ScriptCalculatedFieldState state;
private CalculatedFieldCtx ctx;
@Autowired
private TbelInvokeService tbelInvokeService;
@BeforeEach
void setUp() {
ctx = new CalculatedFieldCtx(getCalculatedField(), tbelInvokeService);
state = new ScriptCalculatedFieldState(ctx.getArgNames());
}
@Test
void testType() {
assertThat(state.getType()).isEqualTo(CalculatedFieldType.SCRIPT);
}
@Test
void testUpdateState() {
state.arguments = new HashMap<>(Map.of("assetHumidity", assetHumidityArgEntry));
Map<String, ArgumentEntry> newArgs = Map.of("deviceTemperature", deviceTemperatureArgEntry);
boolean stateUpdated = state.updateState(newArgs);
assertThat(stateUpdated).isTrue();
assertThat(state.getArguments()).containsExactlyInAnyOrderEntriesOf(
Map.of(
"assetHumidity", assetHumidityArgEntry,
"deviceTemperature", deviceTemperatureArgEntry
)
);
}
@Test
void testUpdateStateWhenUpdateExistingEntry() {
state.arguments = new HashMap<>(Map.of("deviceTemperature", deviceTemperatureArgEntry, "assetHumidity", assetHumidityArgEntry));
SingleValueArgumentEntry newArgEntry = new SingleValueArgumentEntry(ts, 41, 349L);
Map<String, ArgumentEntry> newArgs = Map.of("assetHumidity", newArgEntry);
boolean stateUpdated = state.updateState(newArgs);
assertThat(stateUpdated).isTrue();
assertThat(state.getArguments()).containsExactlyInAnyOrderEntriesOf(
Map.of(
"assetHumidity", newArgEntry,
"deviceTemperature", deviceTemperatureArgEntry
)
);
}
@Test
void testPerformCalculation() throws ExecutionException, InterruptedException {
state.arguments = new HashMap<>(Map.of("deviceTemperature", deviceTemperatureArgEntry, "assetHumidity", assetHumidityArgEntry));
CalculatedFieldResult result = state.performCalculation(ctx).get();
assertThat(result).isNotNull();
Output output = getCalculatedFieldConfig().getOutput();
assertThat(result.getType()).isEqualTo(output.getType());
assertThat(result.getScope()).isEqualTo(output.getScope());
assertThat(result.getResultMap()).isEqualTo(Map.of("averageDeviceTemperature", 13.0, "assetHumidity", 43));
}
@Test
void testPerformCalculationWhenOldTelemetry() throws ExecutionException, InterruptedException {
TsRollingArgumentEntry argumentEntry = new TsRollingArgumentEntry();
TreeMap<Long, Object> values = new TreeMap<>();
values.put(ts - 40000, 4);// will not be used for calculation
values.put(ts - 45000, 2);// will not be used for calculation
values.put(ts - 20, 0);
argumentEntry.setTsRecords(values);
state.arguments = new HashMap<>(Map.of("deviceTemperature", argumentEntry, "assetHumidity", assetHumidityArgEntry));
CalculatedFieldResult result = state.performCalculation(ctx).get();
assertThat(result).isNotNull();
Output output = getCalculatedFieldConfig().getOutput();
assertThat(result.getType()).isEqualTo(output.getType());
assertThat(result.getScope()).isEqualTo(output.getScope());
assertThat(result.getResultMap()).isEqualTo(Map.of("averageDeviceTemperature", 0.0, "assetHumidity", 43));
}
@Test
void testPerformCalculationWhenArgumentsMoreThanLimit() throws ExecutionException, InterruptedException {
TsRollingArgumentEntry argumentEntry = new TsRollingArgumentEntry();
TreeMap<Long, Object> values = new TreeMap<>();
values.put(ts - 20, 1000);// will not be used
values.put(ts - 18, 0);
values.put(ts - 16, 0);
values.put(ts - 14, 0);
values.put(ts - 12, 0);
values.put(ts - 10, 0);
argumentEntry.setTsRecords(values);
state.arguments = new HashMap<>(Map.of("deviceTemperature", argumentEntry, "assetHumidity", assetHumidityArgEntry));
CalculatedFieldResult result = state.performCalculation(ctx).get();
assertThat(result).isNotNull();
Output output = getCalculatedFieldConfig().getOutput();
assertThat(result.getType()).isEqualTo(output.getType());
assertThat(result.getScope()).isEqualTo(output.getScope());
assertThat(result.getResultMap()).isEqualTo(Map.of("averageDeviceTemperature", 0.0, "assetHumidity", 43));
}
@Test
void testIsReadyWhenNotAllArgPresent() {
assertThat(state.isReady()).isFalse();
}
@Test
void testIsReadyWhenAllArgPresent() {
state.arguments = new HashMap<>(Map.of("deviceTemperature", deviceTemperatureArgEntry, "assetHumidity", assetHumidityArgEntry));
assertThat(state.isReady()).isTrue();
}
@Test
void testIsReadyWhenEmptyEntryPresents() {
state.arguments = new HashMap<>(Map.of("deviceTemperature", TsRollingArgumentEntry.EMPTY, "assetHumidity", assetHumidityArgEntry));
assertThat(state.isReady()).isFalse();
}
private TsRollingArgumentEntry createRollingArgEntry() {
TsRollingArgumentEntry argumentEntry = new TsRollingArgumentEntry();
long ts = System.currentTimeMillis();
TreeMap<Long, Object> values = new TreeMap<>();
values.put(ts - 40, 10);
values.put(ts - 30, 12);
values.put(ts - 20, 17);
argumentEntry.setTsRecords(values);
return argumentEntry;
}
private CalculatedField getCalculatedField() {
CalculatedField calculatedField = new CalculatedField();
calculatedField.setTenantId(TENANT_ID);
calculatedField.setEntityId(ASSET_ID);
calculatedField.setType(CalculatedFieldType.SCRIPT);
calculatedField.setName("Test Calculated Field");
calculatedField.setConfigurationVersion(1);
calculatedField.setConfiguration(getCalculatedFieldConfig());
calculatedField.setVersion(1L);
return calculatedField;
}
private CalculatedFieldConfiguration getCalculatedFieldConfig() {
SimpleCalculatedFieldConfiguration config = new SimpleCalculatedFieldConfiguration();
Argument argument1 = new Argument();
argument1.setRefEntityId(DEVICE_ID);
ReferencedEntityKey refEntityKey1 = new ReferencedEntityKey("temperature", ArgumentType.TS_ROLLING, null);
argument1.setRefEntityKey(refEntityKey1);
argument1.setLimit(5);
argument1.setTimeWindow(30000);
Argument argument2 = new Argument();
ReferencedEntityKey refEntityKey2 = new ReferencedEntityKey("humidity", ArgumentType.TS_LATEST, null);
argument1.setRefEntityKey(refEntityKey2);
config.setArguments(Map.of("deviceTemperature", argument1, "assetHumidity", argument2));
config.setExpression("var result = 0; foreach(element : deviceTemperature.entrySet()) { result += element.getValue(); } var map = {}; map.put(\"averageDeviceTemperature\", result / deviceTemperature.size()); map.put(\"assetHumidity\", assetHumidity); return map;");
Output output = new Output();
output.setType(OutputType.ATTRIBUTES);
output.setScope(AttributeScope.SERVER_SCOPE);
config.setOutput(output);
return config;
}
}

213
application/src/test/java/org/thingsboard/server/service/cf/ctx/state/SimpleCalculatedFieldStateTest.java

@ -0,0 +1,213 @@
/**
* 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.service.cf.ctx.state;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.thingsboard.server.common.data.AttributeScope;
import org.thingsboard.server.common.data.cf.CalculatedField;
import org.thingsboard.server.common.data.cf.CalculatedFieldType;
import org.thingsboard.server.common.data.cf.configuration.Argument;
import org.thingsboard.server.common.data.cf.configuration.ArgumentType;
import org.thingsboard.server.common.data.cf.configuration.CalculatedFieldConfiguration;
import org.thingsboard.server.common.data.cf.configuration.Output;
import org.thingsboard.server.common.data.cf.configuration.OutputType;
import org.thingsboard.server.common.data.cf.configuration.ReferencedEntityKey;
import org.thingsboard.server.common.data.cf.configuration.SimpleCalculatedFieldConfiguration;
import org.thingsboard.server.common.data.id.AssetId;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.service.cf.CalculatedFieldResult;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ExecutionException;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
public class SimpleCalculatedFieldStateTest {
private final TenantId TENANT_ID = TenantId.fromUUID(UUID.fromString("5b18e321-3327-4290-b996-d72a65e90382"));
private final DeviceId DEVICE_ID = new DeviceId(UUID.fromString("5512071d-5abc-411d-a907-4cdb6539c2eb"));
private final AssetId ASSET_ID = new AssetId(UUID.fromString("5bc010ae-bcfd-46c8-98b9-8ee8c8955a76"));
private final SingleValueArgumentEntry key1ArgEntry = new SingleValueArgumentEntry(System.currentTimeMillis() - 10, 11, 145L);
private final SingleValueArgumentEntry key2ArgEntry = new SingleValueArgumentEntry(System.currentTimeMillis() - 6, 15, 165L);
private final SingleValueArgumentEntry key3ArgEntry = new SingleValueArgumentEntry(System.currentTimeMillis() - 3, 23, 184L);
private SimpleCalculatedFieldState state;
private CalculatedFieldCtx ctx;
@BeforeEach
void setUp() {
ctx = new CalculatedFieldCtx(getCalculatedField(), null);
state = new SimpleCalculatedFieldState(ctx.getArgNames());
}
@Test
void testType() {
assertThat(state.getType()).isEqualTo(CalculatedFieldType.SIMPLE);
}
@Test
void testUpdateState() {
state.arguments = new HashMap<>(Map.of(
"key1", key1ArgEntry,
"key2", key2ArgEntry
));
Map<String, ArgumentEntry> newArgs = Map.of("key3", key3ArgEntry);
boolean stateUpdated = state.updateState(newArgs);
assertThat(stateUpdated).isTrue();
assertThat(state.getArguments()).containsExactlyInAnyOrderEntriesOf(
Map.of(
"key1", key1ArgEntry,
"key2", key2ArgEntry,
"key3", key3ArgEntry
)
);
}
@Test
void testUpdateStateWhenUpdateExistingEntry() {
state.arguments = new HashMap<>(Map.of("key1", key1ArgEntry));
SingleValueArgumentEntry newArgEntry = new SingleValueArgumentEntry(System.currentTimeMillis(), 18, 190L);
Map<String, ArgumentEntry> newArgs = Map.of("key1", newArgEntry);
boolean stateUpdated = state.updateState(newArgs);
assertThat(stateUpdated).isTrue();
assertThat(state.getArguments()).containsExactlyInAnyOrderEntriesOf(Map.of("key1", newArgEntry));
}
@Test
void testUpdateStateWhenRollingEntryPassed() {
state.arguments = new HashMap<>(Map.of(
"key1", key1ArgEntry,
"key2", key2ArgEntry
));
Map<String, ArgumentEntry> newArgs = Map.of("key3", TsRollingArgumentEntry.EMPTY);
assertThatThrownBy(() -> state.updateState(newArgs))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("Rolling argument entry is not supported for simple calculated fields.");
}
@Test
void testPerformCalculation() throws ExecutionException, InterruptedException {
state.arguments = new HashMap<>(Map.of(
"key1", key1ArgEntry,
"key2", key2ArgEntry,
"key3", key3ArgEntry
));
CalculatedFieldResult result = state.performCalculation(ctx).get();
assertThat(result).isNotNull();
Output output = getCalculatedFieldConfig().getOutput();
assertThat(result.getType()).isEqualTo(output.getType());
assertThat(result.getScope()).isEqualTo(output.getScope());
assertThat(result.getResultMap()).isEqualTo(Map.of("output", 49.0));
}
@Test
void testPerformCalculationWhenPassedNotNumber() {
state.arguments = new HashMap<>(Map.of(
"key1", key1ArgEntry,
"key2", new SingleValueArgumentEntry(System.currentTimeMillis() - 9, "string", 124L),
"key3", key3ArgEntry
));
assertThatThrownBy(() -> state.performCalculation(ctx))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("Argument 'key2' is not a number.");
}
@Test
void testIsReadyWhenNotAllArgPresent() {
assertThat(state.isReady()).isFalse();
}
@Test
void testIsReadyWhenAllArgPresent() {
state.arguments = new HashMap<>(Map.of(
"key1", key1ArgEntry,
"key2", key2ArgEntry,
"key3", key3ArgEntry
));
assertThat(state.isReady()).isTrue();
}
@Test
void testIsReadyWhenEmptyEntryPresents() {
state.arguments = new HashMap<>(Map.of(
"key1", key1ArgEntry,
"key2", key2ArgEntry
));
state.getArguments().put("key3", SingleValueArgumentEntry.EMPTY);
assertThat(state.isReady()).isFalse();
}
private CalculatedField getCalculatedField() {
CalculatedField calculatedField = new CalculatedField();
calculatedField.setTenantId(TENANT_ID);
calculatedField.setEntityId(DEVICE_ID);
calculatedField.setType(CalculatedFieldType.SIMPLE);
calculatedField.setName("Test Calculated Field");
calculatedField.setConfigurationVersion(1);
calculatedField.setConfiguration(getCalculatedFieldConfig());
calculatedField.setVersion(1L);
return calculatedField;
}
private CalculatedFieldConfiguration getCalculatedFieldConfig() {
SimpleCalculatedFieldConfiguration config = new SimpleCalculatedFieldConfiguration();
Argument argument1 = new Argument();
argument1.setRefEntityId(ASSET_ID);
ReferencedEntityKey refEntityKey1 = new ReferencedEntityKey("temp1", ArgumentType.TS_LATEST, null);
argument1.setRefEntityKey(refEntityKey1);
Argument argument2 = new Argument();
argument2.setRefEntityId(ASSET_ID);
ReferencedEntityKey refEntityKey2 = new ReferencedEntityKey("temp2", ArgumentType.ATTRIBUTE, null);
argument2.setRefEntityKey(refEntityKey2);
Argument argument3 = new Argument();
argument3.setRefEntityId(ASSET_ID);
ReferencedEntityKey refEntityKey3 = new ReferencedEntityKey("temp3", ArgumentType.TS_LATEST, null);
argument3.setRefEntityKey(refEntityKey3);
config.setArguments(Map.of("key1", argument1, "key2", argument2, "key3", argument3));
config.setExpression("key1 + key2 + key3");
Output output = new Output();
output.setName("output");
output.setType(OutputType.ATTRIBUTES);
output.setScope(AttributeScope.SERVER_SCOPE);
config.setOutput(output);
return config;
}
}

71
application/src/test/java/org/thingsboard/server/service/cf/ctx/state/SingleValueArgumentEntryTest.java

@ -0,0 +1,71 @@
/**
* Copyright © 2016-2024 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.cf.ctx.state;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
public class SingleValueArgumentEntryTest {
private SingleValueArgumentEntry entry;
private final long ts = System.currentTimeMillis();
@BeforeEach
void setUp() {
entry = new SingleValueArgumentEntry(ts, 11, 363L);
}
@Test
void testArgumentEntryType() {
assertThat(entry.getType()).isEqualTo(ArgumentEntryType.SINGLE_VALUE);
}
@Test
void testUpdateEntryWhenRollingEntryPassed() {
assertThatThrownBy(() -> entry.updateEntry(TsRollingArgumentEntry.EMPTY))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("Unsupported argument entry type for single value argument entry: " + ArgumentEntryType.TS_ROLLING);
}
@Test
void testUpdateEntryWithThaSameTs() {
assertThat(entry.updateEntry(new SingleValueArgumentEntry(ts, 13, 363L))).isFalse();
}
@Test
void testUpdateEntryWhenNewVersionIsNull() {
assertThat(entry.updateEntry(new SingleValueArgumentEntry(ts + 16, 13, null))).isTrue();
assertThat(entry.getValue()).isEqualTo(13);
assertThat(entry.getVersion()).isNull();
}
@Test
void testUpdateEntryWhenNewVersionIsGreaterThanCurrent() {
assertThat(entry.updateEntry(new SingleValueArgumentEntry(ts + 18, 18, 369L))).isTrue();
assertThat(entry.getValue()).isEqualTo(18);
assertThat(entry.getVersion()).isEqualTo(369L);
}
@Test
void testUpdateEntryWhenNewVersionIsLessThanCurrent() {
assertThat(entry.updateEntry(new SingleValueArgumentEntry(ts + 18, 18, 234L))).isFalse();
}
}

93
application/src/test/java/org/thingsboard/server/service/cf/ctx/state/TsRollingArgumentEntryTest.java

@ -0,0 +1,93 @@
/**
* 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.service.cf.ctx.state;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.util.Map;
import java.util.TreeMap;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
public class TsRollingArgumentEntryTest {
private TsRollingArgumentEntry entry;
private final long ts = System.currentTimeMillis();
@BeforeEach
void setUp() {
TreeMap<Long, Object> values = new TreeMap<>();
values.put(ts - 40, 10);
values.put(ts - 30, 12);
values.put(ts - 20, 17);
entry = new TsRollingArgumentEntry(values);
}
@Test
void testArgumentEntryType() {
assertThat(entry.getType()).isEqualTo(ArgumentEntryType.TS_ROLLING);
}
@Test
void testUpdateEntryWhenSingleValueEntryPassed() {
SingleValueArgumentEntry newEntry = new SingleValueArgumentEntry(ts - 10, 23, 123L);
assertThat(entry.updateEntry(newEntry)).isTrue();
assertThat(entry.getTsRecords()).hasSize(4);
assertThat(entry.getTsRecords().get(ts - 10)).isEqualTo(23);
}
@Test
void testUpdateEntryWhenSingleValueEntryWithTheSameTsPassed() {
SingleValueArgumentEntry newEntry = new SingleValueArgumentEntry(ts - 20, 23, 123L);
assertThat(entry.updateEntry(newEntry)).isFalse();
}
@Test
void testUpdateEntryWhenRollingEntryPassed() {
TsRollingArgumentEntry newEntry = new TsRollingArgumentEntry();
TreeMap<Long, Object> values = new TreeMap<>();
values.put(ts - 20, 16);
values.put(ts - 10, 7);
values.put(ts - 5, 1);
newEntry.setTsRecords(values);
assertThat(entry.updateEntry(newEntry)).isTrue();
assertThat(entry.getTsRecords()).hasSize(5);
assertThat(entry.getTsRecords()).isEqualTo(Map.of(
ts - 40, 10,
ts - 30, 12,
ts - 20, 17,
ts - 10, 7,
ts - 5, 1
));
}
@Test
void testUpdateEntryWhenValueIsNotNumber() {
SingleValueArgumentEntry newEntry = new SingleValueArgumentEntry(ts - 10, "string", 123L);
assertThatThrownBy(() -> entry.updateEntry(newEntry))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("Argument type " + ArgumentEntryType.TS_ROLLING + " only supports numeric values.");
}
}
Loading…
Cancel
Save