committed by
GitHub
25 changed files with 1224 additions and 34 deletions
@ -0,0 +1,41 @@ |
|||
/** |
|||
* 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 org.thingsboard.rule.engine.math; |
|||
|
|||
import lombok.AllArgsConstructor; |
|||
import lombok.Data; |
|||
import lombok.NoArgsConstructor; |
|||
|
|||
@Data |
|||
@NoArgsConstructor |
|||
@AllArgsConstructor |
|||
public class TbMathArgument { |
|||
|
|||
private String name; |
|||
private TbMathArgumentType type; |
|||
private String key; |
|||
private String attributeScope; |
|||
private Double defaultValue; |
|||
|
|||
public TbMathArgument(TbMathArgumentType type, String key) { |
|||
this(key, type, key, null, null); |
|||
} |
|||
|
|||
public TbMathArgument(String name, TbMathArgumentType type, String key) { |
|||
this(name, type, key, null, null); |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,22 @@ |
|||
/** |
|||
* 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 org.thingsboard.rule.engine.math; |
|||
|
|||
public enum TbMathArgumentType { |
|||
|
|||
ATTRIBUTE, TIME_SERIES, MESSAGE_BODY, MESSAGE_METADATA, CONSTANT; |
|||
|
|||
} |
|||
@ -0,0 +1,108 @@ |
|||
/** |
|||
* 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 org.thingsboard.rule.engine.math; |
|||
|
|||
import com.fasterxml.jackson.databind.JsonNode; |
|||
import com.fasterxml.jackson.databind.node.ObjectNode; |
|||
import lombok.Getter; |
|||
import org.thingsboard.server.common.data.StringUtils; |
|||
import org.thingsboard.server.common.msg.TbMsgMetaData; |
|||
|
|||
import java.util.Optional; |
|||
|
|||
public class TbMathArgumentValue { |
|||
|
|||
@Getter |
|||
private final double value; |
|||
|
|||
private TbMathArgumentValue(double value) { |
|||
this.value = value; |
|||
} |
|||
|
|||
public static TbMathArgumentValue constant(TbMathArgument arg) { |
|||
return fromString(arg.getKey()); |
|||
} |
|||
|
|||
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()) { |
|||
return defaultOrThrow(defaultValue, "Message body is empty!"); |
|||
} |
|||
var json = jsonNodeOpt.get(); |
|||
if (!json.has(key)) { |
|||
return defaultOrThrow(defaultValue, "Message body has no '" + key + "'!"); |
|||
} |
|||
JsonNode valueNode = json.get(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(TbMathArgument arg, TbMsgMetaData metaData) { |
|||
String key = arg.getKey(); |
|||
Double defaultValue = arg.getDefaultValue(); |
|||
if (metaData == null) { |
|||
return defaultOrThrow(defaultValue, "Message metadata is empty!"); |
|||
} |
|||
var value = metaData.getValue(key); |
|||
if (StringUtils.isEmpty(value)) { |
|||
return defaultOrThrow(defaultValue, "Message metadata has no '" + key + "'!"); |
|||
} |
|||
return fromString(value); |
|||
} |
|||
|
|||
public static TbMathArgumentValue fromLong(long value) { |
|||
return new TbMathArgumentValue(value); |
|||
} |
|||
|
|||
public static TbMathArgumentValue fromDouble(double value) { |
|||
return new TbMathArgumentValue(value); |
|||
} |
|||
|
|||
public static TbMathArgumentValue fromString(String value) { |
|||
try { |
|||
return new TbMathArgumentValue(Double.parseDouble(value)); |
|||
} catch (NumberFormatException ne) { |
|||
throw new RuntimeException("Can't convert value '" + value + "' to double!"); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,397 @@ |
|||
/** |
|||
* 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 org.thingsboard.rule.engine.math; |
|||
|
|||
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 lombok.extern.slf4j.Slf4j; |
|||
import net.objecthunter.exp4j.Expression; |
|||
import net.objecthunter.exp4j.ExpressionBuilder; |
|||
import org.springframework.util.ConcurrentReferenceHashMap; |
|||
import org.thingsboard.common.util.DonAsynchron; |
|||
import org.thingsboard.common.util.JacksonUtil; |
|||
import org.thingsboard.rule.engine.api.RuleNode; |
|||
import org.thingsboard.rule.engine.api.TbContext; |
|||
import org.thingsboard.rule.engine.api.TbNode; |
|||
import org.thingsboard.rule.engine.api.TbNodeConfiguration; |
|||
import org.thingsboard.rule.engine.api.TbNodeException; |
|||
import org.thingsboard.rule.engine.api.util.TbNodeUtils; |
|||
import org.thingsboard.server.common.data.DataConstants; |
|||
import org.thingsboard.server.common.data.StringUtils; |
|||
import org.thingsboard.server.common.data.id.EntityId; |
|||
import org.thingsboard.server.common.data.kv.BasicTsKvEntry; |
|||
import org.thingsboard.server.common.data.kv.DoubleDataEntry; |
|||
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.ConcurrentMap; |
|||
import java.util.concurrent.Semaphore; |
|||
import java.util.concurrent.TimeUnit; |
|||
import java.util.function.BiFunction; |
|||
import java.util.function.Function; |
|||
import java.util.stream.Collectors; |
|||
|
|||
@SuppressWarnings("UnstableApiUsage") |
|||
@Slf4j |
|||
@RuleNode( |
|||
type = ComponentType.ACTION, |
|||
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. " + |
|||
"Use 'CUSTOM' operation to specify complex math expressions." + |
|||
"<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 CUSTOM operation and expression: (x - 32) / 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.", |
|||
uiResources = {"static/rulenode/rulenode-core-config.js"}, |
|||
configDirective = "tbActionNodeMathFunctionConfig", |
|||
icon = "calculate" |
|||
|
|||
) |
|||
public class TbMathNode implements TbNode { |
|||
|
|||
private static final ConcurrentMap<EntityId, Semaphore> semaphores = new ConcurrentReferenceHashMap<>(); |
|||
private final ThreadLocal<Expression> customExpression = new ThreadLocal<>(); |
|||
|
|||
private TbMathNodeConfiguration config; |
|||
private boolean msgBodyToJsonConversionRequired; |
|||
|
|||
@Override |
|||
public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException { |
|||
this.config = TbNodeUtils.convert(configuration, TbMathNodeConfiguration.class); |
|||
var operation = config.getOperation(); |
|||
var argsCount = config.getArguments().size(); |
|||
if (argsCount < operation.getMinArgs() || argsCount > operation.getMaxArgs()) { |
|||
throw new RuntimeException("Args count: " + argsCount + " does not match operation: " + operation.name()); |
|||
} |
|||
if (TbRuleNodeMathFunctionType.CUSTOM.equals(operation)) { |
|||
if (StringUtils.isBlank(config.getCustomFunction())) { |
|||
throw new RuntimeException("Custom function is blank!"); |
|||
} else if (config.getCustomFunction().length() > 256) { |
|||
throw new RuntimeException("Custom function is too complex (length > 256)!"); |
|||
} |
|||
} |
|||
msgBodyToJsonConversionRequired = config.getArguments().stream().anyMatch(arg -> TbMathArgumentType.MESSAGE_BODY.equals(arg.getType())); |
|||
msgBodyToJsonConversionRequired = msgBodyToJsonConversionRequired || TbMathArgumentType.MESSAGE_BODY.equals(config.getResult().getType()); |
|||
} |
|||
|
|||
@Override |
|||
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, 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) { |
|||
TbMathResult mathResultDef = config.getResult(); |
|||
switch (mathResultDef.getType()) { |
|||
case MESSAGE_BODY: |
|||
return Futures.immediateFuture(addToBody(msg, mathResultDef, msgBodyOpt, result)); |
|||
case MESSAGE_METADATA: |
|||
return Futures.immediateFuture(addToMeta(msg, mathResultDef, result)); |
|||
case ATTRIBUTE: |
|||
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 = 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) { |
|||
var jsonNode = JacksonUtil.toJsonNode(msg.getData()); |
|||
if (jsonNode.isObject()) { |
|||
msgBodyOpt = Optional.of((ObjectNode) jsonNode); |
|||
} else { |
|||
throw new RuntimeException("Message body is not a JSON object!"); |
|||
} |
|||
} else { |
|||
msgBodyOpt = Optional.empty(); |
|||
} |
|||
return msgBodyOpt; |
|||
} |
|||
|
|||
private TbMsg addToBodyAndMeta(TbMsg msg, Optional<ObjectNode> msgBodyOpt, double result, TbMathResult mathResultDef) { |
|||
TbMsg tmpMsg = msg; |
|||
if (mathResultDef.isAddToBody()) { |
|||
tmpMsg = addToBody(msg, mathResultDef, msgBodyOpt, result); |
|||
} |
|||
if (mathResultDef.isAddToMetadata()) { |
|||
tmpMsg = addToMeta(msg, mathResultDef, result); |
|||
} |
|||
return tmpMsg; |
|||
} |
|||
|
|||
private TbMsg addToBody(TbMsg msg, TbMathResult mathResultDef, Optional<ObjectNode> msgBodyOpt, double result) { |
|||
ObjectNode body = msgBodyOpt.get(); |
|||
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(); |
|||
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); |
|||
} |
|||
|
|||
private double calculateResult(TbContext ctx, TbMsg msg, List<TbMathArgumentValue> args) { |
|||
switch (config.getOperation()) { |
|||
case ADD: |
|||
return apply(args.get(0), args.get(1), Double::sum); |
|||
case SUB: |
|||
return apply(args.get(0), args.get(1), (a, b) -> a - b); |
|||
case MULT: |
|||
return apply(args.get(0), args.get(1), (a, b) -> a * b); |
|||
case DIV: |
|||
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); |
|||
case CUSTOM: |
|||
var expr = customExpression.get(); |
|||
if (expr == null) { |
|||
expr = new ExpressionBuilder(config.getCustomFunction()) |
|||
.implicitMultiplication(true) |
|||
.variables(config.getArguments().stream().map(TbMathArgument::getName).collect(Collectors.toSet())) |
|||
.build(); |
|||
customExpression.set(expr); |
|||
} |
|||
for (int i = 0; i < config.getArguments().size(); i++) { |
|||
expr.setVariable(config.getArguments().get(i).getName(), args.get(i).getValue()); |
|||
} |
|||
return expr.evaluate(); |
|||
default: |
|||
throw new RuntimeException("Not supported operation: " + config.getOperation()); |
|||
} |
|||
} |
|||
|
|||
private double apply(TbMathArgumentValue arg, Function<Double, Double> function) { |
|||
return function.apply(arg.getValue()); |
|||
} |
|||
|
|||
private double apply(TbMathArgumentValue arg1, TbMathArgumentValue arg2, BiFunction<Double, Double, Double> function) { |
|||
return function.apply(arg1.getValue(), arg2.getValue()); |
|||
} |
|||
|
|||
private ListenableFuture<TbMathArgumentValue> resolveArguments(TbContext ctx, TbMsg msg, Optional<ObjectNode> msgBodyOpt, TbMathArgument arg) { |
|||
switch (arg.getType()) { |
|||
case CONSTANT: |
|||
return Futures.immediateFuture(TbMathArgumentValue.constant(arg)); |
|||
case MESSAGE_BODY: |
|||
return Futures.immediateFuture(TbMathArgumentValue.fromMessageBody(arg, msgBodyOpt)); |
|||
case MESSAGE_METADATA: |
|||
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.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.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() + "!"); |
|||
} |
|||
|
|||
} |
|||
|
|||
private String getAttributeScope(String attrScope) { |
|||
return StringUtils.isEmpty(attrScope) ? DataConstants.SERVER_SCOPE : attrScope; |
|||
} |
|||
|
|||
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()); |
|||
case DOUBLE: |
|||
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); |
|||
} |
|||
} |
|||
} |
|||
|
|||
@Override |
|||
public void destroy() { |
|||
} |
|||
} |
|||
@ -0,0 +1,40 @@ |
|||
/** |
|||
* 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 org.thingsboard.rule.engine.math; |
|||
|
|||
import lombok.Data; |
|||
import org.thingsboard.rule.engine.api.NodeConfiguration; |
|||
|
|||
import java.util.Arrays; |
|||
import java.util.List; |
|||
|
|||
@Data |
|||
public class TbMathNodeConfiguration implements NodeConfiguration<TbMathNodeConfiguration> { |
|||
|
|||
private TbRuleNodeMathFunctionType operation; |
|||
private List<TbMathArgument> arguments; |
|||
private String customFunction; |
|||
private TbMathResult result; |
|||
|
|||
@Override |
|||
public TbMathNodeConfiguration defaultConfiguration() { |
|||
TbMathNodeConfiguration configuration = new TbMathNodeConfiguration(); |
|||
configuration.setOperation(TbRuleNodeMathFunctionType.ADD); |
|||
configuration.setArguments(Arrays.asList(new TbMathArgument("x", TbMathArgumentType.CONSTANT, "2"), new TbMathArgument("y", TbMathArgumentType.CONSTANT, "2"))); |
|||
configuration.setResult(new TbMathResult(TbMathArgumentType.MESSAGE_BODY, "result", 2, false, false, null)); |
|||
return configuration; |
|||
} |
|||
} |
|||
@ -0,0 +1,35 @@ |
|||
/** |
|||
* 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 org.thingsboard.rule.engine.math; |
|||
|
|||
import lombok.AllArgsConstructor; |
|||
import lombok.Data; |
|||
import lombok.NoArgsConstructor; |
|||
|
|||
@Data |
|||
@NoArgsConstructor |
|||
@AllArgsConstructor |
|||
public class TbMathResult { |
|||
|
|||
private TbMathArgumentType type; |
|||
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; |
|||
|
|||
} |
|||
@ -0,0 +1,51 @@ |
|||
/** |
|||
* 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 org.thingsboard.rule.engine.math; |
|||
|
|||
import lombok.Getter; |
|||
|
|||
public enum TbRuleNodeMathFunctionType { |
|||
|
|||
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, |
|||
|
|||
CUSTOM(0, 16, false); //Custom function based on exp4j
|
|||
|
|||
@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, false); |
|||
} |
|||
|
|||
TbRuleNodeMathFunctionType(int minArgs, int maxArgs, boolean integerResult) { |
|||
this.minArgs = minArgs; |
|||
this.maxArgs = maxArgs; |
|||
this.integerResult = integerResult; |
|||
} |
|||
|
|||
} |
|||
File diff suppressed because one or more lines are too long
@ -0,0 +1,339 @@ |
|||
/** |
|||
* 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.TbMathNode; |
|||
import org.thingsboard.rule.engine.math.TbMathNodeConfiguration; |
|||
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(); |
|||
initMocks(); |
|||
} |
|||
|
|||
@After |
|||
public void after() { |
|||
dbExecutor.destroy(); |
|||
} |
|||
|
|||
private void initMocks() { |
|||
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); |
|||
} |
|||
|
|||
private TbMathNode initNode(TbRuleNodeMathFunctionType operation, TbMathResult result, TbMathArgument... arguments) { |
|||
return initNode(operation, null, result, arguments); |
|||
} |
|||
|
|||
private TbMathNode initNodeWithCustomFunction(String expression, TbMathResult result, TbMathArgument... arguments) { |
|||
return initNode(TbRuleNodeMathFunctionType.CUSTOM, expression, result, arguments); |
|||
} |
|||
|
|||
private TbMathNode initNode(TbRuleNodeMathFunctionType operation, String expression, TbMathResult result, TbMathArgument... arguments) { |
|||
try { |
|||
TbMathNodeConfiguration configuration = new TbMathNodeConfiguration(); |
|||
configuration.setOperation(operation); |
|||
if (TbRuleNodeMathFunctionType.CUSTOM.equals(operation)) { |
|||
configuration.setCustomFunction(expression); |
|||
} |
|||
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 testExp4j() { |
|||
var node = initNodeWithCustomFunction("2a+3b", |
|||
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(10, resultJson.get("result").asInt()); |
|||
} |
|||
|
|||
@Test |
|||
public void testSimpleFunctions() { |
|||
testSimpleTwoArgumentFunction(TbRuleNodeMathFunctionType.ADD, 2.1, 2.2, 4.3); |
|||
testSimpleTwoArgumentFunction(TbRuleNodeMathFunctionType.SUB, 2.1, 2.2, -0.1); |
|||
testSimpleTwoArgumentFunction(TbRuleNodeMathFunctionType.MULT, 2.1, 2.0, 4.2); |
|||
testSimpleTwoArgumentFunction(TbRuleNodeMathFunctionType.DIV, 4.2, 2.0, 2.1); |
|||
|
|||
testSimpleOneArgumentFunction(TbRuleNodeMathFunctionType.SIN, Math.toRadians(30), 0.5); |
|||
testSimpleOneArgumentFunction(TbRuleNodeMathFunctionType.SIN, Math.toRadians(90), 1.0); |
|||
|
|||
testSimpleOneArgumentFunction(TbRuleNodeMathFunctionType.SINH, Math.toRadians(0), 0.0); |
|||
testSimpleOneArgumentFunction(TbRuleNodeMathFunctionType.COSH, Math.toRadians(0), 1.0); |
|||
|
|||
testSimpleOneArgumentFunction(TbRuleNodeMathFunctionType.COS, Math.toRadians(60), 0.5); |
|||
testSimpleOneArgumentFunction(TbRuleNodeMathFunctionType.COS, Math.toRadians(0), 1.0); |
|||
|
|||
testSimpleOneArgumentFunction(TbRuleNodeMathFunctionType.TAN, Math.toRadians(45), 1); |
|||
testSimpleOneArgumentFunction(TbRuleNodeMathFunctionType.TAN, Math.toRadians(0), 0); |
|||
|
|||
testSimpleOneArgumentFunction(TbRuleNodeMathFunctionType.ABS, -1, 1); |
|||
testSimpleOneArgumentFunction(TbRuleNodeMathFunctionType.SQRT, 4, 2); |
|||
testSimpleOneArgumentFunction(TbRuleNodeMathFunctionType.CBRT, 8, 2); |
|||
} |
|||
|
|||
private void testSimpleTwoArgumentFunction(TbRuleNodeMathFunctionType function, double arg1, double arg2, double result) { |
|||
initMocks(); |
|||
|
|||
var node = initNode(function, |
|||
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", arg1).put("b", arg2).toString()); |
|||
|
|||
node.onMsg(ctx, msg); |
|||
|
|||
ArgumentCaptor<TbMsg> msgCaptor = ArgumentCaptor.forClass(TbMsg.class); |
|||
Mockito.verify(ctx, Mockito.timeout(5000).times(1)).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(result, resultJson.get("result").asDouble(), 0d); |
|||
} |
|||
|
|||
private void testSimpleOneArgumentFunction(TbRuleNodeMathFunctionType function, double arg1, double result) { |
|||
initMocks(); |
|||
|
|||
var node = initNode(function, |
|||
new TbMathResult(TbMathArgumentType.MESSAGE_BODY, "result", 2, false, false, null), |
|||
new TbMathArgument(TbMathArgumentType.MESSAGE_BODY, "a") |
|||
); |
|||
|
|||
TbMsg msg = TbMsg.newMsg("TEST", originator, new TbMsgMetaData(), JacksonUtil.newObjectNode().put("a", arg1).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(result, resultJson.get("result").asDouble(), 0d); |
|||
} |
|||
|
|||
@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…
Reference in new issue