Math node implementation
This commit is contained in:
parent
fb49273bd6
commit
15ee23c5d5
@ -25,11 +25,12 @@ import lombok.NoArgsConstructor;
|
|||||||
public class TbMathArgument {
|
public class TbMathArgument {
|
||||||
|
|
||||||
private TbMathArgumentType type;
|
private TbMathArgumentType type;
|
||||||
private String value;
|
private String key;
|
||||||
private String attributeScope;
|
private String attributeScope;
|
||||||
|
private Double defaultValue;
|
||||||
|
|
||||||
public TbMathArgument(TbMathArgumentType type, String value) {
|
public TbMathArgument(TbMathArgumentType type, String key) {
|
||||||
this.type = type;
|
this.type = type;
|
||||||
this.value = value;
|
this.key = key;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -33,29 +33,43 @@ public class TbMathArgumentValue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static TbMathArgumentValue constant(TbMathArgument arg) {
|
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()) {
|
if (jsonNodeOpt.isEmpty()) {
|
||||||
throw new RuntimeException("Message body is empty!");
|
return defaultOrThrow(defaultValue, "Message body is empty!");
|
||||||
}
|
}
|
||||||
var json = jsonNodeOpt.get();
|
var json = jsonNodeOpt.get();
|
||||||
if (!json.has(key)) {
|
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);
|
JsonNode valueNode = json.get(key);
|
||||||
if (valueNode.isEmpty() || valueNode.isNull()) {
|
if (valueNode.isNull()) {
|
||||||
throw new RuntimeException("Message body has empty or null '" + key + "'!");
|
return defaultOrThrow(defaultValue, "Message body has null '" + key + "'!");
|
||||||
}
|
}
|
||||||
double value;
|
double value;
|
||||||
if (valueNode.isNumber()) {
|
if (valueNode.isNumber()) {
|
||||||
value = valueNode.doubleValue();
|
value = valueNode.doubleValue();
|
||||||
} else if (valueNode.isTextual()) {
|
} else if (valueNode.isTextual()) {
|
||||||
try {
|
var valueNodeText = valueNode.asText();
|
||||||
value = Double.parseDouble(valueNode.asText());
|
if (StringUtils.isNotBlank(valueNodeText)) {
|
||||||
} catch (NumberFormatException ne) {
|
try {
|
||||||
throw new RuntimeException("Can't convert value '" + valueNode.asText() + "' to double!");
|
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 {
|
} else {
|
||||||
throw new RuntimeException("Can't convert value '" + valueNode.toString() + "' to double!");
|
throw new RuntimeException("Can't convert value '" + valueNode.toString() + "' to double!");
|
||||||
@ -63,13 +77,15 @@ public class TbMathArgumentValue {
|
|||||||
return new TbMathArgumentValue(value);
|
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) {
|
if (metaData == null) {
|
||||||
throw new RuntimeException("Message metadata is empty!");
|
return defaultOrThrow(defaultValue, "Message metadata is empty!");
|
||||||
}
|
}
|
||||||
var value = metaData.getValue(key);
|
var value = metaData.getValue(key);
|
||||||
if (StringUtils.isEmpty(value)) {
|
if (StringUtils.isEmpty(value)) {
|
||||||
throw new RuntimeException("Message metadata has no '" + key + "'!");
|
return defaultOrThrow(defaultValue, "Message metadata has no '" + key + "'!");
|
||||||
}
|
}
|
||||||
return fromString(value);
|
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.Futures;
|
||||||
import com.google.common.util.concurrent.ListenableFuture;
|
import com.google.common.util.concurrent.ListenableFuture;
|
||||||
import com.google.common.util.concurrent.MoreExecutors;
|
import com.google.common.util.concurrent.MoreExecutors;
|
||||||
import com.google.common.util.concurrent.SettableFuture;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.util.ConcurrentReferenceHashMap;
|
import org.springframework.util.ConcurrentReferenceHashMap;
|
||||||
import org.thingsboard.common.util.DonAsynchron;
|
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.data.plugin.ComponentType;
|
||||||
import org.thingsboard.server.common.msg.TbMsg;
|
import org.thingsboard.server.common.msg.TbMsg;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.math.RoundingMode;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
|
||||||
import java.util.concurrent.ConcurrentMap;
|
import java.util.concurrent.ConcurrentMap;
|
||||||
import java.util.concurrent.Semaphore;
|
import java.util.concurrent.Semaphore;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
@ -53,24 +53,34 @@ import java.util.stream.Collectors;
|
|||||||
@Slf4j
|
@Slf4j
|
||||||
@RuleNode(
|
@RuleNode(
|
||||||
type = ComponentType.ACTION,
|
type = ComponentType.ACTION,
|
||||||
name = "math formula",
|
name = "math function",
|
||||||
configClazz = TbMathFormulaConfiguration.class,
|
configClazz = TbMathNodeConfiguration.class,
|
||||||
nodeDescription = "Calculates the mathematics formula based on message and/or database values",
|
nodeDescription = "Apply math function and save the result into the message and/or database",
|
||||||
nodeDetails = "Transform incoming Message with configured JS function to String and log final value into Thingsboard log file. " +
|
nodeDetails = "Supports math operations like: ADD, SUB, MULT, DIV, etc and functions: SIN, COS, TAN, SEC, etc. " +
|
||||||
"Message payload can be accessed via <code>msg</code> property. For example <code>'temperature = ' + msg.temperature ;</code>. " +
|
"<br/><br/>" +
|
||||||
"Message metadata can be accessed via <code>metadata</code> property. For example <code>'name = ' + metadata.customerName;</code>.",
|
"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"
|
icon = "functions"
|
||||||
)
|
)
|
||||||
public class TbMathNode implements TbNode {
|
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;
|
private boolean msgBodyToJsonConversionRequired;
|
||||||
|
|
||||||
@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, TbMathFormulaConfiguration.class);
|
this.config = TbNodeUtils.convert(configuration, TbMathNodeConfiguration.class);
|
||||||
var operation = config.getOperation();
|
var operation = config.getOperation();
|
||||||
var argsCount = config.getArguments().size();
|
var argsCount = config.getArguments().size();
|
||||||
if (argsCount < operation.getMinArgs() || argsCount > operation.getMaxArgs()) {
|
if (argsCount < operation.getMinArgs() || argsCount > operation.getMaxArgs()) {
|
||||||
@ -84,14 +94,49 @@ public class TbMathNode implements TbNode {
|
|||||||
public void onMsg(TbContext ctx, TbMsg msg) {
|
public void onMsg(TbContext ctx, TbMsg msg) {
|
||||||
var originator = msg.getOriginator();
|
var originator = msg.getOriginator();
|
||||||
var originatorSemaphore = semaphores.computeIfAbsent(originator, tmp -> new Semaphore(1, true));
|
var originatorSemaphore = semaphores.computeIfAbsent(originator, tmp -> new Semaphore(1, true));
|
||||||
|
boolean acquired = tryAcquire(originator, originatorSemaphore);
|
||||||
|
|
||||||
var arguments = config.getArguments();
|
if (!acquired) {
|
||||||
Optional<ObjectNode> msgBodyOpt = convertMsgBodyIfRequired(msg);
|
ctx.tellFailure(msg, new RuntimeException("Failed to process message for originator synchronously"));
|
||||||
var argumentValues = Futures.allAsList(arguments.stream()
|
return;
|
||||||
.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());
|
try {
|
||||||
DonAsynchron.withCallback(resultMsgFuture, ctx::tellSuccess, t -> ctx.tellFailure(msg, t), ctx.getDbCallbackExecutor());
|
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, 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) {
|
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:
|
case MESSAGE_METADATA:
|
||||||
return Futures.immediateFuture(addToMeta(msg, mathResultDef, result));
|
return Futures.immediateFuture(addToMeta(msg, mathResultDef, result));
|
||||||
case ATTRIBUTE:
|
case ATTRIBUTE:
|
||||||
ListenableFuture<Void> attrSave = ctx.getTelemetryService().saveAttrAndNotify(
|
ListenableFuture<Void> attrSave = saveAttribute(ctx, msg, result, mathResultDef);
|
||||||
ctx.getTenantId(), msg.getOriginator(), getAttributeScope(mathResultDef.getAttributeScope()), mathResultDef.getValue(), result);
|
|
||||||
return Futures.transform(attrSave, attr -> addToBodyAndMeta(msg, msgBodyOpt, result, mathResultDef), ctx.getDbCallbackExecutor());
|
return Futures.transform(attrSave, attr -> addToBodyAndMeta(msg, msgBodyOpt, result, mathResultDef), ctx.getDbCallbackExecutor());
|
||||||
case TIME_SERIES:
|
case TIME_SERIES:
|
||||||
ListenableFuture<Void> tsSave = ctx.getTelemetryService().saveAndNotify(ctx.getTenantId(), msg.getOriginator(),
|
ListenableFuture<Void> tsSave = saveTimeSeries(ctx, msg, result, mathResultDef);
|
||||||
new BasicTsKvEntry(System.currentTimeMillis(), new DoubleDataEntry(mathResultDef.getValue(), result)));
|
|
||||||
return Futures.transform(tsSave, ts -> addToBodyAndMeta(msg, msgBodyOpt, result, mathResultDef), ctx.getDbCallbackExecutor());
|
return Futures.transform(tsSave, ts -> addToBodyAndMeta(msg, msgBodyOpt, result, mathResultDef), ctx.getDbCallbackExecutor());
|
||||||
default:
|
default:
|
||||||
throw new RuntimeException("Result type is not supported: " + mathResultDef.getType() + "!");
|
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) {
|
private Optional<ObjectNode> convertMsgBodyIfRequired(TbMsg msg) {
|
||||||
Optional<ObjectNode> msgBodyOpt;
|
Optional<ObjectNode> msgBodyOpt;
|
||||||
if (msgBodyToJsonConversionRequired) {
|
if (msgBodyToJsonConversionRequired) {
|
||||||
@ -142,13 +216,21 @@ public class TbMathNode implements TbNode {
|
|||||||
|
|
||||||
private TbMsg addToBody(TbMsg msg, TbMathResult mathResultDef, Optional<ObjectNode> msgBodyOpt, double result) {
|
private TbMsg addToBody(TbMsg msg, TbMathResult mathResultDef, Optional<ObjectNode> msgBodyOpt, double result) {
|
||||||
ObjectNode body = msgBodyOpt.get();
|
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));
|
return TbMsg.transformMsgData(msg, JacksonUtil.toString(body));
|
||||||
}
|
}
|
||||||
|
|
||||||
private TbMsg addToMeta(TbMsg msg, TbMathResult mathResultDef, double result) {
|
private TbMsg addToMeta(TbMsg msg, TbMathResult mathResultDef, double result) {
|
||||||
var md = msg.getMetaData();
|
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);
|
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);
|
return apply(args.get(0), args.get(1), (a, b) -> a / b);
|
||||||
case SIN:
|
case SIN:
|
||||||
return apply(args.get(0), Math::sin);
|
return apply(args.get(0), Math::sin);
|
||||||
|
case SINH:
|
||||||
|
return apply(args.get(0), Math::sinh);
|
||||||
case COS:
|
case COS:
|
||||||
return apply(args.get(0), Math::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:
|
case SQRT:
|
||||||
return apply(args.get(0), Math::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:
|
case ABS:
|
||||||
return apply(args.get(0), Math::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:
|
default:
|
||||||
throw new RuntimeException("Not supported operation: " + config.getOperation());
|
throw new RuntimeException("Not supported operation: " + config.getOperation());
|
||||||
}
|
}
|
||||||
@ -188,19 +322,17 @@ public class TbMathNode implements TbNode {
|
|||||||
case CONSTANT:
|
case CONSTANT:
|
||||||
return Futures.immediateFuture(TbMathArgumentValue.constant(arg));
|
return Futures.immediateFuture(TbMathArgumentValue.constant(arg));
|
||||||
case MESSAGE_BODY:
|
case MESSAGE_BODY:
|
||||||
return Futures.immediateFuture(TbMathArgumentValue.fromMessageBody(arg.getValue(), msgBodyOpt));
|
return Futures.immediateFuture(TbMathArgumentValue.fromMessageBody(arg, msgBodyOpt));
|
||||||
case MESSAGE_METADATA:
|
case MESSAGE_METADATA:
|
||||||
return Futures.immediateFuture(TbMathArgumentValue.fromMessageMetadata(arg.getValue(), msg.getMetaData()));
|
return Futures.immediateFuture(TbMathArgumentValue.fromMessageMetadata(arg, msg.getMetaData()));
|
||||||
case ATTRIBUTE:
|
case ATTRIBUTE:
|
||||||
String scope = getAttributeScope(arg.getAttributeScope());
|
String scope = getAttributeScope(arg.getAttributeScope());
|
||||||
return Futures.transform(ctx.getAttributesService().find(ctx.getTenantId(), msg.getOriginator(), scope, arg.getValue()),
|
return Futures.transform(ctx.getAttributesService().find(ctx.getTenantId(), msg.getOriginator(), scope, arg.getKey()),
|
||||||
opt -> getTbMathArgumentValue(opt.orElseThrow(() ->
|
opt -> getTbMathArgumentValue(arg, opt, "Attribute: " + arg.getKey() + " with scope: " + scope + " not found for entity: " + msg.getOriginator())
|
||||||
new RuntimeException("Attribute: " + arg.getValue() + " with scope: " + scope + " not found for entity: " + msg.getOriginator())))
|
,MoreExecutors.directExecutor());
|
||||||
, MoreExecutors.directExecutor());
|
|
||||||
case TIME_SERIES:
|
case TIME_SERIES:
|
||||||
return Futures.transform(ctx.getTimeseriesService().findLatest(ctx.getTenantId(), msg.getOriginator(), arg.getValue()),
|
return Futures.transform(ctx.getTimeseriesService().findLatest(ctx.getTenantId(), msg.getOriginator(), arg.getKey()),
|
||||||
opt -> getTbMathArgumentValue(opt.orElseThrow(() ->
|
opt -> getTbMathArgumentValue(arg, opt, "Time-series: " + arg.getKey() + " not found for entity: " + msg.getOriginator())
|
||||||
new RuntimeException("Time-series: " + arg.getValue() + " not found for entity: " + msg.getOriginator())))
|
|
||||||
, MoreExecutors.directExecutor());
|
, MoreExecutors.directExecutor());
|
||||||
default:
|
default:
|
||||||
throw new RuntimeException("Unsupported argument type: " + arg.getType() + "!");
|
throw new RuntimeException("Unsupported argument type: " + arg.getType() + "!");
|
||||||
@ -212,14 +344,23 @@ public class TbMathNode implements TbNode {
|
|||||||
return StringUtils.isEmpty(attrScope) ? DataConstants.SERVER_SCOPE : attrScope;
|
return StringUtils.isEmpty(attrScope) ? DataConstants.SERVER_SCOPE : attrScope;
|
||||||
}
|
}
|
||||||
|
|
||||||
private TbMathArgumentValue getTbMathArgumentValue(KvEntry kv) {
|
private TbMathArgumentValue getTbMathArgumentValue(TbMathArgument arg, Optional<? extends KvEntry> kvOpt, String error) {
|
||||||
switch (kv.getDataType()) {
|
if (kvOpt != null && kvOpt.isPresent()) {
|
||||||
case LONG:
|
var kv = kvOpt.get();
|
||||||
return TbMathArgumentValue.fromLong(kv.getLongValue().get());
|
switch (kv.getDataType()) {
|
||||||
case DOUBLE:
|
case LONG:
|
||||||
return TbMathArgumentValue.fromDouble(kv.getDoubleValue().get());
|
return TbMathArgumentValue.fromLong(kv.getLongValue().get());
|
||||||
default:
|
case DOUBLE:
|
||||||
return TbMathArgumentValue.fromString(kv.getValueAsString());
|
return TbMathArgumentValue.fromDouble(kv.getDoubleValue().get());
|
||||||
|
default:
|
||||||
|
return TbMathArgumentValue.fromString(kv.getValueAsString());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (arg.getDefaultValue() != null) {
|
||||||
|
return TbMathArgumentValue.fromDouble(arg.getDefaultValue());
|
||||||
|
} else {
|
||||||
|
throw new RuntimeException(error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -22,18 +22,18 @@ import java.util.Arrays;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
public class TbMathFormulaConfiguration implements NodeConfiguration<TbMathFormulaConfiguration> {
|
public class TbMathNodeConfiguration implements NodeConfiguration<TbMathNodeConfiguration> {
|
||||||
|
|
||||||
private TbRuleNodeMathFunctionType operation;
|
private TbRuleNodeMathFunctionType operation;
|
||||||
private List<TbMathArgument> arguments;
|
private List<TbMathArgument> arguments;
|
||||||
private TbMathResult result;
|
private TbMathResult result;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public TbMathFormulaConfiguration defaultConfiguration() {
|
public TbMathNodeConfiguration defaultConfiguration() {
|
||||||
TbMathFormulaConfiguration configuration = new TbMathFormulaConfiguration();
|
TbMathNodeConfiguration configuration = new TbMathNodeConfiguration();
|
||||||
configuration.setOperation(TbRuleNodeMathFunctionType.ADD);
|
configuration.setOperation(TbRuleNodeMathFunctionType.ADD);
|
||||||
configuration.setArguments(Arrays.asList(new TbMathArgument(TbMathArgumentType.CONSTANT, "2"), new TbMathArgument(TbMathArgumentType.CONSTANT, "2")));
|
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;
|
return configuration;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -25,7 +25,9 @@ import lombok.NoArgsConstructor;
|
|||||||
public class TbMathResult {
|
public class TbMathResult {
|
||||||
|
|
||||||
private TbMathArgumentType type;
|
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 addToBody;
|
||||||
private boolean addToMetadata;
|
private boolean addToMetadata;
|
||||||
private String attributeScope;
|
private String attributeScope;
|
||||||
|
|||||||
@ -19,19 +19,31 @@ import lombok.Getter;
|
|||||||
|
|
||||||
public enum TbRuleNodeMathFunctionType {
|
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
|
@Getter
|
||||||
private final int minArgs;
|
private final int minArgs;
|
||||||
@Getter
|
@Getter
|
||||||
private final int maxArgs;
|
private final int maxArgs;
|
||||||
|
@Getter
|
||||||
|
private final boolean integerResult;
|
||||||
|
|
||||||
|
TbRuleNodeMathFunctionType() {
|
||||||
|
this(1, 1, false);
|
||||||
|
}
|
||||||
|
|
||||||
TbRuleNodeMathFunctionType(int args) {
|
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.minArgs = minArgs;
|
||||||
this.maxArgs = maxArgs;
|
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