diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java index 1763bed7da..674ce3726f 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java @@ -18,8 +18,6 @@ package org.thingsboard.server.service.cf.ctx.state; import lombok.Data; import net.objecthunter.exp4j.Expression; import net.objecthunter.exp4j.ExpressionBuilder; -import net.objecthunter.exp4j.function.Function; -import net.objecthunter.exp4j.function.Functions; import org.mvel2.MVEL; import org.thingsboard.script.api.tbel.TbelInvokeService; import org.thingsboard.server.common.data.AttributeScope; @@ -46,6 +44,8 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import static org.thingsboard.common.util.ExpressionFunctionsUtil.userDefinedFunctions; + @Data public class CalculatedFieldCtx { @@ -282,50 +282,4 @@ public class CalculatedFieldCtx { return "Failed to init CF state. State size exceeds limit of " + (maxStateSize / 1024) + "Kb!"; } - private static final List userDefinedFunctions = List.of( - new Function("ln") { - @Override - public double apply(double... args) { - return Math.log(args[0]); - } - }, - new Function("lg") { - @Override - public double apply(double... args) { - return Math.log10(args[0]); - } - }, - new Function("logab", 2) { - @Override - public double apply(double... args) { - return Math.log(args[1]) / Math.log(args[0]); - } - }, - - // Manually listing built-in functions to avoid inefficient equals-based lookup in Functions.getBuiltinFunction during each evaluation. - Functions.getBuiltinFunction("sin"), - Functions.getBuiltinFunction("cos"), - Functions.getBuiltinFunction("tan"), - Functions.getBuiltinFunction("cot"), - Functions.getBuiltinFunction("log"), - Functions.getBuiltinFunction("log2"), - Functions.getBuiltinFunction("log10"), - Functions.getBuiltinFunction("log1p"), - Functions.getBuiltinFunction("abs"), - Functions.getBuiltinFunction("acos"), - Functions.getBuiltinFunction("asin"), - Functions.getBuiltinFunction("atan"), - Functions.getBuiltinFunction("cbrt"), - Functions.getBuiltinFunction("floor"), - Functions.getBuiltinFunction("sinh"), - Functions.getBuiltinFunction("sqrt"), - Functions.getBuiltinFunction("tanh"), - Functions.getBuiltinFunction("cosh"), - Functions.getBuiltinFunction("ceil"), - Functions.getBuiltinFunction("pow"), - Functions.getBuiltinFunction("exp"), - Functions.getBuiltinFunction("expm1"), - Functions.getBuiltinFunction("signum") - ); - } diff --git a/common/util/pom.xml b/common/util/pom.xml index 82768cdb68..7fdd3036ae 100644 --- a/common/util/pom.xml +++ b/common/util/pom.xml @@ -111,6 +111,11 @@ org.locationtech.jts jts-core + + net.objecthunter + exp4j + ${exp4j.version} + diff --git a/common/util/src/main/java/org/thingsboard/common/util/ExpressionFunctionsUtil.java b/common/util/src/main/java/org/thingsboard/common/util/ExpressionFunctionsUtil.java new file mode 100644 index 0000000000..b1753e7a17 --- /dev/null +++ b/common/util/src/main/java/org/thingsboard/common/util/ExpressionFunctionsUtil.java @@ -0,0 +1,78 @@ +/** + * Copyright © 2016-2025 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.common.util; + +import net.objecthunter.exp4j.function.Function; +import net.objecthunter.exp4j.function.Functions; + +import java.util.ArrayList; +import java.util.List; + +public class ExpressionFunctionsUtil { + + public static final List userDefinedFunctions = new ArrayList<>(); + + static { + userDefinedFunctions.add( + new Function("ln") { + @Override + public double apply(double... args) { + return Math.log(args[0]); + } + } + ); + userDefinedFunctions.add( + new Function("lg") { + @Override + public double apply(double... args) { + return Math.log10(args[0]); + } + } + ); + userDefinedFunctions.add( + new Function("logab", 2) { + @Override + public double apply(double... args) { + return Math.log(args[1]) / Math.log(args[0]); + } + } + ); + userDefinedFunctions.add(Functions.getBuiltinFunction("sin")); + userDefinedFunctions.add(Functions.getBuiltinFunction("cos")); + userDefinedFunctions.add(Functions.getBuiltinFunction("tan")); + userDefinedFunctions.add(Functions.getBuiltinFunction("cot")); + userDefinedFunctions.add(Functions.getBuiltinFunction("log")); + userDefinedFunctions.add(Functions.getBuiltinFunction("log2")); + userDefinedFunctions.add(Functions.getBuiltinFunction("log10")); + userDefinedFunctions.add(Functions.getBuiltinFunction("log1p")); + userDefinedFunctions.add(Functions.getBuiltinFunction("abs")); + userDefinedFunctions.add(Functions.getBuiltinFunction("acos")); + userDefinedFunctions.add(Functions.getBuiltinFunction("asin")); + userDefinedFunctions.add(Functions.getBuiltinFunction("atan")); + userDefinedFunctions.add(Functions.getBuiltinFunction("cbrt")); + userDefinedFunctions.add(Functions.getBuiltinFunction("floor")); + userDefinedFunctions.add(Functions.getBuiltinFunction("sinh")); + userDefinedFunctions.add(Functions.getBuiltinFunction("sqrt")); + userDefinedFunctions.add(Functions.getBuiltinFunction("tanh")); + userDefinedFunctions.add(Functions.getBuiltinFunction("cosh")); + userDefinedFunctions.add(Functions.getBuiltinFunction("ceil")); + userDefinedFunctions.add(Functions.getBuiltinFunction("pow")); + userDefinedFunctions.add(Functions.getBuiltinFunction("exp")); + userDefinedFunctions.add(Functions.getBuiltinFunction("expm1")); + userDefinedFunctions.add(Functions.getBuiltinFunction("signum")); + } + +} 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 500aef0fe5..76ba71163a 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 @@ -53,6 +53,7 @@ import java.util.function.BiFunction; import java.util.function.Function; import java.util.stream.Collectors; +import static org.thingsboard.common.util.ExpressionFunctionsUtil.userDefinedFunctions; import static org.thingsboard.rule.engine.math.TbMathArgumentType.CONSTANT; @SuppressWarnings("UnstableApiUsage") @@ -310,6 +311,7 @@ public class TbMathNode implements TbNode { var expr = customExpression.get(); if (expr == null) { expr = new ExpressionBuilder(config.getCustomFunction()) + .functions(userDefinedFunctions) .implicitMultiplication(true) .variables(config.getArguments().stream().map(TbMathArgument::getName).collect(Collectors.toSet())) .build(); diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/math/TbMathNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/math/TbMathNodeTest.java index 71e9ed8350..dcdd2530df 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/math/TbMathNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/math/TbMathNodeTest.java @@ -56,6 +56,8 @@ import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.dao.attributes.AttributesService; import org.thingsboard.server.dao.timeseries.TimeseriesService; +import java.math.BigDecimal; +import java.math.RoundingMode; import java.util.Arrays; import java.util.List; import java.util.Optional; @@ -848,6 +850,42 @@ public class TbMathNodeTest { verify(ctx, never()).tellFailure(any(), any()); } + @ParameterizedTest + @MethodSource + public void testCustomFunctions(String customFunction, double result) { + var node = initNodeWithCustomFunction(customFunction, + new TbMathResult(TbMathArgumentType.MESSAGE_BODY, "result", 2, false, false, null), + new TbMathArgument("a", TbMathArgumentType.MESSAGE_BODY, "argumentA"), + new TbMathArgument("b", TbMathArgumentType.MESSAGE_BODY, "argumentB") + ); + + TbMsg msg = TbMsg.newMsg() + .type(TbMsgType.POST_TELEMETRY_REQUEST) + .originator(originator) + .metaData(TbMsgMetaData.EMPTY) + .data("{\"argumentA\":2,\"argumentB\":5}") + .build(); + + node.onMsg(ctx, msg); + + ArgumentCaptor msgCaptor = ArgumentCaptor.forClass(TbMsg.class); + verify(ctx, timeout(TIMEOUT)).tellSuccess(msgCaptor.capture()); + TbMsg outMsg = msgCaptor.getValue(); + assertThat(outMsg).isNotNull(); + assertThat(outMsg.getData()).isNotNull(); + var resultJson = JacksonUtil.toJsonNode(outMsg.getData()); + assertThat(resultJson.has("result")).isTrue(); + assertThat(resultJson.get("result").asDouble()).isEqualTo(new BigDecimal(result).setScale(2, RoundingMode.HALF_UP).doubleValue()); + } + + private static Stream testCustomFunctions() { + return Stream.of( + Arguments.of("ln(a)", Math.log(2)), + Arguments.of("lg(a)", Math.log10(2)), + Arguments.of("logab(a, b)", Math.log(5) / Math.log(2)) + ); + } + static class RuleDispatcherExecutor extends AbstractListeningExecutor { @Override protected int getThreadPollSize() {