diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/math/TbMathArgument.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/math/TbMathArgument.java index 2ad5efbb11..3c9715baae 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/math/TbMathArgument.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/math/TbMathArgument.java @@ -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; } } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/math/TbMathArgumentValue.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/math/TbMathArgumentValue.java index 5e28192945..c0fe5d30b5 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/math/TbMathArgumentValue.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/math/TbMathArgumentValue.java @@ -33,29 +33,43 @@ public class TbMathArgumentValue { } public static TbMathArgumentValue constant(TbMathArgument arg) { - return fromString(arg.getValue()); + return fromString(arg.getKey()); } - public static TbMathArgumentValue fromMessageBody(String key, Optional 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 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()) { - try { - value = Double.parseDouble(valueNode.asText()); - } catch (NumberFormatException ne) { - throw new RuntimeException("Can't convert value '" + valueNode.asText() + "' to double!"); + 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!"); @@ -63,13 +77,15 @@ public class TbMathArgumentValue { 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); } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/math/TbMathNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/math/TbMathNode.java index 2a2b94afc8..7e97ee0510 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/math/TbMathNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/math/TbMathNode.java @@ -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 msg property. For example 'temperature = ' + msg.temperature ;. " + - "Message metadata can be accessed via metadata property. For example 'name = ' + metadata.customerName;.", + 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. " + + "

" + + "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." + + "

" + + "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." + + "

" + + "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." + + "

" + + "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 semaphores = new ConcurrentReferenceHashMap<>(); + private static final ConcurrentMap 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); - var arguments = config.getArguments(); - Optional msgBodyOpt = convertMsgBodyIfRequired(msg); - var argumentValues = Futures.allAsList(arguments.stream() - .map(arg -> resolveArguments(ctx, msg, msgBodyOpt, arg)).collect(Collectors.toList())); - ListenableFuture 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()); + if (!acquired) { + ctx.tellFailure(msg, new RuntimeException("Failed to process message for originator synchronously")); + return; + } + + try { + var arguments = config.getArguments(); + Optional msgBodyOpt = convertMsgBodyIfRequired(msg); + var argumentValues = Futures.allAsList(arguments.stream() + .map(arg -> resolveArguments(ctx, msg, msgBodyOpt, arg)).collect(Collectors.toList())); + ListenableFuture 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 updateMsgAndDb(TbContext ctx, TbMsg msg, Optional 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 attrSave = ctx.getTelemetryService().saveAttrAndNotify( - ctx.getTenantId(), msg.getOriginator(), getAttributeScope(mathResultDef.getAttributeScope()), mathResultDef.getValue(), result); + ListenableFuture attrSave = saveAttribute(ctx, msg, result, mathResultDef); return Futures.transform(attrSave, attr -> addToBodyAndMeta(msg, msgBodyOpt, result, mathResultDef), ctx.getDbCallbackExecutor()); case TIME_SERIES: - ListenableFuture tsSave = ctx.getTelemetryService().saveAndNotify(ctx.getTenantId(), msg.getOriginator(), - new BasicTsKvEntry(System.currentTimeMillis(), new DoubleDataEntry(mathResultDef.getValue(), result))); + ListenableFuture 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 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 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 convertMsgBodyIfRequired(TbMsg msg) { Optional msgBodyOpt; if (msgBodyToJsonConversionRequired) { @@ -142,13 +216,21 @@ public class TbMathNode implements TbNode { private TbMsg addToBody(TbMsg msg, TbMathResult mathResultDef, Optional 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()))) - , MoreExecutors.directExecutor()); + 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,14 +344,23 @@ public class TbMathNode implements TbNode { return StringUtils.isEmpty(attrScope) ? DataConstants.SERVER_SCOPE : attrScope; } - private TbMathArgumentValue getTbMathArgumentValue(KvEntry kv) { - 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()); + private TbMathArgumentValue getTbMathArgumentValue(TbMathArgument arg, Optional 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); + } } } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/math/TbMathFormulaConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/math/TbMathNodeConfiguration.java similarity index 80% rename from rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/math/TbMathFormulaConfiguration.java rename to rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/math/TbMathNodeConfiguration.java index 4f1b4cbe60..c706456455 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/math/TbMathFormulaConfiguration.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/math/TbMathNodeConfiguration.java @@ -22,18 +22,18 @@ import java.util.Arrays; import java.util.List; @Data -public class TbMathFormulaConfiguration implements NodeConfiguration { +public class TbMathNodeConfiguration implements NodeConfiguration { private TbRuleNodeMathFunctionType operation; private List 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; } } \ No newline at end of file diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/math/TbMathResult.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/math/TbMathResult.java index 0c627f1e71..833d8b794d 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/math/TbMathResult.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/math/TbMathResult.java @@ -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; diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/math/TbRuleNodeMathFunctionType.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/math/TbRuleNodeMathFunctionType.java index ba734c8662..0ef95389fe 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/math/TbRuleNodeMathFunctionType.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/math/TbRuleNodeMathFunctionType.java @@ -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; } + } diff --git a/rule-engine/rule-engine-components/src/test/java/math/TbMathNodeTest.java b/rule-engine/rule-engine-components/src/test/java/math/TbMathNodeTest.java new file mode 100644 index 0000000000..aa7cbb8e50 --- /dev/null +++ b/rule-engine/rule-engine-components/src/test/java/math/TbMathNodeTest.java @@ -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 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 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 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 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 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); + } + +}