From 5372f22b8a314e84823d39cb4a19a762f206cf59 Mon Sep 17 00:00:00 2001 From: mpetrov Date: Fri, 28 Feb 2025 17:08:24 +0200 Subject: [PATCH 1/5] Added latest debug event call in Calculated Field Dialog on Test function --- .../core/http/calculated-fields.service.ts | 5 +++++ .../calculated-field-dialog.component.ts | 22 ++++++++++++++----- 2 files changed, 22 insertions(+), 5 deletions(-) diff --git a/ui-ngx/src/app/core/http/calculated-fields.service.ts b/ui-ngx/src/app/core/http/calculated-fields.service.ts index fe5b0f7b52..66c0cb609e 100644 --- a/ui-ngx/src/app/core/http/calculated-fields.service.ts +++ b/ui-ngx/src/app/core/http/calculated-fields.service.ts @@ -23,6 +23,7 @@ import { CalculatedField, CalculatedFieldTestScriptInputParams } from '@shared/m import { PageLink } from '@shared/models/page/page-link'; import { EntityId } from '@shared/models/id/entity-id'; import { EntityTestScriptResult } from '@shared/models/entity.models'; +import { CalculatedFieldEventBody } from '@shared/models/event.models'; @Injectable({ providedIn: 'root' @@ -53,4 +54,8 @@ export class CalculatedFieldsService { public testScript(inputParams: CalculatedFieldTestScriptInputParams, config?: RequestConfig): Observable { return this.http.post('/api/calculatedField/testScript', inputParams, defaultHttpOptionsFromConfig(config)); } + + public getLatestCalculatedFieldDebugEvent(id: string, config?: RequestConfig): Observable { + return this.http.get(`/api/calculatedField/${id}/debug`, defaultHttpOptionsFromConfig(config)); + } } diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.ts index c3e3d52a72..65654efd70 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.ts @@ -35,7 +35,7 @@ import { import { noLeadTrailSpacesRegex } from '@shared/models/regex.constants'; import { AttributeScope } from '@shared/models/telemetry/telemetry.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 { ScriptLanguage } from '@shared/models/rule-node.models'; import { CalculatedFieldsService } from '@core/http/calculated-fields.service'; @@ -136,10 +136,22 @@ export class CalculatedFieldDialogComponent extends DialogComponent { - this.configFormGroup.get('expressionSCRIPT').setValue(expression); - this.configFormGroup.get('expressionSCRIPT').markAsDirty(); - }); + const calculatedFieldId = this.data.value?.id?.id; + + (calculatedFieldId + ? 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) + ) + : this.data.getTestScriptDialogFn(this.fromGroupValue, null, false)) + .subscribe(expression => { + this.configFormGroup.get('expressionSCRIPT').setValue(expression); + this.configFormGroup.get('expressionSCRIPT').markAsDirty(); + }); } private applyDialogData(): void { From 99beae211577c39944c136e411ab15d9e8b6f0a2 Mon Sep 17 00:00:00 2001 From: mpetrov Date: Fri, 28 Feb 2025 17:17:55 +0200 Subject: [PATCH 2/5] refactoring --- .../calculated-field-dialog.component.ts | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.ts index 65654efd70..68abd646e3 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.ts @@ -39,6 +39,7 @@ import { map, startWith, switchMap } from 'rxjs/operators'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { ScriptLanguage } from '@shared/models/rule-node.models'; import { CalculatedFieldsService } from '@core/http/calculated-fields.service'; +import { Observable } from 'rxjs'; @Component({ selector: 'tb-calculated-field-dialog', @@ -137,9 +138,10 @@ export class CalculatedFieldDialogComponent extends DialogComponent; - (calculatedFieldId - ? this.calculatedFieldsService.getLatestCalculatedFieldDebugEvent(calculatedFieldId) + if (calculatedFieldId) { + testScriptDialogResult$ = this.calculatedFieldsService.getLatestCalculatedFieldDebugEvent(calculatedFieldId) .pipe( switchMap(event => { const args = event?.arguments ? JSON.parse(event.arguments) : null; @@ -147,11 +149,14 @@ export class CalculatedFieldDialogComponent extends DialogComponent { - this.configFormGroup.get('expressionSCRIPT').setValue(expression); - this.configFormGroup.get('expressionSCRIPT').markAsDirty(); - }); + } else { + testScriptDialogResult$ = this.data.getTestScriptDialogFn(this.fromGroupValue, null, false); + } + + testScriptDialogResult$.subscribe(expression => { + this.configFormGroup.get('expressionSCRIPT').setValue(expression); + this.configFormGroup.get('expressionSCRIPT').markAsDirty(); + }); } private applyDialogData(): void { From ac52098207e4330fa44225f07bc08614d019bf9f Mon Sep 17 00:00:00 2001 From: Andrii Shvaika Date: Fri, 28 Feb 2025 17:46:25 +0200 Subject: [PATCH 3/5] CF: merge function draft --- .../api/tbel/DefaultTbelInvokeService.java | 1 + .../script/api/tbel/TbTimeWindow.java | 2 + .../thingsboard/script/api/tbel/TbUtils.java | 51 ++++++++++++++-- .../api/tbel/TbelCfTsMultiDoubleVal.java | 32 ++++++++++ .../script/api/tbel/TbelCfTsRollingArg.java | 5 -- .../script/api/tbel/TbelCfTsRollingData.java | 61 +++++++++++++++++++ 6 files changed, 143 insertions(+), 9 deletions(-) create mode 100644 common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbelCfTsMultiDoubleVal.java create mode 100644 common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbelCfTsRollingData.java diff --git a/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/DefaultTbelInvokeService.java b/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/DefaultTbelInvokeService.java index f626746a64..962d384a46 100644 --- a/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/DefaultTbelInvokeService.java +++ b/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/DefaultTbelInvokeService.java @@ -136,6 +136,7 @@ public class DefaultTbelInvokeService extends AbstractScriptInvokeService implem parserConfig.registerDataType("TbelCfSingleValueArg", TbelCfSingleValueArg.class, TbelCfSingleValueArg::memorySize); parserConfig.registerDataType("TbelCfTsRollingArg", TbelCfTsRollingArg.class, TbelCfTsRollingArg::memorySize); parserConfig.registerDataType("TbelCfTsDoubleVal", TbelCfTsDoubleVal.class, TbelCfTsDoubleVal::memorySize); + parserConfig.registerDataType("TbelCfTsRollingData", TbelCfTsRollingData.class, TbelCfTsRollingData::memorySize); parserConfig.registerDataType("TbTimeWindow", TbTimeWindow.class, TbTimeWindow::memorySize); TbUtils.register(parserConfig); executor = MoreExecutors.listeningDecorator(ThingsBoardExecutors.newWorkStealingPool(threadPoolSize, "tbel-executor")); diff --git a/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbTimeWindow.java b/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbTimeWindow.java index ce61965317..5048611838 100644 --- a/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbTimeWindow.java +++ b/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbTimeWindow.java @@ -17,9 +17,11 @@ package org.thingsboard.script.api.tbel; import lombok.AllArgsConstructor; import lombok.Data; +import lombok.NoArgsConstructor; @Data @AllArgsConstructor +@NoArgsConstructor public class TbTimeWindow implements TbelCfObject { public static final long OBJ_SIZE = 32L; diff --git a/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbUtils.java b/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbUtils.java index 9a57240d23..b108bbe33e 100644 --- a/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbUtils.java +++ b/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbUtils.java @@ -1,12 +1,12 @@ /** * 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 - * + *

+ * 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. @@ -44,6 +44,7 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.TreeSet; import java.util.regex.Matcher; import static java.lang.Character.MAX_RADIX; @@ -367,6 +368,8 @@ public class TbUtils { byte[].class, int.class))); parserConfig.addImport("parseBinaryArrayToInt", new MethodStub(TbUtils.class.getMethod("parseBinaryArrayToInt", byte[].class, int.class, int.class))); + parserConfig.addImport("merge", new MethodStub(TbUtils.class.getMethod("mergeCfTsRollingArgs", + TbelCfTsRollingArg.class, TbelCfTsRollingArg.class))); } public static String btoa(String input) { @@ -1506,5 +1509,45 @@ public class TbUtils { } return hex; } + + public static TbelCfTsRollingData mergeCfTsRollingArgs(TbelCfTsRollingArg a, TbelCfTsRollingArg b) { + return mergeCfTsRollingArgs(Arrays.asList(a, b), null); + } + + public static TbelCfTsRollingData mergeCfTsRollingArgs(List args, Map settings) { + TreeSet 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 data = new ArrayList<>(); + + int[] lastIndex = new int[args.size()]; + double[] result = new double[args.size()]; + Arrays.fill(result, Double.NaN); + + var tw = new TbTimeWindow(startTs, endTs, allTimestamps.size()); + + 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]++; + } + } + data.add(new TbelCfTsMultiDoubleVal(ts, Arrays.copyOf(result, result.length))); + } + + return new TbelCfTsRollingData(tw, data); + } + } diff --git a/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbelCfTsMultiDoubleVal.java b/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbelCfTsMultiDoubleVal.java new file mode 100644 index 0000000000..33e4a65bb8 --- /dev/null +++ b/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbelCfTsMultiDoubleVal.java @@ -0,0 +1,32 @@ +/** + * 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 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; + + @Override + public long memorySize() { + return OBJ_SIZE + values.length * 8L; + } +} diff --git a/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbelCfTsRollingArg.java b/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbelCfTsRollingArg.java index 807d498a16..74c9d77454 100644 --- a/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbelCfTsRollingArg.java +++ b/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbelCfTsRollingArg.java @@ -266,11 +266,6 @@ public class TbelCfTsRollingArg implements TbelCfArg, Iterable action) { - values.forEach(action); - } - @Override public String getType() { return "TS_ROLLING"; diff --git a/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbelCfTsRollingData.java b/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbelCfTsRollingData.java new file mode 100644 index 0000000000..646e826915 --- /dev/null +++ b/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbelCfTsRollingData.java @@ -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 { + + @Getter + private final TbTimeWindow timeWindow; + @Getter + private final List values; + + public TbelCfTsRollingData(TbTimeWindow timeWindow, List values) { + this.timeWindow = timeWindow; + this.values = Collections.unmodifiableList(values); + } + + @Override + public long memorySize() { + return 12 + values.size() * OBJ_SIZE; + } + + @JsonIgnore + public List getValue() { + return values; + } + + @JsonIgnore + public int getSize() { + return values.size(); + } + + @Override + public Iterator iterator() { + return values.iterator(); + } + +} From 6265ff45a8adca7c139d226d81054d287ca406bf Mon Sep 17 00:00:00 2001 From: Andrii Shvaika Date: Tue, 4 Mar 2025 11:54:28 +0200 Subject: [PATCH 4/5] merge function draft --- .../api/tbel/DefaultTbelInvokeService.java | 2 + .../script/api/tbel/TbTimeWindow.java | 3 + .../thingsboard/script/api/tbel/TbUtils.java | 60 +++++++++++++--- .../api/tbel/TbelCfTsMultiDoubleVal.java | 34 +++++++++ .../script/api/tbel/TbUtilsTest.java | 72 ++++++++++++++++++- 5 files changed, 160 insertions(+), 11 deletions(-) diff --git a/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/DefaultTbelInvokeService.java b/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/DefaultTbelInvokeService.java index 962d384a46..38fed7a2aa 100644 --- a/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/DefaultTbelInvokeService.java +++ b/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/DefaultTbelInvokeService.java @@ -138,6 +138,8 @@ public class DefaultTbelInvokeService extends AbstractScriptInvokeService implem parserConfig.registerDataType("TbelCfTsDoubleVal", TbelCfTsDoubleVal.class, TbelCfTsDoubleVal::memorySize); parserConfig.registerDataType("TbelCfTsRollingData", TbelCfTsRollingData.class, TbelCfTsRollingData::memorySize); parserConfig.registerDataType("TbTimeWindow", TbTimeWindow.class, TbTimeWindow::memorySize); + parserConfig.registerDataType("TbelCfTsDoubleVal", TbelCfTsMultiDoubleVal.class, TbelCfTsMultiDoubleVal::memorySize); + TbUtils.register(parserConfig); executor = MoreExecutors.listeningDecorator(ThingsBoardExecutors.newWorkStealingPool(threadPoolSize, "tbel-executor")); try { diff --git a/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbTimeWindow.java b/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbTimeWindow.java index 5048611838..b430b23757 100644 --- a/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbTimeWindow.java +++ b/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbTimeWindow.java @@ -35,4 +35,7 @@ public class TbTimeWindow implements TbelCfObject { return OBJ_SIZE; } + public boolean matches(long ts) { + return ts >= startTs && ts < endTs; + } } diff --git a/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbUtils.java b/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbUtils.java index 4796cf12bb..e6a0b8d043 100644 --- a/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbUtils.java +++ b/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbUtils.java @@ -1,12 +1,12 @@ /** * 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 - *

+ * + * 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. @@ -368,8 +368,16 @@ public class TbUtils { byte[].class, int.class))); parserConfig.addImport("parseBinaryArrayToInt", new MethodStub(TbUtils.class.getMethod("parseBinaryArrayToInt", byte[].class, int.class, int.class))); - parserConfig.addImport("merge", new MethodStub(TbUtils.class.getMethod("mergeCfTsRollingArgs", + parserConfig.addImport("merge", new MethodStub(TbUtils.class.getMethod("merge", TbelCfTsRollingArg.class, TbelCfTsRollingArg.class))); + parserConfig.addImport("merge", new MethodStub(TbUtils.class.getMethod("merge", + TbelCfTsRollingArg.class, TbelCfTsRollingArg.class, TbTimeWindow.class))); + parserConfig.addImport("merge", new MethodStub(TbUtils.class.getMethod("merge", + TbelCfTsRollingArg.class, TbelCfTsRollingArg.class, TbTimeWindow.class, Map.class))); + parserConfig.addImport("merge", new MethodStub(TbUtils.class.getMethod("merge", + List.class, TbTimeWindow.class))); + parserConfig.addImport("merge", new MethodStub(TbUtils.class.getMethod("merge", + List.class, TbTimeWindow.class, Map.class))); } public static String btoa(String input) { @@ -1510,11 +1518,28 @@ public class TbUtils { return hex; } - public static TbelCfTsRollingData mergeCfTsRollingArgs(TbelCfTsRollingArg a, TbelCfTsRollingArg b) { - return mergeCfTsRollingArgs(Arrays.asList(a, b), null); + public static TbelCfTsRollingData merge(TbelCfTsRollingArg a, TbelCfTsRollingArg b) { + return merge(Arrays.asList(a, b), null, null); } - public static TbelCfTsRollingData mergeCfTsRollingArgs(List args, Map settings) { + public static TbelCfTsRollingData merge(TbelCfTsRollingArg a, TbelCfTsRollingArg b, TbTimeWindow timeWindow) { + return merge(Arrays.asList(a, b), timeWindow, null); + } + + public static TbelCfTsRollingData merge(TbelCfTsRollingArg a, TbelCfTsRollingArg b, TbTimeWindow timeWindow, Map settings) { + return merge(Arrays.asList(a, b), timeWindow, settings); + } + + public static TbelCfTsRollingData merge(List args, TbTimeWindow timeWindow) { + return merge(args, timeWindow, null); + } + + public static TbelCfTsRollingData merge(List args, TbTimeWindow timeWindow, Map settings) { + boolean ignoreNaN = true; + if (settings != null && settings.containsKey("ignoreNaN")) { + ignoreNaN = Boolean.parseBoolean(settings.get("ignoreNaN").toString()); + } + TreeSet allTimestamps = new TreeSet<>(); long startTs = Long.MAX_VALUE; long endTs = Long.MIN_VALUE; @@ -1532,7 +1557,7 @@ public class TbUtils { double[] result = new double[args.size()]; Arrays.fill(result, Double.NaN); - var tw = new TbTimeWindow(startTs, endTs, allTimestamps.size()); + var tw = timeWindow != null ? timeWindow : new TbTimeWindow(startTs, endTs, allTimestamps.size()); for (long ts : allTimestamps) { for (int i = 0; i < args.size(); i++) { @@ -1543,7 +1568,22 @@ public class TbUtils { lastIndex[i]++; } } - data.add(new TbelCfTsMultiDoubleVal(ts, Arrays.copyOf(result, result.length))); + if (tw.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(tw, data); diff --git a/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbelCfTsMultiDoubleVal.java b/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbelCfTsMultiDoubleVal.java index 33e4a65bb8..2743bbd0de 100644 --- a/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbelCfTsMultiDoubleVal.java +++ b/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbelCfTsMultiDoubleVal.java @@ -15,6 +15,7 @@ */ package org.thingsboard.script.api.tbel; +import com.fasterxml.jackson.annotation.JsonIgnore; import lombok.Data; @Data @@ -25,6 +26,39 @@ public class TbelCfTsMultiDoubleVal implements TbelCfObject { 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; diff --git a/common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbUtilsTest.java b/common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbUtilsTest.java index fbd81948d6..f19a323068 100644 --- a/common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbUtilsTest.java +++ b/common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbUtilsTest.java @@ -1109,7 +1109,7 @@ public class TbUtilsTest { String validInput = Base64.getEncoder().encodeToString(new byte[]{1, 2, 3, 4, 5}); ExecutionArrayList actual = TbUtils.base64ToBytesList(ctx, validInput); ExecutionArrayList expected = new ExecutionArrayList<>(ctx); - expected.addAll(List.of((byte) 1, (byte)2, (byte)3, (byte)4, (byte)5)); + expected.addAll(List.of((byte) 1, (byte) 2, (byte) 3, (byte) 4, (byte) 5)); Assertions.assertEquals(expected, actual); String emptyInput = Base64.getEncoder().encodeToString(new byte[]{}); @@ -1123,6 +1123,7 @@ public class TbUtilsTest { TbUtils.base64ToBytesList(ctx, null); }); } + @Test public void bytesToHex_Test() { byte[] bb = {(byte) 0xBB, (byte) 0xAA}; @@ -1136,6 +1137,75 @@ public class TbUtilsTest { Assertions.assertEquals(expected, actual); } + @Test + public void merge_two_rolling_args_ts_match_test() { + TbTimeWindow tw = new TbTimeWindow(0, 60000, 1000); + 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 = TbUtils.merge(arg1, 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, 1000); + 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 = TbUtils.merge(arg1, arg2, new TbTimeWindow(0, 10000, 1000)); + 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, 1000); + 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 = TbUtils.merge(arg1, 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, 1000); + 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 = TbUtils.merge(Arrays.asList(arg1, arg2), new TbTimeWindow(0, 60000, 1000), 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]); + } + private static List toList(byte[] data) { List result = new ArrayList<>(data.length); for (Byte b : data) { From 933d77427ce0eb12967d24f94be279e26fbd5eed Mon Sep 17 00:00:00 2001 From: Andrii Shvaika Date: Tue, 4 Mar 2025 13:13:09 +0200 Subject: [PATCH 5/5] Merge function --- .../cf/ctx/state/TsRollingArgumentEntry.java | 2 +- .../script/api/tbel/TbTimeWindow.java | 1 - .../thingsboard/script/api/tbel/TbUtils.java | 81 --------------- .../script/api/tbel/TbelCfTsRollingArg.java | 98 ++++++++++++++++++- .../script/api/tbel/TbUtilsTest.java | 69 ------------- .../api/tbel/TbelCfTsRollingArgTest.java | 88 ++++++++++++++++- .../shared/models/calculated-field.models.ts | 89 +++++++++++++---- 7 files changed, 254 insertions(+), 174 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/TsRollingArgumentEntry.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/TsRollingArgumentEntry.java index 1114d993a3..b5a680a072 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/TsRollingArgumentEntry.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/TsRollingArgumentEntry.java @@ -89,7 +89,7 @@ public class TsRollingArgumentEntry implements ArgumentEntry { for (var e : tsRecords.entrySet()) { values.add(new TbelCfTsDoubleVal(e.getKey(), e.getValue())); } - return new TbelCfTsRollingArg(limit, timeWindow, values); + return new TbelCfTsRollingArg(timeWindow, values); } @Override diff --git a/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbTimeWindow.java b/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbTimeWindow.java index b430b23757..1761ef6d0a 100644 --- a/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbTimeWindow.java +++ b/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbTimeWindow.java @@ -28,7 +28,6 @@ public class TbTimeWindow implements TbelCfObject { private long startTs; private long endTs; - private int limit; @Override public long memorySize() { diff --git a/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbUtils.java b/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbUtils.java index e6a0b8d043..a43cc30f23 100644 --- a/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbUtils.java +++ b/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbUtils.java @@ -368,16 +368,6 @@ public class TbUtils { byte[].class, int.class))); parserConfig.addImport("parseBinaryArrayToInt", new MethodStub(TbUtils.class.getMethod("parseBinaryArrayToInt", byte[].class, int.class, int.class))); - parserConfig.addImport("merge", new MethodStub(TbUtils.class.getMethod("merge", - TbelCfTsRollingArg.class, TbelCfTsRollingArg.class))); - parserConfig.addImport("merge", new MethodStub(TbUtils.class.getMethod("merge", - TbelCfTsRollingArg.class, TbelCfTsRollingArg.class, TbTimeWindow.class))); - parserConfig.addImport("merge", new MethodStub(TbUtils.class.getMethod("merge", - TbelCfTsRollingArg.class, TbelCfTsRollingArg.class, TbTimeWindow.class, Map.class))); - parserConfig.addImport("merge", new MethodStub(TbUtils.class.getMethod("merge", - List.class, TbTimeWindow.class))); - parserConfig.addImport("merge", new MethodStub(TbUtils.class.getMethod("merge", - List.class, TbTimeWindow.class, Map.class))); } public static String btoa(String input) { @@ -1518,76 +1508,5 @@ public class TbUtils { return hex; } - public static TbelCfTsRollingData merge(TbelCfTsRollingArg a, TbelCfTsRollingArg b) { - return merge(Arrays.asList(a, b), null, null); - } - - public static TbelCfTsRollingData merge(TbelCfTsRollingArg a, TbelCfTsRollingArg b, TbTimeWindow timeWindow) { - return merge(Arrays.asList(a, b), timeWindow, null); - } - - public static TbelCfTsRollingData merge(TbelCfTsRollingArg a, TbelCfTsRollingArg b, TbTimeWindow timeWindow, Map settings) { - return merge(Arrays.asList(a, b), timeWindow, settings); - } - - public static TbelCfTsRollingData merge(List args, TbTimeWindow timeWindow) { - return merge(args, timeWindow, null); - } - - public static TbelCfTsRollingData merge(List args, TbTimeWindow timeWindow, Map settings) { - boolean ignoreNaN = true; - if (settings != null && settings.containsKey("ignoreNaN")) { - ignoreNaN = Boolean.parseBoolean(settings.get("ignoreNaN").toString()); - } - - TreeSet 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 data = new ArrayList<>(); - - int[] lastIndex = new int[args.size()]; - double[] result = new double[args.size()]; - Arrays.fill(result, Double.NaN); - - var tw = timeWindow != null ? timeWindow : new TbTimeWindow(startTs, endTs, allTimestamps.size()); - - 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 (tw.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(tw, data); - } - } diff --git a/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbelCfTsRollingArg.java b/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbelCfTsRollingArg.java index 74c9d77454..3b5a7ac9bd 100644 --- a/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbelCfTsRollingArg.java +++ b/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbelCfTsRollingArg.java @@ -19,11 +19,15 @@ import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; import lombok.Getter; +import org.thingsboard.common.util.JacksonUtil; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.Iterator; import java.util.List; +import java.util.Map; +import java.util.TreeSet; import java.util.function.Consumer; import static org.thingsboard.script.api.tbel.TbelCfTsDoubleVal.OBJ_SIZE; @@ -44,9 +48,9 @@ public class TbelCfTsRollingArg implements TbelCfArg, Iterable values) { + public TbelCfTsRollingArg(long timeWindow, List values) { long ts = System.currentTimeMillis(); - this.timeWindow = new TbTimeWindow(ts - timeWindow, ts, limit); + this.timeWindow = new TbTimeWindow(ts - timeWindow, ts); this.values = Collections.unmodifiableList(values); } @@ -104,6 +108,14 @@ public class TbelCfTsRollingArg implements TbelCfArg, Iterable settings) { + return mergeAll(Collections.singletonList(other), settings); + } + + public TbelCfTsRollingData mergeAll(List others) { + return mergeAll(others, null); + } + + public TbelCfTsRollingData mergeAll(List others, Map settings) { + List 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 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 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 public int getSize() { return values.size(); diff --git a/common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbUtilsTest.java b/common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbUtilsTest.java index f19a323068..71c7a8b78a 100644 --- a/common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbUtilsTest.java +++ b/common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbUtilsTest.java @@ -1137,75 +1137,6 @@ public class TbUtilsTest { Assertions.assertEquals(expected, actual); } - @Test - public void merge_two_rolling_args_ts_match_test() { - TbTimeWindow tw = new TbTimeWindow(0, 60000, 1000); - 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 = TbUtils.merge(arg1, 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, 1000); - 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 = TbUtils.merge(arg1, arg2, new TbTimeWindow(0, 10000, 1000)); - 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, 1000); - 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 = TbUtils.merge(arg1, 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, 1000); - 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 = TbUtils.merge(Arrays.asList(arg1, arg2), new TbTimeWindow(0, 60000, 1000), 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]); - } - private static List toList(byte[] data) { List result = new ArrayList<>(data.length); for (Byte b : data) { diff --git a/common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbelCfTsRollingArgTest.java b/common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbelCfTsRollingArgTest.java index 327b477b6f..69eba2fab2 100644 --- a/common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbelCfTsRollingArgTest.java +++ b/common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbelCfTsRollingArgTest.java @@ -15,10 +15,15 @@ */ package org.thingsboard.script.api.tbel; +import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; 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.Map; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; @@ -33,7 +38,7 @@ public class TbelCfTsRollingArgTest { @BeforeEach void setUp() { rollingArg = new TbelCfTsRollingArg( - new TbTimeWindow(ts - 30000, ts - 10, 10), + new TbTimeWindow(ts - 30000, ts - 10), List.of( new TbelCfTsDoubleVal(ts - 10, Double.NaN), new TbelCfTsDoubleVal(ts - 20, 2.0), @@ -98,7 +103,7 @@ public class TbelCfTsRollingArgTest { void testFirstAndLastWhenOnlyNaNAndIgnoreNaNIsFalse() { assertThat(rollingArg.first()).isEqualTo(2.0); rollingArg = new TbelCfTsRollingArg( - new TbTimeWindow(ts - 30000, ts - 10, 10), + new TbTimeWindow(ts - 30000, ts - 10), List.of( new TbelCfTsDoubleVal(ts - 10, Double.NaN), new TbelCfTsDoubleVal(ts - 40, Double.NaN), @@ -117,7 +122,7 @@ public class TbelCfTsRollingArgTest { @Test 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::max).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."); } + @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]); + } + } \ No newline at end of file diff --git a/ui-ngx/src/app/shared/models/calculated-field.models.ts b/ui-ngx/src/app/shared/models/calculated-field.models.ts index f6d7aaaa1e..4467fecc79 100644 --- a/ui-ngx/src/app/shared/models/calculated-field.models.ts +++ b/ui-ngx/src/app/shared/models/calculated-field.models.ts @@ -272,7 +272,7 @@ export const CalculatedFieldAttributeValueArgumentAutocomplete = { export const CalculatedFieldRollingValueArgumentFunctionsAutocomplete = { max: { 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: [ { name: 'ignoreNaN', @@ -288,7 +288,7 @@ export const CalculatedFieldRollingValueArgumentFunctionsAutocomplete = { }, min: { 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: [ { name: 'ignoreNaN', @@ -304,7 +304,7 @@ export const CalculatedFieldRollingValueArgumentFunctionsAutocomplete = { }, mean: { 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: [ { name: 'ignoreNaN', @@ -318,9 +318,25 @@ export const CalculatedFieldRollingValueArgumentFunctionsAutocomplete = { 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: { 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: [ { name: 'ignoreNaN', @@ -336,7 +352,7 @@ export const CalculatedFieldRollingValueArgumentFunctionsAutocomplete = { }, median: { 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: [ { name: 'ignoreNaN', @@ -352,7 +368,7 @@ export const CalculatedFieldRollingValueArgumentFunctionsAutocomplete = { }, count: { 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: [ { name: 'ignoreNaN', @@ -368,7 +384,7 @@ export const CalculatedFieldRollingValueArgumentFunctionsAutocomplete = { }, last: { 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: [ { name: 'ignoreNaN', @@ -384,7 +400,7 @@ export const CalculatedFieldRollingValueArgumentFunctionsAutocomplete = { }, first: { 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: [ { name: 'ignoreNaN', @@ -400,7 +416,7 @@ export const CalculatedFieldRollingValueArgumentFunctionsAutocomplete = { }, sum: { 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: [ { name: 'ignoreNaN', @@ -413,12 +429,56 @@ export const CalculatedFieldRollingValueArgumentFunctionsAutocomplete = { description: 'The sum of values, or NaN if applicable', 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 = { 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.', children: { ...CalculatedFieldRollingValueArgumentFunctionsAutocomplete, @@ -429,7 +489,7 @@ export const CalculatedFieldRollingValueArgumentAutocomplete = { }, timeWindow: { meta: 'object', - type: '{ startTs: number; endTs: number; limit: number }', + type: '{ startTs: number; endTs: number }', description: 'Time window configuration', children: { startTs: { @@ -441,11 +501,6 @@ export const CalculatedFieldRollingValueArgumentAutocomplete = { meta: 'number', type: 'number', description: 'End time stamp', - }, - limit: { - meta: 'number', - type: 'number', - description: 'Limit', } } } @@ -504,7 +559,7 @@ const calculatedFieldSingleArgumentValueHighlightRules: AceHighlightRules = { } const calculatedFieldRollingArgumentValueFunctionsHighlightRules: Array = - ['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', regex: `\\b${funcName}\\b`, next: 'no_regex'