Merge pull request #13120 from irynamatveieva/cf-expression
Added custom functions to the math node
This commit is contained in:
commit
889208edca
@ -18,8 +18,6 @@ package org.thingsboard.server.service.cf.ctx.state;
|
|||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import net.objecthunter.exp4j.Expression;
|
import net.objecthunter.exp4j.Expression;
|
||||||
import net.objecthunter.exp4j.ExpressionBuilder;
|
import net.objecthunter.exp4j.ExpressionBuilder;
|
||||||
import net.objecthunter.exp4j.function.Function;
|
|
||||||
import net.objecthunter.exp4j.function.Functions;
|
|
||||||
import org.mvel2.MVEL;
|
import org.mvel2.MVEL;
|
||||||
import org.thingsboard.script.api.tbel.TbelInvokeService;
|
import org.thingsboard.script.api.tbel.TbelInvokeService;
|
||||||
import org.thingsboard.server.common.data.AttributeScope;
|
import org.thingsboard.server.common.data.AttributeScope;
|
||||||
@ -46,6 +44,8 @@ import java.util.HashMap;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
|
import static org.thingsboard.common.util.ExpressionFunctionsUtil.userDefinedFunctions;
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
public class CalculatedFieldCtx {
|
public class CalculatedFieldCtx {
|
||||||
|
|
||||||
@ -282,50 +282,4 @@ public class CalculatedFieldCtx {
|
|||||||
return "Failed to init CF state. State size exceeds limit of " + (maxStateSize / 1024) + "Kb!";
|
return "Failed to init CF state. State size exceeds limit of " + (maxStateSize / 1024) + "Kb!";
|
||||||
}
|
}
|
||||||
|
|
||||||
private static final List<Function> 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")
|
|
||||||
);
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -111,6 +111,11 @@
|
|||||||
<groupId>org.locationtech.jts</groupId>
|
<groupId>org.locationtech.jts</groupId>
|
||||||
<artifactId>jts-core</artifactId>
|
<artifactId>jts-core</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>net.objecthunter</groupId>
|
||||||
|
<artifactId>exp4j</artifactId>
|
||||||
|
<version>${exp4j.version}</version>
|
||||||
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
<build>
|
<build>
|
||||||
|
|||||||
@ -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<Function> 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"));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -53,6 +53,7 @@ import java.util.function.BiFunction;
|
|||||||
import java.util.function.Function;
|
import java.util.function.Function;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
import static org.thingsboard.common.util.ExpressionFunctionsUtil.userDefinedFunctions;
|
||||||
import static org.thingsboard.rule.engine.math.TbMathArgumentType.CONSTANT;
|
import static org.thingsboard.rule.engine.math.TbMathArgumentType.CONSTANT;
|
||||||
|
|
||||||
@SuppressWarnings("UnstableApiUsage")
|
@SuppressWarnings("UnstableApiUsage")
|
||||||
@ -310,6 +311,7 @@ public class TbMathNode implements TbNode {
|
|||||||
var expr = customExpression.get();
|
var expr = customExpression.get();
|
||||||
if (expr == null) {
|
if (expr == null) {
|
||||||
expr = new ExpressionBuilder(config.getCustomFunction())
|
expr = new ExpressionBuilder(config.getCustomFunction())
|
||||||
|
.functions(userDefinedFunctions)
|
||||||
.implicitMultiplication(true)
|
.implicitMultiplication(true)
|
||||||
.variables(config.getArguments().stream().map(TbMathArgument::getName).collect(Collectors.toSet()))
|
.variables(config.getArguments().stream().map(TbMathArgument::getName).collect(Collectors.toSet()))
|
||||||
.build();
|
.build();
|
||||||
|
|||||||
@ -56,6 +56,8 @@ import org.thingsboard.server.common.msg.TbMsgMetaData;
|
|||||||
import org.thingsboard.server.dao.attributes.AttributesService;
|
import org.thingsboard.server.dao.attributes.AttributesService;
|
||||||
import org.thingsboard.server.dao.timeseries.TimeseriesService;
|
import org.thingsboard.server.dao.timeseries.TimeseriesService;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.math.RoundingMode;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
@ -848,6 +850,42 @@ public class TbMathNodeTest {
|
|||||||
verify(ctx, never()).tellFailure(any(), any());
|
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<TbMsg> 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<Arguments> 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 {
|
static class RuleDispatcherExecutor extends AbstractListeningExecutor {
|
||||||
@Override
|
@Override
|
||||||
protected int getThreadPollSize() {
|
protected int getThreadPollSize() {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user