From 55504225eab6e068cc9285139a824131774ead1d Mon Sep 17 00:00:00 2001 From: rusikv Date: Wed, 26 Jun 2024 18:50:40 +0300 Subject: [PATCH 01/20] UI: fixed hidden widgets not rendering in edit mode --- .../components/widget/widget.component.ts | 20 ++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/widget.component.ts index a79fa89242..533b2fbda2 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/widget.component.ts @@ -487,6 +487,7 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI } if (!this.widgetContext.inited && this.isReady()) { this.widgetContext.inited = true; + this.widgetContext.destroyed = false; this.dashboardWidget.updateWidgetParams(); this.widgetContext.detectContainerChanges(); if (this.cafs.init) { @@ -1544,19 +1545,20 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI } private checkSize(): boolean { - const width = this.widgetContext.$containerParent.width(); - const height = this.widgetContext.$containerParent.height(); + const parentWidth = this.widgetContext.$containerParent.width(); + const parentHeight = this.widgetContext.$containerParent.height(); + const width = this.widgetContext.$container?.width(); + const height = this.widgetContext.$container?.height(); let sizeChanged = false; - if (!this.widgetContext.width || this.widgetContext.width !== width || - !this.widgetContext.height || this.widgetContext.height !== height) { - if (width > 0 && height > 0) { + if (parentWidth > 0 && parentHeight > 0) { + if (!this.widgetContext.width || !this.widgetContext.height || width !== parentWidth || height !== parentHeight) { if (this.widgetContext.$container) { - this.widgetContext.$container.css('height', height + 'px'); - this.widgetContext.$container.css('width', width + 'px'); + this.widgetContext.$container.css('height', parentHeight + 'px'); + this.widgetContext.$container.css('width', parentWidth + 'px'); } - this.widgetContext.width = width; - this.widgetContext.height = height; + this.widgetContext.width = parentWidth; + this.widgetContext.height = parentHeight; sizeChanged = true; this.widgetSizeDetected = true; } From 9a051640ec776acf6070b44fa511e7c2675f0be8 Mon Sep 17 00:00:00 2001 From: rusikv Date: Mon, 8 Jul 2024 15:29:07 +0300 Subject: [PATCH 02/20] UI: fixed hidden by default chart series not rendering on unhide --- .../widget/lib/chart/time-series-chart-widget.component.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart-widget.component.ts index 0a242eba36..053598763e 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart-widget.component.ts @@ -170,5 +170,9 @@ export class TimeSeriesChartWidgetComponent implements OnInit, OnDestroy, AfterV public toggleLegendKey(legendKey: LegendKey) { this.timeSeriesChart.toggleKey(legendKey.dataKey); + for (const id of Object.keys(this.ctx.subscriptions)) { + const subscription = this.ctx.subscriptions[id]; + subscription.updateDataVisibility(legendKey.dataIndex); + } } } From 7b2e38e04bd51158d769173eb829535871a40304 Mon Sep 17 00:00:00 2001 From: rusikv Date: Tue, 9 Jul 2024 13:27:50 +0300 Subject: [PATCH 03/20] UI: refactoring --- .../chart/time-series-chart-widget.component.ts | 6 +----- .../widget/lib/chart/time-series-chart.ts | 14 ++++++++++++-- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart-widget.component.ts index 053598763e..def776897e 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart-widget.component.ts @@ -169,10 +169,6 @@ export class TimeSeriesChartWidgetComponent implements OnInit, OnDestroy, AfterV } public toggleLegendKey(legendKey: LegendKey) { - this.timeSeriesChart.toggleKey(legendKey.dataKey); - for (const id of Object.keys(this.ctx.subscriptions)) { - const subscription = this.ctx.subscriptions[id]; - subscription.updateDataVisibility(legendKey.dataIndex); - } + this.timeSeriesChart.toggleKey(legendKey.dataKey, legendKey.dataIndex); } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.ts b/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.ts index 913c346712..a76643ea20 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.ts @@ -60,7 +60,14 @@ import { measureAxisNameSize } from '@home/components/widget/lib/chart/echarts-widget.models'; import { DateFormatProcessor, ValueSourceType } from '@shared/models/widget-settings.models'; -import { formattedDataFormDatasourceData, formatValue, isDefinedAndNotNull, isEqual, mergeDeep } from '@core/utils'; +import { + formattedDataFormDatasourceData, + formatValue, + isDefined, + isDefinedAndNotNull, + isEqual, + mergeDeep +} from '@core/utils'; import { DataKey, Datasource, DatasourceType, FormattedData, widgetType } from '@shared/models/widget.models'; import * as echarts from 'echarts/core'; import { CallbackDataParams, PiecewiseVisualMapOption } from 'echarts/types/dist/shared'; @@ -290,7 +297,7 @@ export class TbTimeSeriesChart { } } - public toggleKey(dataKey: DataKey): void { + public toggleKey(dataKey: DataKey, dataIndex?: number): void { const enable = dataKey.hidden; const dataItem = this.dataItems.find(d => d.dataKey === dataKey); if (dataItem) { @@ -310,6 +317,9 @@ export class TbTimeSeriesChart { this.timeSeriesChart.setOption(this.timeSeriesChartOptions, this.stackMode ? {notMerge: true} : {replaceMerge: mergeList}); this.updateAxes(); dataKey.hidden = !enable; + if (isDefined(dataIndex)) { + this.ctx.defaultSubscription.updateDataVisibility(dataIndex); + } if (enable) { this.timeSeriesChart.dispatchAction({ type: 'highlight', From c45cadd4783967d270af3a8d8a1def0f6a2a0cc6 Mon Sep 17 00:00:00 2001 From: rusikv Date: Tue, 9 Jul 2024 16:17:36 +0300 Subject: [PATCH 04/20] UI: revert checkSize method of widget component, clearing width and height of widget context on destroy --- .../components/widget/widget.component.ts | 19 +++++++++---------- .../home/models/widget-component.models.ts | 2 ++ 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/widget.component.ts index 533b2fbda2..bae0389e61 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/widget.component.ts @@ -1545,20 +1545,19 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI } private checkSize(): boolean { - const parentWidth = this.widgetContext.$containerParent.width(); - const parentHeight = this.widgetContext.$containerParent.height(); - const width = this.widgetContext.$container?.width(); - const height = this.widgetContext.$container?.height(); + const width = this.widgetContext.$containerParent.width(); + const height = this.widgetContext.$containerParent.height(); let sizeChanged = false; - if (parentWidth > 0 && parentHeight > 0) { - if (!this.widgetContext.width || !this.widgetContext.height || width !== parentWidth || height !== parentHeight) { + if (!this.widgetContext.width || this.widgetContext.width !== width || + !this.widgetContext.height || this.widgetContext.height !== height) { + if (width > 0 && height > 0) { if (this.widgetContext.$container) { - this.widgetContext.$container.css('height', parentHeight + 'px'); - this.widgetContext.$container.css('width', parentWidth + 'px'); + this.widgetContext.$container.css('height', height + 'px'); + this.widgetContext.$container.css('width', width + 'px'); } - this.widgetContext.width = parentWidth; - this.widgetContext.height = parentHeight; + this.widgetContext.width = width; + this.widgetContext.height = height; sizeChanged = true; this.widgetSizeDetected = true; } diff --git a/ui-ngx/src/app/modules/home/models/widget-component.models.ts b/ui-ngx/src/app/modules/home/models/widget-component.models.ts index 581fded240..fbda78a128 100644 --- a/ui-ngx/src/app/modules/home/models/widget-component.models.ts +++ b/ui-ngx/src/app/modules/home/models/widget-component.models.ts @@ -449,6 +449,8 @@ export class WidgetContext { labelPattern.destroy(); } this.labelPatterns.clear(); + this.width = undefined; + this.height = undefined; this.destroyed = true; } From ff2c426e71f8359347df09103afc695944b3bd9f Mon Sep 17 00:00:00 2001 From: Artem Dzhereleiko Date: Wed, 10 Jul 2024 12:32:51 +0300 Subject: [PATCH 05/20] UI: Fixed notify again with deleted recipient --- .../src/app/shared/components/entity/entity-list.component.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/ui-ngx/src/app/shared/components/entity/entity-list.component.ts b/ui-ngx/src/app/shared/components/entity/entity-list.component.ts index 4e33944f29..383d5bc8f4 100644 --- a/ui-ngx/src/app/shared/components/entity/entity-list.component.ts +++ b/ui-ngx/src/app/shared/components/entity/entity-list.component.ts @@ -179,6 +179,10 @@ export class EntityListComponent implements ControlValueAccessor, OnInit, AfterV (entities) => { this.entities = entities; this.entityListFormGroup.get('entities').setValue(this.entities); + if(!entities.length && this.required) { + this.modelValue = null; + this.propagateChange(this.modelValue); + } } ); } else { From 99d4eb0d9cb2df24bf409c0f16483c934629543b Mon Sep 17 00:00:00 2001 From: Kulikov <44275303+nickAS21@users.noreply.github.com> Date: Wed, 10 Jul 2024 14:31:11 +0300 Subject: [PATCH 06/20] TBEL: new methods (#11129) * tbel: number to string by radix * Revert "tbel: number to string by radix" This reverts commit 0fd285d76b5f25002f84768ab490e750539509d0. * tbel: number to string by radix and new Error * tbel: number to Hexadecimal must be even-length. * tbel: add tests isBinary...isHexadecimal * tbel: Comments 1 * tbel: Comments 2 * tbel: Comments 3 --- .../thingsboard/script/api/tbel/TbUtils.java | 516 ++++++++++++++---- .../script/api/tbel/TbUtilsTest.java | 302 ++++++++-- 2 files changed, 656 insertions(+), 162 deletions(-) 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 283044b279..852f7a054f 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 @@ -46,11 +46,23 @@ import java.util.Map; import java.util.Set; import java.util.regex.Matcher; +import static java.lang.Character.MAX_RADIX; +import static java.lang.Character.MIN_RADIX; + @Slf4j public class TbUtils { private static final byte[] HEX_ARRAY = "0123456789ABCDEF".getBytes(StandardCharsets.US_ASCII); + private static final int ZERO_RADIX = 0; + private static final int OCTAL_RADIX = 8; + private static final int DEC_RADIX = 10; + private static final int HEX_RADIX = 16; + private static final int HEX_LEN_MIN = -1; + private static final int HEX_LEN_INT_MAX = 8; + private static final int HEX_LEN_LONG_MAX = 16; + private static final int BYTES_LEN_LONG_MAX = 8; + private static final LinkedHashMap mdnEncodingReplacements = new LinkedHashMap<>(); static { @@ -103,6 +115,8 @@ public class TbUtils { String.class, int.class))); parserConfig.addImport("parseFloat", new MethodStub(TbUtils.class.getMethod("parseFloat", String.class))); + parserConfig.addImport("parseFloat", new MethodStub(TbUtils.class.getMethod("parseFloat", + String.class, int.class))); parserConfig.addImport("parseDouble", new MethodStub(TbUtils.class.getMethod("parseDouble", String.class))); parserConfig.addImport("parseLittleEndianHexToInt", new MethodStub(TbUtils.class.getMethod("parseLittleEndianHexToInt", @@ -175,6 +189,40 @@ public class TbUtils { float.class, int.class))); parserConfig.addImport("hexToBytes", new MethodStub(TbUtils.class.getMethod("hexToBytes", ExecutionContext.class, String.class))); + parserConfig.addImport("intToHex", new MethodStub(TbUtils.class.getMethod("intToHex", + Integer.class))); + parserConfig.addImport("intToHex", new MethodStub(TbUtils.class.getMethod("intToHex", + Integer.class, boolean.class))); + parserConfig.addImport("intToHex", new MethodStub(TbUtils.class.getMethod("intToHex", + Integer.class, boolean.class, boolean.class))); + parserConfig.addImport("intToHex", new MethodStub(TbUtils.class.getMethod("intToHex", + Integer.class, boolean.class, boolean.class, int.class))); + parserConfig.addImport("longToHex", new MethodStub(TbUtils.class.getMethod("longToHex", + Long.class))); + parserConfig.addImport("longToHex", new MethodStub(TbUtils.class.getMethod("longToHex", + Long.class, boolean.class))); + parserConfig.addImport("longToHex", new MethodStub(TbUtils.class.getMethod("longToHex", + Long.class, boolean.class, boolean.class))); + parserConfig.addImport("longToHex", new MethodStub(TbUtils.class.getMethod("longToHex", + Long.class, boolean.class, boolean.class, int.class))); + parserConfig.addImport("intLongToString", new MethodStub(TbUtils.class.getMethod("intLongToString", + Long.class))); + parserConfig.addImport("intLongToString", new MethodStub(TbUtils.class.getMethod("intLongToString", + Long.class, int.class))); + parserConfig.addImport("intLongToString", new MethodStub(TbUtils.class.getMethod("intLongToString", + Long.class, int.class, boolean.class))); + parserConfig.addImport("intLongToString", new MethodStub(TbUtils.class.getMethod("intLongToString", + Long.class, int.class, boolean.class, boolean.class))); + parserConfig.addImport("floatToHex", new MethodStub(TbUtils.class.getMethod("floatToHex", + Float.class))); + parserConfig.addImport("floatToHex", new MethodStub(TbUtils.class.getMethod("floatToHex", + Float.class, boolean.class))); + parserConfig.addImport("doubleToHex", new MethodStub(TbUtils.class.getMethod("doubleToHex", + Double.class))); + parserConfig.addImport("doubleToHex", new MethodStub(TbUtils.class.getMethod("doubleToHex", + Double.class, boolean.class))); + parserConfig.addImport("printUnsignedBytes", new MethodStub(TbUtils.class.getMethod("printUnsignedBytes", + ExecutionContext.class, List.class))); parserConfig.addImport("base64ToHex", new MethodStub(TbUtils.class.getMethod("base64ToHex", String.class))); parserConfig.addImport("base64ToBytes", new MethodStub(TbUtils.class.getMethod("base64ToBytes", @@ -197,6 +245,18 @@ public class TbUtils { String.class))); parserConfig.addImport("decodeURI", new MethodStub(TbUtils.class.getMethod("decodeURI", String.class))); + parserConfig.addImport("raiseError", new MethodStub(TbUtils.class.getMethod("raiseError", + String.class, Object.class))); + parserConfig.addImport("raiseError", new MethodStub(TbUtils.class.getMethod("raiseError", + String.class))); + parserConfig.addImport("isBinary", new MethodStub(TbUtils.class.getMethod("isBinary", + String.class))); + parserConfig.addImport("isOctal", new MethodStub(TbUtils.class.getMethod("isOctal", + String.class))); + parserConfig.addImport("isDecimal", new MethodStub(TbUtils.class.getMethod("isDecimal", + String.class))); + parserConfig.addImport("isHexadecimal", new MethodStub(TbUtils.class.getMethod("isHexadecimal", + String.class))); } public static String btoa(String input) { @@ -210,6 +270,7 @@ public class TbUtils { public static Object decodeToJson(ExecutionContext ctx, List bytesList) throws IOException { return TbJson.parse(ctx, bytesToString(bytesList)); } + public static Object decodeToJson(ExecutionContext ctx, String jsonStr) throws IOException { return TbJson.parse(ctx, jsonStr); } @@ -269,88 +330,127 @@ public class TbUtils { } public static Integer parseInt(String value) { - int radix = getRadix(value); - return parseInt(value, radix); + return parseInt(value, ZERO_RADIX); } public static Integer parseInt(String value, int radix) { if (StringUtils.isNotBlank(value)) { - try { - String valueP = prepareNumberString(value); - isValidRadix(valueP, radix); - try { - return Integer.parseInt(valueP, radix); - } catch (NumberFormatException e) { - BigInteger bi = new BigInteger(valueP, radix); - if (bi.compareTo(BigInteger.valueOf(Integer.MAX_VALUE)) > 0) - throw new NumberFormatException("Value \"" + value + "\" is greater than the maximum Integer value " + Integer.MAX_VALUE + " !"); - if (bi.compareTo(BigInteger.valueOf(Integer.MIN_VALUE)) < 0) - throw new NumberFormatException("Value \"" + value + "\" is less than the minimum Integer value " + Integer.MIN_VALUE + " !"); - Float f = parseFloat(valueP); - if (f != null) { - return f.intValue(); - } else { - throw new NumberFormatException(e.getMessage()); - } - } - } catch (NumberFormatException e) { - throw new NumberFormatException(e.getMessage()); + String valueP = prepareNumberString(value); + int radixValue = isValidStringAndRadix(valueP, radix, value); + if (radixValue >= 25 && radixValue <= MAX_RADIX) { + return (Integer) compareIntLongValueMinMax(valueP, radixValue, Integer.MAX_VALUE, Integer.MIN_VALUE); } + return switch (radixValue) { + case MIN_RADIX -> parseBinaryStringAsSignedInteger(valueP); + case OCTAL_RADIX, DEC_RADIX, HEX_RADIX -> Integer.parseInt(valueP, radixValue); + default -> throw new IllegalArgumentException("Invalid radix: [" + radix + "]"); + }; } return null; } public static Long parseLong(String value) { - int radix = getRadix(value); - return parseLong(value, radix); + return parseLong(value, ZERO_RADIX); } public static Long parseLong(String value, int radix) { if (StringUtils.isNotBlank(value)) { - try { - String valueP = prepareNumberString(value); - isValidRadix(valueP, radix); - try { - return Long.parseLong(valueP, radix); - } catch (NumberFormatException e) { - BigInteger bi = new BigInteger(valueP, radix); - if (bi.compareTo(BigInteger.valueOf(Long.MAX_VALUE)) > 0) - throw new NumberFormatException("Value \"" + value + "\"is greater than the maximum Long value " + Long.MAX_VALUE + " !"); - if (bi.compareTo(BigInteger.valueOf(Long.MIN_VALUE)) < 0) - throw new NumberFormatException("Value \"" + value + "\" is less than the minimum Long value " + Long.MIN_VALUE + " !"); - Double dd = parseDouble(valueP); - if (dd != null) { - return dd.longValue(); - } else { - throw new NumberFormatException(e.getMessage()); - } - } - } catch (NumberFormatException e) { - throw new NumberFormatException(e.getMessage()); + String valueP = prepareNumberString(value); + int radixValue = isValidStringAndRadix(valueP, radix, value); + if (radixValue >= 25 && radixValue <= MAX_RADIX) { + return (Long) compareIntLongValueMinMax(valueP, radixValue, Long.MAX_VALUE, Long.MIN_VALUE); } + return switch (radixValue) { + case MIN_RADIX -> parseBinaryStringAsSignedLong(valueP); + case OCTAL_RADIX, DEC_RADIX, HEX_RADIX -> Long.parseLong(valueP, radixValue); + default -> throw new IllegalArgumentException("Invalid radix: [" + radix + "]"); + }; } return null; } - private static int getRadix(String value, int... radixS) { - return radixS.length > 0 ? radixS[0] : isHexadecimal(value) ? 16 : 10; + private static int parseBinaryStringAsSignedInteger(String binaryString) { + if (binaryString.length() != 32) { + // Pad the binary string to 64 bits if it is not already + binaryString = String.format("%32s", binaryString).replace(' ', '0'); + } + + // If the MSB is 1, the number is negative in two's complement + if (binaryString.charAt(0) == '1') { + // Calculate the two's complement + String invertedBinaryString = invertBinaryString(binaryString); + int positiveValue = Integer.parseInt(invertedBinaryString, MIN_RADIX) + 1; + return -positiveValue; + } else { + return Integer.parseInt(binaryString, MIN_RADIX); + } + } + private static long parseBinaryStringAsSignedLong(String binaryString) { + if (binaryString.length() != 64) { + // Pad the binary string to 64 bits if it is not already + binaryString = String.format("%64s", binaryString).replace(' ', '0'); + } + + // If the MSB is 1, the number is negative in two's complement + if (binaryString.charAt(0) == '1') { + // Calculate the two's complement + String invertedBinaryString = invertBinaryString(binaryString); + long positiveValue = Long.parseLong(invertedBinaryString, MIN_RADIX) + 1; + return -positiveValue; + } else { + return Long.parseLong(binaryString, MIN_RADIX); + } + } + + private static String invertBinaryString(String binaryString) { + StringBuilder invertedString = new StringBuilder(); + for (char bit : binaryString.toCharArray()) { + invertedString.append(bit == '0' ? '1' : '0'); + } + return invertedString.toString(); + } + + private static int getRadix10_16(String value) { + int radix = isDecimal(value) > 0 ? DEC_RADIX : isHexadecimal(value); + if (radix > 0) { + return radix; + } else { + throw new NumberFormatException("Value: \"" + value + "\" is not numeric or hexDecimal format!"); + } } public static Float parseFloat(String value) { - if (value != null) { - try { - return Float.parseFloat(prepareNumberString(value)); - } catch (NumberFormatException e) { + return parseFloat(value, 0); + } + + public static Float parseFloat(String value, int radix) { + if (StringUtils.isNotBlank(value)) { + String valueP = prepareNumberString(value); + int radixValue = isValidStringAndRadix(valueP, radix, value); + if (radixValue == DEC_RADIX) { + return Float.parseFloat(value); + } else { + int bits = Integer.parseUnsignedInt(valueP, HEX_RADIX); + return Float.intBitsToFloat(bits); } } return null; } public static Double parseDouble(String value) { + int radix = getRadix10_16(value); + return parseDouble(value, radix); + } + + public static Double parseDouble(String value, int radix) { if (value != null) { - try { + String valueP = prepareNumberString(value); + int radixValue = isValidStringAndRadix(valueP, radix, value); + if (radixValue == DEC_RADIX) { return Double.parseDouble(prepareNumberString(value)); - } catch (NumberFormatException e) { + } else { + long bits = Long.parseUnsignedLong(valueP, HEX_RADIX); + return Double.longBitsToDouble(bits); } } return null; @@ -368,9 +468,10 @@ public class TbUtils { return parseHexToInt(hex, true); } - public static int parseHexToInt(String hex, boolean bigEndian) { - byte[] data = prepareHexToBytesNumber(hex, 8); - return parseBytesToInt(data, 0, data.length, bigEndian); + public static Integer parseHexToInt(String value, boolean bigEndian) { + String hexValue = prepareNumberString(value); + String hex = bigEndian ? hexValue : reverseHexStringByOrder(hexValue); + return parseInt(hex, HEX_RADIX); } public static long parseLittleEndianHexToLong(String hex) { @@ -385,9 +486,10 @@ public class TbUtils { return parseHexToLong(hex, true); } - public static long parseHexToLong(String hex, boolean bigEndian) { - byte[] data = prepareHexToBytesNumber(hex, 16); - return parseBytesToLong(data, 0, data.length, bigEndian); + public static Long parseHexToLong(String value, boolean bigEndian) { + String hexValue = prepareNumberString(value); + String hex = bigEndian ? value : reverseHexStringByOrder(hexValue); + return parseLong(hex, HEX_RADIX); } public static float parseLittleEndianHexToFloat(String hex) { @@ -402,9 +504,10 @@ public class TbUtils { return parseHexToFloat(hex, true); } - public static float parseHexToFloat(String hex, boolean bigEndian) { - byte[] data = prepareHexToBytesNumber(hex, 8); - return parseBytesToFloat(data, 0, bigEndian); + public static Float parseHexToFloat(String value, boolean bigEndian) { + String hexValue = prepareNumberString(value); + String hex = bigEndian ? value : reverseHexStringByOrder(hexValue); + return parseFloat(hex, HEX_RADIX); } public static double parseLittleEndianHexToDouble(String hex) { @@ -419,39 +522,171 @@ public class TbUtils { return parseHexToDouble(hex, true); } - public static double parseHexToDouble(String hex, boolean bigEndian) { - byte[] data = prepareHexToBytesNumber(hex, 16); - return parseBytesToDouble(data, 0, bigEndian); + public static double parseHexToDouble(String value, boolean bigEndian) { + String hexValue = prepareNumberString(value); + String hex = bigEndian ? value : reverseHexStringByOrder(hexValue); + return parseDouble(hex, HEX_RADIX); } - private static byte[] prepareHexToBytesNumber(String hex, int len) { - int length = hex.length(); - if (length > len) { - throw new IllegalArgumentException("Hex string is too large. Maximum 8 symbols allowed."); - } - if (length % 2 > 0) { + public static ExecutionArrayList hexToBytes(ExecutionContext ctx, String value) { + String hex = prepareNumberString(value); + int len = hex.length(); + if (len % 2 > 0) { throw new IllegalArgumentException("Hex string must be even-length."); } - byte[] data = new byte[length / 2]; - for (int i = 0; i < length; i += 2) { - data[i / 2] = (byte) ((Character.digit(hex.charAt(i), 16) << 4) + Character.digit(hex.charAt(i + 1), 16)); + ExecutionArrayList data = new ExecutionArrayList<>(ctx); + for (int i = 0; i < hex.length(); i += 2) { + // Extract two characters from the hex string + String byteString = hex.substring(i, i + 2); + // Parse the hex string to a byte + byte byteValue = (byte) Integer.parseInt(byteString, HEX_RADIX); + // Add the byte to the ArrayList + data.add(byteValue); } return data; } - public static ExecutionArrayList hexToBytes(ExecutionContext ctx, String hex) { - int len = hex.length(); - if (len % 2 > 0) { - throw new IllegalArgumentException("Hex string must be even-length."); - } - ExecutionArrayList data = new ExecutionArrayList<>(ctx); - for (int i = 0; i < len; i += 2) { - data.add((byte) ((Character.digit(hex.charAt(i), 16) << 4) - + Character.digit(hex.charAt(i + 1), 16))); + public static List printUnsignedBytes(ExecutionContext ctx, List byteArray) { + ExecutionArrayList data = new ExecutionArrayList<>(ctx); + for (Byte b : byteArray) { + // Convert signed byte to unsigned integer + int unsignedByte = Byte.toUnsignedInt(b); + data.add(unsignedByte); } return data; } + public static String intToHex(Integer i) { + return prepareNumberHexString(i.longValue(), true, false, HEX_LEN_MIN, HEX_LEN_INT_MAX); + } + + public static String intToHex(Integer i, boolean bigEndian) { + return prepareNumberHexString(i.longValue(), bigEndian, false, HEX_LEN_MIN, HEX_LEN_INT_MAX); + } + + public static String intToHex(Integer i, boolean bigEndian, boolean pref) { + return prepareNumberHexString(i.longValue(), bigEndian, pref, HEX_LEN_MIN, HEX_LEN_INT_MAX); + } + + public static String intToHex(Integer i, boolean bigEndian, boolean pref, int len) { + return prepareNumberHexString(i.longValue(), bigEndian, pref, len, HEX_LEN_INT_MAX); + } + + public static String longToHex(Long l) { + return prepareNumberHexString(l, true, false, HEX_LEN_MIN, HEX_LEN_LONG_MAX); + } + public static String longToHex(Long l, boolean bigEndian) { + return prepareNumberHexString(l, bigEndian, false, HEX_LEN_MIN, HEX_LEN_LONG_MAX); + } + + public static String longToHex(Long l, boolean bigEndian, boolean pref) { + return prepareNumberHexString(l, bigEndian, pref, HEX_LEN_MIN, HEX_LEN_LONG_MAX); + } + + public static String longToHex(Long l, boolean bigEndian, boolean pref, int len) { + return prepareNumberHexString(l, bigEndian, pref, len, HEX_LEN_LONG_MAX); + } + + public static String intLongToString(Long number) { + return intLongToString(number, DEC_RADIX); + } + + public static String intLongToString(Long number, int radix) { + return intLongToString(number, radix, true); + } + + public static String intLongToString(Long number, int radix, boolean bigEndian) { + return intLongToString(number, radix, bigEndian, false); + } + + public static String intLongToString(Long number, int radix, boolean bigEndian, boolean pref) { + if (radix >= 25 && radix <= MAX_RADIX) { + return Long.toString(number, radix); + } + return switch (radix) { + case MIN_RADIX -> Long.toBinaryString(number); + case OCTAL_RADIX -> Long.toOctalString(number); + case DEC_RADIX -> Long.toString(number); + case HEX_RADIX -> prepareNumberHexString(number, bigEndian, pref, -1, -1); + default -> throw new IllegalArgumentException("Invalid radix: [" + radix + "]"); + }; + } + + private static Number compareIntLongValueMinMax(String valueP, int radix, Number maxValue, Number minValue) { + boolean isInteger = maxValue.getClass().getSimpleName().equals("Integer"); + try { + if (isInteger) { + return Integer.parseInt(valueP, radix); + } else { + return Long.parseLong(valueP, radix); + } + } catch (NumberFormatException e) { + BigInteger bi = new BigInteger(valueP, radix); + long maxValueL = isInteger ? maxValue.longValue() : (long) maxValue; + if (bi.compareTo(BigInteger.valueOf(maxValueL)) > 0) { + throw new NumberFormatException("Value \"" + valueP + "\"is greater than the maximum " + maxValue.getClass().getSimpleName() + " value " + maxValue + " !"); + } + long minValueL = isInteger ? minValue.longValue() : (long) minValue; + if (bi.compareTo(BigInteger.valueOf(minValueL)) < 0) { + throw new NumberFormatException("Value \"" + valueP + "\" is less than the minimum " + minValue.getClass().getSimpleName() + " value " + minValue + " !"); + } + throw new NumberFormatException(e.getMessage()); + } + } + + private static String prepareNumberHexString(Long number, boolean bigEndian, boolean pref, int len, int hexLenMax) { + String hex = Long.toHexString(number).toUpperCase(); + hexLenMax = hexLenMax < 0 ? hex.length() : hexLenMax; + String hexWithoutZeroFF = removeLeadingZero_FF(hex, number, hexLenMax); + hexWithoutZeroFF = bigEndian ? hexWithoutZeroFF : reverseHexStringByOrder(hexWithoutZeroFF); + len = len == HEX_LEN_MIN ? hexWithoutZeroFF.length() : len; + String result = hexWithoutZeroFF.substring(hexWithoutZeroFF.length() - len); + return pref ? "0x" + result : result; + } + + private static String removeLeadingZero_FF(String hex, Long number, int hexLenMax) { + String hexWithoutZero = hex.replaceFirst("^0+(?!$)", ""); // Remove leading zeros except for the last one + hexWithoutZero = hexWithoutZero.length() % 2 > 0 ? "0" + hexWithoutZero : hexWithoutZero; + if (number >= 0) { + return hexWithoutZero; + } else { + String hexWithoutZeroFF = hexWithoutZero.replaceFirst("^F+(?!$)", ""); + hexWithoutZeroFF = hexWithoutZeroFF.length() % 2 > 0 ? "F" + hexWithoutZeroFF : hexWithoutZeroFF; + if (hexWithoutZeroFF.length() > hexLenMax) { + return hexWithoutZeroFF.substring(hexWithoutZeroFF.length() - hexLenMax); + } else if (hexWithoutZeroFF.length() == hexLenMax) { + return hexWithoutZeroFF; + } else { + return "FF" + hexWithoutZeroFF; + } + } + } + + public static String floatToHex(Float f) { + return floatToHex(f, true); + } + + public static String floatToHex(Float f, boolean bigEndian) { + // Convert the float to its raw integer bits representation + int bits = Float.floatToRawIntBits(f); + + // Format the integer bits as a hexadecimal string + String result = String.format("0x%08X", bits); + return bigEndian ? result : reverseHexStringByOrder(result); + } + + public static String doubleToHex(Double d) { + return doubleToHex(d, true); + } + + public static String doubleToHex(Double d, boolean bigEndian) { + long bits = Double.doubleToRawLongBits(d); + + // Format the integer bits as a hexadecimal string + String result = String.format("0x%16X", bits); + return bigEndian ? result : reverseHexStringByOrder(result); + } + public static String base64ToHex(String base64) { return bytesToHex(Base64.getDecoder().decode(base64)); } @@ -520,8 +755,8 @@ public class TbUtils { if (offset > data.length) { throw new IllegalArgumentException("Offset: " + offset + " is out of bounds for array with length: " + data.length + "!"); } - if (length > 8) { - throw new IllegalArgumentException("Length: " + length + " is too large. Maximum 4 bytes is allowed!"); + if (length > BYTES_LEN_LONG_MAX) { + throw new IllegalArgumentException("Length: " + length + " is too large. Maximum " + BYTES_LEN_LONG_MAX + " bytes is allowed!"); } if (offset + length > data.length) { throw new IllegalArgumentException("Offset: " + offset + " and Length: " + length + " is out of bounds for array with length: " + data.length + "!"); @@ -567,7 +802,7 @@ public class TbUtils { } public static double parseBytesToDouble(byte[] data, int offset, boolean bigEndian) { - byte[] bytesToNumber = prepareBytesToNumber(data, offset, 8, bigEndian); + byte[] bytesToNumber = prepareBytesToNumber(data, offset, BYTES_LEN_LONG_MAX, bigEndian); return ByteBuffer.wrap(bytesToNumber).getDouble(); } @@ -647,6 +882,15 @@ public class TbUtils { return URLDecoder.decode(uri, StandardCharsets.UTF_8); } + public static void raiseError(String message) { + raiseError(message, null); + } + + public static void raiseError(String message, Object value) { + String msg = value == null ? message : message + " for value " + value; + throw new RuntimeException(msg); + } + private static void parseRecursive(Object json, Map map, List excludeList, String path, boolean pathInKey) { if (json instanceof Map.Entry) { Map.Entry entry = (Map.Entry) json; @@ -687,42 +931,104 @@ public class TbUtils { } } - private static boolean isHexadecimal(String value) { - return value != null && (value.contains("0x") || value.contains("0X")); - } - private static String prepareNumberString(String value) { if (value != null) { value = value.trim(); - if (isHexadecimal(value)) { - value = value.replace("0x", ""); - value = value.replace("0X", ""); - } + value = value.replace("0x", ""); + value = value.replace("0X", ""); value = value.replace(",", "."); } return value; } - private static boolean isValidRadix(String value, int radix) { - for (int i = 0; i < value.length(); i++) { - if (i == 0 && value.charAt(i) == '-') { - if (value.length() == 1) - throw new NumberFormatException("Failed radix [" + radix + "] for value: \"" + value + "\"!"); - else - continue; + private static int isValidStringAndRadix(String valueP, int radix, String value) { + int radixValue; + if (radix == 0) { + radixValue = getRadix10_16(valueP); + } else if (radix >= 25 && radix <= MAX_RADIX) { + return radix; + } else { + radixValue = switch (radix) { + case MIN_RADIX -> isBinary(valueP); + case OCTAL_RADIX -> isOctal(valueP); + case DEC_RADIX -> isDecimal(valueP); + case HEX_RADIX -> isHexadecimal(valueP); + default -> throw new IllegalArgumentException("Invalid radix: [" + radix + "]"); + + }; + } + + if (radixValue > 0) { + if (value.startsWith("0x")) radixValue = HEX_RADIX; + if (radixValue == HEX_RADIX) { + valueP = value.startsWith("-") ? value.substring(1) : value; + if (valueP.length() % 2 > 0) { + throw new NumberFormatException("The hexadecimal value: \"" + value + "\" must be of even length, or if the decimal value must be a number!"); + } + } + return radixValue; + } else { + if (radix > 0) { + throw new NumberFormatException("Failed radix [" + radix + "] for value: \"" + value + "\", must be [" + radixValue + "] !"); + } else { + throw new NumberFormatException("Invalid \"" + value + "\". It is not numeric or hexadecimal format!"); } - if (Character.digit(value.charAt(i), radix) < 0) - throw new NumberFormatException("Failed radix: [" + radix + "] for value: \"" + value + "\"!"); } - return true; } - private static byte isValidIntegerToByte (Integer val) { - if (val > 255 || val.intValue() < -128) { + public static int isBinary(String str) { + if (str == null || str.isEmpty()) { + return -1; + } + return str.matches("[01]+") ? MIN_RADIX : -1; + } + + public static int isOctal(String str) { + if (str == null || str.isEmpty()) { + return -1; + } + return str.matches("[0-7]+") ? OCTAL_RADIX : -1; + } + + public static int isDecimal(String str) { + if (str == null || str.isEmpty()) { + return -1; + } + return str.matches("-?\\d+(\\.\\d+)?") ? DEC_RADIX : -1; + } + + public static int isHexadecimal(String str) { + if (str == null || str.isEmpty()) { + return -1; + } + return str.matches("^-?(0[xX])?[0-9a-fA-F]+$") ? HEX_RADIX : -1; + } + + private static byte isValidIntegerToByte(Integer val) { + if (val > 255 || val < -128) { throw new NumberFormatException("The value '" + val + "' could not be correctly converted to a byte. " + "Integer to byte conversion requires the use of only 8 bits (with a range of min/max = -128/255)!"); } else { return val.byteValue(); } } + + private static String reverseHexStringByOrder(String value) { + if (value.startsWith("-")) { + throw new IllegalArgumentException("The hexadecimal string must be without a negative sign."); + } + boolean isHexPref = value.startsWith("0x"); + String hex = isHexPref ? value.substring(2) : value; + if (hex.length() % 2 > 0) { + throw new IllegalArgumentException("The hexadecimal string must be even-length."); + } + // Split the hex string into bytes (2 characters each) + StringBuilder reversedHex = new StringBuilder(BYTES_LEN_LONG_MAX); + for (int i = hex.length() - 2; i >= 0; i -= 2) { + reversedHex.append(hex, i, i + 2); + } + String result = reversedHex.toString(); + return isHexPref ? "0x" + result : result; + } } + 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 b4e7e7411d..1d4e12c00e 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 @@ -15,12 +15,14 @@ */ package org.thingsboard.script.api.tbel; +import com.google.common.collect.Lists; import com.google.common.primitives.Bytes; +import com.google.common.primitives.Ints; import lombok.extern.slf4j.Slf4j; -import org.junit.jupiter.api.Test; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; import org.mvel2.ExecutionContext; import org.mvel2.ParserContext; import org.mvel2.SandboxedParserConfiguration; @@ -31,28 +33,28 @@ import java.io.IOException; import java.math.BigInteger; import java.nio.ByteBuffer; import java.util.ArrayList; +import java.util.Arrays; import java.util.Calendar; +import java.util.Collections; import java.util.List; import java.util.Random; +import static java.lang.Character.MAX_RADIX; +import static java.lang.Character.MIN_RADIX; + @Slf4j public class TbUtilsTest { private ExecutionContext ctx; - private final String intValHex = "41EA62CC"; private final float floatVal = 29.29824f; - private final String floatValStr = "29.29824"; - - private final String floatValHexRev = "CC62EA41"; private final float floatValRev = -5.948442E7f; private final long longVal = 0x409B04B10CB295EAL; private final String longValHex = "409B04B10CB295EA"; - private final long longValRev = 0xEA95B20CB1049B40L; private final String longValHexRev = "EA95B20CB1049B40"; - private final String doubleValStr = "1729.1729"; + private final double doubleVal = 1729.1729; private final double doubleValRev = -2.7208640774822924E205; @@ -86,10 +88,9 @@ public class TbUtilsTest { Assertions.assertEquals(0xBAAB, TbUtils.parseHexToInt("ABBA", false)); Assertions.assertEquals(0xAABBCC, TbUtils.parseHexToInt("AABBCC", true)); Assertions.assertEquals(0xAABBCC, TbUtils.parseHexToInt("CCBBAA", false)); - Assertions.assertEquals(0xAABBCCDD, TbUtils.parseHexToInt("AABBCCDD", true)); - Assertions.assertEquals(0xAABBCCDD, TbUtils.parseHexToInt("DDCCBBAA", false)); - Assertions.assertEquals(0xDDCCBBAA, TbUtils.parseHexToInt("DDCCBBAA", true)); - Assertions.assertEquals(0xDDCCBBAA, TbUtils.parseHexToInt("AABBCCDD", false)); + Assertions.assertThrows(NumberFormatException.class, () -> TbUtils.parseHexToInt("AABBCCDD", true)); + Assertions.assertEquals(0x11BBCC22, TbUtils.parseHexToInt("11BBCC22", true)); + Assertions.assertEquals(0x11BBCC22, TbUtils.parseHexToInt("22CCBB11", false)); } @Test @@ -208,15 +209,28 @@ public class TbUtilsTest { Assertions.assertNull(TbUtils.parseInt("")); Assertions.assertNull(TbUtils.parseInt(" ")); - Assertions.assertEquals(java.util.Optional.of(0).get(), TbUtils.parseInt("0")); - Assertions.assertEquals(java.util.Optional.of(0).get(), TbUtils.parseInt("-0")); + Assertions.assertEquals((Integer) 0, TbUtils.parseInt("0")); + Assertions.assertEquals((Integer) 0, TbUtils.parseInt("-0")); Assertions.assertEquals(java.util.Optional.of(473).get(), TbUtils.parseInt("473")); Assertions.assertEquals(java.util.Optional.of(-255).get(), TbUtils.parseInt("-0xFF")); - Assertions.assertThrows(NumberFormatException.class, () -> TbUtils.parseInt("FF")); + Assertions.assertThrows(NumberFormatException.class, () -> TbUtils.parseInt("-0xFF123")); + Assertions.assertEquals(java.util.Optional.of(-255).get(), TbUtils.parseInt("-FF")); + Assertions.assertEquals(java.util.Optional.of(255).get(), TbUtils.parseInt("FF")); + Assertions.assertThrows(IllegalArgumentException.class, () -> TbUtils.parseInt("FFF")); + Assertions.assertEquals(java.util.Optional.of(-2578).get(), TbUtils.parseInt("-0A12")); + Assertions.assertEquals(java.util.Optional.of(-2578).get(), TbUtils.parseHexToInt("-0A12")); + Assertions.assertThrows(IllegalArgumentException.class, () -> TbUtils.parseHexToInt("A12", false)); + Assertions.assertEquals(java.util.Optional.of(-14866).get(), TbUtils.parseBigEndianHexToInt("-3A12")); + Assertions.assertThrows(IllegalArgumentException.class, () -> TbUtils.parseLittleEndianHexToInt("-A12")); + Assertions.assertThrows(NumberFormatException.class, () -> TbUtils.parseInt("0xFG")); Assertions.assertEquals(java.util.Optional.of(102).get(), TbUtils.parseInt("1100110", 2)); + Assertions.assertEquals(java.util.Optional.of(-102).get(), TbUtils.parseInt("1111111111111111111111111111111111111111111111111111111110011010", 2)); Assertions.assertThrows(NumberFormatException.class, () -> TbUtils.parseInt("1100210", 2)); + Assertions.assertEquals(java.util.Optional.of(13158).get(), TbUtils.parseInt("11001101100110", 2)); + Assertions.assertEquals(java.util.Optional.of(-13158).get(), TbUtils.parseInt("1111111111111111111111111111111111111111111111111100110010011010", 2)); + Assertions.assertEquals(java.util.Optional.of(63).get(), TbUtils.parseInt("77", 8)); Assertions.assertThrows(NumberFormatException.class, () -> TbUtils.parseInt("18", 8)); @@ -224,19 +238,32 @@ public class TbUtilsTest { Assertions.assertEquals(java.util.Optional.of(-255).get(), TbUtils.parseInt("-FF", 16)); Assertions.assertThrows(NumberFormatException.class, () -> TbUtils.parseInt("FG", 16)); - Assertions.assertEquals(java.util.Optional.of(Integer.MAX_VALUE).get(), TbUtils.parseInt(Integer.toString(Integer.MAX_VALUE), 10)); Assertions.assertThrows(NumberFormatException.class, () -> TbUtils.parseInt(BigInteger.valueOf(Integer.MAX_VALUE).add(BigInteger.valueOf(1)).toString(10), 10)); Assertions.assertEquals(java.util.Optional.of(Integer.MIN_VALUE).get(), TbUtils.parseInt(Integer.toString(Integer.MIN_VALUE), 10)); Assertions.assertThrows(NumberFormatException.class, () -> TbUtils.parseInt(BigInteger.valueOf(Integer.MIN_VALUE).subtract(BigInteger.valueOf(1)).toString(10), 10)); Assertions.assertEquals(java.util.Optional.of(506070563).get(), TbUtils.parseInt("KonaIn", 30)); + Assertions.assertThrows(NumberFormatException.class, () -> TbUtils.parseInt("KonaIn", 10)); + Assertions.assertThrows(NumberFormatException.class, () -> TbUtils.parseInt(".456", 10)); + Assertions.assertThrows(NumberFormatException.class, () -> TbUtils.parseInt("4562.", 10)); + + Assertions.assertThrows(IllegalArgumentException.class, () -> TbUtils.parseInt("KonaIn", MAX_RADIX + 1)); + Assertions.assertThrows(IllegalArgumentException.class, () -> TbUtils.parseInt("KonaIn", MIN_RADIX - 1)); + Assertions.assertThrows(IllegalArgumentException.class, () -> TbUtils.parseInt("KonaIn", 12)); } @Test public void parseFloat() { + String floatValStr = "29.29824"; Assertions.assertEquals(java.util.Optional.of(floatVal).get(), TbUtils.parseFloat(floatValStr)); + String floatValHex = "41EA62CC"; + Assertions.assertEquals(0, Float.compare(floatVal, TbUtils.parseHexToFloat(floatValHex))); + Assertions.assertEquals(0, Float.compare(floatValRev, TbUtils.parseHexToFloat(floatValHex, false))); + Assertions.assertEquals(0, Float.compare(floatVal, TbUtils.parseBigEndianHexToFloat(floatValHex))); + String floatValHexRev = "CC62EA41"; + Assertions.assertEquals(0, Float.compare(floatVal, TbUtils.parseLittleEndianHexToFloat(floatValHexRev))); } @Test @@ -246,21 +273,13 @@ public class TbUtilsTest { Assertions.assertEquals(0, Float.compare(29.298f, actualF)); } - @Test - public void parseHexToFloat() { - Assertions.assertEquals(0, Float.compare(floatVal, TbUtils.parseHexToFloat(intValHex))); - Assertions.assertEquals(0, Float.compare(floatValRev, TbUtils.parseHexToFloat(intValHex, false))); - Assertions.assertEquals(0, Float.compare(floatVal, TbUtils.parseBigEndianHexToFloat(intValHex))); - Assertions.assertEquals(0, Float.compare(floatVal, TbUtils.parseLittleEndianHexToFloat(floatValHexRev))); - } - @Test public void arseBytesToFloat() { byte[] floatValByte = {65, -22, 98, -52}; Assertions.assertEquals(0, Float.compare(floatVal, TbUtils.parseBytesToFloat(floatValByte, 0))); Assertions.assertEquals(0, Float.compare(floatValRev, TbUtils.parseBytesToFloat(floatValByte, 0, false))); - List floatVaList = Bytes.asList(floatValByte); + List floatVaList = Bytes.asList(floatValByte); Assertions.assertEquals(0, Float.compare(floatVal, TbUtils.parseBytesToFloat(floatVaList, 0))); Assertions.assertEquals(0, Float.compare(floatValRev, TbUtils.parseBytesToFloat(floatVaList, 0, false))); } @@ -271,14 +290,15 @@ public class TbUtilsTest { Assertions.assertNull(TbUtils.parseLong("")); Assertions.assertNull(TbUtils.parseLong(" ")); - Assertions.assertEquals(java.util.Optional.of(0L).get(), TbUtils.parseLong("0")); - Assertions.assertEquals(java.util.Optional.of(0L).get(), TbUtils.parseLong("-0")); + Assertions.assertEquals((Long) 0L, TbUtils.parseLong("0")); + Assertions.assertEquals((Long) 0L, TbUtils.parseLong("-0")); Assertions.assertEquals(java.util.Optional.of(473L).get(), TbUtils.parseLong("473")); Assertions.assertEquals(java.util.Optional.of(-65535L).get(), TbUtils.parseLong("-0xFFFF")); - Assertions.assertThrows(NumberFormatException.class, () -> TbUtils.parseLong("FFFFFFFF")); + Assertions.assertEquals(java.util.Optional.of(4294967295L).get(), TbUtils.parseLong("FFFFFFFF")); Assertions.assertThrows(NumberFormatException.class, () -> TbUtils.parseLong("0xFGFFFFFF")); Assertions.assertEquals(java.util.Optional.of(13158L).get(), TbUtils.parseLong("11001101100110", 2)); + Assertions.assertEquals(java.util.Optional.of(-13158L).get(), TbUtils.parseLong("1111111111111111111111111111111111111111111111111100110010011010", 2)); Assertions.assertThrows(NumberFormatException.class, () -> TbUtils.parseLong("11001101100210", 2)); Assertions.assertEquals(java.util.Optional.of(9223372036854775807L).get(), TbUtils.parseLong("777777777777777777777", 8)); @@ -295,10 +315,7 @@ public class TbUtilsTest { Assertions.assertEquals(java.util.Optional.of(218840926543L).get(), TbUtils.parseLong("KonaLong", 27)); Assertions.assertThrows(NumberFormatException.class, () -> TbUtils.parseLong("KonaLong", 10)); - } - @Test - public void parseHexToLong() { Assertions.assertEquals(longVal, TbUtils.parseHexToLong(longValHex)); Assertions.assertEquals(longVal, TbUtils.parseHexToLong(longValHexRev, false)); Assertions.assertEquals(longVal, TbUtils.parseBigEndianHexToLong(longValHex)); @@ -312,14 +329,20 @@ public class TbUtilsTest { Bytes.reverse(longValByte); Assertions.assertEquals(longVal, TbUtils.parseBytesToLong(longValByte, 0, 8, false)); - List longVaList = Bytes.asList(longValByte); + List longVaList = Bytes.asList(longValByte); Assertions.assertEquals(longVal, TbUtils.parseBytesToLong(longVaList, 0, 8, false)); + long longValRev = 0xEA95B20CB1049B40L; Assertions.assertEquals(longValRev, TbUtils.parseBytesToLong(longVaList, 0, 8)); } @Test public void parsDouble() { + String doubleValStr = "1729.1729"; Assertions.assertEquals(java.util.Optional.of(doubleVal).get(), TbUtils.parseDouble(doubleValStr)); + Assertions.assertEquals(0, Double.compare(doubleVal, TbUtils.parseHexToDouble(longValHex))); + Assertions.assertEquals(0, Double.compare(doubleValRev, TbUtils.parseHexToDouble(longValHex, false))); + Assertions.assertEquals(0, Double.compare(doubleVal, TbUtils.parseBigEndianHexToDouble(longValHex))); + Assertions.assertEquals(0, Double.compare(doubleVal, TbUtils.parseLittleEndianHexToDouble(longValHexRev))); } @Test @@ -329,21 +352,13 @@ public class TbUtilsTest { Assertions.assertEquals(0, Double.compare(1729.173, actualD)); } - @Test - public void parseHexToDouble() { - Assertions.assertEquals(0, Double.compare(doubleVal, TbUtils.parseHexToDouble(longValHex))); - Assertions.assertEquals(0, Double.compare(doubleValRev, TbUtils.parseHexToDouble(longValHex, false))); - Assertions.assertEquals(0, Double.compare(doubleVal, TbUtils.parseBigEndianHexToDouble(longValHex))); - Assertions.assertEquals(0, Double.compare(doubleVal, TbUtils.parseLittleEndianHexToDouble(longValHexRev))); - } - @Test public void parseBytesToDouble() { byte[] doubleValByte = {64, -101, 4, -79, 12, -78, -107, -22}; Assertions.assertEquals(0, Double.compare(doubleVal, TbUtils.parseBytesToDouble(doubleValByte, 0))); Assertions.assertEquals(0, Double.compare(doubleValRev, TbUtils.parseBytesToDouble(doubleValByte, 0, false))); - List doubleVaList = Bytes.asList(doubleValByte); + List doubleVaList = Bytes.asList(doubleValByte); Assertions.assertEquals(0, Double.compare(doubleVal, TbUtils.parseBytesToDouble(doubleVaList, 0))); Assertions.assertEquals(0, Double.compare(doubleValRev, TbUtils.parseBytesToDouble(doubleVaList, 0, false))); } @@ -354,16 +369,17 @@ public class TbUtilsTest { ExecutionHashMap expectedJson = new ExecutionHashMap<>(1, ctx); expectedJson.put("hello", "world"); List expectedBytes = TbUtils.stringToBytes(ctx, expectedStr); - Object actualJson = TbUtils.decodeToJson(ctx, expectedBytes); - Assertions.assertEquals(expectedJson,actualJson); + Object actualJson = TbUtils.decodeToJson(ctx, expectedBytes); + Assertions.assertEquals(expectedJson, actualJson); } + @Test public void parseStringDecodeToJson() throws IOException { String expectedStr = "{\"hello\": \"world\"}"; ExecutionHashMap expectedJson = new ExecutionHashMap<>(1, ctx); expectedJson.put("hello", "world"); - Object actualJson = TbUtils.decodeToJson(ctx, expectedStr); - Assertions.assertEquals(expectedJson,actualJson); + Object actualJson = TbUtils.decodeToJson(ctx, expectedStr); + Assertions.assertEquals(expectedJson, actualJson); } @Test @@ -386,8 +402,8 @@ public class TbUtilsTest { @Test public void bytesFromList() { - byte[] arrayBytes = {(byte)0x00, (byte)0x08, (byte)0x10, (byte)0x1C, (byte)0xFF, (byte)0xFC, (byte)0xAD, (byte)0x88, (byte)0x75, (byte)0x74, (byte)0x8A, (byte)0x82}; - Object[] arrayMix = { "0x00", 8, "16", "0x1C", 255, (byte)0xFC, 173, 136, 117, 116, -118, "-126"}; + byte[] arrayBytes = {(byte) 0x00, (byte) 0x08, (byte) 0x10, (byte) 0x1C, (byte) 0xFF, (byte) 0xFC, (byte) 0xAD, (byte) 0x88, (byte) 0x75, (byte) 0x74, (byte) 0x8A, (byte) 0x82}; + Object[] arrayMix = {"0x00", 8, "16", "0x1C", 255, (byte) 0xFC, 173, 136, 117, 116, -118, "-126"}; String expected = new String(arrayBytes); ArrayList listBytes = new ArrayList<>(arrayBytes.length); @@ -397,12 +413,36 @@ public class TbUtilsTest { Assertions.assertEquals(expected, TbUtils.bytesToString(listBytes)); ArrayList listMix = new ArrayList<>(arrayMix.length); - for (Object element : arrayMix) { - listMix.add(element); - } + Collections.addAll(listMix, arrayMix); Assertions.assertEquals(expected, TbUtils.bytesToString(listMix)); } + @Test + public void bytesFromList_SpecSymbol() { + List listHex = new ArrayList<>(Arrays.asList("1D", "0x1D", "1F", "0x1F", "0x20", "0x20")); + byte[] expectedBytes = new byte[]{29, 29, 31, 31, 32, 32}; + String actualStr = TbUtils.bytesToString(listHex); + byte[] actualBytes = actualStr.getBytes(); + Assertions.assertArrayEquals(expectedBytes, actualBytes); + Assertions.assertTrue(actualStr.isBlank()); + listHex = new ArrayList<>(Arrays.asList("0x21", "0x21")); + expectedBytes = new byte[]{33, 33}; + actualStr = TbUtils.bytesToString(listHex); + actualBytes = actualStr.getBytes(); + Assertions.assertArrayEquals(expectedBytes, actualBytes); + Assertions.assertFalse(actualStr.isBlank()); + Assertions.assertEquals("!!", actualStr); + listHex = new ArrayList<>(Arrays.asList("21", "0x21")); + expectedBytes = new byte[]{21, 33}; + actualStr = TbUtils.bytesToString(listHex); + actualBytes = actualStr.getBytes(); + Assertions.assertArrayEquals(expectedBytes, actualBytes); + Assertions.assertFalse(actualStr.isBlank()); + Assertions.assertEquals("!", actualStr.substring(1)); + Assertions.assertEquals('\u0015', actualStr.charAt(0)); + Assertions.assertEquals(21, actualStr.charAt(0)); + } + @Test public void bytesFromList_Error() { List listHex = new ArrayList<>(); @@ -411,14 +451,7 @@ public class TbUtilsTest { TbUtils.bytesToString(listHex); Assertions.fail("Should throw NumberFormatException"); } catch (NumberFormatException e) { - Assertions.assertTrue(e.getMessage().contains("Failed radix: [16] for value: \"FG\"!")); - } - listHex.add(0, "1F"); - try { - TbUtils.bytesToString(listHex); - Assertions.fail("Should throw NumberFormatException"); - } catch (NumberFormatException e) { - Assertions.assertTrue(e.getMessage().contains("Failed radix: [10] for value: \"1F\"!")); + Assertions.assertTrue(e.getMessage().contains("Value: \"FG\" is not numeric or hexDecimal format!")); } List listIntString = new ArrayList<>(); @@ -482,6 +515,161 @@ public class TbUtilsTest { String uriDecodeActual = TbUtils.decodeURI(uriEncodeActual); Assertions.assertEquals(uriOriginal, uriDecodeActual); } + + @Test + public void intToHex_Test() { + Assertions.assertEquals("FFF5EE", TbUtils.intToHex(-2578)); + Assertions.assertEquals("0xFFD8FFA6", TbUtils.intToHex(0xFFD8FFA6, true, true)); + Assertions.assertEquals("0xA6FFD8FF", TbUtils.intToHex(0xFFD8FFA6, false, true)); + Assertions.assertEquals("0x7FFFFFFF", TbUtils.intToHex(Integer.MAX_VALUE, true, true)); + Assertions.assertEquals("0x80000000", TbUtils.intToHex(Integer.MIN_VALUE, true, true)); + Assertions.assertEquals("0xAB", TbUtils.intToHex(0xAB, true, true)); + Assertions.assertEquals("0xABCD", TbUtils.intToHex(0xABCD, true, true)); + Assertions.assertEquals("0xABCDEF", TbUtils.intToHex(0xABCDEF, true, true)); + Assertions.assertEquals("0xCDAB", TbUtils.intToHex(0xABCDEF, false, true, 4)); + Assertions.assertEquals("0xAB", TbUtils.intToHex(171, true, true)); + Assertions.assertEquals("0xAB", TbUtils.intToHex(0xAB, false, true)); + Assertions.assertEquals("0xAB", TbUtils.intToHex(0xAB, true, true, 2)); + Assertions.assertEquals("AB", TbUtils.intToHex(0xAB, false, false, 2)); + Assertions.assertEquals("AB", TbUtils.intToHex(171, true, false)); + Assertions.assertEquals("0xAB", TbUtils.intToHex(0xAB, true, true)); + Assertions.assertEquals("0xAB", TbUtils.intToHex(0xAB, false, true)); + Assertions.assertEquals("AB", TbUtils.intToHex(0xAB, false, false)); + + Assertions.assertEquals("0xABCD", TbUtils.intToHex(0xABCD, true, true)); + Assertions.assertEquals("0xCDAB", TbUtils.intToHex(0xABCD, false, true)); + Assertions.assertEquals("0xCD", TbUtils.intToHex(0xABCD, true, true, 2)); + Assertions.assertEquals("AB", TbUtils.intToHex(0xABCD, false, false, 2)); + } + + @Test + public void longToHex_Test() { + Assertions.assertEquals("0x7FFFFFFFFFFFFFFF", TbUtils.longToHex(Long.MAX_VALUE, true, true)); + Assertions.assertEquals("0x8000000000000000", TbUtils.longToHex(Long.MIN_VALUE, true, true)); + Assertions.assertEquals("0xFFD8FFA6FFD8FFA6", TbUtils.longToHex(0xFFD8FFA6FFD8FFA6L, true, true)); + Assertions.assertEquals("0xA6FFD8FFA6FFCEFF", TbUtils.longToHex(0xFFCEFFA6FFD8FFA6L, false, true)); + Assertions.assertEquals("0xAB", TbUtils.longToHex(0xABL, true, true)); + Assertions.assertEquals("0xABCD", TbUtils.longToHex(0xABCDL, true, true)); + Assertions.assertEquals("0xABCDEF", TbUtils.longToHex(0xABCDEFL, true, true)); + Assertions.assertEquals("0xABEFCDAB", TbUtils.longToHex(0xABCDEFABCDEFL, false, true, 8)); + Assertions.assertEquals("0xAB", TbUtils.longToHex(0xABL, true, true, 2)); + Assertions.assertEquals("AB", TbUtils.longToHex(0xABL, false, false, 2)); + + Assertions.assertEquals("0xFFA6", TbUtils.longToHex(0xFFD8FFA6FFD8FFA6L, true, true, 4)); + Assertions.assertEquals("D8FF", TbUtils.longToHex(0xFFD8FFA6FFD8FFA6L, false, false, 4)); + } + + @Test + public void numberToString_Test() { + Assertions.assertEquals("11001101100110", TbUtils.intLongToString(13158L, 2)); + Assertions.assertEquals("1111111111111111111111111111111111111111111111111111111110011010", TbUtils.intLongToString(-102L, 2)); + Assertions.assertEquals("1111111111111111111111111111111111111111111111111100110010011010", TbUtils.intLongToString(-13158L, 2)); + Assertions.assertEquals("777777777777777777777", TbUtils.intLongToString(Long.MAX_VALUE, 8)); + Assertions.assertEquals("1000000000000000000000", TbUtils.intLongToString(Long.MIN_VALUE, 8)); + Assertions.assertEquals("9223372036854775807", TbUtils.intLongToString(Long.MAX_VALUE)); + Assertions.assertEquals("-9223372036854775808", TbUtils.intLongToString(Long.MIN_VALUE)); + Assertions.assertEquals("3366", TbUtils.intLongToString(13158L, 16)); + Assertions.assertEquals("FFCC9A", TbUtils.intLongToString(-13158L, 16)); + Assertions.assertEquals("0xFFCC9A", TbUtils.intLongToString(-13158L, 16, true, true)); + + Assertions.assertEquals("0x0400", TbUtils.intLongToString(1024L, 16, true, true)); + Assertions.assertNotEquals("400", TbUtils.intLongToString(1024L, 16)); + Assertions.assertEquals("0xFFFC00", TbUtils.intLongToString(-1024L, 16, true, true)); + Assertions.assertNotEquals("0xFC00", TbUtils.intLongToString(-1024L, 16, true, true)); + + Assertions.assertEquals("hazelnut", TbUtils.intLongToString(1356099454469L, MAX_RADIX)); + } + + @Test + public void intToHexWithPrintUnsignedBytes_Test() { + Integer value = -40; + String intToHexLe = TbUtils.intToHex(value, false, true); + String intToHexBe = TbUtils.intToHex(value, true, true); + + List hexTopByteLe = TbUtils.hexToBytes(ctx, intToHexLe); + List hexTopByteBe = TbUtils.hexToBytes(ctx, intToHexBe); + + byte[] arrayBytes = {-40, -1}; + List expectedHexTopByteLe = Bytes.asList(arrayBytes); + List expectedHexTopByteBe = Lists.reverse(expectedHexTopByteLe); + Assertions.assertEquals(expectedHexTopByteLe, hexTopByteLe); + Assertions.assertEquals(expectedHexTopByteBe, hexTopByteBe); + + List actualLe = TbUtils.printUnsignedBytes(ctx, hexTopByteLe); + List actualBe = TbUtils.printUnsignedBytes(ctx, hexTopByteBe); + List expectedLe = Ints.asList(216, 255); + List expectedBe = Lists.reverse(expectedLe); + Assertions.assertEquals(expectedLe, actualLe); + Assertions.assertEquals(expectedBe, actualBe); + } + + @Test + public void floatToHex_Test() { + Float value = 20.89f; + String expectedHex = "0x41A71EB8"; + String valueHexRev = "0xB81EA741"; + String actual = TbUtils.floatToHex(value); + Assertions.assertEquals(expectedHex, actual); + Float valueActual = TbUtils.parseHexToFloat(actual); + Assertions.assertEquals(value, valueActual); + valueActual = TbUtils.parseHexToFloat(valueHexRev, false); + Assertions.assertEquals(value, valueActual); + } + + @Test + public void doubleToHex_Test() { + String expectedHex = "0x409B04B10CB295EA"; + String actual = TbUtils.doubleToHex(doubleVal); + Assertions.assertEquals(expectedHex, actual); + Double valueActual = TbUtils.parseHexToDouble(actual); + Assertions.assertEquals(doubleVal, valueActual); + actual = TbUtils.doubleToHex(doubleVal, false); + String expectedHexRev = "0xEA95B20CB1049B40"; + Assertions.assertEquals(expectedHexRev, actual); + } + + @Test + public void raiseError_Test() { + String message = "frequency_weighting_type must be 0, 1 or 2."; + Object value = 4; + try { + TbUtils.raiseError(message, value); + Assertions.fail("Should throw NumberFormatException"); + } catch (RuntimeException e) { + Assertions.assertTrue(e.getMessage().contains("frequency_weighting_type must be 0, 1 or 2. for value 4")); + } + try { + TbUtils.raiseError(message); + Assertions.fail("Should throw NumberFormatException"); + } catch (RuntimeException e) { + Assertions.assertTrue(e.getMessage().contains("frequency_weighting_type must be 0, 1 or 2.")); + } + } + + @Test + public void isBinary_Test() { + Assertions.assertEquals(2, TbUtils.isBinary("1100110")); + Assertions.assertEquals(-1, TbUtils.isBinary("2100110")); + } + + @Test + public void isOctal_Test() { + Assertions.assertEquals(8, TbUtils.isOctal("4567734")); + Assertions.assertEquals(-1, TbUtils.isOctal("8100110")); + } + + @Test + public void isDecimal_Test() { + Assertions.assertEquals(10, TbUtils.isDecimal("4567039")); + Assertions.assertEquals(-1, TbUtils.isDecimal("C100110")); + } + + @Test + public void isHexadecimal_Test() { + Assertions.assertEquals(16, TbUtils.isHexadecimal("F5D7039")); + Assertions.assertEquals(-1, TbUtils.isHexadecimal("K100110")); + } + private static List toList(byte[] data) { List result = new ArrayList<>(data.length); for (Byte b : data) { From 93b0ea3c59866cc739bf62fd8907dcf5438419f2 Mon Sep 17 00:00:00 2001 From: Artem Dzhereleiko Date: Wed, 10 Jul 2024 14:48:16 +0300 Subject: [PATCH 07/20] UI: Fixed String input widgets --- .../system/widget_types/update_server_string_attribute.json | 2 +- .../system/widget_types/update_shared_string_attribute.json | 2 +- .../json/system/widget_types/update_string_timeseries.json | 2 +- ui-ngx/src/app/core/services/utils.service.ts | 4 ++++ 4 files changed, 7 insertions(+), 3 deletions(-) diff --git a/application/src/main/data/json/system/widget_types/update_server_string_attribute.json b/application/src/main/data/json/system/widget_types/update_server_string_attribute.json index 193300cb06..d8c33bffcc 100644 --- a/application/src/main/data/json/system/widget_types/update_server_string_attribute.json +++ b/application/src/main/data/json/system/widget_types/update_server_string_attribute.json @@ -11,7 +11,7 @@ "resources": [], "templateHtml": "
\n
\n
\n
\n
\n \n {{ settings.showLabel ? labelValue : '' }}\n \n \n {{requiredErrorMessage}}\n \n \n
\n \n
\n \n \n
\n
\n \n
\n {{ 'widgets.input-widgets.no-entity-selected' | translate }}\n
\n
\n {{ 'widgets.input-widgets.no-attribute-selected' | translate }}\n
\n
\n {{ 'widgets.input-widgets.timeseries-not-allowed' | translate }}\n
\n
\n
\n
", "templateCss": ".attribute-update-form {\n overflow: hidden;\n height: 100%;\n display: flex;\n flex-direction: column;\n}\n\n.attribute-update-form__grid {\n display: flex;\n}\n.grid__element:first-child {\n flex: 1;\n}\n.grid__element:last-child {\n margin-top: 19px;\n margin-left: 7px;\n}\n.grid__element {\n display: flex;\n}\n\n.attribute-update-form .mat-button.mat-icon-button {\n width: 32px;\n min-width: 32px;\n height: 32px;\n min-height: 32px;\n padding: 0 !important;\n margin: 0 !important;\n line-height: 20px;\n}\n\n.attribute-update-form .mat-icon-button mat-icon {\n width: 20px;\n min-width: 20px;\n height: 20px;\n min-height: 20px;\n font-size: 20px;\n}\n\n.tb-toast {\n font-size: 14px!important;\n}", - "controllerScript": "let $scope;\nlet settings;\nlet attributeService;\nlet utils;\nlet translate;\n\nself.onInit = function() {\n self.ctx.ngZone.run(function() {\n init(); \n self.ctx.detectChanges(true);\n });\n};\n\n\nfunction init() {\n $scope = self.ctx.$scope;\n attributeService = $scope.$injector.get(self.ctx.servicesMap.get('attributeService'));\n utils = $scope.$injector.get(self.ctx.servicesMap.get('utils'));\n translate = $scope.$injector.get(self.ctx.servicesMap.get('translate'));\n $scope.toastTargetId = 'input-widget' + utils.guid();\n settings = utils.deepClone(self.ctx.settings) || {};\n settings.showLabel = utils.defaultValue(settings.showLabel, true);\n settings.showResultMessage = utils.defaultValue(settings.showResultMessage, true);\n settings.isRequired = utils.defaultValue(settings.isRequired, true);\n $scope.settings = settings;\n $scope.isValidParameter = true;\n $scope.dataKeyDetected = false;\n\n $scope.requiredErrorMessage = utils.customTranslation(settings.requiredErrorMessage, settings.requiredErrorMessage) || translate.instant('widgets.input-widgets.entity-attribute-required');\n $scope.labelValue = utils.customTranslation(settings.labelValue, settings.labelValue) || translate.instant('widgets.input-widgets.value');\n \n var validators = [$scope.validators.minLength(settings.minLength),\n $scope.validators.maxLength(settings.maxLength)];\n \n if (settings.isRequired) {\n validators.push($scope.validators.required);\n }\n \n $scope.attributeUpdateFormGroup = $scope.fb.group({\n currentValue: [undefined, validators]\n });\n\n if (self.ctx.datasources && self.ctx.datasources.length) {\n var datasource = self.ctx.datasources[0];\n if (datasource.type === 'entity') {\n if (datasource.entityType && datasource.entityId) {\n $scope.entityName = datasource.entityName;\n if (settings.widgetTitle && settings.widgetTitle.length) {\n $scope.titleTemplate = utils.customTranslation(settings.widgetTitle, settings.widgetTitle);\n } else {\n $scope.titleTemplate = self.ctx.widgetConfig.title;\n }\n\n $scope.entityDetected = true;\n }\n }\n if (datasource.dataKeys.length) {\n if (datasource.dataKeys[0].type !== \"attribute\") {\n $scope.isValidParameter = false;\n } else {\n $scope.currentKey = datasource.dataKeys[0].name;\n $scope.dataKeyType = datasource.dataKeys[0].type;\n $scope.dataKeyDetected = true;\n }\n }\n }\n\n self.ctx.widgetTitle = utils.createLabelFromDatasource(self.ctx.datasources[0], $scope.titleTemplate);\n\n $scope.updateAttribute = function () {\n $scope.isFocused = false;\n if ($scope.entityDetected) {\n var datasource = self.ctx.datasources[0];\n \n var value = $scope.attributeUpdateFormGroup.get('currentValue').value;\n \n if (!$scope.attributeUpdateFormGroup.get('currentValue').value.length) {\n value = null;\n }\n\n attributeService.saveEntityAttributes(\n datasource.entity.id,\n 'SERVER_SCOPE',\n [\n {\n key: $scope.currentKey,\n value\n }\n ]\n ).subscribe(\n function success() {\n $scope.originalValue = $scope.attributeUpdateFormGroup.get('currentValue').value;\n if (settings.showResultMessage) {\n $scope.showSuccessToast(translate.instant('widgets.input-widgets.update-successful'), 1000, 'bottom', 'left', $scope.toastTargetId);\n }\n },\n function fail() {\n if (settings.showResultMessage) {\n $scope.showErrorToast(translate.instant('widgets.input-widgets.update-failed'), 'bottom', 'left', $scope.toastTargetId);\n }\n }\n );\n }\n };\n\n $scope.changeFocus = function () {\n if ($scope.attributeUpdateFormGroup.get('currentValue').value === $scope.originalValue) {\n $scope.isFocused = false;\n }\n }\n}\n\nself.onDataUpdated = function() {\n try {\n if ($scope.dataKeyDetected) {\n if (!$scope.isFocused) {\n $scope.originalValue = self.ctx.data[0].data[0][1];\n $scope.attributeUpdateFormGroup.get('currentValue').patchValue($scope.originalValue);\n self.ctx.detectChanges();\n }\n }\n } catch (e) {\n console.log(e);\n }\n}\n\nself.onResize = function() {\n\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true\n }\n}\n\nself.onDestroy = function() {\n\n}", + "controllerScript": "let $scope;\nlet settings;\nlet attributeService;\nlet utils;\nlet translate;\n\nself.onInit = function() {\n self.ctx.ngZone.run(function() {\n init(); \n self.ctx.detectChanges(true);\n });\n};\n\n\nfunction init() {\n $scope = self.ctx.$scope;\n attributeService = $scope.$injector.get(self.ctx.servicesMap.get('attributeService'));\n utils = $scope.$injector.get(self.ctx.servicesMap.get('utils'));\n translate = $scope.$injector.get(self.ctx.servicesMap.get('translate'));\n $scope.toastTargetId = 'input-widget' + utils.guid();\n settings = utils.deepClone(self.ctx.settings) || {};\n settings.showLabel = utils.defaultValue(settings.showLabel, true);\n settings.showResultMessage = utils.defaultValue(settings.showResultMessage, true);\n settings.isRequired = utils.defaultValue(settings.isRequired, true);\n $scope.settings = settings;\n $scope.isValidParameter = true;\n $scope.dataKeyDetected = false;\n\n $scope.requiredErrorMessage = utils.customTranslation(settings.requiredErrorMessage, settings.requiredErrorMessage) || translate.instant('widgets.input-widgets.entity-attribute-required');\n $scope.labelValue = utils.customTranslation(settings.labelValue, settings.labelValue) || translate.instant('widgets.input-widgets.value');\n \n var validators = [];\n if (utils.isDefinedAndNotNull(settings.minLength)) {\n validators.push($scope.validators.minLength(settings.minLength));\n }\n if (utils.isDefinedAndNotNull(settings.maxLength)) {\n validators.push($scope.validators.maxLength(settings.maxLength));\n }\n if (settings.isRequired) {\n validators.push($scope.validators.required);\n }\n \n $scope.attributeUpdateFormGroup = $scope.fb.group({\n currentValue: [undefined, validators]\n });\n\n if (self.ctx.datasources && self.ctx.datasources.length) {\n var datasource = self.ctx.datasources[0];\n if (datasource.type === 'entity') {\n if (datasource.entityType && datasource.entityId) {\n $scope.entityName = datasource.entityName;\n if (settings.widgetTitle && settings.widgetTitle.length) {\n $scope.titleTemplate = utils.customTranslation(settings.widgetTitle, settings.widgetTitle);\n } else {\n $scope.titleTemplate = self.ctx.widgetConfig.title;\n }\n\n $scope.entityDetected = true;\n }\n }\n if (datasource.dataKeys.length) {\n if (datasource.dataKeys[0].type !== \"attribute\") {\n $scope.isValidParameter = false;\n } else {\n $scope.currentKey = datasource.dataKeys[0].name;\n $scope.dataKeyType = datasource.dataKeys[0].type;\n $scope.dataKeyDetected = true;\n }\n }\n }\n\n self.ctx.widgetTitle = utils.createLabelFromDatasource(self.ctx.datasources[0], $scope.titleTemplate);\n\n $scope.updateAttribute = function () {\n $scope.isFocused = false;\n if ($scope.entityDetected) {\n var datasource = self.ctx.datasources[0];\n \n var value = $scope.attributeUpdateFormGroup.get('currentValue').value;\n \n if (!$scope.attributeUpdateFormGroup.get('currentValue').value.length) {\n value = null;\n }\n\n attributeService.saveEntityAttributes(\n datasource.entity.id,\n 'SERVER_SCOPE',\n [\n {\n key: $scope.currentKey,\n value\n }\n ]\n ).subscribe(\n function success() {\n $scope.originalValue = $scope.attributeUpdateFormGroup.get('currentValue').value;\n if (settings.showResultMessage) {\n $scope.showSuccessToast(translate.instant('widgets.input-widgets.update-successful'), 1000, 'bottom', 'left', $scope.toastTargetId);\n }\n },\n function fail() {\n if (settings.showResultMessage) {\n $scope.showErrorToast(translate.instant('widgets.input-widgets.update-failed'), 'bottom', 'left', $scope.toastTargetId);\n }\n }\n );\n }\n };\n\n $scope.changeFocus = function () {\n if ($scope.attributeUpdateFormGroup.get('currentValue').value === $scope.originalValue) {\n $scope.isFocused = false;\n }\n }\n}\n\nself.onDataUpdated = function() {\n try {\n if ($scope.dataKeyDetected) {\n if (!$scope.isFocused) {\n $scope.originalValue = self.ctx.data[0].data[0][1];\n $scope.attributeUpdateFormGroup.get('currentValue').patchValue($scope.originalValue);\n self.ctx.detectChanges();\n }\n }\n } catch (e) {\n console.log(e);\n }\n}\n\nself.onResize = function() {\n\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true\n }\n}\n\nself.onDestroy = function() {\n\n}", "settingsSchema": "", "dataKeySettingsSchema": "{}\n", "settingsDirective": "tb-update-string-attribute-widget-settings", diff --git a/application/src/main/data/json/system/widget_types/update_shared_string_attribute.json b/application/src/main/data/json/system/widget_types/update_shared_string_attribute.json index 4c5333ec59..7578838066 100644 --- a/application/src/main/data/json/system/widget_types/update_shared_string_attribute.json +++ b/application/src/main/data/json/system/widget_types/update_shared_string_attribute.json @@ -11,7 +11,7 @@ "resources": [], "templateHtml": "
\n
\n
\n
\n
\n \n {{ settings.showLabel ? labelValue : '' }}\n \n \n {{requiredErrorMessage}}\n \n \n
\n \n
\n \n \n
\n
\n \n
\n
\n {{ 'widgets.input-widgets.no-attribute-selected' | translate }}\n
\n
\n {{ 'widgets.input-widgets.timeseries-not-allowed' | translate }}\n
\n
\n
\n
", "templateCss": ".attribute-update-form {\n overflow: hidden;\n height: 100%;\n display: flex;\n flex-direction: column;\n}\n\n.attribute-update-form__grid {\n display: flex;\n}\n.grid__element:first-child {\n flex: 1;\n}\n.grid__element:last-child {\n margin-top: 19px;\n margin-left: 7px;\n}\n.grid__element {\n display: flex;\n}\n\n.attribute-update-form .mat-button.mat-icon-button {\n width: 32px;\n min-width: 32px;\n height: 32px;\n min-height: 32px;\n padding: 0 !important;\n margin: 0 !important;\n line-height: 20px;\n}\n\n.attribute-update-form .mat-icon-button mat-icon {\n width: 20px;\n min-width: 20px;\n height: 20px;\n min-height: 20px;\n font-size: 20px;\n}\n\n.tb-toast {\n font-size: 14px!important;\n}", - "controllerScript": "let $scope;\nlet settings;\nlet attributeService;\nlet utils;\nlet translate;\n\nself.onInit = function() {\n self.ctx.ngZone.run(function() {\n init(); \n self.ctx.detectChanges(true);\n });\n};\n\n\nfunction init() {\n\n $scope = self.ctx.$scope;\n attributeService = $scope.$injector.get(self.ctx.servicesMap.get('attributeService'));\n utils = $scope.$injector.get(self.ctx.servicesMap.get('utils'));\n translate = $scope.$injector.get(self.ctx.servicesMap.get('translate'));\n $scope.toastTargetId = 'input-widget' + utils.guid();\n settings = utils.deepClone(self.ctx.settings) || {};\n settings.showLabel = utils.defaultValue(settings.showLabel, true);\n settings.showResultMessage = utils.defaultValue(settings.showResultMessage, true);\n settings.isRequired = utils.defaultValue(settings.isRequired, true);\n $scope.settings = settings;\n $scope.isValidParameter = true;\n $scope.dataKeyDetected = false; \n $scope.message = translate.instant('widgets.input-widgets.no-entity-selected');\n \n $scope.requiredErrorMessage = utils.customTranslation(settings.requiredErrorMessage, settings.requiredErrorMessage) || translate.instant('widgets.input-widgets.entity-attribute-required');\n $scope.labelValue = utils.customTranslation(settings.labelValue, settings.labelValue) || translate.instant('widgets.input-widgets.value');\n \n var validators = [$scope.validators.minLength(settings.minLength),\n $scope.validators.maxLength(settings.maxLength)];\n \n if (settings.isRequired) {\n validators.push($scope.validators.required);\n }\n \n $scope.attributeUpdateFormGroup = $scope.fb.group({\n currentValue: [undefined, validators]\n });\n\n if (self.ctx.datasources && self.ctx.datasources.length) {\n var datasource = self.ctx.datasources[0];\n if (datasource.type === 'entity') {\n if (datasource.entityType === 'DEVICE') {\n if (datasource.entityType && datasource.entityId) {\n $scope.entityName = datasource.entityName;\n if (settings.widgetTitle && settings.widgetTitle.length) {\n $scope.titleTemplate = utils.customTranslation(settings.widgetTitle, settings.widgetTitle);\n } else {\n $scope.titleTemplate = self.ctx.widgetConfig.title;\n }\n \n $scope.entityDetected = true;\n }\n } else {\n $scope.message = translate.instant('widgets.input-widgets.not-allowed-entity');\n }\n }\n if (datasource.dataKeys.length) {\n if (datasource.dataKeys[0].type !== \"attribute\") {\n $scope.isValidParameter = false;\n } else {\n $scope.currentKey = datasource.dataKeys[0].name;\n $scope.dataKeyType = datasource.dataKeys[0].type;\n $scope.dataKeyDetected = true;\n }\n }\n }\n\n self.ctx.widgetTitle = utils.createLabelFromDatasource(self.ctx.datasources[0], $scope.titleTemplate);\n\n $scope.updateAttribute = function () {\n $scope.isFocused = false;\n if ($scope.entityDetected) {\n var datasource = self.ctx.datasources[0];\n var value = $scope.attributeUpdateFormGroup.get('currentValue').value;\n \n if (!$scope.attributeUpdateFormGroup.get('currentValue').value.length) {\n value = null;\n }\n\n attributeService.saveEntityAttributes(\n datasource.entity.id,\n 'SHARED_SCOPE',\n [\n {\n key: $scope.currentKey,\n value\n }\n ]\n ).subscribe(\n function success() {\n $scope.originalValue = $scope.attributeUpdateFormGroup.get('currentValue').value;\n if (settings.showResultMessage) {\n $scope.showSuccessToast(translate.instant('widgets.input-widgets.update-successful'), 1000, 'bottom', 'left', $scope.toastTargetId);\n }\n },\n function fail() {\n if (settings.showResultMessage) {\n $scope.showErrorToast(translate.instant('widgets.input-widgets.update-failed'), 'bottom', 'left', $scope.toastTargetId);\n }\n }\n );\n }\n };\n\n $scope.changeFocus = function () {\n if ($scope.attributeUpdateFormGroup.get('currentValue').value === $scope.originalValue) {\n $scope.isFocused = false;\n }\n }\n}\n\nself.onDataUpdated = function() {\n try {\n if ($scope.dataKeyDetected) {\n if (!$scope.isFocused) {\n $scope.originalValue = self.ctx.data[0].data[0][1];\n $scope.attributeUpdateFormGroup.get('currentValue').patchValue($scope.originalValue);\n self.ctx.detectChanges();\n }\n }\n } catch (e) {\n console.log(e);\n }\n}\n\nself.onResize = function() {\n\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true\n }\n}\n\nself.onDestroy = function() {\n\n}", + "controllerScript": "let $scope;\nlet settings;\nlet attributeService;\nlet utils;\nlet translate;\n\nself.onInit = function() {\n self.ctx.ngZone.run(function() {\n init(); \n self.ctx.detectChanges(true);\n });\n};\n\n\nfunction init() {\n\n $scope = self.ctx.$scope;\n attributeService = $scope.$injector.get(self.ctx.servicesMap.get('attributeService'));\n utils = $scope.$injector.get(self.ctx.servicesMap.get('utils'));\n translate = $scope.$injector.get(self.ctx.servicesMap.get('translate'));\n $scope.toastTargetId = 'input-widget' + utils.guid();\n settings = utils.deepClone(self.ctx.settings) || {};\n settings.showLabel = utils.defaultValue(settings.showLabel, true);\n settings.showResultMessage = utils.defaultValue(settings.showResultMessage, true);\n settings.isRequired = utils.defaultValue(settings.isRequired, true);\n $scope.settings = settings;\n $scope.isValidParameter = true;\n $scope.dataKeyDetected = false; \n $scope.message = translate.instant('widgets.input-widgets.no-entity-selected');\n \n $scope.requiredErrorMessage = utils.customTranslation(settings.requiredErrorMessage, settings.requiredErrorMessage) || translate.instant('widgets.input-widgets.entity-attribute-required');\n $scope.labelValue = utils.customTranslation(settings.labelValue, settings.labelValue) || translate.instant('widgets.input-widgets.value');\n \n var validators = [];\n if (utils.isDefinedAndNotNull(settings.minLength)) {\n validators.push($scope.validators.minLength(settings.minLength));\n }\n if (utils.isDefinedAndNotNull(settings.maxLength)) {\n validators.push($scope.validators.maxLength(settings.maxLength));\n }\n if (settings.isRequired) {\n validators.push($scope.validators.required);\n }\n \n $scope.attributeUpdateFormGroup = $scope.fb.group({\n currentValue: [undefined, validators]\n });\n\n if (self.ctx.datasources && self.ctx.datasources.length) {\n var datasource = self.ctx.datasources[0];\n if (datasource.type === 'entity') {\n if (datasource.entityType === 'DEVICE') {\n if (datasource.entityType && datasource.entityId) {\n $scope.entityName = datasource.entityName;\n if (settings.widgetTitle && settings.widgetTitle.length) {\n $scope.titleTemplate = utils.customTranslation(settings.widgetTitle, settings.widgetTitle);\n } else {\n $scope.titleTemplate = self.ctx.widgetConfig.title;\n }\n \n $scope.entityDetected = true;\n }\n } else {\n $scope.message = translate.instant('widgets.input-widgets.not-allowed-entity');\n }\n }\n if (datasource.dataKeys.length) {\n if (datasource.dataKeys[0].type !== \"attribute\") {\n $scope.isValidParameter = false;\n } else {\n $scope.currentKey = datasource.dataKeys[0].name;\n $scope.dataKeyType = datasource.dataKeys[0].type;\n $scope.dataKeyDetected = true;\n }\n }\n }\n\n self.ctx.widgetTitle = utils.createLabelFromDatasource(self.ctx.datasources[0], $scope.titleTemplate);\n\n $scope.updateAttribute = function () {\n $scope.isFocused = false;\n if ($scope.entityDetected) {\n var datasource = self.ctx.datasources[0];\n var value = $scope.attributeUpdateFormGroup.get('currentValue').value;\n \n if (!$scope.attributeUpdateFormGroup.get('currentValue').value.length) {\n value = null;\n }\n\n attributeService.saveEntityAttributes(\n datasource.entity.id,\n 'SHARED_SCOPE',\n [\n {\n key: $scope.currentKey,\n value\n }\n ]\n ).subscribe(\n function success() {\n $scope.originalValue = $scope.attributeUpdateFormGroup.get('currentValue').value;\n if (settings.showResultMessage) {\n $scope.showSuccessToast(translate.instant('widgets.input-widgets.update-successful'), 1000, 'bottom', 'left', $scope.toastTargetId);\n }\n },\n function fail() {\n if (settings.showResultMessage) {\n $scope.showErrorToast(translate.instant('widgets.input-widgets.update-failed'), 'bottom', 'left', $scope.toastTargetId);\n }\n }\n );\n }\n };\n\n $scope.changeFocus = function () {\n if ($scope.attributeUpdateFormGroup.get('currentValue').value === $scope.originalValue) {\n $scope.isFocused = false;\n }\n }\n}\n\nself.onDataUpdated = function() {\n try {\n if ($scope.dataKeyDetected) {\n if (!$scope.isFocused) {\n $scope.originalValue = self.ctx.data[0].data[0][1];\n $scope.attributeUpdateFormGroup.get('currentValue').patchValue($scope.originalValue);\n self.ctx.detectChanges();\n }\n }\n } catch (e) {\n console.log(e);\n }\n}\n\nself.onResize = function() {\n\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true\n }\n}\n\nself.onDestroy = function() {\n\n}", "settingsSchema": "", "dataKeySettingsSchema": "{}\n", "settingsDirective": "tb-update-string-attribute-widget-settings", diff --git a/application/src/main/data/json/system/widget_types/update_string_timeseries.json b/application/src/main/data/json/system/widget_types/update_string_timeseries.json index 43a96d6f69..d54224ef36 100644 --- a/application/src/main/data/json/system/widget_types/update_string_timeseries.json +++ b/application/src/main/data/json/system/widget_types/update_string_timeseries.json @@ -11,7 +11,7 @@ "resources": [], "templateHtml": "
\n
\n
\n
\n
\n \n {{ settings.showLabel ? labelValue : '' }}\n \n \n {{requiredErrorMessage}}\n \n \n
\n \n
\n \n \n
\n
\n \n
\n {{ 'widgets.input-widgets.no-entity-selected' | translate }}\n
\n
\n {{ 'widgets.input-widgets.no-timeseries-selected' | translate }}\n
\n
\n {{ 'widgets.input-widgets.attribute-not-allowed' | translate }}\n
\n
\n
\n
", "templateCss": ".attribute-update-form {\n overflow: hidden;\n height: 100%;\n display: flex;\n flex-direction: column;\n}\n\n.attribute-update-form__grid {\n display: flex;\n}\n.grid__element:first-child {\n flex: 1;\n}\n.grid__element:last-child {\n margin-top: 19px;\n margin-left: 7px;\n}\n.grid__element {\n display: flex;\n}\n\n.attribute-update-form .mat-button.mat-icon-button {\n width: 32px;\n min-width: 32px;\n height: 32px;\n min-height: 32px;\n padding: 0 !important;\n margin: 0 !important;\n line-height: 20px;\n}\n\n.attribute-update-form .mat-icon-button mat-icon {\n width: 20px;\n min-width: 20px;\n height: 20px;\n min-height: 20px;\n font-size: 20px;\n}\n\n.tb-toast {\n font-size: 14px!important;\n}", - "controllerScript": "let $scope;\nlet settings;\nlet utils;\nlet translate;\nlet http;\n\nself.onInit = function() {\n self.ctx.ngZone.run(function() {\n init(); \n self.ctx.detectChanges(true);\n });\n};\n\n\nfunction init() {\n\n $scope = self.ctx.$scope;\n utils = $scope.$injector.get(self.ctx.servicesMap.get('utils'));\n translate = $scope.$injector.get(self.ctx.servicesMap.get('translate'));\n http = $scope.$injector.get(self.ctx.servicesMap.get('http'));\n $scope.toastTargetId = 'input-widget' + utils.guid();\n settings = utils.deepClone(self.ctx.settings) || {};\n settings.showLabel = utils.defaultValue(settings.showLabel, true);\n settings.showResultMessage = utils.defaultValue(settings.showResultMessage, true);\n $scope.settings = settings;\n $scope.isValidParameter = true;\n $scope.dataKeyDetected = false;\n $scope.requiredErrorMessage = utils.customTranslation(settings.requiredErrorMessage, settings.requiredErrorMessage) || translate.instant('widgets.input-widgets.entity-timeseries-required');\n $scope.labelValue = utils.customTranslation(settings.labelValue, settings.labelValue) || translate.instant('widgets.input-widgets.value');\n\n $scope.attributeUpdateFormGroup = $scope.fb.group(\n {currentValue: [undefined, [$scope.validators.required,\n $scope.validators.minLength(settings.minLength),\n $scope.validators.maxLength(settings.maxLength)]]}\n );\n\n if (self.ctx.datasources && self.ctx.datasources.length) {\n var datasource = self.ctx.datasources[0];\n if (datasource.type === 'entity') {\n if (datasource.entityType && datasource.entityId) {\n $scope.entityName = datasource.entityName;\n if (settings.widgetTitle && settings.widgetTitle.length) {\n $scope.titleTemplate = utils.customTranslation(settings.widgetTitle, settings.widgetTitle);\n } else {\n $scope.titleTemplate = self.ctx.widgetConfig.title;\n }\n\n $scope.entityDetected = true;\n }\n }\n if (datasource.dataKeys.length) {\n if (datasource.dataKeys[0].type !== \"timeseries\") {\n $scope.isValidParameter = false;\n } else {\n $scope.currentKey = datasource.dataKeys[0].name;\n $scope.dataKeyType = datasource.dataKeys[0].type;\n $scope.dataKeyDetected = true;\n }\n }\n }\n\n self.ctx.widgetTitle = utils.createLabelFromDatasource(self.ctx.datasources[0], $scope.titleTemplate);\n\n $scope.updateAttribute = function () {\n $scope.isFocused = false;\n if ($scope.entityDetected) {\n var datasource = self.ctx.datasources[0];\n\n let observable = saveEntityTimeseries(\n datasource.entityType,\n datasource.entityId,\n [\n {\n key: $scope.currentKey,\n value: $scope.attributeUpdateFormGroup.get('currentValue').value\n }\n ]\n );\n if (observable) {\n observable.subscribe(\n function success() {\n $scope.originalValue = $scope.attributeUpdateFormGroup.get('currentValue').value;\n if (settings.showResultMessage) {\n $scope.showSuccessToast(translate.instant('widgets.input-widgets.update-successful'), 1000, 'bottom', 'left', $scope.toastTargetId);\n }\n },\n function fail() {\n if (settings.showResultMessage) {\n $scope.showErrorToast(translate.instant('widgets.input-widgets.update-failed'), 'bottom', 'left', $scope.toastTargetId);\n }\n }\n );\n }\n }\n };\n\n $scope.changeFocus = function () {\n if ($scope.attributeUpdateFormGroup.get('currentValue').value === $scope.originalValue) {\n $scope.isFocused = false;\n }\n }\n\n function saveEntityTimeseries(entityType, entityId, telemetries) {\n var telemetriesData = {};\n for (var a = 0; a < telemetries.length; a++) {\n if (typeof telemetries[a].value !== 'undefined' && telemetries[a].value !== null) {\n telemetriesData[telemetries[a].key] = telemetries[a].value;\n }\n }\n if (Object.keys(telemetriesData).length) {\n var url = '/api/plugins/telemetry/' + entityType + '/' + entityId + '/timeseries/scope';\n return http.post(url, telemetriesData);\n }\n return null;\n }\n}\n\nself.onDataUpdated = function() {\n\n try {\n if ($scope.dataKeyDetected) {\n if (!$scope.isFocused) {\n $scope.originalValue = self.ctx.data[0].data[0][1];\n $scope.attributeUpdateFormGroup.get('currentValue').patchValue($scope.originalValue);\n self.ctx.detectChanges();\n }\n }\n } catch (e) {\n console.log(e);\n }\n}\n\nself.onResize = function() {\n\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true\n }\n}\n\nself.onDestroy = function() {\n\n}\n", + "controllerScript": "let $scope;\nlet settings;\nlet utils;\nlet translate;\nlet http;\n\nself.onInit = function() {\n self.ctx.ngZone.run(function() {\n init(); \n self.ctx.detectChanges(true);\n });\n};\n\n\nfunction init() {\n\n $scope = self.ctx.$scope;\n utils = $scope.$injector.get(self.ctx.servicesMap.get('utils'));\n translate = $scope.$injector.get(self.ctx.servicesMap.get('translate'));\n http = $scope.$injector.get(self.ctx.servicesMap.get('http'));\n $scope.toastTargetId = 'input-widget' + utils.guid();\n settings = utils.deepClone(self.ctx.settings) || {};\n settings.showLabel = utils.defaultValue(settings.showLabel, true);\n settings.showResultMessage = utils.defaultValue(settings.showResultMessage, true);\n $scope.settings = settings;\n $scope.isValidParameter = true;\n $scope.dataKeyDetected = false;\n $scope.requiredErrorMessage = utils.customTranslation(settings.requiredErrorMessage, settings.requiredErrorMessage) || translate.instant('widgets.input-widgets.entity-timeseries-required');\n $scope.labelValue = utils.customTranslation(settings.labelValue, settings.labelValue) || translate.instant('widgets.input-widgets.value');\n \n var validators = [];\n if (utils.isDefinedAndNotNull(settings.minLength)) {\n validators.push($scope.validators.minLength(settings.minLength));\n }\n if (utils.isDefinedAndNotNull(settings.maxLength)) {\n validators.push($scope.validators.maxLength(settings.maxLength));\n }\n if (settings.isRequired) {\n validators.push($scope.validators.required);\n }\n\n $scope.attributeUpdateFormGroup = $scope.fb.group(\n {currentValue: [undefined, validators]}\n );\n\n if (self.ctx.datasources && self.ctx.datasources.length) {\n var datasource = self.ctx.datasources[0];\n if (datasource.type === 'entity') {\n if (datasource.entityType && datasource.entityId) {\n $scope.entityName = datasource.entityName;\n if (settings.widgetTitle && settings.widgetTitle.length) {\n $scope.titleTemplate = utils.customTranslation(settings.widgetTitle, settings.widgetTitle);\n } else {\n $scope.titleTemplate = self.ctx.widgetConfig.title;\n }\n\n $scope.entityDetected = true;\n }\n }\n if (datasource.dataKeys.length) {\n if (datasource.dataKeys[0].type !== \"timeseries\") {\n $scope.isValidParameter = false;\n } else {\n $scope.currentKey = datasource.dataKeys[0].name;\n $scope.dataKeyType = datasource.dataKeys[0].type;\n $scope.dataKeyDetected = true;\n }\n }\n }\n\n self.ctx.widgetTitle = utils.createLabelFromDatasource(self.ctx.datasources[0], $scope.titleTemplate);\n\n $scope.updateAttribute = function () {\n $scope.isFocused = false;\n if ($scope.entityDetected) {\n var datasource = self.ctx.datasources[0];\n\n let observable = saveEntityTimeseries(\n datasource.entityType,\n datasource.entityId,\n [\n {\n key: $scope.currentKey,\n value: $scope.attributeUpdateFormGroup.get('currentValue').value\n }\n ]\n );\n if (observable) {\n observable.subscribe(\n function success() {\n $scope.originalValue = $scope.attributeUpdateFormGroup.get('currentValue').value;\n if (settings.showResultMessage) {\n $scope.showSuccessToast(translate.instant('widgets.input-widgets.update-successful'), 1000, 'bottom', 'left', $scope.toastTargetId);\n }\n },\n function fail() {\n if (settings.showResultMessage) {\n $scope.showErrorToast(translate.instant('widgets.input-widgets.update-failed'), 'bottom', 'left', $scope.toastTargetId);\n }\n }\n );\n }\n }\n };\n\n $scope.changeFocus = function () {\n if ($scope.attributeUpdateFormGroup.get('currentValue').value === $scope.originalValue) {\n $scope.isFocused = false;\n }\n }\n\n function saveEntityTimeseries(entityType, entityId, telemetries) {\n var telemetriesData = {};\n for (var a = 0; a < telemetries.length; a++) {\n if (typeof telemetries[a].value !== 'undefined' && telemetries[a].value !== null) {\n telemetriesData[telemetries[a].key] = telemetries[a].value;\n }\n }\n if (Object.keys(telemetriesData).length) {\n var url = '/api/plugins/telemetry/' + entityType + '/' + entityId + '/timeseries/scope';\n return http.post(url, telemetriesData);\n }\n return null;\n }\n}\n\nself.onDataUpdated = function() {\n\n try {\n if ($scope.dataKeyDetected) {\n if (!$scope.isFocused) {\n $scope.originalValue = self.ctx.data[0].data[0][1];\n $scope.attributeUpdateFormGroup.get('currentValue').patchValue($scope.originalValue);\n self.ctx.detectChanges();\n }\n }\n } catch (e) {\n console.log(e);\n }\n}\n\nself.onResize = function() {\n\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true\n }\n}\n\nself.onDestroy = function() {\n\n}\n", "settingsSchema": "", "dataKeySettingsSchema": "{}\n", "settingsDirective": "tb-update-string-attribute-widget-settings", diff --git a/ui-ngx/src/app/core/services/utils.service.ts b/ui-ngx/src/app/core/services/utils.service.ts index 097c3b8d49..ae2754c0eb 100644 --- a/ui-ngx/src/app/core/services/utils.service.ts +++ b/ui-ngx/src/app/core/services/utils.service.ts @@ -456,6 +456,10 @@ export class UtilsService { return isDefined(value); } + public isDefinedAndNotNull(value: any): boolean { + return isDefinedAndNotNull(value); + } + public defaultValue(value: any, defaultValue: any): any { if (isDefinedAndNotNull(value)) { return value; From a5556b9e549cc4503b3a5c9c106b52524645ea59 Mon Sep 17 00:00:00 2001 From: rusikv Date: Wed, 10 Jul 2024 18:25:26 +0300 Subject: [PATCH 08/20] UI: fixed invalid tooltip in chart fill settings --- .../chart/bar-chart-with-labels-basic-config.component.html | 2 +- .../chart/bar-chart-with-labels-widget-settings.component.html | 2 +- .../lib/settings/common/chart/chart-bar-settings.component.html | 2 +- .../settings/common/chart/chart-fill-settings.component.html | 2 +- .../lib/settings/common/chart/chart-fill-settings.component.ts | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/bar-chart-with-labels-basic-config.component.html b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/bar-chart-with-labels-basic-config.component.html index 0d8537a3a6..774e6d88c5 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/bar-chart-with-labels-basic-config.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/bar-chart-with-labels-basic-config.component.html @@ -160,7 +160,7 @@ diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/chart-fill-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/chart-fill-settings.component.html index d79e84d98e..fe92ec2af3 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/chart-fill-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/chart-fill-settings.component.html @@ -18,7 +18,7 @@
-
{{ title | translate }}
+
{{ rowTitle | translate }}
{{ chartFillTypeTranslationMap.get(type) | translate }} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/chart-fill-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/chart-fill-settings.component.ts index 718c4cce02..c909df3511 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/chart-fill-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/chart-fill-settings.component.ts @@ -55,7 +55,7 @@ export class ChartFillSettingsComponent implements OnInit, ControlValueAccessor disabled: boolean; @Input() - title = 'widgets.chart.fill'; + rowTitle = 'widgets.chart.fill'; @Input() fillNoneTitle = 'widgets.chart.fill-type-none'; From 50b9d693860941a93091828533ae1dd0b9577ae9 Mon Sep 17 00:00:00 2001 From: Artem Dzhereleiko Date: Thu, 11 Jul 2024 11:59:03 +0300 Subject: [PATCH 09/20] UI: Refactoring --- .../entity/entity-list.component.ts | 25 +++++++++++++++---- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/ui-ngx/src/app/shared/components/entity/entity-list.component.ts b/ui-ngx/src/app/shared/components/entity/entity-list.component.ts index 383d5bc8f4..db785ab034 100644 --- a/ui-ngx/src/app/shared/components/entity/entity-list.component.ts +++ b/ui-ngx/src/app/shared/components/entity/entity-list.component.ts @@ -25,7 +25,15 @@ import { SimpleChanges, ViewChild } from '@angular/core'; -import { ControlValueAccessor, UntypedFormBuilder, UntypedFormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms'; +import { + ControlValueAccessor, + NG_VALIDATORS, + NG_VALUE_ACCESSOR, + UntypedFormBuilder, + UntypedFormGroup, + ValidationErrors, + Validators +} from '@angular/forms'; import { Observable } from 'rxjs'; import { filter, map, mergeMap, share, tap } from 'rxjs/operators'; import { Store } from '@ngrx/store'; @@ -49,6 +57,11 @@ import { SubscriptSizing } from '@angular/material/form-field'; provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => EntityListComponent), multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => EntityListComponent), + multi: true } ] }) @@ -179,10 +192,6 @@ export class EntityListComponent implements ControlValueAccessor, OnInit, AfterV (entities) => { this.entities = entities; this.entityListFormGroup.get('entities').setValue(this.entities); - if(!entities.length && this.required) { - this.modelValue = null; - this.propagateChange(this.modelValue); - } } ); } else { @@ -193,6 +202,12 @@ export class EntityListComponent implements ControlValueAccessor, OnInit, AfterV this.dirty = true; } + validate(): ValidationErrors | null { + return this.entityListFormGroup.valid ? null : { + entities: {valid: false} + }; + } + reset() { this.entities = []; this.entityListFormGroup.get('entities').setValue(this.entities); From 65789f2e7bcb5e4ca9d88edb9a37d1471ff60652 Mon Sep 17 00:00:00 2001 From: YevhenBondarenko Date: Thu, 11 Jul 2024 11:03:37 +0200 Subject: [PATCH 10/20] fixed adding body condition and content-type creation --- .../rule/engine/rest/TbHttpClient.java | 26 ++++++++++--------- 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbHttpClient.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbHttpClient.java index 1db9dd2fc5..1c02d882d0 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbHttpClient.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbHttpClient.java @@ -31,6 +31,7 @@ import org.springframework.web.client.RestClientResponseException; import org.springframework.web.reactive.function.BodyInserters; import org.springframework.web.reactive.function.client.WebClient; import org.springframework.web.reactive.function.client.WebClient.RequestBodySpec; +import org.springframework.web.reactive.function.client.WebClientResponseException; import org.springframework.web.util.UriComponentsBuilder; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.rule.engine.api.TbContext; @@ -43,11 +44,13 @@ import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgMetaData; import reactor.netty.http.client.HttpClient; +import reactor.netty.resources.ConnectionProvider; import reactor.netty.transport.ProxyProvider; import javax.net.ssl.SSLException; import java.net.URI; import java.nio.charset.StandardCharsets; +import java.time.Duration; import java.util.Base64; import java.util.List; import java.util.Map; @@ -57,6 +60,8 @@ import java.util.concurrent.TimeUnit; import java.util.function.BiConsumer; import java.util.function.Consumer; +import static io.netty.channel.ChannelOption.SO_KEEPALIVE; + @Data @Slf4j public class TbHttpClient { @@ -170,7 +175,7 @@ public class TbHttpClient { BiConsumer onFailure) { try { if (semaphore != null && !semaphore.tryAcquire(config.getReadTimeoutMs(), TimeUnit.MILLISECONDS)) { - ctx.tellFailure(msg, new RuntimeException("Timeout during waiting for reply!")); + onFailure.accept(msg, new RuntimeException("Timeout during waiting for reply!")); return; } @@ -183,10 +188,10 @@ public class TbHttpClient { .uri(uri) .headers(headers -> prepareHeaders(headers, msg)); - if (HttpMethod.POST.equals(method) || HttpMethod.PUT.equals(method) || - HttpMethod.PATCH.equals(method) || HttpMethod.DELETE.equals(method) || + if ((HttpMethod.POST.equals(method) || HttpMethod.PUT.equals(method) || + HttpMethod.PATCH.equals(method) || HttpMethod.DELETE.equals(method)) && !config.isIgnoreRequestBody()) { - request.body(BodyInserters.fromValue(getData(msg, config.isIgnoreRequestBody(), config.isParseToPlainText()))); + request.body(BodyInserters.fromValue(getData(msg, config.isParseToPlainText()))); } request @@ -236,11 +241,9 @@ public class TbHttpClient { return uri; } - private String getData(TbMsg tbMsg, boolean ignoreBody, boolean parseToPlainText) { - if (!ignoreBody && parseToPlainText) { - return JacksonUtil.toPlainText(tbMsg.getData()); - } - return tbMsg.getData(); + private Object getData(TbMsg tbMsg, boolean parseToPlainText) { + String data = tbMsg.getData(); + return parseToPlainText ? JacksonUtil.toPlainText(data) : JacksonUtil.toJsonNode(data); } private TbMsg processResponse(TbContext ctx, TbMsg origMsg, ResponseEntity response) { @@ -283,10 +286,9 @@ public class TbHttpClient { private TbMsg processException(TbMsg origMsg, Throwable e) { TbMsgMetaData metaData = origMsg.getMetaData(); metaData.putValue(ERROR, e.getClass() + ": " + e.getMessage()); - if (e instanceof RestClientResponseException) { - RestClientResponseException restClientResponseException = (RestClientResponseException) e; + if (e instanceof WebClientResponseException restClientResponseException) { metaData.putValue(STATUS, restClientResponseException.getStatusText()); - metaData.putValue(STATUS_CODE, restClientResponseException.getRawStatusCode() + ""); + metaData.putValue(STATUS_CODE, restClientResponseException.getStatusCode().value() + ""); metaData.putValue(ERROR_BODY, restClientResponseException.getResponseBodyAsString()); } return TbMsg.transformMsgMetadata(origMsg, metaData); From aa27c84ddea08bb2eb99935ed461bd0fbe4ffb8d Mon Sep 17 00:00:00 2001 From: Artem Dzhereleiko Date: Thu, 11 Jul 2024 12:20:10 +0300 Subject: [PATCH 11/20] UI: Refactoring --- .../entity/entity-list.component.ts | 28 ++++++++----------- 1 file changed, 12 insertions(+), 16 deletions(-) diff --git a/ui-ngx/src/app/shared/components/entity/entity-list.component.ts b/ui-ngx/src/app/shared/components/entity/entity-list.component.ts index db785ab034..a4d7c9bab0 100644 --- a/ui-ngx/src/app/shared/components/entity/entity-list.component.ts +++ b/ui-ngx/src/app/shared/components/entity/entity-list.component.ts @@ -36,8 +36,6 @@ import { } from '@angular/forms'; import { Observable } from 'rxjs'; import { filter, map, mergeMap, share, tap } from 'rxjs/operators'; -import { Store } from '@ngrx/store'; -import { AppState } from '@app/core/core.state'; import { TranslateService } from '@ngx-translate/core'; import { EntityType } from '@shared/models/entity-type.models'; import { BaseData } from '@shared/models/base-data'; @@ -69,7 +67,7 @@ export class EntityListComponent implements ControlValueAccessor, OnInit, AfterV entityListFormGroup: UntypedFormGroup; - modelValue: Array | null; + private modelValue: Array | null; @Input() entityType: EntityType; @@ -121,17 +119,16 @@ export class EntityListComponent implements ControlValueAccessor, OnInit, AfterV private propagateChange = (v: any) => { }; - constructor(private store: Store, - public translate: TranslateService, + constructor(public translate: TranslateService, private entityService: EntityService, private fb: UntypedFormBuilder) { this.entityListFormGroup = this.fb.group({ - entities: [this.entities, this.required ? [Validators.required] : []], + entities: [this.entities], entity: [null] }); } - updateValidators() { + private updateValidators() { this.entityListFormGroup.get('entities').setValidators(this.required ? [Validators.required] : []); this.entityListFormGroup.get('entities').updateValueAndValidity(); } @@ -208,7 +205,7 @@ export class EntityListComponent implements ControlValueAccessor, OnInit, AfterV }; } - reset() { + private reset() { this.entities = []; this.entityListFormGroup.get('entities').setValue(this.entities); this.modelValue = null; @@ -220,7 +217,7 @@ export class EntityListComponent implements ControlValueAccessor, OnInit, AfterV this.dirty = true; } - add(entity: BaseData): void { + private add(entity: BaseData): void { if (!this.modelValue || this.modelValue.indexOf(entity.id.id) === -1) { if (!this.modelValue) { this.modelValue = []; @@ -233,7 +230,7 @@ export class EntityListComponent implements ControlValueAccessor, OnInit, AfterV this.clear(); } - remove(entity: BaseData) { + public remove(entity: BaseData) { let index = this.entities.indexOf(entity); if (index >= 0) { this.entities.splice(index, 1); @@ -248,11 +245,11 @@ export class EntityListComponent implements ControlValueAccessor, OnInit, AfterV } } - displayEntityFn(entity?: BaseData): string | undefined { + public displayEntityFn(entity?: BaseData): string | undefined { return entity ? entity.name : undefined; } - fetchEntities(searchText?: string): Observable>> { + private fetchEntities(searchText?: string): Observable>> { this.searchText = searchText; return this.entityService.getEntitiesByNameFilter(this.entityType, searchText, @@ -260,14 +257,14 @@ export class EntityListComponent implements ControlValueAccessor, OnInit, AfterV map((data) => data ? data : [])); } - onFocus() { + public onFocus() { if (this.dirty) { this.entityListFormGroup.get('entity').updateValueAndValidity({onlySelf: true, emitEvent: true}); this.dirty = false; } } - clear(value: string = '') { + private clear(value: string = '') { this.entityInput.nativeElement.value = value; this.entityListFormGroup.get('entity').patchValue(value, {emitEvent: true}); setTimeout(() => { @@ -276,8 +273,7 @@ export class EntityListComponent implements ControlValueAccessor, OnInit, AfterV }, 0); } - textIsNotEmpty(text: string): boolean { + public textIsNotEmpty(text: string): boolean { return (text && text.length > 0); } - } From 58185d4a4c42bb8bfb363e467eaeb1c82afd1325 Mon Sep 17 00:00:00 2001 From: YevhenBondarenko Date: Thu, 11 Jul 2024 11:23:03 +0200 Subject: [PATCH 12/20] hotfix Connection reset --- .../main/java/org/thingsboard/rule/engine/rest/TbHttpClient.java | 1 + 1 file changed, 1 insertion(+) diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbHttpClient.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbHttpClient.java index 1c02d882d0..24304f33b4 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbHttpClient.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbHttpClient.java @@ -137,6 +137,7 @@ public class TbHttpClient { this.webClient = WebClient.builder() .clientConnector(new ReactorClientHttpConnector(httpClient)) + .defaultHeader(HttpHeaders.CONNECTION, "close") //In previous realization this header was present! (Added for hotfix "Connection reset") .build(); } catch (SSLException e) { throw new TbNodeException(e); From 196ec5e59de33834c1d962a71eddd9c494395d07 Mon Sep 17 00:00:00 2001 From: YevhenBondarenko Date: Thu, 11 Jul 2024 11:33:29 +0200 Subject: [PATCH 13/20] fixed tests --- .../java/org/thingsboard/rule/engine/rest/TbHttpClient.java | 5 ----- .../thingsboard/rule/engine/rest/TbRestApiCallNodeTest.java | 2 +- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbHttpClient.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbHttpClient.java index 24304f33b4..26de3ad5e5 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbHttpClient.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbHttpClient.java @@ -27,7 +27,6 @@ import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.http.client.reactive.ReactorClientHttpConnector; -import org.springframework.web.client.RestClientResponseException; import org.springframework.web.reactive.function.BodyInserters; import org.springframework.web.reactive.function.client.WebClient; import org.springframework.web.reactive.function.client.WebClient.RequestBodySpec; @@ -44,13 +43,11 @@ import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgMetaData; import reactor.netty.http.client.HttpClient; -import reactor.netty.resources.ConnectionProvider; import reactor.netty.transport.ProxyProvider; import javax.net.ssl.SSLException; import java.net.URI; import java.nio.charset.StandardCharsets; -import java.time.Duration; import java.util.Base64; import java.util.List; import java.util.Map; @@ -60,8 +57,6 @@ import java.util.concurrent.TimeUnit; import java.util.function.BiConsumer; import java.util.function.Consumer; -import static io.netty.channel.ChannelOption.SO_KEEPALIVE; - @Data @Slf4j public class TbHttpClient { diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/rest/TbRestApiCallNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/rest/TbRestApiCallNodeTest.java index 98695c1570..fad0ae66da 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/rest/TbRestApiCallNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/rest/TbRestApiCallNodeTest.java @@ -165,7 +165,7 @@ public class TbRestApiCallNodeTest { try { assertEquals(path, request.getRequestLine().getUri(), "Request path matches"); assertTrue(request.containsHeader("Content-Type"), "Content-Type included"); - assertEquals("text/plain;charset=UTF-8", + assertEquals("application/json", request.getFirstHeader("Content-Type").getValue(), "Content-Type value"); assertTrue(request.containsHeader("Content-Length"), "Content-Length included"); assertEquals("2", From 95178d12ea2f874f1ea29439428c3533239c1dad Mon Sep 17 00:00:00 2001 From: Andrii Landiak Date: Thu, 11 Jul 2024 14:48:57 +0300 Subject: [PATCH 14/20] Fix OAuth2EdgeTest --- .../org/thingsboard/server/edge/NotificationEdgeTest.java | 1 - .../java/org/thingsboard/server/edge/OAuth2EdgeTest.java | 7 +------ 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/application/src/test/java/org/thingsboard/server/edge/NotificationEdgeTest.java b/application/src/test/java/org/thingsboard/server/edge/NotificationEdgeTest.java index 890ec00913..6d4ebb3676 100644 --- a/application/src/test/java/org/thingsboard/server/edge/NotificationEdgeTest.java +++ b/application/src/test/java/org/thingsboard/server/edge/NotificationEdgeTest.java @@ -18,7 +18,6 @@ package org.thingsboard.server.edge; import com.google.protobuf.AbstractMessage; import org.junit.Assert; import org.junit.Test; -import org.springframework.test.context.TestPropertySource; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod; diff --git a/application/src/test/java/org/thingsboard/server/edge/OAuth2EdgeTest.java b/application/src/test/java/org/thingsboard/server/edge/OAuth2EdgeTest.java index c25410b562..beae36718d 100644 --- a/application/src/test/java/org/thingsboard/server/edge/OAuth2EdgeTest.java +++ b/application/src/test/java/org/thingsboard/server/edge/OAuth2EdgeTest.java @@ -59,12 +59,7 @@ public class OAuth2EdgeTest extends AbstractEdgeTest { oAuth2Info.setEnabled(false); oAuth2Info.setEdgeEnabled(false); doPost("/api/oauth2/config", oAuth2Info, OAuth2Info.class); - Assert.assertTrue(edgeImitator.waitForMessages()); - latestMessage = edgeImitator.getLatestMessage(); - Assert.assertTrue(latestMessage instanceof OAuth2UpdateMsg); - oAuth2UpdateMsg = (OAuth2UpdateMsg) latestMessage; - result = JacksonUtil.fromString(oAuth2UpdateMsg.getEntity(), OAuth2Info.class, true); - Assert.assertEquals(oAuth2Info, result); + Assert.assertFalse(edgeImitator.waitForMessages(5)); edgeImitator.ignoreType(OAuth2UpdateMsg.class); loginTenantAdmin(); From 094f840f119b0db11997ab8c5828901b53ff3afc Mon Sep 17 00:00:00 2001 From: Sergey Matvienko Date: Thu, 11 Jul 2024 14:26:59 +0200 Subject: [PATCH 15/20] JpaAlarmDao debug log added --- .../alarm/AlarmCreateOrUpdateActiveRequest.java | 4 ++++ .../common/data/alarm/AlarmUpdateRequest.java | 4 ++++ .../server/dao/sql/alarm/JpaAlarmDao.java | 15 ++++++++++++--- 3 files changed, 20 insertions(+), 3 deletions(-) diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmCreateOrUpdateActiveRequest.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmCreateOrUpdateActiveRequest.java index c39c603a9e..4cf3d5a790 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmCreateOrUpdateActiveRequest.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmCreateOrUpdateActiveRequest.java @@ -19,6 +19,7 @@ import com.fasterxml.jackson.databind.JsonNode; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Builder; import lombok.Data; +import lombok.ToString; import org.thingsboard.server.common.data.id.AlarmId; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.EntityId; @@ -53,9 +54,12 @@ public class AlarmCreateOrUpdateActiveRequest implements AlarmModificationReques private long startTs; @Schema(description = "Timestamp of the alarm end time(last time update), in milliseconds", example = "1634111163522") private long endTs; + + @ToString.Exclude @NoXss @Schema(description = "JSON object with alarm details") private JsonNode details; + @Valid @Schema(description = "JSON object with propagation details") private AlarmPropagationInfo propagation; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmUpdateRequest.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmUpdateRequest.java index 8eb418d6dc..1556e5fa73 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmUpdateRequest.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmUpdateRequest.java @@ -19,6 +19,7 @@ import com.fasterxml.jackson.databind.JsonNode; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Builder; import lombok.Data; +import lombok.ToString; import org.thingsboard.server.common.data.id.AlarmId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.UserId; @@ -47,9 +48,12 @@ public class AlarmUpdateRequest implements AlarmModificationRequest { private long startTs; @Schema(description = "Timestamp of the alarm end time(last time update), in milliseconds", example = "1634111163522") private long endTs; + + @ToString.Exclude @NoXss @Schema(description = "JSON object with alarm details") private JsonNode details; + @Valid @Schema(description = "JSON object with propagation details") private AlarmPropagationInfo propagation; diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmDao.java index b0bf2cc666..133eb70521 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmDao.java @@ -342,9 +342,12 @@ public class JpaAlarmDao extends JpaAbstractDao implements A @Override public AlarmApiCallResult createOrUpdateActiveAlarm(AlarmCreateOrUpdateActiveRequest request, boolean alarmCreationEnabled) { + UUID tenantUUID = request.getTenantId().getId(); + log.debug("[{}] createOrUpdateActiveAlarm [{}] {}", tenantUUID, alarmCreationEnabled, request); + AlarmPropagationInfo ap = getSafePropagationInfo(request.getPropagation()); return toAlarmApiResult(alarmRepository.createOrUpdateActiveAlarm( - request.getTenantId().getId(), + tenantUUID, request.getCustomerId() != null ? request.getCustomerId().getId() : CustomerId.NULL_UUID, request.getEdgeAlarmId() != null ? request.getEdgeAlarmId().getId() : UUID.randomUUID(), System.currentTimeMillis(), @@ -364,10 +367,14 @@ public class JpaAlarmDao extends JpaAbstractDao implements A @Override public AlarmApiCallResult updateAlarm(AlarmUpdateRequest request) { + UUID tenantUUID = request.getTenantId().getId(); + UUID alarmUUID = request.getAlarmId().getId(); + log.debug("[{}][{}] updateAlarm {}", tenantUUID, alarmUUID, request); + AlarmPropagationInfo ap = getSafePropagationInfo(request.getPropagation()); return toAlarmApiResult(alarmRepository.updateAlarm( - request.getTenantId().getId(), - request.getAlarmId().getId(), + tenantUUID, + alarmUUID, request.getSeverity().name(), request.getStartTs(), request.getEndTs(), getDetailsAsString(request.getDetails()), @@ -380,11 +387,13 @@ public class JpaAlarmDao extends JpaAbstractDao implements A @Override public AlarmApiCallResult acknowledgeAlarm(TenantId tenantId, AlarmId id, long ackTs) { + log.debug("[{}][{}] acknowledgeAlarm [{}]", tenantId, id, ackTs); return toAlarmApiResult(alarmRepository.acknowledgeAlarm(tenantId.getId(), id.getId(), ackTs)); } @Override public AlarmApiCallResult clearAlarm(TenantId tenantId, AlarmId id, long clearTs, JsonNode details) { + log.debug("[{}][{}] clearAlarm [{}]", tenantId, id, clearTs); return toAlarmApiResult(alarmRepository.clearAlarm(tenantId.getId(), id.getId(), clearTs, details != null ? getDetailsAsString(details) : null)); } From dfb4c01cca89c96d1930b65271b8ea6fd1afc135 Mon Sep 17 00:00:00 2001 From: rusikv Date: Fri, 12 Jul 2024 12:28:43 +0300 Subject: [PATCH 16/20] UI: refactoring --- .../chart/bar-chart-with-labels-basic-config.component.html | 2 +- .../chart/bar-chart-with-labels-widget-settings.component.html | 2 +- .../lib/settings/common/chart/chart-bar-settings.component.html | 2 +- .../settings/common/chart/chart-fill-settings.component.html | 2 +- .../lib/settings/common/chart/chart-fill-settings.component.ts | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/bar-chart-with-labels-basic-config.component.html b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/bar-chart-with-labels-basic-config.component.html index 774e6d88c5..76b458e4e4 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/bar-chart-with-labels-basic-config.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/bar-chart-with-labels-basic-config.component.html @@ -160,7 +160,7 @@
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/chart-fill-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/chart-fill-settings.component.html index fe92ec2af3..e56fc1ec00 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/chart-fill-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/chart-fill-settings.component.html @@ -18,7 +18,7 @@
-
{{ rowTitle | translate }}
+
{{ titleText | translate }}
{{ chartFillTypeTranslationMap.get(type) | translate }} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/chart-fill-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/chart-fill-settings.component.ts index c909df3511..bf02937221 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/chart-fill-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/chart-fill-settings.component.ts @@ -55,7 +55,7 @@ export class ChartFillSettingsComponent implements OnInit, ControlValueAccessor disabled: boolean; @Input() - rowTitle = 'widgets.chart.fill'; + titleText = 'widgets.chart.fill'; @Input() fillNoneTitle = 'widgets.chart.fill-type-none'; From 9fc6929a77374bbf9a4d9c7f65c1e1b52843269a Mon Sep 17 00:00:00 2001 From: Andrii Landiak Date: Mon, 15 Jul 2024 15:44:34 +0300 Subject: [PATCH 17/20] Use cacheEnabled to disable caching in case cache.specs field 'maxSize' is 0 --- .../server/cache/CacheSpecsMap.java | 2 +- .../cache/RedisTbTransactionalCache.java | 29 ++++++- .../server/dao/service/AlarmServiceTest.java | 87 +++++++++---------- 3 files changed, 70 insertions(+), 48 deletions(-) diff --git a/common/cache/src/main/java/org/thingsboard/server/cache/CacheSpecsMap.java b/common/cache/src/main/java/org/thingsboard/server/cache/CacheSpecsMap.java index 583a871fe1..abca4624e6 100644 --- a/common/cache/src/main/java/org/thingsboard/server/cache/CacheSpecsMap.java +++ b/common/cache/src/main/java/org/thingsboard/server/cache/CacheSpecsMap.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.cache; +import jakarta.annotation.PostConstruct; import lombok.Data; import lombok.Getter; import org.springframework.beans.factory.annotation.Value; @@ -22,7 +23,6 @@ import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.Configuration; import org.thingsboard.server.common.data.CacheConstants; -import jakarta.annotation.PostConstruct; import java.util.Map; @Configuration diff --git a/common/cache/src/main/java/org/thingsboard/server/cache/RedisTbTransactionalCache.java b/common/cache/src/main/java/org/thingsboard/server/cache/RedisTbTransactionalCache.java index abfbb398c9..c130318b8b 100644 --- a/common/cache/src/main/java/org/thingsboard/server/cache/RedisTbTransactionalCache.java +++ b/common/cache/src/main/java/org/thingsboard/server/cache/RedisTbTransactionalCache.java @@ -29,7 +29,6 @@ import org.springframework.data.redis.core.types.Expiration; import org.springframework.data.redis.serializer.RedisSerializer; import org.springframework.data.redis.serializer.StringRedisSerializer; import org.thingsboard.server.common.data.FstStatsService; -import redis.clients.jedis.Connection; import redis.clients.jedis.Jedis; import redis.clients.jedis.JedisPool; import redis.clients.jedis.util.JedisClusterCRC16; @@ -57,6 +56,7 @@ public abstract class RedisTbTransactionalCache valueSerializer; private final Expiration evictExpiration; private final Expiration cacheTtl; + private final boolean cacheEnabled; public RedisTbTransactionalCache(String cacheName, CacheSpecsMap cacheSpecsMap, @@ -73,10 +73,19 @@ public abstract class RedisTbTransactionalCache Expiration.from(t, TimeUnit.MINUTES)) .orElseGet(Expiration::persistent); + this.cacheEnabled = Optional.ofNullable(cacheSpecsMap) + .map(CacheSpecsMap::getSpecs) + .map(x -> x.get(cacheName)) + .map(CacheSpecs::getMaxSize) + .map(size -> size > 0) + .orElse(false); } @Override public TbCacheValueWrapper get(K key) { + if (!cacheEnabled) { + return null; + } try (var connection = connectionFactory.getConnection()) { byte[] rawKey = getRawKey(key); byte[] rawValue = connection.get(rawKey); @@ -98,6 +107,9 @@ public abstract class RedisTbTransactionalCache keys) { + if (!cacheEnabled) { + return; + } //Redis expects at least 1 key to delete. Otherwise - ERR wrong number of arguments for 'del' command if (keys.isEmpty()) { return; @@ -130,6 +151,9 @@ public abstract class RedisTbTransactionalCache alarms = alarmService.findAlarmsV2(tenantId, AlarmQueryV2.builder() .affectedEntityId(childId) - .severityList(Arrays.asList(AlarmSeverity.CRITICAL)) - .statusList(Arrays.asList(AlarmSearchStatus.ACTIVE, AlarmSearchStatus.UNACK)).pageLink( + .severityList(List.of(AlarmSeverity.CRITICAL)) + .statusList(List.of(AlarmSearchStatus.ACTIVE, AlarmSearchStatus.UNACK)).pageLink( new TimePageLink(1, 0, "", new SortOrder("createdTime", SortOrder.Direction.DESC), 0L, System.currentTimeMillis()) ).build()); @@ -257,8 +256,8 @@ public class AlarmServiceTest extends AbstractServiceTest { // Check parent relation alarms = alarmService.findAlarmsV2(tenantId, AlarmQueryV2.builder() .affectedEntityId(parentId) - .severityList(Arrays.asList(AlarmSeverity.CRITICAL)) - .statusList(Arrays.asList(AlarmSearchStatus.ACTIVE, AlarmSearchStatus.UNACK)).pageLink( + .severityList(List.of(AlarmSeverity.CRITICAL)) + .statusList(List.of(AlarmSearchStatus.ACTIVE, AlarmSearchStatus.UNACK)).pageLink( new TimePageLink(1, 0, "", new SortOrder("createdTime", SortOrder.Direction.DESC), 0L, System.currentTimeMillis()) ).build()); @@ -272,8 +271,8 @@ public class AlarmServiceTest extends AbstractServiceTest { // Check child relation alarms = alarmService.findAlarmsV2(tenantId, AlarmQueryV2.builder() .affectedEntityId(childId) - .severityList(Arrays.asList(AlarmSeverity.CRITICAL)) - .statusList(Arrays.asList(AlarmSearchStatus.ACTIVE, AlarmSearchStatus.UNACK)).pageLink( + .severityList(List.of(AlarmSeverity.CRITICAL)) + .statusList(List.of(AlarmSearchStatus.ACTIVE, AlarmSearchStatus.UNACK)).pageLink( new TimePageLink(1, 0, "", new SortOrder("createdTime", SortOrder.Direction.DESC), 0L, System.currentTimeMillis()) ).build()); @@ -284,8 +283,8 @@ public class AlarmServiceTest extends AbstractServiceTest { // Check parent relation alarms = alarmService.findAlarmsV2(tenantId, AlarmQueryV2.builder() .affectedEntityId(parentId) - .severityList(Arrays.asList(AlarmSeverity.CRITICAL)) - .statusList(Arrays.asList(AlarmSearchStatus.ACTIVE, AlarmSearchStatus.UNACK)).pageLink( + .severityList(List.of(AlarmSeverity.CRITICAL)) + .statusList(List.of(AlarmSearchStatus.ACTIVE, AlarmSearchStatus.UNACK)).pageLink( new TimePageLink(1, 0, "", new SortOrder("createdTime", SortOrder.Direction.DESC), 0L, System.currentTimeMillis()) ).build()); @@ -298,8 +297,8 @@ public class AlarmServiceTest extends AbstractServiceTest { alarms = alarmService.findAlarmsV2(tenantId, AlarmQueryV2.builder() .affectedEntityId(childId) - .severityList(Arrays.asList(AlarmSeverity.CRITICAL)) - .statusList(Arrays.asList(AlarmSearchStatus.ACTIVE, AlarmSearchStatus.ACK)).pageLink( + .severityList(List.of(AlarmSeverity.CRITICAL)) + .statusList(List.of(AlarmSearchStatus.ACTIVE, AlarmSearchStatus.ACK)).pageLink( new TimePageLink(1, 0, "", new SortOrder("createdTime", SortOrder.Direction.DESC), 0L, System.currentTimeMillis()) ).build()); @@ -310,8 +309,8 @@ public class AlarmServiceTest extends AbstractServiceTest { // Check not existing relation alarms = alarmService.findAlarmsV2(tenantId, AlarmQueryV2.builder() .affectedEntityId(childId) - .severityList(Arrays.asList(AlarmSeverity.CRITICAL)) - .statusList(Arrays.asList(AlarmSearchStatus.ACTIVE, AlarmSearchStatus.UNACK)).pageLink( + .severityList(List.of(AlarmSeverity.CRITICAL)) + .statusList(List.of(AlarmSearchStatus.ACTIVE, AlarmSearchStatus.UNACK)).pageLink( new TimePageLink(1, 0, "", new SortOrder("createdTime", SortOrder.Direction.DESC), 0L, System.currentTimeMillis()) ).build()); @@ -323,8 +322,8 @@ public class AlarmServiceTest extends AbstractServiceTest { alarms = alarmService.findAlarmsV2(tenantId, AlarmQueryV2.builder() .affectedEntityId(childId) - .severityList(Arrays.asList(AlarmSeverity.CRITICAL)) - .statusList(Arrays.asList(AlarmSearchStatus.CLEARED, AlarmSearchStatus.ACK)).pageLink( + .severityList(List.of(AlarmSeverity.CRITICAL)) + .statusList(List.of(AlarmSearchStatus.CLEARED, AlarmSearchStatus.ACK)).pageLink( new TimePageLink(1, 0, "", new SortOrder("createdTime", SortOrder.Direction.DESC), 0L, System.currentTimeMillis()) ).build()); @@ -334,7 +333,7 @@ public class AlarmServiceTest extends AbstractServiceTest { } @Test - public void testFindAssignedAlarm() throws ExecutionException, InterruptedException { + public void testFindAssignedAlarm() { AssetId parentId = new AssetId(Uuids.timeBased()); AssetId childId = new AssetId(Uuids.timeBased()); @@ -368,7 +367,6 @@ public class AlarmServiceTest extends AbstractServiceTest { PageData alarms = alarmService.findAlarms(tenantId, AlarmQuery.builder() .assigneeId(tenantUser.getId()) - .fetchOriginator(true) .pageLink(new TimePageLink(1, 0, "", new SortOrder("createdTime", SortOrder.Direction.DESC), 0L, System.currentTimeMillis()) ).build()); @@ -405,7 +403,7 @@ public class AlarmServiceTest extends AbstractServiceTest { } @Test - public void testFindCustomerAlarm() throws ExecutionException, InterruptedException { + public void testFindCustomerAlarm() { Customer customer = new Customer(); customer.setTitle("TestCustomer"); customer.setTenantId(tenantId); @@ -451,10 +449,10 @@ public class AlarmServiceTest extends AbstractServiceTest { pageLink.setStartTs(0L); pageLink.setEndTs(System.currentTimeMillis()); pageLink.setSearchPropagatedAlarms(true); - pageLink.setSeverityList(Arrays.asList(AlarmSeverity.CRITICAL, AlarmSeverity.WARNING)); - pageLink.setStatusList(Arrays.asList(AlarmSearchStatus.ACTIVE)); + pageLink.setSeverityList(List.of(AlarmSeverity.CRITICAL, AlarmSeverity.WARNING)); + pageLink.setStatusList(List.of(AlarmSearchStatus.ACTIVE)); - PageData tenantAlarms = alarmService.findAlarmDataByQueryForEntities(tenantId, toQuery(pageLink), Arrays.asList(tenantDevice.getId(), customerDevice.getId())); + PageData tenantAlarms = alarmService.findAlarmDataByQueryForEntities(tenantId, toQuery(pageLink), List.of(tenantDevice.getId(), customerDevice.getId())); Assert.assertEquals(2, tenantAlarms.getData().size()); PageData customerAlarms = alarmService.findAlarmDataByQueryForEntities(tenantId, toQuery(pageLink), Collections.singletonList(customerDevice.getId())); @@ -473,7 +471,7 @@ public class AlarmServiceTest extends AbstractServiceTest { } @Test - public void testFindPropagatedCustomerAssetAlarm() throws ExecutionException, InterruptedException { + public void testFindPropagatedCustomerAssetAlarm() { Customer customer = new Customer(); customer.setTitle("TestCustomer"); customer.setTenantId(tenantId); @@ -525,8 +523,8 @@ public class AlarmServiceTest extends AbstractServiceTest { pageLink.setStartTs(0L); pageLink.setEndTs(System.currentTimeMillis()); pageLink.setSearchPropagatedAlarms(true); - pageLink.setSeverityList(Arrays.asList(AlarmSeverity.CRITICAL, AlarmSeverity.WARNING)); - pageLink.setStatusList(Arrays.asList(AlarmSearchStatus.ACTIVE)); + pageLink.setSeverityList(List.of(AlarmSeverity.CRITICAL, AlarmSeverity.WARNING)); + pageLink.setStatusList(List.of(AlarmSearchStatus.ACTIVE)); //TEST that propagated alarms are visible on the asset level. PageData customerAlarms = alarmService.findAlarmDataByQueryForEntities(tenantId, toQuery(pageLink), Collections.singletonList(customerAsset.getId())); @@ -576,7 +574,7 @@ public class AlarmServiceTest extends AbstractServiceTest { pageLink.setStartTs(0L); pageLink.setEndTs(System.currentTimeMillis()); pageLink.setSearchPropagatedAlarms(true); - pageLink.setSeverityList(Arrays.asList(AlarmSeverity.CRITICAL, AlarmSeverity.WARNING)); + pageLink.setSeverityList(List.of(AlarmSeverity.CRITICAL, AlarmSeverity.WARNING)); pageLink.setStatusList(Collections.singletonList(AlarmSearchStatus.ACTIVE)); //TEST that propagated alarms are visible on the asset level. @@ -599,7 +597,7 @@ public class AlarmServiceTest extends AbstractServiceTest { } @Test - public void testFindHighestAlarmSeverity() throws ExecutionException, InterruptedException { + public void testFindHighestAlarmSeverity() { Customer customer = new Customer(); customer.setTitle("TestCustomer"); customer.setTenantId(tenantId); @@ -679,8 +677,8 @@ public class AlarmServiceTest extends AbstractServiceTest { pageLink.setStartTs(0L); pageLink.setEndTs(System.currentTimeMillis()); pageLink.setSearchPropagatedAlarms(false); - pageLink.setSeverityList(Arrays.asList(AlarmSeverity.CRITICAL, AlarmSeverity.WARNING)); - pageLink.setStatusList(Arrays.asList(AlarmSearchStatus.ACTIVE)); + pageLink.setSeverityList(List.of(AlarmSeverity.CRITICAL, AlarmSeverity.WARNING)); + pageLink.setStatusList(List.of(AlarmSearchStatus.ACTIVE)); PageData alarms = alarmService.findAlarmDataByQueryForEntities(tenantId, toQuery(pageLink), Collections.singletonList(childId)); @@ -695,8 +693,8 @@ public class AlarmServiceTest extends AbstractServiceTest { pageLink.setStartTs(0L); pageLink.setEndTs(System.currentTimeMillis()); pageLink.setSearchPropagatedAlarms(false); - pageLink.setSeverityList(Arrays.asList(AlarmSeverity.CRITICAL, AlarmSeverity.WARNING)); - pageLink.setStatusList(Arrays.asList(AlarmSearchStatus.ACTIVE)); + pageLink.setSeverityList(List.of(AlarmSeverity.CRITICAL, AlarmSeverity.WARNING)); + pageLink.setStatusList(List.of(AlarmSearchStatus.ACTIVE)); alarms = alarmService.findAlarmDataByQueryForEntities(tenantId, toQuery(pageLink), Collections.singletonList(childId)); Assert.assertNotNull(alarms.getData()); @@ -722,8 +720,8 @@ public class AlarmServiceTest extends AbstractServiceTest { pageLink.setStartTs(0L); pageLink.setEndTs(System.currentTimeMillis()); pageLink.setSearchPropagatedAlarms(true); - pageLink.setSeverityList(Arrays.asList(AlarmSeverity.CRITICAL, AlarmSeverity.WARNING)); - pageLink.setStatusList(Arrays.asList(AlarmSearchStatus.ACTIVE)); + pageLink.setSeverityList(List.of(AlarmSeverity.CRITICAL, AlarmSeverity.WARNING)); + pageLink.setStatusList(List.of(AlarmSearchStatus.ACTIVE)); alarms = alarmService.findAlarmDataByQueryForEntities(tenantId, toQuery(pageLink), Collections.singletonList(childId)); Assert.assertNotNull(alarms.getData()); @@ -738,8 +736,8 @@ public class AlarmServiceTest extends AbstractServiceTest { pageLink.setStartTs(0L); pageLink.setEndTs(System.currentTimeMillis()); pageLink.setSearchPropagatedAlarms(true); - pageLink.setSeverityList(Arrays.asList(AlarmSeverity.CRITICAL, AlarmSeverity.WARNING)); - pageLink.setStatusList(Arrays.asList(AlarmSearchStatus.ACTIVE)); + pageLink.setSeverityList(List.of(AlarmSeverity.CRITICAL, AlarmSeverity.WARNING)); + pageLink.setStatusList(List.of(AlarmSearchStatus.ACTIVE)); alarms = alarmService.findAlarmDataByQueryForEntities(tenantId, toQuery(pageLink), Collections.singletonList(parentId)); Assert.assertNotNull(alarms.getData()); @@ -748,7 +746,6 @@ public class AlarmServiceTest extends AbstractServiceTest { PageData alarmsInfoData = alarmService.findAlarms(tenantId, AlarmQuery.builder() .affectedEntityId(childId) - .fetchOriginator(true) .status(AlarmStatus.ACTIVE_UNACK).pageLink( new TimePageLink(10, 0, "", new SortOrder("createdTime", SortOrder.Direction.DESC), 0L, System.currentTimeMillis()) @@ -759,7 +756,6 @@ public class AlarmServiceTest extends AbstractServiceTest { alarmsInfoData = alarmService.findAlarms(tenantId, AlarmQuery.builder() .affectedEntityId(parentId) - .fetchOriginator(true) .status(AlarmStatus.ACTIVE_UNACK).pageLink( new TimePageLink(10, 0, "", new SortOrder("createdTime", SortOrder.Direction.DESC), 0L, System.currentTimeMillis()) @@ -770,7 +766,6 @@ public class AlarmServiceTest extends AbstractServiceTest { alarmsInfoData = alarmService.findAlarms(tenantId, AlarmQuery.builder() .affectedEntityId(parentId2) - .fetchOriginator(true) .status(AlarmStatus.ACTIVE_UNACK).pageLink( new TimePageLink(10, 0, "", new SortOrder("createdTime", SortOrder.Direction.DESC), 0L, System.currentTimeMillis()) @@ -786,8 +781,8 @@ public class AlarmServiceTest extends AbstractServiceTest { pageLink.setStartTs(0L); pageLink.setEndTs(System.currentTimeMillis()); pageLink.setSearchPropagatedAlarms(true); - pageLink.setSeverityList(Arrays.asList(AlarmSeverity.CRITICAL, AlarmSeverity.WARNING)); - pageLink.setStatusList(Arrays.asList(AlarmSearchStatus.ACTIVE)); + pageLink.setSeverityList(List.of(AlarmSeverity.CRITICAL, AlarmSeverity.WARNING)); + pageLink.setStatusList(List.of(AlarmSearchStatus.ACTIVE)); alarms = alarmService.findAlarmDataByQueryForEntities(tenantId, toQuery(pageLink), Collections.singletonList(parentId)); Assert.assertNotNull(alarms.getData()); @@ -803,8 +798,8 @@ public class AlarmServiceTest extends AbstractServiceTest { pageLink.setStartTs(0L); pageLink.setEndTs(System.currentTimeMillis()); pageLink.setSearchPropagatedAlarms(true); - pageLink.setSeverityList(Arrays.asList(AlarmSeverity.CRITICAL, AlarmSeverity.WARNING)); - pageLink.setStatusList(Arrays.asList(AlarmSearchStatus.ACTIVE)); + pageLink.setSeverityList(List.of(AlarmSeverity.CRITICAL, AlarmSeverity.WARNING)); + pageLink.setStatusList(List.of(AlarmSearchStatus.ACTIVE)); alarms = alarmService.findAlarmDataByQueryForEntities(tenantId, toQuery(pageLink), Collections.singletonList(childId)); Assert.assertNotNull(alarms.getData()); @@ -813,7 +808,7 @@ public class AlarmServiceTest extends AbstractServiceTest { } @Test - public void testCountAlarmsUsingAlarmDataQuery() throws ExecutionException, InterruptedException { + public void testCountAlarmsUsingAlarmDataQuery() { AssetId childId = new AssetId(Uuids.timeBased()); long ts = System.currentTimeMillis(); @@ -829,7 +824,7 @@ public class AlarmServiceTest extends AbstractServiceTest { .startTs(0L) .endTs(System.currentTimeMillis()) .searchPropagatedAlarms(false) - .severityList(Arrays.asList(AlarmSeverity.CRITICAL, AlarmSeverity.WARNING)) + .severityList(List.of(AlarmSeverity.CRITICAL, AlarmSeverity.WARNING)) .statusList(List.of(AlarmSearchStatus.ACTIVE)) .build(); @@ -841,7 +836,7 @@ public class AlarmServiceTest extends AbstractServiceTest { .startTs(0L) .endTs(System.currentTimeMillis()) .searchPropagatedAlarms(true) - .severityList(Arrays.asList(AlarmSeverity.CRITICAL, AlarmSeverity.WARNING)) + .severityList(List.of(AlarmSeverity.CRITICAL, AlarmSeverity.WARNING)) .statusList(List.of(AlarmSearchStatus.ACTIVE)) .build(); @@ -866,7 +861,7 @@ public class AlarmServiceTest extends AbstractServiceTest { .startTs(0L) .endTs(System.currentTimeMillis()) .searchPropagatedAlarms(true) - .severityList(Arrays.asList(AlarmSeverity.CRITICAL, AlarmSeverity.WARNING)) + .severityList(List.of(AlarmSeverity.CRITICAL, AlarmSeverity.WARNING)) .statusList(List.of(AlarmSearchStatus.ACTIVE, AlarmSearchStatus.CLEARED)) .build(); @@ -939,6 +934,6 @@ public class AlarmServiceTest extends AbstractServiceTest { ).build()); Assert.assertNotNull(alarms.getData()); Assert.assertEquals(0, alarms.getData().size()); - } + } From 1720803eba5d1cf3f53926e9484f9f015cba5993 Mon Sep 17 00:00:00 2001 From: Andrii Landiak Date: Tue, 16 Jul 2024 12:46:05 +0300 Subject: [PATCH 18/20] Improvement after review: reuse getAndPutInTransaction, write test --- .../cache/RedisTbTransactionalCache.java | 10 +++ .../server/cache/TbTransactionalCache.java | 34 ++----- .../server/cache/CacheSpecsMapTest.java | 3 +- .../cache/RedisTbTransactionalCacheTest.java | 89 +++++++++++++++++++ 4 files changed, 109 insertions(+), 27 deletions(-) create mode 100644 dao/src/test/java/org/thingsboard/server/dao/cache/RedisTbTransactionalCacheTest.java diff --git a/common/cache/src/main/java/org/thingsboard/server/cache/RedisTbTransactionalCache.java b/common/cache/src/main/java/org/thingsboard/server/cache/RedisTbTransactionalCache.java index c130318b8b..8e7bc86d53 100644 --- a/common/cache/src/main/java/org/thingsboard/server/cache/RedisTbTransactionalCache.java +++ b/common/cache/src/main/java/org/thingsboard/server/cache/RedisTbTransactionalCache.java @@ -39,6 +39,8 @@ import java.util.Collection; import java.util.List; import java.util.Optional; import java.util.concurrent.TimeUnit; +import java.util.function.Function; +import java.util.function.Supplier; @Slf4j public abstract class RedisTbTransactionalCache implements TbTransactionalCache { @@ -177,6 +179,14 @@ public abstract class RedisTbTransactionalCache(this, connection); } + @Override + public R getAndPutInTransaction(K key, Supplier dbCall, Function cacheValueToResult, Function dbValueToCacheValue, boolean cacheNullValue) { + if (!cacheEnabled) { + return dbCall.get(); + } + return TbTransactionalCache.super.getAndPutInTransaction(key, dbCall, cacheValueToResult, dbValueToCacheValue, cacheNullValue); + } + private RedisConnection getConnection(byte[] rawKey) { if (!connectionFactory.isRedisClusterAware()) { return connectionFactory.getConnection(); diff --git a/common/cache/src/main/java/org/thingsboard/server/cache/TbTransactionalCache.java b/common/cache/src/main/java/org/thingsboard/server/cache/TbTransactionalCache.java index be0b38b65e..4d57736cc1 100644 --- a/common/cache/src/main/java/org/thingsboard/server/cache/TbTransactionalCache.java +++ b/common/cache/src/main/java/org/thingsboard/server/cache/TbTransactionalCache.java @@ -60,15 +60,20 @@ public interface TbTransactionalCache dbCall, boolean cacheNullValue) { + return getAndPutInTransaction(key, dbCall, Function.identity(), Function.identity(), cacheNullValue); + } + + default R getAndPutInTransaction(K key, Supplier dbCall, Function cacheValueToResult, Function dbValueToCacheValue, boolean cacheNullValue) { TbCacheValueWrapper cacheValueWrapper = get(key); if (cacheValueWrapper != null) { - return cacheValueWrapper.get(); + V cacheValue = cacheValueWrapper.get(); + return cacheValue != null ? cacheValueToResult.apply(cacheValue) : null; } var cacheTransaction = newTransactionForKey(key); try { - V dbValue = dbCall.get(); + R dbValue = dbCall.get(); if (dbValue != null || cacheNullValue) { - cacheTransaction.putIfAbsent(key, dbValue); + cacheTransaction.putIfAbsent(key, dbValueToCacheValue.apply(dbValue)); cacheTransaction.commit(); return dbValue; } else { @@ -94,27 +99,4 @@ public interface TbTransactionalCache R getAndPutInTransaction(K key, Supplier dbCall, Function cacheValueToResult, Function dbValueToCacheValue, boolean cacheNullValue) { - TbCacheValueWrapper cacheValueWrapper = get(key); - if (cacheValueWrapper != null) { - var cacheValue = cacheValueWrapper.get(); - return cacheValue == null ? null : cacheValueToResult.apply(cacheValue); - } - var cacheTransaction = newTransactionForKey(key); - try { - R dbValue = dbCall.get(); - if (dbValue != null || cacheNullValue) { - cacheTransaction.putIfAbsent(key, dbValueToCacheValue.apply(dbValue)); - cacheTransaction.commit(); - return dbValue; - } else { - cacheTransaction.rollback(); - return null; - } - } catch (Throwable e) { - cacheTransaction.rollback(); - throw e; - } - } - } diff --git a/common/cache/src/test/java/org/thingsboard/server/cache/CacheSpecsMapTest.java b/common/cache/src/test/java/org/thingsboard/server/cache/CacheSpecsMapTest.java index aec5e01f39..eb83cc2821 100644 --- a/common/cache/src/test/java/org/thingsboard/server/cache/CacheSpecsMapTest.java +++ b/common/cache/src/test/java/org/thingsboard/server/cache/CacheSpecsMapTest.java @@ -62,4 +62,5 @@ public class CacheSpecsMapTest { public void givenCacheConfig_whenCacheManagerReady_thenVerifyNonExistedCaches() { assertThat(cacheManager.getCache("rainbows_and_unicorns")).isNull(); } -} \ No newline at end of file + +} diff --git a/dao/src/test/java/org/thingsboard/server/dao/cache/RedisTbTransactionalCacheTest.java b/dao/src/test/java/org/thingsboard/server/dao/cache/RedisTbTransactionalCacheTest.java new file mode 100644 index 0000000000..914472098c --- /dev/null +++ b/dao/src/test/java/org/thingsboard/server/dao/cache/RedisTbTransactionalCacheTest.java @@ -0,0 +1,89 @@ +/** + * Copyright © 2016-2024 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.server.dao.cache; + +import lombok.extern.slf4j.Slf4j; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.data.redis.connection.RedisConnection; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.context.junit.jupiter.SpringExtension; +import org.thingsboard.server.cache.CacheSpecsMap; +import org.thingsboard.server.cache.RedisSslCredentials; +import org.thingsboard.server.cache.TBRedisCacheConfiguration; +import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.relation.RelationTypeGroup; +import org.thingsboard.server.dao.relation.RelationCacheKey; +import org.thingsboard.server.dao.relation.RelationRedisCache; + +import java.util.List; +import java.util.UUID; + +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.when; + +@ExtendWith(SpringExtension.class) +@ContextConfiguration(classes = {RelationRedisCache.class, CacheSpecsMap.class, TBRedisCacheConfiguration.class}) +@TestPropertySource(properties = { + "cache.type=redis", + "cache.specs.relations.timeToLiveInMinutes=1440", + "cache.specs.relations.maxSize=0", +}) +@Slf4j +public class RedisTbTransactionalCacheTest { + + @MockBean + private RelationRedisCache relationRedisCache; + @MockBean + private RedisConnectionFactory connectionFactory; + @MockBean + private RedisConnection redisConnection; + @MockBean + private RedisSslCredentials redisSslCredentials; + + + @BeforeEach + public void setup() { + when(connectionFactory.getConnection()).thenReturn(redisConnection); + } + + @Test + public void testEvictNoOpWhenCacheDisabled() { + relationRedisCache.evict(List.of(createRelationCacheKey())); + + verify(connectionFactory, never()).getConnection(); + verifyNoInteractions(redisConnection); + } + + @Test + public void testEvictOrPutNoOpWhenCacheDisabled() { + relationRedisCache.evictOrPut(createRelationCacheKey(), null); + + verify(connectionFactory, never()).getConnection(); + verifyNoInteractions(redisConnection); + } + + private RelationCacheKey createRelationCacheKey() { + return new RelationCacheKey(new DeviceId(UUID.randomUUID()), new DeviceId(UUID.randomUUID()), null, RelationTypeGroup.COMMON); + } + +} \ No newline at end of file From fca1eef2c1871c6c084e1d6ede3c2dc4843af3f8 Mon Sep 17 00:00:00 2001 From: Andrii Landiak Date: Tue, 16 Jul 2024 14:54:12 +0300 Subject: [PATCH 19/20] Improve RedisTbTransactionalCacheTest --- .../cache/RedisTbTransactionalCacheTest.java | 25 +++++++------------ 1 file changed, 9 insertions(+), 16 deletions(-) diff --git a/dao/src/test/java/org/thingsboard/server/dao/cache/RedisTbTransactionalCacheTest.java b/dao/src/test/java/org/thingsboard/server/dao/cache/RedisTbTransactionalCacheTest.java index 914472098c..12b6f5d7d1 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/cache/RedisTbTransactionalCacheTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/cache/RedisTbTransactionalCacheTest.java @@ -16,7 +16,6 @@ package org.thingsboard.server.dao.cache; import lombok.extern.slf4j.Slf4j; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.boot.test.mock.mockito.MockBean; @@ -60,23 +59,17 @@ public class RedisTbTransactionalCacheTest { @MockBean private RedisSslCredentials redisSslCredentials; - - @BeforeEach - public void setup() { + @Test + public void testNoOpWhenCacheDisabled() { when(connectionFactory.getConnection()).thenReturn(redisConnection); - } - @Test - public void testEvictNoOpWhenCacheDisabled() { + relationRedisCache.put(createRelationCacheKey(), null); + relationRedisCache.putIfAbsent(createRelationCacheKey(), null); + relationRedisCache.evict(createRelationCacheKey()); relationRedisCache.evict(List.of(createRelationCacheKey())); - - verify(connectionFactory, never()).getConnection(); - verifyNoInteractions(redisConnection); - } - - @Test - public void testEvictOrPutNoOpWhenCacheDisabled() { - relationRedisCache.evictOrPut(createRelationCacheKey(), null); + relationRedisCache.getAndPutInTransaction(createRelationCacheKey(), null, false); + relationRedisCache.getAndPutInTransaction(createRelationCacheKey(), null, null, null, false); + relationRedisCache.getOrFetchFromDB(createRelationCacheKey(), null, false, false); verify(connectionFactory, never()).getConnection(); verifyNoInteractions(redisConnection); @@ -86,4 +79,4 @@ public class RedisTbTransactionalCacheTest { return new RelationCacheKey(new DeviceId(UUID.randomUUID()), new DeviceId(UUID.randomUUID()), null, RelationTypeGroup.COMMON); } -} \ No newline at end of file +} From 1aa53c3a1571cba577205e2800be1a09f224fda1 Mon Sep 17 00:00:00 2001 From: Artem Dzhereleiko Date: Thu, 18 Jul 2024 15:11:26 +0300 Subject: [PATCH 20/20] UI: Prevent action on preview and edit mode --- .../unread-notification-widget.component.ts | 24 ++++++++++++------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/cards/unread-notification-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/cards/unread-notification-widget.component.ts index fcad768e66..2dfded3f96 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/cards/unread-notification-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/cards/unread-notification-widget.component.ts @@ -203,23 +203,29 @@ export class UnreadNotificationWidgetComponent implements OnInit, OnDestroy { } markAsRead(id: string) { - const cmd = NotificationSubscriber.createMarkAsReadCommand(this.notificationWsService, [id]); - cmd.subscribe(); + if (!this.ctx.isEdit && !this.ctx.isPreview) { + const cmd = NotificationSubscriber.createMarkAsReadCommand(this.notificationWsService, [id]); + cmd.subscribe(); + } } markAsAllRead($event: Event) { - if ($event) { - $event.stopPropagation(); + if (!this.ctx.isEdit && !this.ctx.isPreview) { + if ($event) { + $event.stopPropagation(); + } + const cmd = NotificationSubscriber.createMarkAllAsReadCommand(this.notificationWsService); + cmd.subscribe(); } - const cmd = NotificationSubscriber.createMarkAllAsReadCommand(this.notificationWsService); - cmd.subscribe(); } viewAll($event: Event) { - if ($event) { - $event.stopPropagation(); + if (!this.ctx.isEdit && !this.ctx.isPreview) { + if ($event) { + $event.stopPropagation(); + } + this.router.navigateByUrl(this.router.parseUrl('/notification/inbox')).then(() => {}); } - this.router.navigateByUrl(this.router.parseUrl('/notification/inbox')).then(() => {}); } trackById(index: number, item: NotificationRequest): string {