Merge branch 'feature/cf-merge-function'
This commit is contained in:
commit
7d4bb547b5
@ -89,7 +89,7 @@ public class TsRollingArgumentEntry implements ArgumentEntry {
|
|||||||
for (var e : tsRecords.entrySet()) {
|
for (var e : tsRecords.entrySet()) {
|
||||||
values.add(new TbelCfTsDoubleVal(e.getKey(), e.getValue()));
|
values.add(new TbelCfTsDoubleVal(e.getKey(), e.getValue()));
|
||||||
}
|
}
|
||||||
return new TbelCfTsRollingArg(limit, timeWindow, values);
|
return new TbelCfTsRollingArg(timeWindow, values);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@ -136,7 +136,10 @@ public class DefaultTbelInvokeService extends AbstractScriptInvokeService implem
|
|||||||
parserConfig.registerDataType("TbelCfSingleValueArg", TbelCfSingleValueArg.class, TbelCfSingleValueArg::memorySize);
|
parserConfig.registerDataType("TbelCfSingleValueArg", TbelCfSingleValueArg.class, TbelCfSingleValueArg::memorySize);
|
||||||
parserConfig.registerDataType("TbelCfTsRollingArg", TbelCfTsRollingArg.class, TbelCfTsRollingArg::memorySize);
|
parserConfig.registerDataType("TbelCfTsRollingArg", TbelCfTsRollingArg.class, TbelCfTsRollingArg::memorySize);
|
||||||
parserConfig.registerDataType("TbelCfTsDoubleVal", TbelCfTsDoubleVal.class, TbelCfTsDoubleVal::memorySize);
|
parserConfig.registerDataType("TbelCfTsDoubleVal", TbelCfTsDoubleVal.class, TbelCfTsDoubleVal::memorySize);
|
||||||
|
parserConfig.registerDataType("TbelCfTsRollingData", TbelCfTsRollingData.class, TbelCfTsRollingData::memorySize);
|
||||||
parserConfig.registerDataType("TbTimeWindow", TbTimeWindow.class, TbTimeWindow::memorySize);
|
parserConfig.registerDataType("TbTimeWindow", TbTimeWindow.class, TbTimeWindow::memorySize);
|
||||||
|
parserConfig.registerDataType("TbelCfTsDoubleVal", TbelCfTsMultiDoubleVal.class, TbelCfTsMultiDoubleVal::memorySize);
|
||||||
|
|
||||||
TbUtils.register(parserConfig);
|
TbUtils.register(parserConfig);
|
||||||
executor = MoreExecutors.listeningDecorator(ThingsBoardExecutors.newWorkStealingPool(threadPoolSize, "tbel-executor"));
|
executor = MoreExecutors.listeningDecorator(ThingsBoardExecutors.newWorkStealingPool(threadPoolSize, "tbel-executor"));
|
||||||
try {
|
try {
|
||||||
|
|||||||
@ -17,20 +17,24 @@ package org.thingsboard.script.api.tbel;
|
|||||||
|
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
@AllArgsConstructor
|
@AllArgsConstructor
|
||||||
|
@NoArgsConstructor
|
||||||
public class TbTimeWindow implements TbelCfObject {
|
public class TbTimeWindow implements TbelCfObject {
|
||||||
|
|
||||||
public static final long OBJ_SIZE = 32L;
|
public static final long OBJ_SIZE = 32L;
|
||||||
|
|
||||||
private long startTs;
|
private long startTs;
|
||||||
private long endTs;
|
private long endTs;
|
||||||
private int limit;
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public long memorySize() {
|
public long memorySize() {
|
||||||
return OBJ_SIZE;
|
return OBJ_SIZE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean matches(long ts) {
|
||||||
|
return ts >= startTs && ts < endTs;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -44,6 +44,7 @@ import java.util.LinkedHashMap;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
import java.util.TreeSet;
|
||||||
import java.util.regex.Matcher;
|
import java.util.regex.Matcher;
|
||||||
|
|
||||||
import static java.lang.Character.MAX_RADIX;
|
import static java.lang.Character.MAX_RADIX;
|
||||||
@ -1506,5 +1507,6 @@ public class TbUtils {
|
|||||||
}
|
}
|
||||||
return hex;
|
return hex;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,66 @@
|
|||||||
|
/**
|
||||||
|
* 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.script.api.tbel;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class TbelCfTsMultiDoubleVal implements TbelCfObject {
|
||||||
|
|
||||||
|
public static final long OBJ_SIZE = 32L; // Approximate calculation;
|
||||||
|
|
||||||
|
private final long ts;
|
||||||
|
private final double[] values;
|
||||||
|
|
||||||
|
@JsonIgnore
|
||||||
|
public double getV1() {
|
||||||
|
return getV(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonIgnore
|
||||||
|
public double getV2() {
|
||||||
|
return getV(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonIgnore
|
||||||
|
public double getV3() {
|
||||||
|
return getV(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonIgnore
|
||||||
|
public double getV4() {
|
||||||
|
return getV(3);
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonIgnore
|
||||||
|
public double getV5() {
|
||||||
|
return getV(4);
|
||||||
|
}
|
||||||
|
|
||||||
|
private double getV(int idx) {
|
||||||
|
if (values.length < idx + 1) {
|
||||||
|
throw new IllegalArgumentException("Can't get value at index " + idx + ". There are " + values.length + " values present.");
|
||||||
|
} else {
|
||||||
|
return values[idx];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long memorySize() {
|
||||||
|
return OBJ_SIZE + values.length * 8L;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -19,11 +19,15 @@ import com.fasterxml.jackson.annotation.JsonCreator;
|
|||||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
|
import org.thingsboard.common.util.JacksonUtil;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.Iterator;
|
import java.util.Iterator;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.TreeSet;
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
import static org.thingsboard.script.api.tbel.TbelCfTsDoubleVal.OBJ_SIZE;
|
import static org.thingsboard.script.api.tbel.TbelCfTsDoubleVal.OBJ_SIZE;
|
||||||
@ -44,9 +48,9 @@ public class TbelCfTsRollingArg implements TbelCfArg, Iterable<TbelCfTsDoubleVal
|
|||||||
this.values = Collections.unmodifiableList(values);
|
this.values = Collections.unmodifiableList(values);
|
||||||
}
|
}
|
||||||
|
|
||||||
public TbelCfTsRollingArg(int limit, long timeWindow, List<TbelCfTsDoubleVal> values) {
|
public TbelCfTsRollingArg(long timeWindow, List<TbelCfTsDoubleVal> values) {
|
||||||
long ts = System.currentTimeMillis();
|
long ts = System.currentTimeMillis();
|
||||||
this.timeWindow = new TbTimeWindow(ts - timeWindow, ts, limit);
|
this.timeWindow = new TbTimeWindow(ts - timeWindow, ts);
|
||||||
this.values = Collections.unmodifiableList(values);
|
this.values = Collections.unmodifiableList(values);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -104,6 +108,14 @@ public class TbelCfTsRollingArg implements TbelCfArg, Iterable<TbelCfTsDoubleVal
|
|||||||
return min;
|
return min;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public double avg() {
|
||||||
|
return avg(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public double avg(boolean ignoreNaN) {
|
||||||
|
return mean(ignoreNaN);
|
||||||
|
}
|
||||||
|
|
||||||
public double mean() {
|
public double mean() {
|
||||||
return mean(true);
|
return mean(true);
|
||||||
}
|
}
|
||||||
@ -256,6 +268,88 @@ public class TbelCfTsRollingArg implements TbelCfArg, Iterable<TbelCfTsDoubleVal
|
|||||||
return sum;
|
return sum;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public TbelCfTsRollingData merge(TbelCfTsRollingArg other) {
|
||||||
|
return mergeAll(Collections.singletonList(other), null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public TbelCfTsRollingData merge(TbelCfTsRollingArg other, Map<String, Object> settings) {
|
||||||
|
return mergeAll(Collections.singletonList(other), settings);
|
||||||
|
}
|
||||||
|
|
||||||
|
public TbelCfTsRollingData mergeAll(List<TbelCfTsRollingArg> others) {
|
||||||
|
return mergeAll(others, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public TbelCfTsRollingData mergeAll(List<TbelCfTsRollingArg> others, Map<String, Object> settings) {
|
||||||
|
List<TbelCfTsRollingArg> args = new ArrayList<>(others.size() + 1);
|
||||||
|
args.add(this);
|
||||||
|
args.addAll(others);
|
||||||
|
|
||||||
|
boolean ignoreNaN = true;
|
||||||
|
if (settings != null && settings.containsKey("ignoreNaN")) {
|
||||||
|
ignoreNaN = Boolean.parseBoolean(settings.get("ignoreNaN").toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
TbTimeWindow timeWindow = null;
|
||||||
|
if (settings != null && settings.containsKey("timeWindow")) {
|
||||||
|
var twVar = settings.get("timeWindow");
|
||||||
|
if (twVar instanceof TbTimeWindow) {
|
||||||
|
timeWindow = (TbTimeWindow) settings.get("timeWindow");
|
||||||
|
} else if (twVar instanceof Map twMap) {
|
||||||
|
timeWindow = new TbTimeWindow(Long.valueOf(twMap.get("startTs").toString()), Long.valueOf(twMap.get("endTs").toString()));
|
||||||
|
} else {
|
||||||
|
timeWindow = JacksonUtil.fromString(settings.get("timeWindow").toString(), TbTimeWindow.class);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TreeSet<Long> allTimestamps = new TreeSet<>();
|
||||||
|
long startTs = Long.MAX_VALUE;
|
||||||
|
long endTs = Long.MIN_VALUE;
|
||||||
|
for (TbelCfTsRollingArg arg : args) {
|
||||||
|
for (TbelCfTsDoubleVal val : arg.getValues()) {
|
||||||
|
allTimestamps.add(val.getTs());
|
||||||
|
}
|
||||||
|
startTs = Math.min(startTs, arg.getTimeWindow().getStartTs());
|
||||||
|
endTs = Math.max(endTs, arg.getTimeWindow().getEndTs());
|
||||||
|
}
|
||||||
|
|
||||||
|
List<TbelCfTsMultiDoubleVal> data = new ArrayList<>();
|
||||||
|
|
||||||
|
int[] lastIndex = new int[args.size()];
|
||||||
|
double[] result = new double[args.size()];
|
||||||
|
Arrays.fill(result, Double.NaN);
|
||||||
|
|
||||||
|
for (long ts : allTimestamps) {
|
||||||
|
for (int i = 0; i < args.size(); i++) {
|
||||||
|
var arg = args.get(i);
|
||||||
|
var values = arg.getValues();
|
||||||
|
while (lastIndex[i] < values.size() && values.get(lastIndex[i]).getTs() <= ts) {
|
||||||
|
result[i] = values.get(lastIndex[i]).getValue();
|
||||||
|
lastIndex[i]++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (timeWindow == null || timeWindow.matches(ts)) {
|
||||||
|
if (ignoreNaN) {
|
||||||
|
boolean skip = false;
|
||||||
|
for (int i = 0; i < args.size(); i++) {
|
||||||
|
if (Double.isNaN(result[i])) {
|
||||||
|
skip = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!skip) {
|
||||||
|
data.add(new TbelCfTsMultiDoubleVal(ts, Arrays.copyOf(result, result.length)));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
data.add(new TbelCfTsMultiDoubleVal(ts, Arrays.copyOf(result, result.length)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new TbelCfTsRollingData(timeWindow != null ? timeWindow : new TbTimeWindow(startTs, endTs), data);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@JsonIgnore
|
@JsonIgnore
|
||||||
public int getSize() {
|
public int getSize() {
|
||||||
return values.size();
|
return values.size();
|
||||||
@ -266,11 +360,6 @@ public class TbelCfTsRollingArg implements TbelCfArg, Iterable<TbelCfTsDoubleVal
|
|||||||
return values.iterator();
|
return values.iterator();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public void forEach(Consumer<? super TbelCfTsDoubleVal> action) {
|
|
||||||
values.forEach(action);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getType() {
|
public String getType() {
|
||||||
return "TS_ROLLING";
|
return "TS_ROLLING";
|
||||||
|
|||||||
@ -0,0 +1,61 @@
|
|||||||
|
/**
|
||||||
|
* 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.script.api.tbel;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.Iterator;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
|
import static org.thingsboard.script.api.tbel.TbelCfTsDoubleVal.OBJ_SIZE;
|
||||||
|
|
||||||
|
public class TbelCfTsRollingData implements TbelCfObject, Iterable<TbelCfTsMultiDoubleVal> {
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
private final TbTimeWindow timeWindow;
|
||||||
|
@Getter
|
||||||
|
private final List<TbelCfTsMultiDoubleVal> values;
|
||||||
|
|
||||||
|
public TbelCfTsRollingData(TbTimeWindow timeWindow, List<TbelCfTsMultiDoubleVal> values) {
|
||||||
|
this.timeWindow = timeWindow;
|
||||||
|
this.values = Collections.unmodifiableList(values);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long memorySize() {
|
||||||
|
return 12 + values.size() * OBJ_SIZE;
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonIgnore
|
||||||
|
public List<TbelCfTsMultiDoubleVal> getValue() {
|
||||||
|
return values;
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonIgnore
|
||||||
|
public int getSize() {
|
||||||
|
return values.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Iterator<TbelCfTsMultiDoubleVal> iterator() {
|
||||||
|
return values.iterator();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -1123,6 +1123,7 @@ public class TbUtilsTest {
|
|||||||
TbUtils.base64ToBytesList(ctx, null);
|
TbUtils.base64ToBytesList(ctx, null);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void bytesToHex_Test() {
|
public void bytesToHex_Test() {
|
||||||
byte[] bb = {(byte) 0xBB, (byte) 0xAA};
|
byte[] bb = {(byte) 0xBB, (byte) 0xAA};
|
||||||
|
|||||||
@ -15,10 +15,15 @@
|
|||||||
*/
|
*/
|
||||||
package org.thingsboard.script.api.tbel;
|
package org.thingsboard.script.api.tbel;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Assertions;
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.thingsboard.common.util.JacksonUtil;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||||
@ -33,7 +38,7 @@ public class TbelCfTsRollingArgTest {
|
|||||||
@BeforeEach
|
@BeforeEach
|
||||||
void setUp() {
|
void setUp() {
|
||||||
rollingArg = new TbelCfTsRollingArg(
|
rollingArg = new TbelCfTsRollingArg(
|
||||||
new TbTimeWindow(ts - 30000, ts - 10, 10),
|
new TbTimeWindow(ts - 30000, ts - 10),
|
||||||
List.of(
|
List.of(
|
||||||
new TbelCfTsDoubleVal(ts - 10, Double.NaN),
|
new TbelCfTsDoubleVal(ts - 10, Double.NaN),
|
||||||
new TbelCfTsDoubleVal(ts - 20, 2.0),
|
new TbelCfTsDoubleVal(ts - 20, 2.0),
|
||||||
@ -98,7 +103,7 @@ public class TbelCfTsRollingArgTest {
|
|||||||
void testFirstAndLastWhenOnlyNaNAndIgnoreNaNIsFalse() {
|
void testFirstAndLastWhenOnlyNaNAndIgnoreNaNIsFalse() {
|
||||||
assertThat(rollingArg.first()).isEqualTo(2.0);
|
assertThat(rollingArg.first()).isEqualTo(2.0);
|
||||||
rollingArg = new TbelCfTsRollingArg(
|
rollingArg = new TbelCfTsRollingArg(
|
||||||
new TbTimeWindow(ts - 30000, ts - 10, 10),
|
new TbTimeWindow(ts - 30000, ts - 10),
|
||||||
List.of(
|
List.of(
|
||||||
new TbelCfTsDoubleVal(ts - 10, Double.NaN),
|
new TbelCfTsDoubleVal(ts - 10, Double.NaN),
|
||||||
new TbelCfTsDoubleVal(ts - 40, Double.NaN),
|
new TbelCfTsDoubleVal(ts - 40, Double.NaN),
|
||||||
@ -117,7 +122,7 @@ public class TbelCfTsRollingArgTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testEmptyValues() {
|
void testEmptyValues() {
|
||||||
rollingArg = new TbelCfTsRollingArg(new TbTimeWindow(0, 10, 10), List.of());
|
rollingArg = new TbelCfTsRollingArg(new TbTimeWindow(0, 10), List.of());
|
||||||
assertThatThrownBy(rollingArg::sum).isInstanceOf(IllegalArgumentException.class).hasMessage("Rolling argument values are empty.");
|
assertThatThrownBy(rollingArg::sum).isInstanceOf(IllegalArgumentException.class).hasMessage("Rolling argument values are empty.");
|
||||||
assertThatThrownBy(rollingArg::max).isInstanceOf(IllegalArgumentException.class).hasMessage("Rolling argument values are empty.");
|
assertThatThrownBy(rollingArg::max).isInstanceOf(IllegalArgumentException.class).hasMessage("Rolling argument values are empty.");
|
||||||
assertThatThrownBy(rollingArg::min).isInstanceOf(IllegalArgumentException.class).hasMessage("Rolling argument values are empty.");
|
assertThatThrownBy(rollingArg::min).isInstanceOf(IllegalArgumentException.class).hasMessage("Rolling argument values are empty.");
|
||||||
@ -128,4 +133,81 @@ public class TbelCfTsRollingArgTest {
|
|||||||
assertThatThrownBy(rollingArg::last).isInstanceOf(IllegalArgumentException.class).hasMessage("Rolling argument values are empty.");
|
assertThatThrownBy(rollingArg::last).isInstanceOf(IllegalArgumentException.class).hasMessage("Rolling argument values are empty.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void merge_two_rolling_args_ts_match_test() {
|
||||||
|
TbTimeWindow tw = new TbTimeWindow(0, 60000);
|
||||||
|
TbelCfTsRollingArg arg1 = new TbelCfTsRollingArg(tw, Arrays.asList(new TbelCfTsDoubleVal(1000, 1), new TbelCfTsDoubleVal(5000, 2), new TbelCfTsDoubleVal(15000, 3)));
|
||||||
|
TbelCfTsRollingArg arg2 = new TbelCfTsRollingArg(tw, Arrays.asList(new TbelCfTsDoubleVal(1000, 11), new TbelCfTsDoubleVal(5000, 12), new TbelCfTsDoubleVal(15000, 13)));
|
||||||
|
|
||||||
|
var result = arg1.merge(arg2);
|
||||||
|
Assertions.assertEquals(3, result.getSize());
|
||||||
|
Assertions.assertNotNull(result.getValues());
|
||||||
|
Assertions.assertNotNull(result.getValues().get(0));
|
||||||
|
Assertions.assertEquals(1000L, result.getValues().get(0).getTs());
|
||||||
|
Assertions.assertEquals(1, result.getValues().get(0).getValues()[0]);
|
||||||
|
Assertions.assertEquals(11, result.getValues().get(0).getValues()[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void merge_two_rolling_args_with_timewindow_test() {
|
||||||
|
TbTimeWindow tw = new TbTimeWindow(0, 60000);
|
||||||
|
TbelCfTsRollingArg arg1 = new TbelCfTsRollingArg(tw, Arrays.asList(new TbelCfTsDoubleVal(1000, 1), new TbelCfTsDoubleVal(5000, 2), new TbelCfTsDoubleVal(15000, 3)));
|
||||||
|
TbelCfTsRollingArg arg2 = new TbelCfTsRollingArg(tw, Arrays.asList(new TbelCfTsDoubleVal(1000, 11), new TbelCfTsDoubleVal(5000, 12), new TbelCfTsDoubleVal(15000, 13)));
|
||||||
|
|
||||||
|
var result = arg1.merge(arg2, Collections.singletonMap("timeWindow", new TbTimeWindow(0, 10000)));
|
||||||
|
Assertions.assertEquals(2, result.getSize());
|
||||||
|
Assertions.assertNotNull(result.getValues());
|
||||||
|
Assertions.assertNotNull(result.getValues().get(0));
|
||||||
|
Assertions.assertEquals(1000L, result.getValues().get(0).getTs());
|
||||||
|
Assertions.assertEquals(1, result.getValues().get(0).getValues()[0]);
|
||||||
|
Assertions.assertEquals(11, result.getValues().get(0).getValues()[1]);
|
||||||
|
|
||||||
|
result = arg1.merge(arg2, Collections.singletonMap("timeWindow", Map.of("startTs", 0L, "endTs", 10000)));
|
||||||
|
Assertions.assertEquals(2, result.getSize());
|
||||||
|
Assertions.assertNotNull(result.getValues());
|
||||||
|
Assertions.assertNotNull(result.getValues().get(0));
|
||||||
|
Assertions.assertEquals(1000L, result.getValues().get(0).getTs());
|
||||||
|
Assertions.assertEquals(1, result.getValues().get(0).getValues()[0]);
|
||||||
|
Assertions.assertEquals(11, result.getValues().get(0).getValues()[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void merge_two_rolling_args_ts_mismatch_default_test() {
|
||||||
|
TbTimeWindow tw = new TbTimeWindow(0, 60000);
|
||||||
|
TbelCfTsRollingArg arg1 = new TbelCfTsRollingArg(tw, Arrays.asList(new TbelCfTsDoubleVal(100, 1), new TbelCfTsDoubleVal(5000, 2), new TbelCfTsDoubleVal(15000, 3)));
|
||||||
|
TbelCfTsRollingArg arg2 = new TbelCfTsRollingArg(tw, Arrays.asList(new TbelCfTsDoubleVal(200, 11), new TbelCfTsDoubleVal(5000, 12), new TbelCfTsDoubleVal(15000, 13)));
|
||||||
|
|
||||||
|
var result = arg1.merge(arg2);
|
||||||
|
Assertions.assertEquals(3, result.getSize());
|
||||||
|
Assertions.assertNotNull(result.getValues());
|
||||||
|
|
||||||
|
TbelCfTsMultiDoubleVal item0 = result.getValues().get(0);
|
||||||
|
Assertions.assertNotNull(item0);
|
||||||
|
Assertions.assertEquals(200L, item0.getTs());
|
||||||
|
Assertions.assertEquals(1, item0.getValues()[0]);
|
||||||
|
Assertions.assertEquals(11, item0.getValues()[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void merge_two_rolling_args_ts_mismatch_ignore_nan_disabled_test() {
|
||||||
|
TbTimeWindow tw = new TbTimeWindow(0, 60000);
|
||||||
|
TbelCfTsRollingArg arg1 = new TbelCfTsRollingArg(tw, Arrays.asList(new TbelCfTsDoubleVal(100, 1), new TbelCfTsDoubleVal(5000, 2), new TbelCfTsDoubleVal(15000, 3)));
|
||||||
|
TbelCfTsRollingArg arg2 = new TbelCfTsRollingArg(tw, Arrays.asList(new TbelCfTsDoubleVal(200, 11), new TbelCfTsDoubleVal(5000, 12), new TbelCfTsDoubleVal(15000, 13)));
|
||||||
|
|
||||||
|
var result = arg1.merge(arg2, Collections.singletonMap("ignoreNaN", false));
|
||||||
|
Assertions.assertEquals(4, result.getSize());
|
||||||
|
Assertions.assertNotNull(result.getValues());
|
||||||
|
|
||||||
|
TbelCfTsMultiDoubleVal item0 = result.getValues().get(0);
|
||||||
|
Assertions.assertNotNull(item0);
|
||||||
|
Assertions.assertEquals(100L, item0.getTs());
|
||||||
|
Assertions.assertEquals(1, item0.getValues()[0]);
|
||||||
|
Assertions.assertEquals(Double.NaN, item0.getValues()[1]);
|
||||||
|
|
||||||
|
TbelCfTsMultiDoubleVal item1 = result.getValues().get(1);
|
||||||
|
Assertions.assertEquals(200L, item1.getTs());
|
||||||
|
Assertions.assertEquals(1, item1.getValues()[0]);
|
||||||
|
Assertions.assertEquals(11, item1.getValues()[1]);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -23,6 +23,7 @@ import { CalculatedField, CalculatedFieldTestScriptInputParams } from '@shared/m
|
|||||||
import { PageLink } from '@shared/models/page/page-link';
|
import { PageLink } from '@shared/models/page/page-link';
|
||||||
import { EntityId } from '@shared/models/id/entity-id';
|
import { EntityId } from '@shared/models/id/entity-id';
|
||||||
import { EntityTestScriptResult } from '@shared/models/entity.models';
|
import { EntityTestScriptResult } from '@shared/models/entity.models';
|
||||||
|
import { CalculatedFieldEventBody } from '@shared/models/event.models';
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root'
|
providedIn: 'root'
|
||||||
@ -53,4 +54,8 @@ export class CalculatedFieldsService {
|
|||||||
public testScript(inputParams: CalculatedFieldTestScriptInputParams, config?: RequestConfig): Observable<EntityTestScriptResult> {
|
public testScript(inputParams: CalculatedFieldTestScriptInputParams, config?: RequestConfig): Observable<EntityTestScriptResult> {
|
||||||
return this.http.post<EntityTestScriptResult>('/api/calculatedField/testScript', inputParams, defaultHttpOptionsFromConfig(config));
|
return this.http.post<EntityTestScriptResult>('/api/calculatedField/testScript', inputParams, defaultHttpOptionsFromConfig(config));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public getLatestCalculatedFieldDebugEvent(id: string, config?: RequestConfig): Observable<CalculatedFieldEventBody> {
|
||||||
|
return this.http.get<CalculatedFieldEventBody>(`/api/calculatedField/${id}/debug`, defaultHttpOptionsFromConfig(config));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -35,10 +35,11 @@ import {
|
|||||||
import { noLeadTrailSpacesRegex } from '@shared/models/regex.constants';
|
import { noLeadTrailSpacesRegex } from '@shared/models/regex.constants';
|
||||||
import { AttributeScope } from '@shared/models/telemetry/telemetry.models';
|
import { AttributeScope } from '@shared/models/telemetry/telemetry.models';
|
||||||
import { EntityType } from '@shared/models/entity-type.models';
|
import { EntityType } from '@shared/models/entity-type.models';
|
||||||
import { map, startWith } from 'rxjs/operators';
|
import { map, startWith, switchMap } from 'rxjs/operators';
|
||||||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||||
import { ScriptLanguage } from '@shared/models/rule-node.models';
|
import { ScriptLanguage } from '@shared/models/rule-node.models';
|
||||||
import { CalculatedFieldsService } from '@core/http/calculated-fields.service';
|
import { CalculatedFieldsService } from '@core/http/calculated-fields.service';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'tb-calculated-field-dialog',
|
selector: 'tb-calculated-field-dialog',
|
||||||
@ -136,7 +137,23 @@ export class CalculatedFieldDialogComponent extends DialogComponent<CalculatedFi
|
|||||||
}
|
}
|
||||||
|
|
||||||
onTestScript(): void {
|
onTestScript(): void {
|
||||||
this.data.getTestScriptDialogFn(this.fromGroupValue, null, false).subscribe(expression => {
|
const calculatedFieldId = this.data.value?.id?.id;
|
||||||
|
let testScriptDialogResult$: Observable<string>;
|
||||||
|
|
||||||
|
if (calculatedFieldId) {
|
||||||
|
testScriptDialogResult$ = this.calculatedFieldsService.getLatestCalculatedFieldDebugEvent(calculatedFieldId)
|
||||||
|
.pipe(
|
||||||
|
switchMap(event => {
|
||||||
|
const args = event?.arguments ? JSON.parse(event.arguments) : null;
|
||||||
|
return this.data.getTestScriptDialogFn(this.fromGroupValue, args, false);
|
||||||
|
}),
|
||||||
|
takeUntilDestroyed(this.destroyRef)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
testScriptDialogResult$ = this.data.getTestScriptDialogFn(this.fromGroupValue, null, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
testScriptDialogResult$.subscribe(expression => {
|
||||||
this.configFormGroup.get('expressionSCRIPT').setValue(expression);
|
this.configFormGroup.get('expressionSCRIPT').setValue(expression);
|
||||||
this.configFormGroup.get('expressionSCRIPT').markAsDirty();
|
this.configFormGroup.get('expressionSCRIPT').markAsDirty();
|
||||||
});
|
});
|
||||||
|
|||||||
@ -272,7 +272,7 @@ export const CalculatedFieldAttributeValueArgumentAutocomplete = {
|
|||||||
export const CalculatedFieldRollingValueArgumentFunctionsAutocomplete = {
|
export const CalculatedFieldRollingValueArgumentFunctionsAutocomplete = {
|
||||||
max: {
|
max: {
|
||||||
meta: 'function',
|
meta: 'function',
|
||||||
description: 'Computes the maximum value in the list of rolling argument values. Returns NaN if any value is NaN and ignoreNaN is false.',
|
description: 'Returns the maximum value of the rolling argument values. Returns NaN if any value is NaN and ignoreNaN is false.',
|
||||||
args: [
|
args: [
|
||||||
{
|
{
|
||||||
name: 'ignoreNaN',
|
name: 'ignoreNaN',
|
||||||
@ -288,7 +288,7 @@ export const CalculatedFieldRollingValueArgumentFunctionsAutocomplete = {
|
|||||||
},
|
},
|
||||||
min: {
|
min: {
|
||||||
meta: 'function',
|
meta: 'function',
|
||||||
description: 'Computes the minimum value in the list of rolling argument values. Returns NaN if any value is NaN and ignoreNaN is false.',
|
description: 'Returns the minimum value of the rolling argument values. Returns NaN if any value is NaN and ignoreNaN is false.',
|
||||||
args: [
|
args: [
|
||||||
{
|
{
|
||||||
name: 'ignoreNaN',
|
name: 'ignoreNaN',
|
||||||
@ -304,7 +304,7 @@ export const CalculatedFieldRollingValueArgumentFunctionsAutocomplete = {
|
|||||||
},
|
},
|
||||||
mean: {
|
mean: {
|
||||||
meta: 'function',
|
meta: 'function',
|
||||||
description: 'Computes the mean value of the rolling argument values list. Returns NaN if any value is NaN and ignoreNaN is false.',
|
description: 'Computes the mean value of the rolling argument values. Returns NaN if any value is NaN and ignoreNaN is false.',
|
||||||
args: [
|
args: [
|
||||||
{
|
{
|
||||||
name: 'ignoreNaN',
|
name: 'ignoreNaN',
|
||||||
@ -318,9 +318,25 @@ export const CalculatedFieldRollingValueArgumentFunctionsAutocomplete = {
|
|||||||
type: 'number'
|
type: 'number'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
avg: {
|
||||||
|
meta: 'function',
|
||||||
|
description: 'Computes the average value of the rolling argument values. Returns NaN if any value is NaN and ignoreNaN is false.',
|
||||||
|
args: [
|
||||||
|
{
|
||||||
|
name: 'ignoreNaN',
|
||||||
|
description: 'Whether to ignore NaN values. Equals true by default.',
|
||||||
|
type: 'boolean',
|
||||||
|
optional: true,
|
||||||
|
}
|
||||||
|
],
|
||||||
|
return: {
|
||||||
|
description: 'The average value, or NaN if applicable',
|
||||||
|
type: 'number'
|
||||||
|
}
|
||||||
|
},
|
||||||
std: {
|
std: {
|
||||||
meta: 'function',
|
meta: 'function',
|
||||||
description: 'Computes the standard deviation in the list of rolling argument values. Returns NaN if any value is NaN and ignoreNaN is false.',
|
description: 'Computes the standard deviation of the rolling argument values. Returns NaN if any value is NaN and ignoreNaN is false.',
|
||||||
args: [
|
args: [
|
||||||
{
|
{
|
||||||
name: 'ignoreNaN',
|
name: 'ignoreNaN',
|
||||||
@ -336,7 +352,7 @@ export const CalculatedFieldRollingValueArgumentFunctionsAutocomplete = {
|
|||||||
},
|
},
|
||||||
median: {
|
median: {
|
||||||
meta: 'function',
|
meta: 'function',
|
||||||
description: 'Computes the median value of the rolling argument values list. Returns NaN if any value is NaN and ignoreNaN is false.',
|
description: 'Computes the median value of the rolling argument values. Returns NaN if any value is NaN and ignoreNaN is false.',
|
||||||
args: [
|
args: [
|
||||||
{
|
{
|
||||||
name: 'ignoreNaN',
|
name: 'ignoreNaN',
|
||||||
@ -352,7 +368,7 @@ export const CalculatedFieldRollingValueArgumentFunctionsAutocomplete = {
|
|||||||
},
|
},
|
||||||
count: {
|
count: {
|
||||||
meta: 'function',
|
meta: 'function',
|
||||||
description: 'Counts values in the list of rolling argument values. Counts non-NaN values if ignoreNaN is true, otherwise - total size.',
|
description: 'Counts values of the rolling argument. Counts non-NaN values if ignoreNaN is true, otherwise - total size.',
|
||||||
args: [
|
args: [
|
||||||
{
|
{
|
||||||
name: 'ignoreNaN',
|
name: 'ignoreNaN',
|
||||||
@ -368,7 +384,7 @@ export const CalculatedFieldRollingValueArgumentFunctionsAutocomplete = {
|
|||||||
},
|
},
|
||||||
last: {
|
last: {
|
||||||
meta: 'function',
|
meta: 'function',
|
||||||
description: 'Returns the last non-NaN value in the list of rolling argument values if ignoreNaN is true, otherwise - the last value.',
|
description: 'Returns the last non-NaN value of the rolling argument values if ignoreNaN is true, otherwise - the last value.',
|
||||||
args: [
|
args: [
|
||||||
{
|
{
|
||||||
name: 'ignoreNaN',
|
name: 'ignoreNaN',
|
||||||
@ -384,7 +400,7 @@ export const CalculatedFieldRollingValueArgumentFunctionsAutocomplete = {
|
|||||||
},
|
},
|
||||||
first: {
|
first: {
|
||||||
meta: 'function',
|
meta: 'function',
|
||||||
description: 'Returns the first non-NaN value in the list of rolling argument values if ignoreNaN is true, otherwise - the first value.',
|
description: 'Returns the first non-NaN value of the rolling argument values if ignoreNaN is true, otherwise - the first value.',
|
||||||
args: [
|
args: [
|
||||||
{
|
{
|
||||||
name: 'ignoreNaN',
|
name: 'ignoreNaN',
|
||||||
@ -400,7 +416,7 @@ export const CalculatedFieldRollingValueArgumentFunctionsAutocomplete = {
|
|||||||
},
|
},
|
||||||
sum: {
|
sum: {
|
||||||
meta: 'function',
|
meta: 'function',
|
||||||
description: 'Computes the sum of values in the list of rolling argument values. Returns NaN if any value is NaN and ignoreNaN is false.',
|
description: 'Computes the sum of rolling argument values. Returns NaN if any value is NaN and ignoreNaN is false.',
|
||||||
args: [
|
args: [
|
||||||
{
|
{
|
||||||
name: 'ignoreNaN',
|
name: 'ignoreNaN',
|
||||||
@ -413,12 +429,56 @@ export const CalculatedFieldRollingValueArgumentFunctionsAutocomplete = {
|
|||||||
description: 'The sum of values, or NaN if applicable',
|
description: 'The sum of values, or NaN if applicable',
|
||||||
type: 'number'
|
type: 'number'
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
merge: {
|
||||||
|
meta: 'function',
|
||||||
|
description: 'Merges current object with other time series rolling argument into a single object by aligning their timestamped values. Supports optional configurable settings.',
|
||||||
|
args: [
|
||||||
|
{
|
||||||
|
name: 'other',
|
||||||
|
description: "A time series rolling argument to be merged with the current object.",
|
||||||
|
type: "object",
|
||||||
|
optional: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "settings",
|
||||||
|
description: "Optional settings controlling the merging process. Supported keys: 'ignoreNaN' (boolean, equals true by default) to determine whether NaN values should be ignored; 'timeWindow' (object, empty by default) to apply time window filtering.",
|
||||||
|
type: "object",
|
||||||
|
optional: true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
return: {
|
||||||
|
description: 'A new object containing merged timestamped values from all provided arguments, aligned based on timestamps and filtered according to settings.',
|
||||||
|
type: '{ values: { ts: number; values: number[]; }[]; timeWindow: { startTs: number; endTs: number } }; }',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mergeAll: {
|
||||||
|
meta: 'function',
|
||||||
|
description: 'Merges current object with other time series rolling arguments into a single object by aligning their timestamped values. Supports optional configurable settings.',
|
||||||
|
args: [
|
||||||
|
{
|
||||||
|
name: 'others',
|
||||||
|
description: "A list of time series rolling arguments to be merged with the current object.",
|
||||||
|
type: "object[]",
|
||||||
|
optional: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "settings",
|
||||||
|
description: "Optional settings controlling the merging process. Supported keys: 'ignoreNaN' (boolean, equals true by default) to determine whether NaN values should be ignored; 'timeWindow' (object, empty by default) to apply time window filtering.",
|
||||||
|
type: "object",
|
||||||
|
optional: true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
return: {
|
||||||
|
description: 'A new object containing merged timestamped values from all provided arguments, aligned based on timestamps and filtered according to settings.',
|
||||||
|
type: '{ values: { ts: number; values: number[]; }[]; timeWindow: { startTs: number; endTs: number } }; }',
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const CalculatedFieldRollingValueArgumentAutocomplete = {
|
export const CalculatedFieldRollingValueArgumentAutocomplete = {
|
||||||
meta: 'object',
|
meta: 'object',
|
||||||
type: '{ values: { ts: number; value: any; }[]; timeWindow: { startTs: number; endTs: number; limit: number } }; }',
|
type: '{ values: { ts: number; value: number; }[]; timeWindow: { startTs: number; endTs: number } }; }',
|
||||||
description: 'Calculated field rolling value argument.',
|
description: 'Calculated field rolling value argument.',
|
||||||
children: {
|
children: {
|
||||||
...CalculatedFieldRollingValueArgumentFunctionsAutocomplete,
|
...CalculatedFieldRollingValueArgumentFunctionsAutocomplete,
|
||||||
@ -429,7 +489,7 @@ export const CalculatedFieldRollingValueArgumentAutocomplete = {
|
|||||||
},
|
},
|
||||||
timeWindow: {
|
timeWindow: {
|
||||||
meta: 'object',
|
meta: 'object',
|
||||||
type: '{ startTs: number; endTs: number; limit: number }',
|
type: '{ startTs: number; endTs: number }',
|
||||||
description: 'Time window configuration',
|
description: 'Time window configuration',
|
||||||
children: {
|
children: {
|
||||||
startTs: {
|
startTs: {
|
||||||
@ -441,11 +501,6 @@ export const CalculatedFieldRollingValueArgumentAutocomplete = {
|
|||||||
meta: 'number',
|
meta: 'number',
|
||||||
type: 'number',
|
type: 'number',
|
||||||
description: 'End time stamp',
|
description: 'End time stamp',
|
||||||
},
|
|
||||||
limit: {
|
|
||||||
meta: 'number',
|
|
||||||
type: 'number',
|
|
||||||
description: 'Limit',
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -504,7 +559,7 @@ const calculatedFieldSingleArgumentValueHighlightRules: AceHighlightRules = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const calculatedFieldRollingArgumentValueFunctionsHighlightRules: Array<AceHighlightRule> =
|
const calculatedFieldRollingArgumentValueFunctionsHighlightRules: Array<AceHighlightRule> =
|
||||||
['max', 'min', 'mean', 'std', 'median', 'count', 'last', 'first', 'sum'].map(funcName => ({
|
['max', 'min', 'avg', 'mean', 'std', 'median', 'count', 'last', 'first', 'sum', 'merge', 'mergeAll'].map(funcName => ({
|
||||||
token: 'tb.calculated-field-func',
|
token: 'tb.calculated-field-func',
|
||||||
regex: `\\b${funcName}\\b`,
|
regex: `\\b${funcName}\\b`,
|
||||||
next: 'no_regex'
|
next: 'no_regex'
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user