Math node implementation
This commit is contained in:
parent
fb49273bd6
commit
15ee23c5d5
@ -25,11 +25,12 @@ import lombok.NoArgsConstructor;
|
||||
public class TbMathArgument {
|
||||
|
||||
private TbMathArgumentType type;
|
||||
private String value;
|
||||
private String key;
|
||||
private String attributeScope;
|
||||
private Double defaultValue;
|
||||
|
||||
public TbMathArgument(TbMathArgumentType type, String value) {
|
||||
public TbMathArgument(TbMathArgumentType type, String key) {
|
||||
this.type = type;
|
||||
this.value = value;
|
||||
this.key = key;
|
||||
}
|
||||
}
|
||||
|
||||
@ -33,43 +33,59 @@ public class TbMathArgumentValue {
|
||||
}
|
||||
|
||||
public static TbMathArgumentValue constant(TbMathArgument arg) {
|
||||
return fromString(arg.getValue());
|
||||
return fromString(arg.getKey());
|
||||
}
|
||||
|
||||
public static TbMathArgumentValue fromMessageBody(String key, Optional<ObjectNode> jsonNodeOpt) {
|
||||
private static TbMathArgumentValue defaultOrThrow(Double defaultValue, String error) {
|
||||
if (defaultValue != null) {
|
||||
return new TbMathArgumentValue(defaultValue);
|
||||
}
|
||||
throw new RuntimeException(error);
|
||||
}
|
||||
|
||||
public static TbMathArgumentValue fromMessageBody(TbMathArgument arg, Optional<ObjectNode> jsonNodeOpt) {
|
||||
String key = arg.getKey();
|
||||
Double defaultValue = arg.getDefaultValue();
|
||||
if (jsonNodeOpt.isEmpty()) {
|
||||
throw new RuntimeException("Message body is empty!");
|
||||
return defaultOrThrow(defaultValue, "Message body is empty!");
|
||||
}
|
||||
var json = jsonNodeOpt.get();
|
||||
if (!json.has(key)) {
|
||||
throw new RuntimeException("Message body has no '" + key + "'!");
|
||||
return defaultOrThrow(defaultValue, "Message body has no '" + key + "'!");
|
||||
}
|
||||
JsonNode valueNode = json.get(key);
|
||||
if (valueNode.isEmpty() || valueNode.isNull()) {
|
||||
throw new RuntimeException("Message body has empty or null '" + key + "'!");
|
||||
if (valueNode.isNull()) {
|
||||
return defaultOrThrow(defaultValue, "Message body has null '" + key + "'!");
|
||||
}
|
||||
double value;
|
||||
if (valueNode.isNumber()) {
|
||||
value = valueNode.doubleValue();
|
||||
} else if (valueNode.isTextual()) {
|
||||
var valueNodeText = valueNode.asText();
|
||||
if (StringUtils.isNotBlank(valueNodeText)) {
|
||||
try {
|
||||
value = Double.parseDouble(valueNode.asText());
|
||||
} catch (NumberFormatException ne) {
|
||||
throw new RuntimeException("Can't convert value '" + valueNode.asText() + "' to double!");
|
||||
}
|
||||
} else {
|
||||
return defaultOrThrow(defaultValue, "Message value is empty for '" + key + "'!");
|
||||
}
|
||||
} else {
|
||||
throw new RuntimeException("Can't convert value '" + valueNode.toString() + "' to double!");
|
||||
}
|
||||
return new TbMathArgumentValue(value);
|
||||
}
|
||||
|
||||
public static TbMathArgumentValue fromMessageMetadata(String key, TbMsgMetaData metaData) {
|
||||
public static TbMathArgumentValue fromMessageMetadata(TbMathArgument arg, TbMsgMetaData metaData) {
|
||||
String key = arg.getKey();
|
||||
Double defaultValue = arg.getDefaultValue();
|
||||
if (metaData == null) {
|
||||
throw new RuntimeException("Message metadata is empty!");
|
||||
return defaultOrThrow(defaultValue, "Message metadata is empty!");
|
||||
}
|
||||
var value = metaData.getValue(key);
|
||||
if (StringUtils.isEmpty(value)) {
|
||||
throw new RuntimeException("Message metadata has no '" + key + "'!");
|
||||
return defaultOrThrow(defaultValue, "Message metadata has no '" + key + "'!");
|
||||
}
|
||||
return fromString(value);
|
||||
}
|
||||
|
||||
@ -19,7 +19,6 @@ import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
import com.google.common.util.concurrent.Futures;
|
||||
import com.google.common.util.concurrent.ListenableFuture;
|
||||
import com.google.common.util.concurrent.MoreExecutors;
|
||||
import com.google.common.util.concurrent.SettableFuture;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.util.ConcurrentReferenceHashMap;
|
||||
import org.thingsboard.common.util.DonAsynchron;
|
||||
@ -39,9 +38,10 @@ import org.thingsboard.server.common.data.kv.KvEntry;
|
||||
import org.thingsboard.server.common.data.plugin.ComponentType;
|
||||
import org.thingsboard.server.common.msg.TbMsg;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ConcurrentMap;
|
||||
import java.util.concurrent.Semaphore;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
@ -53,24 +53,34 @@ import java.util.stream.Collectors;
|
||||
@Slf4j
|
||||
@RuleNode(
|
||||
type = ComponentType.ACTION,
|
||||
name = "math formula",
|
||||
configClazz = TbMathFormulaConfiguration.class,
|
||||
nodeDescription = "Calculates the mathematics formula based on message and/or database values",
|
||||
nodeDetails = "Transform incoming Message with configured JS function to String and log final value into Thingsboard log file. " +
|
||||
"Message payload can be accessed via <code>msg</code> property. For example <code>'temperature = ' + msg.temperature ;</code>. " +
|
||||
"Message metadata can be accessed via <code>metadata</code> property. For example <code>'name = ' + metadata.customerName;</code>.",
|
||||
name = "math function",
|
||||
configClazz = TbMathNodeConfiguration.class,
|
||||
nodeDescription = "Apply math function and save the result into the message and/or database",
|
||||
nodeDetails = "Supports math operations like: ADD, SUB, MULT, DIV, etc and functions: SIN, COS, TAN, SEC, etc. " +
|
||||
"<br/><br/>" +
|
||||
"You may use constant, message field, metadata field, attribute, and latest time-series as an arguments values. " +
|
||||
"The result of the function may be also stored to message field, metadata field, attribute or time-series value." +
|
||||
"<br/><br/>" +
|
||||
"Primary use case for this rule node is to take one or more values from the database and modify them based on data from the message. " +
|
||||
"For example, you may increase `totalWaterConsumption` based on the `deltaWaterConsumption` reported by device." +
|
||||
"<br/><br/>" +
|
||||
"Alternative use case is the replacement of simple JS `script` nodes with more light-weight and performant implementation. " +
|
||||
"For example, you may transform Fahrenheit to Celsius (C = (F - 32) / 1.8) using combination of two math node functions: SUB 32 and DIV 1.8." +
|
||||
"<br/><br/>" +
|
||||
"The execution is synchronized in scope of message originator (e.g. device) and server node. " +
|
||||
"If you have rule nodes in different rule chains, they will process messages from the same originator synchronously in the scope of the server node.",
|
||||
icon = "functions"
|
||||
)
|
||||
public class TbMathNode implements TbNode {
|
||||
|
||||
private static ConcurrentMap<EntityId, Semaphore> semaphores = new ConcurrentReferenceHashMap<>();
|
||||
private static final ConcurrentMap<EntityId, Semaphore> semaphores = new ConcurrentReferenceHashMap<>();
|
||||
|
||||
private TbMathFormulaConfiguration config;
|
||||
private TbMathNodeConfiguration config;
|
||||
private boolean msgBodyToJsonConversionRequired;
|
||||
|
||||
@Override
|
||||
public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
|
||||
this.config = TbNodeUtils.convert(configuration, TbMathFormulaConfiguration.class);
|
||||
this.config = TbNodeUtils.convert(configuration, TbMathNodeConfiguration.class);
|
||||
var operation = config.getOperation();
|
||||
var argsCount = config.getArguments().size();
|
||||
if (argsCount < operation.getMinArgs() || argsCount > operation.getMaxArgs()) {
|
||||
@ -84,14 +94,49 @@ public class TbMathNode implements TbNode {
|
||||
public void onMsg(TbContext ctx, TbMsg msg) {
|
||||
var originator = msg.getOriginator();
|
||||
var originatorSemaphore = semaphores.computeIfAbsent(originator, tmp -> new Semaphore(1, true));
|
||||
boolean acquired = tryAcquire(originator, originatorSemaphore);
|
||||
|
||||
if (!acquired) {
|
||||
ctx.tellFailure(msg, new RuntimeException("Failed to process message for originator synchronously"));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
var arguments = config.getArguments();
|
||||
Optional<ObjectNode> msgBodyOpt = convertMsgBodyIfRequired(msg);
|
||||
var argumentValues = Futures.allAsList(arguments.stream()
|
||||
.map(arg -> resolveArguments(ctx, msg, msgBodyOpt, arg)).collect(Collectors.toList()));
|
||||
ListenableFuture<TbMsg> resultMsgFuture = Futures.transformAsync(argumentValues, args ->
|
||||
updateMsgAndDb(ctx, msg, msgBodyOpt, calculateResult(ctx, msg, args)), ctx.getDbCallbackExecutor());
|
||||
DonAsynchron.withCallback(resultMsgFuture, ctx::tellSuccess, t -> ctx.tellFailure(msg, t), ctx.getDbCallbackExecutor());
|
||||
DonAsynchron.withCallback(resultMsgFuture, resultMsg -> {
|
||||
try {
|
||||
ctx.tellSuccess(resultMsg);
|
||||
} finally {
|
||||
originatorSemaphore.release();
|
||||
}
|
||||
}, t -> {
|
||||
try {
|
||||
ctx.tellFailure(msg, t);
|
||||
} finally {
|
||||
originatorSemaphore.release();
|
||||
}
|
||||
}, ctx.getDbCallbackExecutor());
|
||||
} catch (Throwable e) {
|
||||
originatorSemaphore.release();
|
||||
log.warn("[{}] Failed to process message: {}", originator, msg, e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean tryAcquire(EntityId originator, Semaphore originatorSemaphore) {
|
||||
boolean acquired;
|
||||
try {
|
||||
acquired = originatorSemaphore.tryAcquire(20, TimeUnit.SECONDS);
|
||||
} catch (InterruptedException e) {
|
||||
acquired = false;
|
||||
log.debug("[{}] Failed to acquire semaphore", originator, e);
|
||||
}
|
||||
return acquired;
|
||||
}
|
||||
|
||||
private ListenableFuture<TbMsg> updateMsgAndDb(TbContext ctx, TbMsg msg, Optional<ObjectNode> msgBodyOpt, double result) {
|
||||
@ -102,18 +147,47 @@ public class TbMathNode implements TbNode {
|
||||
case MESSAGE_METADATA:
|
||||
return Futures.immediateFuture(addToMeta(msg, mathResultDef, result));
|
||||
case ATTRIBUTE:
|
||||
ListenableFuture<Void> attrSave = ctx.getTelemetryService().saveAttrAndNotify(
|
||||
ctx.getTenantId(), msg.getOriginator(), getAttributeScope(mathResultDef.getAttributeScope()), mathResultDef.getValue(), result);
|
||||
ListenableFuture<Void> attrSave = saveAttribute(ctx, msg, result, mathResultDef);
|
||||
return Futures.transform(attrSave, attr -> addToBodyAndMeta(msg, msgBodyOpt, result, mathResultDef), ctx.getDbCallbackExecutor());
|
||||
case TIME_SERIES:
|
||||
ListenableFuture<Void> tsSave = ctx.getTelemetryService().saveAndNotify(ctx.getTenantId(), msg.getOriginator(),
|
||||
new BasicTsKvEntry(System.currentTimeMillis(), new DoubleDataEntry(mathResultDef.getValue(), result)));
|
||||
ListenableFuture<Void> tsSave = saveTimeSeries(ctx, msg, result, mathResultDef);
|
||||
return Futures.transform(tsSave, ts -> addToBodyAndMeta(msg, msgBodyOpt, result, mathResultDef), ctx.getDbCallbackExecutor());
|
||||
default:
|
||||
throw new RuntimeException("Result type is not supported: " + mathResultDef.getType() + "!");
|
||||
}
|
||||
}
|
||||
|
||||
private ListenableFuture<Void> saveTimeSeries(TbContext ctx, TbMsg msg, double result, TbMathResult mathResultDef) {
|
||||
|
||||
return ctx.getTelemetryService().saveAndNotify(ctx.getTenantId(), msg.getOriginator(),
|
||||
new BasicTsKvEntry(System.currentTimeMillis(), new DoubleDataEntry(mathResultDef.getKey(), result)));
|
||||
}
|
||||
|
||||
private ListenableFuture<Void> saveAttribute(TbContext ctx, TbMsg msg, double result, TbMathResult mathResultDef) {
|
||||
String attributeScope = getAttributeScope(mathResultDef.getAttributeScope());
|
||||
if (isIntegerResult(mathResultDef, config.getOperation())) {
|
||||
var value = toIntValue(mathResultDef, result);
|
||||
return ctx.getTelemetryService().saveAttrAndNotify(
|
||||
ctx.getTenantId(), msg.getOriginator(), attributeScope, mathResultDef.getKey(), value);
|
||||
} else {
|
||||
var value = toDoubleValue(mathResultDef, result);
|
||||
return ctx.getTelemetryService().saveAttrAndNotify(
|
||||
ctx.getTenantId(), msg.getOriginator(), attributeScope, mathResultDef.getKey(), value);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isIntegerResult(TbMathResult mathResultDef, TbRuleNodeMathFunctionType function) {
|
||||
return function.isIntegerResult() || mathResultDef.getResultValuePrecision() == 0;
|
||||
}
|
||||
|
||||
private long toIntValue(TbMathResult mathResultDef, double value) {
|
||||
return (long) value;
|
||||
}
|
||||
|
||||
private double toDoubleValue(TbMathResult mathResultDef, double value) {
|
||||
return BigDecimal.valueOf(value).setScale(mathResultDef.getResultValuePrecision(), RoundingMode.HALF_UP).doubleValue();
|
||||
}
|
||||
|
||||
private Optional<ObjectNode> convertMsgBodyIfRequired(TbMsg msg) {
|
||||
Optional<ObjectNode> msgBodyOpt;
|
||||
if (msgBodyToJsonConversionRequired) {
|
||||
@ -142,13 +216,21 @@ public class TbMathNode implements TbNode {
|
||||
|
||||
private TbMsg addToBody(TbMsg msg, TbMathResult mathResultDef, Optional<ObjectNode> msgBodyOpt, double result) {
|
||||
ObjectNode body = msgBodyOpt.get();
|
||||
body.put(mathResultDef.getValue(), result);
|
||||
if (isIntegerResult(mathResultDef, config.getOperation())) {
|
||||
body.put(mathResultDef.getKey(), toIntValue(mathResultDef, result));
|
||||
} else {
|
||||
body.put(mathResultDef.getKey(), toDoubleValue(mathResultDef, result));
|
||||
}
|
||||
return TbMsg.transformMsgData(msg, JacksonUtil.toString(body));
|
||||
}
|
||||
|
||||
private TbMsg addToMeta(TbMsg msg, TbMathResult mathResultDef, double result) {
|
||||
var md = msg.getMetaData();
|
||||
md.putValue(mathResultDef.getValue(), Double.toString(result));
|
||||
if (isIntegerResult(mathResultDef, config.getOperation())) {
|
||||
md.putValue(mathResultDef.getKey(), Long.toString(toIntValue(mathResultDef, result)));
|
||||
} else {
|
||||
md.putValue(mathResultDef.getKey(), Double.toString(toDoubleValue(mathResultDef, result)));
|
||||
}
|
||||
return TbMsg.transformMsg(msg, md);
|
||||
}
|
||||
|
||||
@ -164,12 +246,64 @@ public class TbMathNode implements TbNode {
|
||||
return apply(args.get(0), args.get(1), (a, b) -> a / b);
|
||||
case SIN:
|
||||
return apply(args.get(0), Math::sin);
|
||||
case SINH:
|
||||
return apply(args.get(0), Math::sinh);
|
||||
case COS:
|
||||
return apply(args.get(0), Math::cos);
|
||||
case COSH:
|
||||
return apply(args.get(0), Math::cosh);
|
||||
case TAN:
|
||||
return apply(args.get(0), Math::tan);
|
||||
case TANH:
|
||||
return apply(args.get(0), Math::tanh);
|
||||
case ACOS:
|
||||
return apply(args.get(0), Math::acos);
|
||||
case ASIN:
|
||||
return apply(args.get(0), Math::asin);
|
||||
case ATAN:
|
||||
return apply(args.get(0), Math::atan);
|
||||
case ATAN2:
|
||||
return apply(args.get(0), args.get(1), Math::atan2);
|
||||
case EXP:
|
||||
return apply(args.get(0), Math::exp);
|
||||
case EXPM1:
|
||||
return apply(args.get(0), Math::expm1);
|
||||
case SQRT:
|
||||
return apply(args.get(0), Math::sqrt);
|
||||
case CBRT:
|
||||
return apply(args.get(0), Math::cbrt);
|
||||
case GET_EXP:
|
||||
return apply(args.get(0), (x) -> (double) Math.getExponent(x));
|
||||
case HYPOT:
|
||||
return apply(args.get(0), args.get(1), Math::hypot);
|
||||
case LOG:
|
||||
return apply(args.get(0), Math::log);
|
||||
case LOG10:
|
||||
return apply(args.get(0), Math::log10);
|
||||
case LOG1P:
|
||||
return apply(args.get(0), Math::log1p);
|
||||
case CEIL:
|
||||
return apply(args.get(0), Math::ceil);
|
||||
case FLOOR:
|
||||
return apply(args.get(0), Math::floor);
|
||||
case FLOOR_DIV:
|
||||
return apply(args.get(0), args.get(1), (a, b) -> (double) Math.floorDiv(a.longValue(), b.longValue()));
|
||||
case FLOOR_MOD:
|
||||
return apply(args.get(0), args.get(1), (a, b) -> (double) Math.floorMod(a.longValue(), b.longValue()));
|
||||
case ABS:
|
||||
return apply(args.get(0), Math::abs);
|
||||
case MIN:
|
||||
return apply(args.get(0), args.get(1), Math::min);
|
||||
case MAX:
|
||||
return apply(args.get(0), args.get(1), Math::max);
|
||||
case POW:
|
||||
return apply(args.get(0), args.get(1), Math::pow);
|
||||
case SIGNUM:
|
||||
return apply(args.get(0), Math::signum);
|
||||
case RAD:
|
||||
return apply(args.get(0), Math::toRadians);
|
||||
case DEG:
|
||||
return apply(args.get(0), Math::toDegrees);
|
||||
default:
|
||||
throw new RuntimeException("Not supported operation: " + config.getOperation());
|
||||
}
|
||||
@ -188,19 +322,17 @@ public class TbMathNode implements TbNode {
|
||||
case CONSTANT:
|
||||
return Futures.immediateFuture(TbMathArgumentValue.constant(arg));
|
||||
case MESSAGE_BODY:
|
||||
return Futures.immediateFuture(TbMathArgumentValue.fromMessageBody(arg.getValue(), msgBodyOpt));
|
||||
return Futures.immediateFuture(TbMathArgumentValue.fromMessageBody(arg, msgBodyOpt));
|
||||
case MESSAGE_METADATA:
|
||||
return Futures.immediateFuture(TbMathArgumentValue.fromMessageMetadata(arg.getValue(), msg.getMetaData()));
|
||||
return Futures.immediateFuture(TbMathArgumentValue.fromMessageMetadata(arg, msg.getMetaData()));
|
||||
case ATTRIBUTE:
|
||||
String scope = getAttributeScope(arg.getAttributeScope());
|
||||
return Futures.transform(ctx.getAttributesService().find(ctx.getTenantId(), msg.getOriginator(), scope, arg.getValue()),
|
||||
opt -> getTbMathArgumentValue(opt.orElseThrow(() ->
|
||||
new RuntimeException("Attribute: " + arg.getValue() + " with scope: " + scope + " not found for entity: " + msg.getOriginator())))
|
||||
return Futures.transform(ctx.getAttributesService().find(ctx.getTenantId(), msg.getOriginator(), scope, arg.getKey()),
|
||||
opt -> getTbMathArgumentValue(arg, opt, "Attribute: " + arg.getKey() + " with scope: " + scope + " not found for entity: " + msg.getOriginator())
|
||||
,MoreExecutors.directExecutor());
|
||||
case TIME_SERIES:
|
||||
return Futures.transform(ctx.getTimeseriesService().findLatest(ctx.getTenantId(), msg.getOriginator(), arg.getValue()),
|
||||
opt -> getTbMathArgumentValue(opt.orElseThrow(() ->
|
||||
new RuntimeException("Time-series: " + arg.getValue() + " not found for entity: " + msg.getOriginator())))
|
||||
return Futures.transform(ctx.getTimeseriesService().findLatest(ctx.getTenantId(), msg.getOriginator(), arg.getKey()),
|
||||
opt -> getTbMathArgumentValue(arg, opt, "Time-series: " + arg.getKey() + " not found for entity: " + msg.getOriginator())
|
||||
, MoreExecutors.directExecutor());
|
||||
default:
|
||||
throw new RuntimeException("Unsupported argument type: " + arg.getType() + "!");
|
||||
@ -212,7 +344,9 @@ public class TbMathNode implements TbNode {
|
||||
return StringUtils.isEmpty(attrScope) ? DataConstants.SERVER_SCOPE : attrScope;
|
||||
}
|
||||
|
||||
private TbMathArgumentValue getTbMathArgumentValue(KvEntry kv) {
|
||||
private TbMathArgumentValue getTbMathArgumentValue(TbMathArgument arg, Optional<? extends KvEntry> kvOpt, String error) {
|
||||
if (kvOpt != null && kvOpt.isPresent()) {
|
||||
var kv = kvOpt.get();
|
||||
switch (kv.getDataType()) {
|
||||
case LONG:
|
||||
return TbMathArgumentValue.fromLong(kv.getLongValue().get());
|
||||
@ -221,6 +355,13 @@ public class TbMathNode implements TbNode {
|
||||
default:
|
||||
return TbMathArgumentValue.fromString(kv.getValueAsString());
|
||||
}
|
||||
} else {
|
||||
if (arg.getDefaultValue() != null) {
|
||||
return TbMathArgumentValue.fromDouble(arg.getDefaultValue());
|
||||
} else {
|
||||
throw new RuntimeException(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@ -22,18 +22,18 @@ import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
public class TbMathFormulaConfiguration implements NodeConfiguration<TbMathFormulaConfiguration> {
|
||||
public class TbMathNodeConfiguration implements NodeConfiguration<TbMathNodeConfiguration> {
|
||||
|
||||
private TbRuleNodeMathFunctionType operation;
|
||||
private List<TbMathArgument> arguments;
|
||||
private TbMathResult result;
|
||||
|
||||
@Override
|
||||
public TbMathFormulaConfiguration defaultConfiguration() {
|
||||
TbMathFormulaConfiguration configuration = new TbMathFormulaConfiguration();
|
||||
public TbMathNodeConfiguration defaultConfiguration() {
|
||||
TbMathNodeConfiguration configuration = new TbMathNodeConfiguration();
|
||||
configuration.setOperation(TbRuleNodeMathFunctionType.ADD);
|
||||
configuration.setArguments(Arrays.asList(new TbMathArgument(TbMathArgumentType.CONSTANT, "2"), new TbMathArgument(TbMathArgumentType.CONSTANT, "2")));
|
||||
configuration.setResult(new TbMathResult(TbMathArgumentType.MESSAGE_BODY, "result", false, false, null));
|
||||
configuration.setResult(new TbMathResult(TbMathArgumentType.MESSAGE_BODY, "result", 2, false, false, null));
|
||||
return configuration;
|
||||
}
|
||||
}
|
||||
@ -25,7 +25,9 @@ import lombok.NoArgsConstructor;
|
||||
public class TbMathResult {
|
||||
|
||||
private TbMathArgumentType type;
|
||||
private String value;
|
||||
private String key;
|
||||
// 0 means integer, x > 0 means x decimal points after ".";
|
||||
private int resultValuePrecision;
|
||||
private boolean addToBody;
|
||||
private boolean addToMetadata;
|
||||
private String attributeScope;
|
||||
|
||||
@ -19,19 +19,31 @@ import lombok.Getter;
|
||||
|
||||
public enum TbRuleNodeMathFunctionType {
|
||||
|
||||
ADD(2), SUB(2), MULT(2), DIV(2), SIN(1), COS(1), SQRT(1), ABS(1);
|
||||
ADD(2), SUB(2), MULT(2), DIV(2),
|
||||
SIN, SINH, COS, COSH, TAN, TANH, ACOS, ASIN, ATAN, ATAN2(2),
|
||||
EXP, EXPM1, SQRT, CBRT, GET_EXP(1, 1, true), HYPOT(2), LOG, LOG10, LOG1P,
|
||||
CEIL(1, 1, true), FLOOR(1, 1, true), FLOOR_DIV(2), FLOOR_MOD(2),
|
||||
ABS, MIN(2), MAX(2), POW, SIGNUM, RAD, DEG;
|
||||
|
||||
@Getter
|
||||
private final int minArgs;
|
||||
@Getter
|
||||
private final int maxArgs;
|
||||
@Getter
|
||||
private final boolean integerResult;
|
||||
|
||||
TbRuleNodeMathFunctionType() {
|
||||
this(1, 1, false);
|
||||
}
|
||||
|
||||
TbRuleNodeMathFunctionType(int args) {
|
||||
this(args, args);
|
||||
this(args, args, false);
|
||||
}
|
||||
|
||||
TbRuleNodeMathFunctionType(int minArgs, int maxArgs) {
|
||||
TbRuleNodeMathFunctionType(int minArgs, int maxArgs, boolean integerResult) {
|
||||
this.minArgs = minArgs;
|
||||
this.maxArgs = maxArgs;
|
||||
this.integerResult = integerResult;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -0,0 +1,231 @@
|
||||
/**
|
||||
* Copyright © 2016-2022 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 math;
|
||||
|
||||
import com.datastax.oss.driver.api.core.uuid.Uuids;
|
||||
import com.google.common.util.concurrent.Futures;
|
||||
import org.junit.After;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.Mockito;
|
||||
import org.mockito.junit.MockitoJUnitRunner;
|
||||
import org.thingsboard.common.util.AbstractListeningExecutor;
|
||||
import org.thingsboard.common.util.JacksonUtil;
|
||||
import org.thingsboard.rule.engine.api.TbContext;
|
||||
import org.thingsboard.rule.engine.api.TbNodeConfiguration;
|
||||
import org.thingsboard.rule.engine.api.TbNodeException;
|
||||
import org.thingsboard.rule.engine.math.TbMathArgument;
|
||||
import org.thingsboard.rule.engine.math.TbMathArgumentType;
|
||||
import org.thingsboard.rule.engine.math.TbMathNodeConfiguration;
|
||||
import org.thingsboard.rule.engine.math.TbMathNode;
|
||||
import org.thingsboard.rule.engine.math.TbMathResult;
|
||||
import org.thingsboard.rule.engine.math.TbRuleNodeMathFunctionType;
|
||||
import org.thingsboard.server.common.data.DataConstants;
|
||||
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.BaseAttributeKvEntry;
|
||||
import org.thingsboard.server.common.data.kv.BasicTsKvEntry;
|
||||
import org.thingsboard.server.common.data.kv.DoubleDataEntry;
|
||||
import org.thingsboard.server.common.data.kv.LongDataEntry;
|
||||
import org.thingsboard.server.common.msg.TbMsg;
|
||||
import org.thingsboard.server.common.msg.TbMsgMetaData;
|
||||
import org.thingsboard.server.dao.attributes.AttributesService;
|
||||
import org.thingsboard.server.dao.timeseries.TimeseriesService;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Optional;
|
||||
|
||||
import static org.mockito.Mockito.lenient;
|
||||
|
||||
@RunWith(MockitoJUnitRunner.class)
|
||||
public class TbMathNodeTest {
|
||||
|
||||
private EntityId originator = new DeviceId(Uuids.timeBased());
|
||||
private TenantId tenantId = TenantId.fromUUID(Uuids.timeBased());
|
||||
|
||||
@Mock
|
||||
private TbContext ctx;
|
||||
@Mock
|
||||
private AttributesService attributesService;
|
||||
@Mock
|
||||
private TimeseriesService tsService;
|
||||
|
||||
private AbstractListeningExecutor dbExecutor;
|
||||
|
||||
@Before
|
||||
public void before() {
|
||||
dbExecutor = new AbstractListeningExecutor() {
|
||||
@Override
|
||||
protected int getThreadPollSize() {
|
||||
return 3;
|
||||
}
|
||||
};
|
||||
dbExecutor.init();
|
||||
Mockito.reset(ctx);
|
||||
Mockito.reset(attributesService);
|
||||
Mockito.reset(tsService);
|
||||
lenient().when(ctx.getAttributesService()).thenReturn(attributesService);
|
||||
lenient().when(ctx.getTimeseriesService()).thenReturn(tsService);
|
||||
lenient().when(ctx.getTenantId()).thenReturn(tenantId);
|
||||
lenient().when(ctx.getDbCallbackExecutor()).thenReturn(dbExecutor);
|
||||
}
|
||||
|
||||
@After
|
||||
public void after() {
|
||||
dbExecutor.destroy();
|
||||
}
|
||||
|
||||
|
||||
private TbMathNode initNode(TbRuleNodeMathFunctionType operation, TbMathResult result, TbMathArgument... arguments) {
|
||||
try {
|
||||
TbMathNodeConfiguration configuration = new TbMathNodeConfiguration();
|
||||
configuration.setOperation(operation);
|
||||
configuration.setResult(result);
|
||||
configuration.setArguments(Arrays.asList(arguments));
|
||||
TbMathNode node = new TbMathNode();
|
||||
node.init(ctx, new TbNodeConfiguration(JacksonUtil.valueToTree(configuration)));
|
||||
return node;
|
||||
} catch (TbNodeException ex) {
|
||||
throw new IllegalStateException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_2_plus_2_body() {
|
||||
var node = initNode(TbRuleNodeMathFunctionType.ADD,
|
||||
new TbMathResult(TbMathArgumentType.MESSAGE_BODY, "result", 2, false, false, null),
|
||||
new TbMathArgument(TbMathArgumentType.MESSAGE_BODY, "a"),
|
||||
new TbMathArgument(TbMathArgumentType.MESSAGE_BODY, "b")
|
||||
);
|
||||
|
||||
TbMsg msg = TbMsg.newMsg("TEST", originator, new TbMsgMetaData(), JacksonUtil.newObjectNode().put("a", 2).put("b", 2).toString());
|
||||
|
||||
node.onMsg(ctx, msg);
|
||||
|
||||
ArgumentCaptor<TbMsg> msgCaptor = ArgumentCaptor.forClass(TbMsg.class);
|
||||
Mockito.verify(ctx, Mockito.timeout(5000)).tellSuccess(msgCaptor.capture());
|
||||
|
||||
TbMsg resultMsg = msgCaptor.getValue();
|
||||
Assert.assertNotNull(resultMsg);
|
||||
Assert.assertNotNull(resultMsg.getData());
|
||||
var resultJson = JacksonUtil.toJsonNode(resultMsg.getData());
|
||||
Assert.assertTrue(resultJson.has("result"));
|
||||
Assert.assertEquals(4, resultJson.get("result").asInt());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_2_plus_2_meta() {
|
||||
var node = initNode(TbRuleNodeMathFunctionType.ADD,
|
||||
new TbMathResult(TbMathArgumentType.MESSAGE_METADATA, "result", 0, false, false, null),
|
||||
new TbMathArgument(TbMathArgumentType.MESSAGE_BODY, "a"),
|
||||
new TbMathArgument(TbMathArgumentType.MESSAGE_BODY, "b")
|
||||
);
|
||||
|
||||
TbMsg msg = TbMsg.newMsg("TEST", originator, new TbMsgMetaData(), JacksonUtil.newObjectNode().put("a", 2).put("b", 2).toString());
|
||||
|
||||
node.onMsg(ctx, msg);
|
||||
|
||||
ArgumentCaptor<TbMsg> msgCaptor = ArgumentCaptor.forClass(TbMsg.class);
|
||||
Mockito.verify(ctx, Mockito.timeout(5000)).tellSuccess(msgCaptor.capture());
|
||||
|
||||
TbMsg resultMsg = msgCaptor.getValue();
|
||||
Assert.assertNotNull(resultMsg);
|
||||
Assert.assertNotNull(resultMsg.getData());
|
||||
Assert.assertNotNull(resultMsg.getMetaData());
|
||||
var result = resultMsg.getMetaData().getValue("result");
|
||||
Assert.assertNotNull(result);
|
||||
Assert.assertEquals("4", result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_2_plus_2_attr_and_ts() {
|
||||
var node = initNode(TbRuleNodeMathFunctionType.ADD,
|
||||
new TbMathResult(TbMathArgumentType.MESSAGE_BODY, "result", 2, false, false, null),
|
||||
new TbMathArgument(TbMathArgumentType.ATTRIBUTE, "a"),
|
||||
new TbMathArgument(TbMathArgumentType.TIME_SERIES, "b")
|
||||
);
|
||||
|
||||
TbMsg msg = TbMsg.newMsg("TEST", originator, new TbMsgMetaData(), JacksonUtil.newObjectNode().toString());
|
||||
|
||||
Mockito.when(attributesService.find(tenantId, originator, DataConstants.SERVER_SCOPE, "a"))
|
||||
.thenReturn(Futures.immediateFuture(Optional.of(new BaseAttributeKvEntry(System.currentTimeMillis(), new DoubleDataEntry("a", 2.0)))));
|
||||
|
||||
Mockito.when(tsService.findLatest(tenantId, originator, "b"))
|
||||
.thenReturn(Futures.immediateFuture(Optional.of(new BasicTsKvEntry(System.currentTimeMillis(), new LongDataEntry("b", 2L)))));
|
||||
|
||||
node.onMsg(ctx, msg);
|
||||
|
||||
ArgumentCaptor<TbMsg> msgCaptor = ArgumentCaptor.forClass(TbMsg.class);
|
||||
Mockito.verify(ctx, Mockito.timeout(5000)).tellSuccess(msgCaptor.capture());
|
||||
|
||||
TbMsg resultMsg = msgCaptor.getValue();
|
||||
Assert.assertNotNull(resultMsg);
|
||||
Assert.assertNotNull(resultMsg.getData());
|
||||
var resultJson = JacksonUtil.toJsonNode(resultMsg.getData());
|
||||
Assert.assertTrue(resultJson.has("result"));
|
||||
Assert.assertEquals(4, resultJson.get("result").asInt());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_sqrt_5_body() {
|
||||
var node = initNode(TbRuleNodeMathFunctionType.SQRT,
|
||||
new TbMathResult(TbMathArgumentType.MESSAGE_BODY, "result", 3, false, false, null),
|
||||
new TbMathArgument(TbMathArgumentType.MESSAGE_BODY, "a")
|
||||
);
|
||||
|
||||
TbMsg msg = TbMsg.newMsg("TEST", originator, new TbMsgMetaData(), JacksonUtil.newObjectNode().put("a", 5).toString());
|
||||
|
||||
node.onMsg(ctx, msg);
|
||||
|
||||
ArgumentCaptor<TbMsg> msgCaptor = ArgumentCaptor.forClass(TbMsg.class);
|
||||
Mockito.verify(ctx, Mockito.timeout(5000)).tellSuccess(msgCaptor.capture());
|
||||
|
||||
TbMsg resultMsg = msgCaptor.getValue();
|
||||
Assert.assertNotNull(resultMsg);
|
||||
Assert.assertNotNull(resultMsg.getData());
|
||||
var resultJson = JacksonUtil.toJsonNode(resultMsg.getData());
|
||||
Assert.assertTrue(resultJson.has("result"));
|
||||
Assert.assertEquals(2.236, resultJson.get("result").asDouble(), 0.0);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_sqrt_5_meta() {
|
||||
var node = initNode(TbRuleNodeMathFunctionType.SQRT,
|
||||
new TbMathResult(TbMathArgumentType.MESSAGE_METADATA, "result", 3, false, false, null),
|
||||
new TbMathArgument(TbMathArgumentType.MESSAGE_BODY, "a")
|
||||
);
|
||||
|
||||
TbMsg msg = TbMsg.newMsg("TEST", originator, new TbMsgMetaData(), JacksonUtil.newObjectNode().put("a", 5).toString());
|
||||
|
||||
node.onMsg(ctx, msg);
|
||||
|
||||
ArgumentCaptor<TbMsg> msgCaptor = ArgumentCaptor.forClass(TbMsg.class);
|
||||
Mockito.verify(ctx, Mockito.timeout(5000)).tellSuccess(msgCaptor.capture());
|
||||
|
||||
TbMsg resultMsg = msgCaptor.getValue();
|
||||
Assert.assertNotNull(resultMsg);
|
||||
Assert.assertNotNull(resultMsg.getData());
|
||||
var result = resultMsg.getMetaData().getValue("result");
|
||||
Assert.assertNotNull(result);
|
||||
Assert.assertEquals("2.236", result);
|
||||
}
|
||||
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user