From 0fd285d76b5f25002f84768ab490e750539509d0 Mon Sep 17 00:00:00 2001 From: nick Date: Tue, 2 Jul 2024 14:00:46 +0300 Subject: [PATCH 01/48] tbel: number to string by radix --- .../thingsboard/script/api/tbel/TbUtils.java | 489 +++++++++++++++--- .../script/api/tbel/TbUtilsTest.java | 278 ++++++++-- 2 files changed, 625 insertions(+), 142 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..c8c66cada0 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,22 @@ 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 zeroRadix = 0; + private static final int octalRadix = 8; + private static final int decRadix = 10; + private static final int hexRadix = 16; + private static final int hexLenMin = -1; + private static final int hexLenIntMax = 8; + private static final int hexLenLongMax = 16; + private static final LinkedHashMap mdnEncodingReplacements = new LinkedHashMap<>(); static { @@ -103,6 +114,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 +188,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 +244,18 @@ public class TbUtils { String.class))); parserConfig.addImport("decodeURI", new MethodStub(TbUtils.class.getMethod("decodeURI", String.class))); + parserConfig.addImport("newError", new MethodStub(TbUtils.class.getMethod("newError", + String.class, Object.class))); + parserConfig.addImport("newError", new MethodStub(TbUtils.class.getMethod("newError", + 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 +269,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,29 +329,28 @@ public class TbUtils { } public static Integer parseInt(String value) { - int radix = getRadix(value); - return parseInt(value, radix); + return parseInt(value, zeroRadix); } public static Integer parseInt(String value, int radix) { if (StringUtils.isNotBlank(value)) { try { String valueP = prepareNumberString(value); - isValidRadix(valueP, radix); + int radixValue = isValidStringAndRadix(valueP, radix, value); 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()); + if (radixValue >= 25 && radixValue <= MAX_RADIX) { + return Integer.parseInt(valueP, radixValue); } + return switch (radixValue) { + case MIN_RADIX -> parseBinaryStringAsSignedInteger(valueP); + case octalRadix, decRadix, hexRadix -> Integer.parseInt(valueP, radixValue); + default -> throw new IllegalArgumentException("Invalid radix: [" + radix + "]"); + }; + } catch (NumberFormatException e) { + Integer iMax = Integer.MAX_VALUE; + Integer iMin = Integer.MIN_VALUE; + compareIntLongValueMinMax(valueP, radixValue, iMax.longValue(), iMin.longValue(), "Integer"); + throw new NumberFormatException(e.getMessage()); } } catch (NumberFormatException e) { throw new NumberFormatException(e.getMessage()); @@ -301,29 +360,26 @@ public class TbUtils { } public static Long parseLong(String value) { - int radix = getRadix(value); - return parseLong(value, radix); + return parseLong(value, zeroRadix); } public static Long parseLong(String value, int radix) { if (StringUtils.isNotBlank(value)) { try { String valueP = prepareNumberString(value); - isValidRadix(valueP, radix); + int radixValue = isValidStringAndRadix(valueP, radix, value); 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()); + if (radixValue >= 25 && radixValue <= MAX_RADIX) { + return Long.parseLong(valueP, radixValue); } + return switch (radixValue) { + case MIN_RADIX -> parseBinaryStringAsSignedLong(valueP); + case octalRadix, decRadix, hexRadix -> Long.parseLong(valueP, radixValue); + default -> throw new IllegalArgumentException("Invalid radix: [" + radix + "]"); + }; + } catch (NumberFormatException e) { + compareIntLongValueMinMax(valueP, radixValue, Long.MAX_VALUE, Long.MIN_VALUE, "Long"); + throw new NumberFormatException(e.getMessage()); } } catch (NumberFormatException e) { throw new NumberFormatException(e.getMessage()); @@ -332,25 +388,96 @@ public class TbUtils { 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, 2) + 1; + return -positiveValue; + } else { + return Integer.parseInt(binaryString, 2); + } + } + 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, 2) + 1; + return -positiveValue; + } else { + return Long.parseLong(binaryString, 2); + } + } + + 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 ? decRadix : 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) { + 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); try { - return Float.parseFloat(prepareNumberString(value)); + if (radixValue == decRadix) { + return Float.parseFloat(value); + } else { + int bits = Integer.parseUnsignedInt(valueP, hexRadix); + return Float.intBitsToFloat(bits); + } } catch (NumberFormatException e) { + throw new NumberFormatException(e.getMessage()); } } 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 { - return Double.parseDouble(prepareNumberString(value)); + String valueP = prepareNumberString(value); + int radixValue = isValidStringAndRadix(valueP, radix, value); + if (radixValue == decRadix) { + return Double.parseDouble(prepareNumberString(value)); + } else { + long bits = Long.parseUnsignedLong(valueP, hexRadix); + return Double.longBitsToDouble(bits); + } } catch (NumberFormatException e) { + throw new NumberFormatException(e.getMessage()); } } return null; @@ -368,9 +495,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, hexRadix); } public static long parseLittleEndianHexToLong(String hex) { @@ -385,9 +513,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, 16); } public static float parseLittleEndianHexToFloat(String hex) { @@ -402,9 +531,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, hexRadix); } public static double parseLittleEndianHexToDouble(String hex) { @@ -419,39 +549,157 @@ 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, hexRadix); } - 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, 16); + // 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, hexLenMin, hexLenIntMax); + } + + public static String intToHex(Integer i, boolean bigEndian) { + return prepareNumberHexString(i.longValue(), bigEndian, false, hexLenMin, hexLenIntMax); + } + + public static String intToHex(Integer i, boolean bigEndian, boolean pref) { + return prepareNumberHexString(i.longValue(), bigEndian, pref, hexLenMin, hexLenIntMax); + } + + public static String intToHex(Integer i, boolean bigEndian, boolean pref, int len) { + return prepareNumberHexString(i.longValue(), bigEndian, pref, len, hexLenIntMax); + } + + public static String longToHex(Long l) { + return prepareNumberHexString(l, true, false, hexLenMin, hexLenLongMax); + } + public static String longToHex(Long l, boolean bigEndian) { + return prepareNumberHexString(l, bigEndian, false, hexLenMin, hexLenLongMax); + } + + public static String longToHex(Long l, boolean bigEndian, boolean pref) { + return prepareNumberHexString(l, bigEndian, pref, hexLenMin, hexLenLongMax); + } + + public static String longToHex(Long l, boolean bigEndian, boolean pref, int len) { + return prepareNumberHexString(l, bigEndian, pref, len, hexLenLongMax); + } + + public static String intLongToString(Long number) { + return intLongToString(number, 10); + } + + 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 8 -> Long.toOctalString(number); + case 10 -> Long.toString(number); + case 16 -> prepareNumberHexString(number, bigEndian, pref, -1, -1); + default -> throw new IllegalArgumentException("Invalid radix: [" + radix + "]"); + }; + } + + private static void compareIntLongValueMinMax(String valueP, int radix, Long maxValue, Long minValue, String clazzName) { + BigInteger bi = new BigInteger(valueP, radix); + if (bi.compareTo(BigInteger.valueOf(maxValue)) > 0) { + throw new NumberFormatException("Value \"" + valueP + "\"is greater than the maximum " + clazzName + " value " + maxValue + " !"); + } else if (bi.compareTo(BigInteger.valueOf(minValue)) < 0) { + throw new NumberFormatException("Value \"" + valueP + "\" is less than the minimum " + clazzName + " value " + minValue + " !"); + } + } + + 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 == hexLenMin ? 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 + 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)); } @@ -647,6 +895,15 @@ public class TbUtils { return URLDecoder.decode(uri, StandardCharsets.UTF_8); } + public static void newError(String message) { + newError(message, null); + } + + public static void newError(String message, Object value) { + String msg = value == null ? message : message + " 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 +944,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 8 -> isOctal(valueP); + case 10 -> isDecimal(valueP); + case 16 -> isHexadecimal(valueP); + default -> throw new IllegalArgumentException("Invalid radix: [" + radix + "]"); + + }; + } + + if (radixValue > 0) { + if (value.startsWith("0x")) radixValue = 16; + if (radixValue == 16) { + 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]+") ? octalRadix : -1; + } + + public static int isDecimal(String str) { + if (str == null || str.isEmpty()) { + return -1; + } + return str.matches("-?\\d+(\\.\\d+)?") ? decRadix : -1; + } + + public static int isHexadecimal(String str) { + if (str == null || str.isEmpty()) { + return -1; + } + return str.matches("^-?(0[xX])?[0-9a-fA-F]+$") ? hexRadix : -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(8); + 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..42c8e84b7d 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,137 @@ 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() { + // 0011 0011 0110 0110 == 13158L + // 1100 1100 1001 1010 == -13158L + 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("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 newError_Test() { + String message = "frequency_weighting_type must be 0, 1 or 2."; + Object value = 4; + try { + TbUtils.newError(message, value); + Assertions.fail("Should throw NumberFormatException"); + } catch (RuntimeException e) { + Assertions.assertTrue(e.getMessage().contains("frequency_weighting_type must be 0, 1 or 2. Value = 4")); + } + try { + TbUtils.newError(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 isNumeric_Test() { + + + } + private static List toList(byte[] data) { List result = new ArrayList<>(data.length); for (Byte b : data) { From 4d5200295322f211d3b9c014da104c6d00922193 Mon Sep 17 00:00:00 2001 From: nick Date: Tue, 2 Jul 2024 15:37:19 +0300 Subject: [PATCH 02/48] Revert "tbel: number to string by radix" This reverts commit 0fd285d76b5f25002f84768ab490e750539509d0. --- .../thingsboard/script/api/tbel/TbUtils.java | 489 +++--------------- .../script/api/tbel/TbUtilsTest.java | 278 ++-------- 2 files changed, 142 insertions(+), 625 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 c8c66cada0..283044b279 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,22 +46,11 @@ 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 zeroRadix = 0; - private static final int octalRadix = 8; - private static final int decRadix = 10; - private static final int hexRadix = 16; - private static final int hexLenMin = -1; - private static final int hexLenIntMax = 8; - private static final int hexLenLongMax = 16; - private static final LinkedHashMap mdnEncodingReplacements = new LinkedHashMap<>(); static { @@ -114,8 +103,6 @@ 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", @@ -188,40 +175,6 @@ 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", @@ -244,18 +197,6 @@ public class TbUtils { String.class))); parserConfig.addImport("decodeURI", new MethodStub(TbUtils.class.getMethod("decodeURI", String.class))); - parserConfig.addImport("newError", new MethodStub(TbUtils.class.getMethod("newError", - String.class, Object.class))); - parserConfig.addImport("newError", new MethodStub(TbUtils.class.getMethod("newError", - 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) { @@ -269,7 +210,6 @@ 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); } @@ -329,28 +269,29 @@ public class TbUtils { } public static Integer parseInt(String value) { - return parseInt(value, zeroRadix); + int radix = getRadix(value); + return parseInt(value, radix); } public static Integer parseInt(String value, int radix) { if (StringUtils.isNotBlank(value)) { try { String valueP = prepareNumberString(value); - int radixValue = isValidStringAndRadix(valueP, radix, value); + isValidRadix(valueP, radix); try { - if (radixValue >= 25 && radixValue <= MAX_RADIX) { - return Integer.parseInt(valueP, radixValue); - } - return switch (radixValue) { - case MIN_RADIX -> parseBinaryStringAsSignedInteger(valueP); - case octalRadix, decRadix, hexRadix -> Integer.parseInt(valueP, radixValue); - default -> throw new IllegalArgumentException("Invalid radix: [" + radix + "]"); - }; + return Integer.parseInt(valueP, radix); } catch (NumberFormatException e) { - Integer iMax = Integer.MAX_VALUE; - Integer iMin = Integer.MIN_VALUE; - compareIntLongValueMinMax(valueP, radixValue, iMax.longValue(), iMin.longValue(), "Integer"); - throw new NumberFormatException(e.getMessage()); + 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()); @@ -360,26 +301,29 @@ public class TbUtils { } public static Long parseLong(String value) { - return parseLong(value, zeroRadix); + int radix = getRadix(value); + return parseLong(value, radix); } public static Long parseLong(String value, int radix) { if (StringUtils.isNotBlank(value)) { try { String valueP = prepareNumberString(value); - int radixValue = isValidStringAndRadix(valueP, radix, value); + isValidRadix(valueP, radix); try { - if (radixValue >= 25 && radixValue <= MAX_RADIX) { - return Long.parseLong(valueP, radixValue); + 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()); } - return switch (radixValue) { - case MIN_RADIX -> parseBinaryStringAsSignedLong(valueP); - case octalRadix, decRadix, hexRadix -> Long.parseLong(valueP, radixValue); - default -> throw new IllegalArgumentException("Invalid radix: [" + radix + "]"); - }; - } catch (NumberFormatException e) { - compareIntLongValueMinMax(valueP, radixValue, Long.MAX_VALUE, Long.MIN_VALUE, "Long"); - throw new NumberFormatException(e.getMessage()); } } catch (NumberFormatException e) { throw new NumberFormatException(e.getMessage()); @@ -388,96 +332,25 @@ public class TbUtils { return null; } - 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, 2) + 1; - return -positiveValue; - } else { - return Integer.parseInt(binaryString, 2); - } - } - 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, 2) + 1; - return -positiveValue; - } else { - return Long.parseLong(binaryString, 2); - } - } - - 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 ? decRadix : isHexadecimal(value); - if (radix > 0) { - return radix; - } else { - throw new NumberFormatException("Value: \"" + value + "\" is not numeric or hexDecimal format!"); - } + private static int getRadix(String value, int... radixS) { + return radixS.length > 0 ? radixS[0] : isHexadecimal(value) ? 16 : 10; } public static Float parseFloat(String value) { - 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 (value != null) { try { - if (radixValue == decRadix) { - return Float.parseFloat(value); - } else { - int bits = Integer.parseUnsignedInt(valueP, hexRadix); - return Float.intBitsToFloat(bits); - } + return Float.parseFloat(prepareNumberString(value)); } catch (NumberFormatException e) { - throw new NumberFormatException(e.getMessage()); } } 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 == decRadix) { - return Double.parseDouble(prepareNumberString(value)); - } else { - long bits = Long.parseUnsignedLong(valueP, hexRadix); - return Double.longBitsToDouble(bits); - } + return Double.parseDouble(prepareNumberString(value)); } catch (NumberFormatException e) { - throw new NumberFormatException(e.getMessage()); } } return null; @@ -495,10 +368,9 @@ public class TbUtils { return parseHexToInt(hex, true); } - public static Integer parseHexToInt(String value, boolean bigEndian) { - String hexValue = prepareNumberString(value); - String hex = bigEndian ? hexValue : reverseHexStringByOrder(hexValue); - return parseInt(hex, hexRadix); + public static int parseHexToInt(String hex, boolean bigEndian) { + byte[] data = prepareHexToBytesNumber(hex, 8); + return parseBytesToInt(data, 0, data.length, bigEndian); } public static long parseLittleEndianHexToLong(String hex) { @@ -513,10 +385,9 @@ public class TbUtils { return parseHexToLong(hex, true); } - public static Long parseHexToLong(String value, boolean bigEndian) { - String hexValue = prepareNumberString(value); - String hex = bigEndian ? value : reverseHexStringByOrder(hexValue); - return parseLong(hex, 16); + public static long parseHexToLong(String hex, boolean bigEndian) { + byte[] data = prepareHexToBytesNumber(hex, 16); + return parseBytesToLong(data, 0, data.length, bigEndian); } public static float parseLittleEndianHexToFloat(String hex) { @@ -531,10 +402,9 @@ public class TbUtils { return parseHexToFloat(hex, true); } - public static Float parseHexToFloat(String value, boolean bigEndian) { - String hexValue = prepareNumberString(value); - String hex = bigEndian ? value : reverseHexStringByOrder(hexValue); - return parseFloat(hex, hexRadix); + public static float parseHexToFloat(String hex, boolean bigEndian) { + byte[] data = prepareHexToBytesNumber(hex, 8); + return parseBytesToFloat(data, 0, bigEndian); } public static double parseLittleEndianHexToDouble(String hex) { @@ -549,155 +419,37 @@ public class TbUtils { return parseHexToDouble(hex, true); } - public static double parseHexToDouble(String value, boolean bigEndian) { - String hexValue = prepareNumberString(value); - String hex = bigEndian ? value : reverseHexStringByOrder(hexValue); - return parseDouble(hex, hexRadix); + public static double parseHexToDouble(String hex, boolean bigEndian) { + byte[] data = prepareHexToBytesNumber(hex, 16); + return parseBytesToDouble(data, 0, bigEndian); } - public static ExecutionArrayList hexToBytes(ExecutionContext ctx, String value) { - String hex = prepareNumberString(value); - int len = hex.length(); - if (len % 2 > 0) { + 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) { throw new IllegalArgumentException("Hex string must be even-length."); } - 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, 16); - // Add the byte to the ArrayList - data.add(byteValue); + 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)); } return data; } - 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, hexLenMin, hexLenIntMax); - } - - public static String intToHex(Integer i, boolean bigEndian) { - return prepareNumberHexString(i.longValue(), bigEndian, false, hexLenMin, hexLenIntMax); - } - - public static String intToHex(Integer i, boolean bigEndian, boolean pref) { - return prepareNumberHexString(i.longValue(), bigEndian, pref, hexLenMin, hexLenIntMax); - } - - public static String intToHex(Integer i, boolean bigEndian, boolean pref, int len) { - return prepareNumberHexString(i.longValue(), bigEndian, pref, len, hexLenIntMax); - } - - public static String longToHex(Long l) { - return prepareNumberHexString(l, true, false, hexLenMin, hexLenLongMax); - } - public static String longToHex(Long l, boolean bigEndian) { - return prepareNumberHexString(l, bigEndian, false, hexLenMin, hexLenLongMax); - } - - public static String longToHex(Long l, boolean bigEndian, boolean pref) { - return prepareNumberHexString(l, bigEndian, pref, hexLenMin, hexLenLongMax); - } - - public static String longToHex(Long l, boolean bigEndian, boolean pref, int len) { - return prepareNumberHexString(l, bigEndian, pref, len, hexLenLongMax); - } - - public static String intLongToString(Long number) { - return intLongToString(number, 10); - } - - 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 8 -> Long.toOctalString(number); - case 10 -> Long.toString(number); - case 16 -> prepareNumberHexString(number, bigEndian, pref, -1, -1); - default -> throw new IllegalArgumentException("Invalid radix: [" + radix + "]"); - }; - } - - private static void compareIntLongValueMinMax(String valueP, int radix, Long maxValue, Long minValue, String clazzName) { - BigInteger bi = new BigInteger(valueP, radix); - if (bi.compareTo(BigInteger.valueOf(maxValue)) > 0) { - throw new NumberFormatException("Value \"" + valueP + "\"is greater than the maximum " + clazzName + " value " + maxValue + " !"); - } else if (bi.compareTo(BigInteger.valueOf(minValue)) < 0) { - throw new NumberFormatException("Value \"" + valueP + "\" is less than the minimum " + clazzName + " value " + minValue + " !"); + 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."); } - } - - 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 == hexLenMin ? 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 - 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; - } + 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 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); + return data; } public static String base64ToHex(String base64) { @@ -895,15 +647,6 @@ public class TbUtils { return URLDecoder.decode(uri, StandardCharsets.UTF_8); } - public static void newError(String message) { - newError(message, null); - } - - public static void newError(String message, Object value) { - String msg = value == null ? message : message + " 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; @@ -944,104 +687,42 @@ 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(); - value = value.replace("0x", ""); - value = value.replace("0X", ""); + if (isHexadecimal(value)) { + value = value.replace("0x", ""); + value = value.replace("0X", ""); + } value = value.replace(",", "."); } return value; } - 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 8 -> isOctal(valueP); - case 10 -> isDecimal(valueP); - case 16 -> isHexadecimal(valueP); - default -> throw new IllegalArgumentException("Invalid radix: [" + radix + "]"); - - }; - } - - if (radixValue > 0) { - if (value.startsWith("0x")) radixValue = 16; - if (radixValue == 16) { - 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!"); + 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; } + if (Character.digit(value.charAt(i), radix) < 0) + throw new NumberFormatException("Failed radix: [" + radix + "] for value: \"" + value + "\"!"); } + return true; } - 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]+") ? octalRadix : -1; - } - - public static int isDecimal(String str) { - if (str == null || str.isEmpty()) { - return -1; - } - return str.matches("-?\\d+(\\.\\d+)?") ? decRadix : -1; - } - - public static int isHexadecimal(String str) { - if (str == null || str.isEmpty()) { - return -1; - } - return str.matches("^-?(0[xX])?[0-9a-fA-F]+$") ? hexRadix : -1; - } - - private static byte isValidIntegerToByte(Integer val) { - if (val > 255 || val < -128) { + private static byte isValidIntegerToByte (Integer val) { + if (val > 255 || val.intValue() < -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(8); - 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 42c8e84b7d..b4e7e7411d 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,14 +15,12 @@ */ 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; @@ -33,28 +31,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; @@ -88,9 +86,10 @@ 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.assertThrows(NumberFormatException.class, () -> TbUtils.parseHexToInt("AABBCCDD", true)); - Assertions.assertEquals(0x11BBCC22, TbUtils.parseHexToInt("11BBCC22", true)); - Assertions.assertEquals(0x11BBCC22, TbUtils.parseHexToInt("22CCBB11", 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)); } @Test @@ -209,28 +208,15 @@ public class TbUtilsTest { Assertions.assertNull(TbUtils.parseInt("")); Assertions.assertNull(TbUtils.parseInt(" ")); - Assertions.assertEquals((Integer) 0, TbUtils.parseInt("0")); - Assertions.assertEquals((Integer) 0, TbUtils.parseInt("-0")); + Assertions.assertEquals(java.util.Optional.of(0).get(), TbUtils.parseInt("0")); + Assertions.assertEquals(java.util.Optional.of(0).get(), 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("-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("FF")); 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)); @@ -238,32 +224,19 @@ 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 @@ -273,13 +246,21 @@ 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))); } @@ -290,15 +271,14 @@ public class TbUtilsTest { Assertions.assertNull(TbUtils.parseLong("")); Assertions.assertNull(TbUtils.parseLong(" ")); - Assertions.assertEquals((Long) 0L, TbUtils.parseLong("0")); - Assertions.assertEquals((Long) 0L, TbUtils.parseLong("-0")); + Assertions.assertEquals(java.util.Optional.of(0L).get(), TbUtils.parseLong("0")); + Assertions.assertEquals(java.util.Optional.of(0L).get(), 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.assertEquals(java.util.Optional.of(4294967295L).get(), TbUtils.parseLong("FFFFFFFF")); + Assertions.assertThrows(NumberFormatException.class, () -> 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)); @@ -315,7 +295,10 @@ 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)); @@ -329,20 +312,14 @@ 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 @@ -352,13 +329,21 @@ 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))); } @@ -369,17 +354,16 @@ 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 @@ -402,8 +386,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); @@ -413,36 +397,12 @@ public class TbUtilsTest { Assertions.assertEquals(expected, TbUtils.bytesToString(listBytes)); ArrayList listMix = new ArrayList<>(arrayMix.length); - Collections.addAll(listMix, arrayMix); + for (Object element : arrayMix) { + listMix.add(element); + } 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<>(); @@ -451,7 +411,14 @@ public class TbUtilsTest { TbUtils.bytesToString(listHex); Assertions.fail("Should throw NumberFormatException"); } catch (NumberFormatException e) { - Assertions.assertTrue(e.getMessage().contains("Value: \"FG\" is not numeric or hexDecimal format!")); + 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\"!")); } List listIntString = new ArrayList<>(); @@ -515,137 +482,6 @@ 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() { - // 0011 0011 0110 0110 == 13158L - // 1100 1100 1001 1010 == -13158L - 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("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 newError_Test() { - String message = "frequency_weighting_type must be 0, 1 or 2."; - Object value = 4; - try { - TbUtils.newError(message, value); - Assertions.fail("Should throw NumberFormatException"); - } catch (RuntimeException e) { - Assertions.assertTrue(e.getMessage().contains("frequency_weighting_type must be 0, 1 or 2. Value = 4")); - } - try { - TbUtils.newError(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 isNumeric_Test() { - - - } - private static List toList(byte[] data) { List result = new ArrayList<>(data.length); for (Byte b : data) { From 2ae1f3158e36b6850e7ad3144adaf805729a2f6f Mon Sep 17 00:00:00 2001 From: mpetrov Date: Fri, 26 Jul 2024 18:41:48 +0300 Subject: [PATCH 03/48] Removed Unexpected Tooltips from toolbar on Edit Connectors Tables --- .../mapping-table/mapping-table.component.html | 2 +- .../modbus-master-table/modbus-master-table.component.html | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-table/mapping-table.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-table/mapping-table.component.html index 9f0566b566..39cc1162c0 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-table/mapping-table.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-table/mapping-table.component.html @@ -18,7 +18,7 @@
-
+
{{mappingTypeTranslationsMap.get(mappingType) | translate}}
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-master-table/modbus-master-table.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-master-table/modbus-master-table.component.html index e16aa0c51f..7dd4e29795 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-master-table/modbus-master-table.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-master-table/modbus-master-table.component.html @@ -18,7 +18,7 @@
-
+
{{ 'gateway.servers-slaves' | translate}}
From 569ab0b87b4f52a43aeec669eeb8bc4bcd003eff Mon Sep 17 00:00:00 2001 From: dashevchenko Date: Fri, 26 Jul 2024 18:42:08 +0300 Subject: [PATCH 04/48] added support for new efento measurement types --- .../efento/CoapEfentoTransportResource.java | 289 +++++++++++------- .../src/main/proto/efento/proto_config.proto | 90 ++++-- .../main/proto/efento/proto_device_info.proto | 25 +- .../efento/proto_measurement_types.proto | 227 +++++++++----- .../proto/efento/proto_measurements.proto | 139 +++++---- .../src/main/proto/efento/proto_rule.proto | 20 ++ 6 files changed, 506 insertions(+), 284 deletions(-) diff --git a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/efento/CoapEfentoTransportResource.java b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/efento/CoapEfentoTransportResource.java index 370c03c27f..6e481cc21d 100644 --- a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/efento/CoapEfentoTransportResource.java +++ b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/efento/CoapEfentoTransportResource.java @@ -35,14 +35,13 @@ import org.thingsboard.server.common.data.device.profile.CoapDeviceProfileTransp import org.thingsboard.server.common.data.device.profile.DeviceProfileTransportConfiguration; import org.thingsboard.server.common.data.device.profile.EfentoCoapDeviceTypeConfiguration; import org.thingsboard.server.common.adaptor.AdaptorException; -import org.thingsboard.server.common.adaptor.ProtoConverter; -import org.thingsboard.server.common.adaptor.AdaptorException; import org.thingsboard.server.common.transport.auth.SessionInfoCreator; import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.gen.transport.coap.ConfigProtos; import org.thingsboard.server.gen.transport.coap.DeviceInfoProtos; import org.thingsboard.server.gen.transport.coap.MeasurementTypeProtos; import org.thingsboard.server.gen.transport.coap.MeasurementsProtos; +import org.thingsboard.server.gen.transport.coap.MeasurementsProtos.ProtoChannel; import org.thingsboard.server.transport.coap.AbstractCoapTransportResource; import org.thingsboard.server.transport.coap.CoapTransportContext; import org.thingsboard.server.transport.coap.callback.CoapDeviceAuthCallback; @@ -51,15 +50,18 @@ import org.thingsboard.server.transport.coap.efento.utils.CoapEfentoUtils; import java.nio.ByteBuffer; import java.text.SimpleDateFormat; -import java.util.ArrayList; import java.util.Date; import java.util.List; import java.util.Map; import java.util.TreeMap; import java.util.UUID; import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; import static com.google.gson.JsonParser.parseString; +import static org.thingsboard.server.gen.transport.coap.MeasurementTypeProtos.MeasurementType.MEASUREMENT_TYPE_FLOODING; +import static org.thingsboard.server.gen.transport.coap.MeasurementTypeProtos.MeasurementType.MEASUREMENT_TYPE_OK_ALARM; +import static org.thingsboard.server.gen.transport.coap.MeasurementTypeProtos.MeasurementType.MEASUREMENT_TYPE_OUTPUT_CONTROL; import static org.thingsboard.server.transport.coap.CoapTransportService.CONFIGURATION; import static org.thingsboard.server.transport.coap.CoapTransportService.CURRENT_TIMESTAMP; import static org.thingsboard.server.transport.coap.CoapTransportService.DEVICE_INFO; @@ -228,121 +230,190 @@ public class CoapEfentoTransportResource extends AbstractCoapTransportResource { int measurementPeriodBase = protoMeasurements.getMeasurementPeriodBase(); int measurementPeriodFactor = protoMeasurements.getMeasurementPeriodFactor(); int signal = protoMeasurements.getSignal(); - List channelsList = protoMeasurements.getChannelsList(); + long nextTransmissionAtMillis = TimeUnit.SECONDS.toMillis(protoMeasurements.getNextTransmissionAt()); + + List channelsList = protoMeasurements.getChannelsList(); + if (CollectionUtils.isEmpty(channelsList)) { + throw new IllegalStateException("[" + sessionId + "]: Failed to get Efento measurements, reason: channels list is empty!"); + } + Map valuesMap = new TreeMap<>(); - if (!CollectionUtils.isEmpty(channelsList)) { - int channel = 0; - JsonObject values; - for (MeasurementsProtos.ProtoChannel protoChannel : channelsList) { - channel++; - boolean isBinarySensor = false; - MeasurementTypeProtos.MeasurementType measurementType = protoChannel.getType(); - String measurementTypeName = measurementType.name(); - if (measurementType.equals(MeasurementTypeProtos.MeasurementType.OK_ALARM) - || measurementType.equals(MeasurementTypeProtos.MeasurementType.FLOODING)) { - isBinarySensor = true; - } - if (measurementPeriodFactor == 0 && isBinarySensor) { - measurementPeriodFactor = 14; - } else { - measurementPeriodFactor = 1; + for (int channel = 0; channel < channelsList.size(); channel++) { + ProtoChannel protoChannel = channelsList.get(channel); + boolean isBinarySensor = isBinarySensor(protoChannel.getType()); + int channelPeriodFactor = (measurementPeriodFactor == 0 ? (isBinarySensor ? 14 : 1) : measurementPeriodFactor); + int measurementPeriod = measurementPeriodBase * channelPeriodFactor; + long measurementPeriodMillis = TimeUnit.SECONDS.toMillis(measurementPeriod); + long startTimestampMillis = TimeUnit.SECONDS.toMillis(protoChannel.getTimestamp()); + List sampleOffsetsList = protoChannel.getSampleOffsetsList(); + + if (CollectionUtils.isEmpty(sampleOffsetsList)) { + log.trace("[{}][{}] sampleOffsetsList list is empty!", sessionId, protoChannel.getType().name()); + continue; + } + + for (int i = 0; i < sampleOffsetsList.size(); i++) { + int sampleOffset = sampleOffsetsList.get(i); + if (isSensorError(sampleOffset)) { + log.warn("[{}],[{}] Sensor error value! Ignoring.", sessionId, sampleOffset); + continue; } - int measurementPeriod = measurementPeriodBase * measurementPeriodFactor; - long measurementPeriodMillis = TimeUnit.SECONDS.toMillis(measurementPeriod); - long nextTransmissionAtMillis = TimeUnit.SECONDS.toMillis(protoMeasurements.getNextTransmissionAt()); - int startPoint = protoChannel.getStartPoint(); - int startTimestamp = protoChannel.getTimestamp(); - long startTimestampMillis = TimeUnit.SECONDS.toMillis(startTimestamp); - List sampleOffsetsList = protoChannel.getSampleOffsetsList(); - if (!CollectionUtils.isEmpty(sampleOffsetsList)) { - int sampleOfssetsListSize = sampleOffsetsList.size(); - for (int i = 0; i < sampleOfssetsListSize; i++) { - int sampleOffset = sampleOffsetsList.get(i); - Integer previousSampleOffset = isBinarySensor && i > 0 ? sampleOffsetsList.get(i - 1) : null; - if (sampleOffset == -32768) { - log.warn("[{}],[{}] Sensor error value! Ignoring.", sessionId, sampleOffset); - } else { - switch (measurementType) { - case TEMPERATURE: - values = valuesMap.computeIfAbsent(startTimestampMillis, k -> - CoapEfentoUtils.setDefaultMeasurements(serialNumber, batteryStatus, measurementPeriod, nextTransmissionAtMillis, signal, k)); - values.addProperty("temperature_" + channel, ((double) (startPoint + sampleOffset)) / 10f); - startTimestampMillis = startTimestampMillis + measurementPeriodMillis; - break; - case WATER_METER: - values = valuesMap.computeIfAbsent(startTimestampMillis, k -> - CoapEfentoUtils.setDefaultMeasurements(serialNumber, batteryStatus, measurementPeriod, nextTransmissionAtMillis, signal, k)); - values.addProperty("pulse_counter_water_" + channel, ((double) (startPoint + sampleOffset))); - startTimestampMillis = startTimestampMillis + measurementPeriodMillis; - break; - case HUMIDITY: - values = valuesMap.computeIfAbsent(startTimestampMillis, k -> - CoapEfentoUtils.setDefaultMeasurements(serialNumber, batteryStatus, measurementPeriod, nextTransmissionAtMillis, signal, k)); - values.addProperty("humidity_" + channel, (double) (startPoint + sampleOffset)); - startTimestampMillis = startTimestampMillis + measurementPeriodMillis; - break; - case ATMOSPHERIC_PRESSURE: - values = valuesMap.computeIfAbsent(startTimestampMillis, k -> - CoapEfentoUtils.setDefaultMeasurements(serialNumber, batteryStatus, measurementPeriod, nextTransmissionAtMillis, signal, k)); - values.addProperty("pressure_" + channel, (double) (startPoint + sampleOffset) / 10f); - startTimestampMillis = startTimestampMillis + measurementPeriodMillis; - break; - case DIFFERENTIAL_PRESSURE: - values = valuesMap.computeIfAbsent(startTimestampMillis, k -> - CoapEfentoUtils.setDefaultMeasurements(serialNumber, batteryStatus, measurementPeriod, nextTransmissionAtMillis, signal, k)); - values.addProperty("pressure_diff_" + channel, (double) (startPoint + sampleOffset)); - startTimestampMillis = startTimestampMillis + measurementPeriodMillis; - break; - case OK_ALARM: - boolean currentIsOk = sampleOffset < 0; - if (previousSampleOffset != null) { - boolean previousIsOk = previousSampleOffset < 0; - boolean isOk = previousIsOk && currentIsOk; - boolean isAlarm = !previousIsOk && !currentIsOk; - if (isOk || isAlarm) { - break; - } - } - String data = currentIsOk ? "OK" : "ALARM"; - long sampleOffsetMillis = TimeUnit.SECONDS.toMillis(sampleOffset); - long measurementTimestamp = startTimestampMillis + Math.abs(sampleOffsetMillis); - values = valuesMap.computeIfAbsent(measurementTimestamp - 1000, k -> - CoapEfentoUtils.setDefaultMeasurements(serialNumber, batteryStatus, measurementPeriod, nextTransmissionAtMillis, signal, k)); - values.addProperty("ok_alarm_" + channel, data); - break; - case PULSE_CNT: - values = valuesMap.computeIfAbsent(startTimestampMillis, k -> - CoapEfentoUtils.setDefaultMeasurements(serialNumber, batteryStatus, measurementPeriod, nextTransmissionAtMillis, signal, k)); - values.addProperty("pulse_cnt_" + channel, (double) (startPoint + sampleOffset)); - startTimestampMillis = startTimestampMillis + measurementPeriodMillis; - break; - case NO_SENSOR: - case UNRECOGNIZED: - log.trace("[{}][{}] Sensor error value! Ignoring.", sessionId, measurementTypeName); - break; - default: - log.trace("[{}],[{}] Unsupported measurementType! Ignoring.", sessionId, measurementTypeName); - break; - } - } + + JsonObject values; + if (isBinarySensor) { + long sampleOffsetMillis = TimeUnit.SECONDS.toMillis(sampleOffset); + long measurementTimestamp = startTimestampMillis + Math.abs(sampleOffsetMillis) - 1; + values = valuesMap.computeIfAbsent(measurementTimestamp - 1000, k -> + CoapEfentoUtils.setDefaultMeasurements(serialNumber, batteryStatus, measurementPeriod, nextTransmissionAtMillis, signal, k)); + Integer previousSampleOffset = i > 0 ? sampleOffsetsList.get(i - 1) : null; + boolean previousValueIsOk = previousSampleOffset != null && previousSampleOffset < 0; + boolean valueIsOk = sampleOffset < 0; + if (binaryValueNotChanged(valueIsOk, previousValueIsOk)) { + break; } + addBinarySample(protoChannel, valueIsOk, values, channel + 1); } else { - log.trace("[{}][{}] sampleOffsetsList list is empty!", sessionId, measurementTypeName); + long timestampMillis = startTimestampMillis + i * measurementPeriodMillis; + values = valuesMap.computeIfAbsent(timestampMillis, k -> CoapEfentoUtils.setDefaultMeasurements( + serialNumber, batteryStatus, measurementPeriod, nextTransmissionAtMillis, signal, k)); + addContinuesSample(protoChannel, sampleOffset, values, channel + 1, sessionId); } } - } else { - throw new IllegalStateException("[" + sessionId + "]: Failed to get Efento measurements, reason: channels list is empty!"); } - if (!CollectionUtils.isEmpty(valuesMap)) { - List efentoMeasurements = new ArrayList<>(); - for (Long ts : valuesMap.keySet()) { - EfentoTelemetry measurement = new EfentoTelemetry(ts, valuesMap.get(ts)); - efentoMeasurements.add(measurement); - } - return efentoMeasurements; - } else { + + if (CollectionUtils.isEmpty(valuesMap)) { throw new IllegalStateException("[" + sessionId + "]: Failed to collect Efento measurements, reason, values map is empty!"); } + + return valuesMap.entrySet().stream() + .map(entry -> new EfentoTelemetry(entry.getKey(), entry.getValue())) + .collect(Collectors.toList()); + } + + private boolean isBinarySensor(MeasurementTypeProtos.MeasurementType type) { + return type == MEASUREMENT_TYPE_OK_ALARM || type == MEASUREMENT_TYPE_FLOODING || type == MEASUREMENT_TYPE_OUTPUT_CONTROL; + } + + private boolean isSensorError(int sampleOffset) { + return sampleOffset >= 8355840 && sampleOffset <= 8388607; + } + + private void addContinuesSample(ProtoChannel protoChannel, int sampleOffset, JsonObject values, int channelNumber, UUID sessionId) { + int startPoint = protoChannel.getStartPoint(); + + switch (protoChannel.getType()) { + case MEASUREMENT_TYPE_TEMPERATURE: + values.addProperty("temperature_" + channelNumber, ((double) (startPoint + sampleOffset)) / 10f); + break; + case MEASUREMENT_TYPE_WATER_METER: + values.addProperty("pulse_counter_water_" + channelNumber, ((double) (startPoint + sampleOffset))); + break; + case MEASUREMENT_TYPE_HUMIDITY: + values.addProperty("humidity_" + channelNumber, (double) (startPoint + sampleOffset)); + break; + case MEASUREMENT_TYPE_ATMOSPHERIC_PRESSURE: + values.addProperty("pressure_" + channelNumber, (double) (startPoint + sampleOffset) / 10f); + break; + case MEASUREMENT_TYPE_DIFFERENTIAL_PRESSURE: + values.addProperty("pressure_diff_" + channelNumber, (double) (startPoint + sampleOffset)); + break; + case MEASUREMENT_TYPE_PULSE_CNT: + values.addProperty("pulse_cnt_" + channelNumber, (double) (startPoint + sampleOffset)); + break; + case MEASUREMENT_TYPE_IAQ: + values.addProperty("iaq_" + channelNumber, (startPoint + sampleOffset)); + break; + case MEASUREMENT_TYPE_ELECTRICITY_METER: + values.addProperty("watt_hour_" + channelNumber, (double) (startPoint + sampleOffset)); + break; + case MEASUREMENT_TYPE_SOIL_MOISTURE: + values.addProperty("soil_moisture_" + channelNumber, (double) (startPoint + sampleOffset)); + break; + case MEASUREMENT_TYPE_AMBIENT_LIGHT: + values.addProperty("ambient_light_" + channelNumber, (double) (startPoint + sampleOffset) / 10f); + break; + case MEASUREMENT_TYPE_HIGH_PRESSURE: + values.addProperty("high_pressure_" + channelNumber, (double) (startPoint + sampleOffset)); + break; + case MEASUREMENT_TYPE_DISTANCE_MM: + values.addProperty("distance_mm_" + channelNumber, (double) (startPoint + sampleOffset)); + break; + case MEASUREMENT_TYPE_WATER_METER_ACC_MINOR: + values.addProperty("acc_counter_water_minor_" + channelNumber, (double) (startPoint + sampleOffset)); + break; + case MEASUREMENT_TYPE_WATER_METER_ACC_MAJOR: + values.addProperty("acc_counter_water_major_" + channelNumber, (double) (startPoint + sampleOffset)); + break; + case MEASUREMENT_TYPE_HUMIDITY_ACCURATE: + values.addProperty("humidity_relative_" + channelNumber, (double) (startPoint + sampleOffset) / 10f); + break; + case MEASUREMENT_TYPE_STATIC_IAQ: + values.addProperty("static_iaq_" + channelNumber, (double) (startPoint + sampleOffset)); + break; + case MEASUREMENT_TYPE_CO2_EQUIVALENT: + values.addProperty("co2_ppm_" + channelNumber, (double) (startPoint + sampleOffset)); + break; + case MEASUREMENT_TYPE_BREATH_VOC: + values.addProperty("breath_voc_ppm_" + channelNumber, (double) (startPoint + sampleOffset)); + break; + case MEASUREMENT_TYPE_PERCENTAGE: + values.addProperty("percentage_" + channelNumber, (double) (startPoint + sampleOffset) / 100f); + break; + case MEASUREMENT_TYPE_VOLTAGE: + values.addProperty("voltage_" + channelNumber, (double) (startPoint + sampleOffset) / 10f); + break; + case MEASUREMENT_TYPE_CURRENT: + values.addProperty("current_" + channelNumber, (double) (startPoint + sampleOffset) / 100f); + break; + case MEASUREMENT_TYPE_PULSE_CNT_ACC_MINOR: + values.addProperty("pulse_cnt_minor_" + channelNumber, (double) (startPoint + sampleOffset)); + break; + case MEASUREMENT_TYPE_PULSE_CNT_ACC_MAJOR: + values.addProperty("pulse_cnt_major_" + channelNumber, (double) (startPoint + sampleOffset)); + break; + case MEASUREMENT_TYPE_ELEC_METER_ACC_MINOR: + values.addProperty("elec_meter_minor_" + channelNumber, (double) (startPoint + sampleOffset)); + break; + case MEASUREMENT_TYPE_ELEC_METER_ACC_MAJOR: + values.addProperty("elec_meter_major_" + channelNumber, (double) (startPoint + sampleOffset)); + break; + case MEASUREMENT_TYPE_PULSE_CNT_ACC_WIDE_MINOR: + values.addProperty("pulse_cnt_wide_minor_" + channelNumber, (double) (startPoint + sampleOffset)); + break; + case MEASUREMENT_TYPE_PULSE_CNT_ACC_WIDE_MAJOR: + values.addProperty("pulse_cnt_wide_major_" + channelNumber, (double) (startPoint + sampleOffset)); + break; + case MEASUREMENT_TYPE_CURRENT_PRECISE: + values.addProperty("current_precise_" + channelNumber, (double) (startPoint + sampleOffset)/1000f); + break; + case MEASUREMENT_TYPE_NO_SENSOR: + case UNRECOGNIZED: + log.trace("[{}][{}] Sensor error value! Ignoring.", sessionId, protoChannel.getType().name()); + break; + default: + log.trace("[{}],[{}] Unsupported measurementType! Ignoring.", sessionId, protoChannel.getType().name()); + break; + } + } + + private void addBinarySample(ProtoChannel protoChannel, boolean valueIsOk, JsonObject values, int channel) { + switch (protoChannel.getType()) { + case MEASUREMENT_TYPE_OK_ALARM: + values.addProperty("ok_alarm_" + channel, valueIsOk ? "OK" : "ALARM"); + case MEASUREMENT_TYPE_FLOODING: + values.addProperty("flooding_" + channel, valueIsOk ? "OK" : "WATER_DETECTED"); + case MEASUREMENT_TYPE_OUTPUT_CONTROL: + values.addProperty("output_control_" + channel, valueIsOk ? "OFF" : "ON"); + } + } + + private static boolean binaryValueNotChanged(boolean currentIsOk, boolean previousIsOk) { + boolean isOk = previousIsOk && currentIsOk; + boolean isAlarm = !previousIsOk && !currentIsOk; + if (isOk || isAlarm) { + return true; + } + return false; } private EfentoTelemetry getEfentoDeviceInfo(DeviceInfoProtos.ProtoDeviceInfo protoDeviceInfo) { diff --git a/common/transport/coap/src/main/proto/efento/proto_config.proto b/common/transport/coap/src/main/proto/efento/proto_config.proto index 6212681403..25dbec883f 100644 --- a/common/transport/coap/src/main/proto/efento/proto_config.proto +++ b/common/transport/coap/src/main/proto/efento/proto_config.proto @@ -22,26 +22,13 @@ option java_package = "org.thingsboard.server.gen.transport.coap"; option java_outer_classname = "ConfigProtos"; /* Message containing optional channels control parameters */ -message ProtoChannelControl { +message ProtoOutputControlState { /* Channel index */ uint32 channel_index = 1; - /* Control parameters. Maximal number equals 4. This field is channel specific: */ - /* IO_control channel: */ - /* - control_params[0]: */ - /* - Byte 0: On state configuration */ - /* 0x01 - Low */ - /* 0x02 - High */ - /* 0x03 - High-Z (disconnected) */ - /* - Byte 1: Off state configuration */ - /* 0x01 - Low */ - /* 0x02 - High */ - /* 0x03 - High-Z (disconnected) */ - /* - Byte 2: Power on channel state */ - /* 0x01 - On */ - /* 0x02 - Off */ - repeated uint32 control_params = 2; + /* Channel state ON/OFF. Range (1 - OFF; 2 - ON) */ + uint32 channel_state = 2; } /* Message containing request data for accesing calibration parameters */ @@ -58,6 +45,39 @@ message ProtoCalibrationParameters { repeated int32 parameters = 3; } +enum BleAdvertisingPeriodMode { + + /* Invalid value */ + BLE_ADVERTISING_PERIOD_MODE_UNSPECIFIED = 0; + + /* Default behavior - faster advertising when measurement period is < 15s. */ + BLE_ADVERTISING_PERIOD_MODE_DEFAULT = 1; + + /* User-configured normal interval is used. */ + BLE_ADVERTISING_PERIOD_MODE_NORMAL = 2; + + /* User-configured fast interval is used. */ + BLE_ADVERTISING_PERIOD_MODE_FAST = 3; +} + +/* Message containing BLE advertising period configuration */ +message ProtoBleAdvertisingPeriod { + + /* BLE advertising mode: */ + /* - 1: Default, BLE advertising interval is set to 1022.5ms or some lower value, based on continuous measurement period. */ + /* - 2: Normal, uses user-configured value from 'normal' field. */ + /* - 3: Fast, uses user-configured value from 'fast' field (must be lower than or equal to 'normal' field). */ + BleAdvertisingPeriodMode mode = 1; + + /* BLE advertising interval when in normal mode, configured in 0.625ms steps. */ + /* Range: [32:16384] */ + uint32 normal = 2; + + /* BLE advertising interval when in fast mode, configured in 0.625ms steps. */ + /* Range: [32:16384] */ + uint32 fast = 3; +} + /* Main message sent in the payload. Each field in this message is independent of the others - only parameters that should be */ /* changed need to be sent in the payload. */ /* If the value of a selected parameter shall not be changed, do not include it in the payload */ @@ -115,16 +135,26 @@ message ProtoConfig { /* 65535 - disable transfer limit function */ uint32 transfer_limit_timer = 10; - /* Data (measurements) server IP address */ - /* IP of the data server as string in form x.x.x.x. For example: 18.184.24.239 */ + /* For firmware >= 6.07.00: */ + /* IP or URL address of the data (measurements) server */ + /* The IP or URL of the data server, provided as string with a maximum length of 31 characters */ + /* For example, use "18.184.24.239" for an IP address or "efento.test.io" for a URL */ + /* For firmware < 6.07.00: */ + /* IP address of the data (measurements) server */ + /* For example, use "18.184.24.239" */ string data_server_ip = 11; /* Data (measurements) server port */ /* Range: [1:65535] */ uint32 data_server_port = 12; - /* Update server IP address */ - /* IP of data server as string in form x.x.x.x. For example: 18.184.24.239 */ + /* For firmware >= 6.07.00: */ + /* IP or URL address of the update server */ + /* The IP or URL of the update server, provided as string with a maximum length of 31 characters */ + /* For example, use "18.184.24.239" for an IP address or "efento.test.io" for a URL */ + /* For firmware < 6.07.00: */ + /* IP address of the update server */ + /* For example, use "18.184.24.239" */ string update_server_ip = 13; /* Update server port for UDP transmission */ @@ -144,7 +174,8 @@ message ProtoConfig { /* 0xFFFFFFFF or 1000000 - automatic selection */ uint32 plmn_selection = 17; - /* Device will power off its cellular modem for requested number of seconds. Maximum number of seconds 604800 (7 days) */ + /* Device will power off its cellular modem for requested number of seconds. */ + /* Range: [60:604800] (1 minute : 7 days) */ /* This field is only sent by server */ uint32 disable_modem_request = 18; @@ -263,9 +294,8 @@ message ProtoConfig { /* Calendar configuration. Up to 6 calendars are supported */ repeated ProtoCalendar calendars = 47; - /* Control parameters for channels. Maximal number of requests equals 6 */ - /* This field is only sent by server */ - repeated ProtoChannelControl channels_control_request = 48; + /* DEPRECATED - Used for backward compatibility */ + reserved 48; /* Set/get calibration parameters for single channel. */ ProtoCalibrationParameters calibration_parameters_request = 49; @@ -295,7 +325,7 @@ message ProtoConfig { reserved 52, 53; /* Encryption key configuration. Sensor sends in this field two last bytes of SHA256 hash calculated from its current */ - /* encryption_key configuration. */ + /* encryption_key configuration. When encryption key is disabled one byte 0x7F (DEL) is sent. */ /* Max length: 16 bytes. */ /* 0x7F - encryption key disabled. */ bytes encryption_key = 54; @@ -309,4 +339,14 @@ message ProtoConfig { /* String with special character 0x7F (DEL) only indicates that automatic password is turn on */ /* Password can only be set to custom value if apn_user_name has been configured (is not automatic) */ string apn_password = 56; + + /* Reserved by versions above 06.20.00 */ + reserved 57; + + /* Control output state on channel pin. Maximal number of requests equals 3 */ + /* This field is only sent by server */ + repeated ProtoOutputControlState output_control_state_request = 58; + + /* BLE advertising period configuration. */ + ProtoBleAdvertisingPeriod ble_advertising_period = 59; } \ No newline at end of file diff --git a/common/transport/coap/src/main/proto/efento/proto_device_info.proto b/common/transport/coap/src/main/proto/efento/proto_device_info.proto index 791d01acd7..a0cb9bc2f2 100644 --- a/common/transport/coap/src/main/proto/efento/proto_device_info.proto +++ b/common/transport/coap/src/main/proto/efento/proto_device_info.proto @@ -106,6 +106,11 @@ message ProtoModem /* parameters[32] - Rx_time - [0.1s] - Range: [0:2147483647]. Unknown value: -1 */ /* parameters[33] - Tx_time - [0.1s] - Range: [0:2147483647]. Unknown value: -1 */ repeated sint32 parameters = 2; + + /* ICCID of inserted/soldered sim card. String up to 22 characters long. */ + /* 0x7F if sim card is not detected, empty (not sent) if device does not have modem. */ + /* This field is only sent by device */ + string sim_card_identification = 3; } message ProtoUpdateInfo @@ -115,16 +120,16 @@ message ProtoUpdateInfo uint32 timestamp = 1; /* Update status, possible values: */ - /* - 0x1 - No update yet */ - /* - 0x2 - No error */ - /* - 0x3 - UDP socekt error */ - /* - 0x4 - Hash error */ - /* - 0x5 - Missing packet error */ - /* - 0x6 - Invalid data error */ - /* - 0x7 - Sending timeout error */ - /* - 0x8 - No SW to update error */ - /* - 0x9 - Sending unexpected error */ - /* - 0x10 - Unexpected error */ + /* - 1 - No update yet */ + /* - 2 - No error */ + /* - 3 - UDP socekt error */ + /* - 4 - Hash error */ + /* - 5 - Missing packet error */ + /* - 6 - Invalid data error */ + /* - 7 - Sending timeout error */ + /* - 8 - No SW to update error */ + /* - 9 - Sending unexpected error */ + /* - 10 - Unexpected error */ uint32 status = 2; } diff --git a/common/transport/coap/src/main/proto/efento/proto_measurement_types.proto b/common/transport/coap/src/main/proto/efento/proto_measurement_types.proto index 66a6c95d09..98029bed51 100644 --- a/common/transport/coap/src/main/proto/efento/proto_measurement_types.proto +++ b/common/transport/coap/src/main/proto/efento/proto_measurement_types.proto @@ -19,79 +19,160 @@ option java_package = "org.thingsboard.server.gen.transport.coap"; option java_outer_classname = "MeasurementTypeProtos"; enum MeasurementType { - NO_SENSOR = 0; - - /* [°C] - Celsius degree. Resolution 0.1°C. Range [-273.2-4000.0]. Type: Continuous */ - TEMPERATURE = 1; - - /* [% RH] - Relative humidity. Resolution 1%. Range [0-100]. Type: Continuous */ - HUMIDITY = 2; - - /* [hPa] - Hectopascal (1hPa = 100Pa). Resolution 0.1hPa. Range: [1.0-2000.0]. Type: Continuous */ - ATMOSPHERIC_PRESSURE = 3; - - /* [Pa] - Pascal. Resolution 1Pa. Range [-10000-10000]Type: Continuous */ - DIFFERENTIAL_PRESSURE = 4; - - /* Sign indicates state: (+) ALARM, (-) OK. Type: Binary */ - OK_ALARM = 5; - - /* [IAQ] - Iaq index. Resolution 1IAQ. Range [0-500]. Sensor return also calibration status */ - /* as offset to measured value: */ - /* - offset 3000: Sensor not stabilized (always returns 25 IAQ value) */ - /* - offset 2000: Calibration required (sensor returns not accurate values) */ - /* - offset 1000: Calibration on-going (sensor returns not accurate values) */ - /* - offset 0: Calibration done (best accuracy of IAQ sensor) */ - /* Type: Continuous */ - IAQ = 6; - - /* Sign indicates water presence: (+) water not detected, (-) water detected. Type: Binary */ - FLOODING = 7; - - /* [NB] Number of pulses. Resolution 1 pulse. Range [0-16711679]. Type: Continuous */ - PULSE_CNT = 8; - - /* [Wh] - Watthour; Resolution 1Wh. Range [0-16711679]. Number of Watthours in a single period. Type: Continuous */ - ELECTRICITY_METER = 9; - - /* [l] - Liter. Resolution 1l. Range [0-16711679]. Number of litres in a single period. Type: Continuous */ - WATER_METER = 10; - - /* [kPa] - Kilopascal (1kPa = 1000Pa); Resolution 1kPa. Range [-1000-0]. Soil moisture (tension). Type: Continuous */ - SOIL_MOISTURE = 11; - - /* [ppm] - Parts per million. Resolution 1ppm. Range [0-1000000]. Carbon monoxide concentration. Type: Continuous */ - CO_GAS = 12; - - /* [ppm] - Parts per million. Resolution 0.01ppm. Range [0-1000000.00]. Nitrogen dioxide concentration. Type: Continuous*/ - NO2_GAS = 13; - - /* [ppm] - Parts per million. Resolution 1ppm. Range [0-1000000]. Hydrogen sulfide concentration. Type: Continuous */ - H2S_GAS = 14; - - /* [lx] - Illuminance. Resolution 0.1lx. Range [0-100000.0]. Type: Continuous */ - AMBIENT_LIGHT = 15; - - /* [µg/m^3] - Micro gram per cubic meter. Resolution 1µg/m^3 Range [0-1000]. */ - /* particles with an aerodynamic diameter less than 1 micrometer. Type: Continuous */ - PM_1_0 = 16; // µg/m^3 - - /* [µg/m^3] - Micro gram per cubic meter. Resolution 1µg/m^3 Range [0-1000]. */ - /* particles with an aerodynamic diameter less than 2.5 micrometers. Type: Continuous */ - PM_2_5 = 17; // µg/m^3 - - /* [µg/m^3] - Micro gram per cubic meter. Resolution 1µg/m^3 Range [0-1000]. */ - /* particles with an aerodynamic diameter less than 10 micrometers. Type: Continuous */ - PM_10_0 = 18; // µg/m^3 - - /* [dB] - Decibels. Resolution 0.1 dB. Range: [0-130.0]. Noise level. Type: Continuous */ - NOISE_LEVEL = 19; // 0.1 dB - - /* [ppm] - Parts per million. Resolution 1ppm. Range [0-1000000]. Ammonia concentration. Type: Continuous */ - NH3_GAS = 20; - - /* [ppm] - Parts per million. Resolution 1ppm. Range [0-1000000]. Methane concentration. Type: Continuous */ - CH4_GAS = 21; + /* [] - No sensor on the channel */ + MEASUREMENT_TYPE_NO_SENSOR = 0; + + /* [°C] - Celsius degree. Resolution 0.1°C. Range [-273.2:4000.0]. Type: Continuous */ + MEASUREMENT_TYPE_TEMPERATURE = 1; + + /* [% RH] - Relative humidity. Resolution 1%. Range [0:100]. Type: Continuous */ + MEASUREMENT_TYPE_HUMIDITY = 2; + + /* [hPa] - Hectopascal (1hPa = 100Pa). Resolution 0.1hPa. Range: [1.0:2000.0]. Atmospheric pressure. Type: Continuous */ + MEASUREMENT_TYPE_ATMOSPHERIC_PRESSURE = 3; + + /* [Pa] - Pascal. Resolution 1Pa. Range [-10000:10000]. Differential pressure. Type: Continuous */ + MEASUREMENT_TYPE_DIFFERENTIAL_PRESSURE = 4; + + /* Sign indicates state: (+) ALARM, (-) OK. Type: Binary */ + MEASUREMENT_TYPE_OK_ALARM = 5; + + /* [IAQ] - IAQ index. Resolution 1IAQ. Range [0:500]. To get IAQ index the value should be divided by 3. */ + /* Sensor return also calibration status as metadata (is the remainder when the absolute value is divided by 3): */ + /* - 0: Calibration required (sensor returns not accurate values) */ + /* - 1: Calibration on-going (sensor returns not accurate values) */ + /* - 2: Calibration done (best accuracy of IAQ sensor) */ + /* Type: Continuous */ + MEASUREMENT_TYPE_IAQ = 6; + + /* Sign indicates water presence: (+) water not detected, (-) water detected. Type: Binary */ + MEASUREMENT_TYPE_FLOODING = 7; + + /* [NB] Number of pulses. Resolution 1 pulse. Range [0:8000000]. Type: Continuous */ + MEASUREMENT_TYPE_PULSE_CNT = 8; + + /* [Wh] - Watthour; Resolution 1Wh. Range [0:8000000]. Number of Watthours in a single period. Type: Continuous */ + MEASUREMENT_TYPE_ELECTRICITY_METER = 9; + + /* [l] - Liter. Resolution 1l. Range [0:8000000]. Number of litres in a single period. Type: Continuous */ + MEASUREMENT_TYPE_WATER_METER = 10; + + /* [kPa] - Kilopascal (1kPa = 1000Pa); Resolution 1kPa. Range [-1000:0]. Soil moisture (tension). Type: Continuous */ + MEASUREMENT_TYPE_SOIL_MOISTURE = 11; + + /* [ppm] - Parts per million. Resolution 1ppm. Range [0:1000000]. Carbon monoxide concentration. Type: Continuous */ + MEASUREMENT_TYPE_CO_GAS = 12; + + /* [ppm] - Parts per million. Resolution 1.0ppm. Range [0:1000000]. Nitrogen dioxide concentration. Type: Continuous */ + MEASUREMENT_TYPE_NO2_GAS = 13; + + /* [ppm] - Parts per million. Resolution 0.01ppm. Range [0.00:80000.00]. Hydrogen sulfide concentration. Type: Continuous */ + MEASUREMENT_TYPE_H2S_GAS = 14; + + /* [lx] - Lux. Resolution 0.1lx. Range [0.0:100000.0]. Illuminance. Type: Continuous */ + MEASUREMENT_TYPE_AMBIENT_LIGHT = 15; + + /* [µg/m^3] - Micro gram per cubic meter. Resolution 1µg/m^3. Range [0:1000]. */ + /* Particles with an aerodynamic diameter less than 1 micrometer. Type: Continuous */ + MEASUREMENT_TYPE_PM_1_0 = 16; + + /* [µg/m^3] - Micro gram per cubic meter. Resolution 1µg/m^3. Range [0:1000]. */ + /* Particles with an aerodynamic diameter less than 2.5 micrometers. Type: Continuous */ + MEASUREMENT_TYPE_PM_2_5 = 17; + + /* [µg/m^3] - Micro gram per cubic meter. Resolution 1µg/m^3. Range [0:1000]. */ + /* Particles with an aerodynamic diameter less than 10 micrometers. Type: Continuous */ + MEASUREMENT_TYPE_PM_10_0 = 18; + + /* [dB] - Decibels. Resolution 0.1 dB. Range: [0.0:200.0]. Noise level. Type: Continuous */ + MEASUREMENT_TYPE_NOISE_LEVEL = 19; + + /* [ppm] - Parts per million. Resolution 1ppm. Range [0:1000000]. Ammonia concentration. Type: Continuous */ + MEASUREMENT_TYPE_NH3_GAS = 20; + + /* [ppm] - Parts per million. Resolution 1ppm. Range [0:1000000]. Methane concentration. Type: Continuous */ + MEASUREMENT_TYPE_CH4_GAS = 21; + + /* [kPa] - Kilopascal (1kPa = 1000Pa, 100kPa = 1bar). Resolution 1kPa. Range [0:200000]. Pressure. Type: Continuous */ + MEASUREMENT_TYPE_HIGH_PRESSURE = 22; + + /* [mm] - Millimeter. Resolution 1mm. Range [0:100000]. Distance. Type: Continuous */ + MEASUREMENT_TYPE_DISTANCE_MM = 23; + + /* [l] - Liter. Resolution 1l. Range [0:1000000]. Accumulative water meter (minor). Type: Continuous */ + MEASUREMENT_TYPE_WATER_METER_ACC_MINOR = 24; + + /* [hl] - Hectoliter. Resolution 1hl. Range [0:1000000]. Accumulative water meter (major). Type: Continuous */ + MEASUREMENT_TYPE_WATER_METER_ACC_MAJOR = 25; + + /* [ppm] - Parts per million. Resolution 1ppm. Range [0:1000000]. Carbon dioxide concentration. Type: Continuous */ + MEASUREMENT_TYPE_CO2_GAS = 26; + + /* [% RH] - Relative humidity. Resolution 0.1%. Range [0.0:100.0]. Type: Continuous */ + MEASUREMENT_TYPE_HUMIDITY_ACCURATE = 27; + + /* [sIAQ] - Static IAQ index. Resolution 1IAQ. Range [0:10000]. To get static IAQ index the value should be divided by 3. */ + /* Sensor return also calibration status as metadata (is the remainder when the absolute value is divided by 3): */ + /* - 0: Calibration required (sensor returns not accurate values) */ + /* - 1: Calibration on-going (sensor returns not accurate values) */ + /* - 2: Calibration done (best accuracy of IAQ sensor) */ + /* Type: Continuous */ + MEASUREMENT_TYPE_STATIC_IAQ = 28; + + /* [ppm] - Parts per million. Resolution 1ppm. Range [0:1000000]. CO2 equivalent. */ + /* To get CO2 equivalent the value should be divided by 3. */ + /* Sensor return also calibration status as metadata (is the remainder when the absolute value is divided by 3): */ + /* - 0: Calibration required (sensor returns not accurate values) */ + /* - 1: Calibration on-going (sensor returns not accurate values) */ + /* - 2: Calibration done (best accuracy of IAQ sensor) */ + /* Type: Continuous */ + MEASUREMENT_TYPE_CO2_EQUIVALENT = 29; + + /* [ppm] - Parts per million. Resolution 1ppm. Range [0:100000]. Breath VOC estimate. */ + /* To get breath VOC estimate the value should be divided by 3. */ + /* Sensor return also calibration status as metadata (is the remainder when the absolute value is divided by 3): */ + /* - 0: Calibration required (sensor returns not accurate values) */ + /* - 1: Calibration on-going (sensor returns not accurate values) */ + /* - 2: Calibration done (best accuracy of IAQ sensor) */ + /* Type: Continuous */ + MEASUREMENT_TYPE_BREATH_VOC = 30; + + /* Special measurement type reserved for cellular gateway. */ + /* Type: Continuous */ + MEASUREMENT_TYPE_CELLULAR_GATEWAY = 31; + + /* [%] - Percentage. Resolution 0.01%. Range [0.00:100.00]. Type: Continuous */ + MEASUREMENT_TYPE_PERCENTAGE = 32; + + /* [mV] - Milivolt. Resolution 0.1mV. Range [0.0:100000.0]. Type: Continuous */ + MEASUREMENT_TYPE_VOLTAGE = 33; + + /* [mA] - Milliampere. Resolution 0.01mA. Range [0.0:10000.00]. Type: Continuous */ + MEASUREMENT_TYPE_CURRENT = 34; + + /* [NB] Number of pulses. Resolution 1 pulse. Range [0:1000000]. Type: Continuous */ + MEASUREMENT_TYPE_PULSE_CNT_ACC_MINOR = 35; + + /* [kNB] Number of kilopulses. Resolution 1 kilopulse. Range [0:1000000]. Type: Continuous */ + MEASUREMENT_TYPE_PULSE_CNT_ACC_MAJOR = 36; + + /* [Wh] - Watt-hour; Resolution 1Wh. Range [0:1000000]. Number of watt-hours in a single period. Type: Continuous */ + MEASUREMENT_TYPE_ELEC_METER_ACC_MINOR = 37; + + /* [kWh] - Kilowatt-hour; Resolution 1kWh. Range [0:1000000]. Number of kilowatt-hours in a single period. Type: Continuous */ + MEASUREMENT_TYPE_ELEC_METER_ACC_MAJOR = 38; + + /* [NB] Number of pulses (wide range). Resolution 1 pulse. Range [0:999999]. Type: Continuous */ + MEASUREMENT_TYPE_PULSE_CNT_ACC_WIDE_MINOR = 39; + + /* [MNB] Number of megapulses (wide range). Resolution 1 megapulse. Range [0:999999]. Type: Continuous */ + MEASUREMENT_TYPE_PULSE_CNT_ACC_WIDE_MAJOR = 40; + + /* [mA] - Milliampere. Resolution 0.001mA. Range [-4 000.000:4 000.000]. Type: Continuous */ + MEASUREMENT_TYPE_CURRENT_PRECISE = 41; + + /* Sign indicates state: (+) ON, (-) OFF. Type: Binary */ + MEASUREMENT_TYPE_OUTPUT_CONTROL = 42; } diff --git a/common/transport/coap/src/main/proto/efento/proto_measurements.proto b/common/transport/coap/src/main/proto/efento/proto_measurements.proto index f8549feb54..80af07d958 100644 --- a/common/transport/coap/src/main/proto/efento/proto_measurements.proto +++ b/common/transport/coap/src/main/proto/efento/proto_measurements.proto @@ -19,103 +19,108 @@ import "efento/proto_measurement_types.proto"; option java_package = "org.thingsboard.server.gen.transport.coap"; option java_outer_classname = "MeasurementsProtos"; -message ProtoChannel{ - /* Type of channel */ +message ProtoChannel { + + /* Type of channel */ MeasurementType type = 1; - /* Timestamp of the first sample (the oldest one) in seconds since UNIX EPOCH 01-01-1970 */ + /* Timestamp of the first sample (the oldest one) in seconds since UNIX EPOCH 01-01-1970 */ int32 timestamp = 2; - /* Only used for 'Continuous' sensor types. Value used as the starting point for calculating the values of all */ - /* measurements in the package. */ - /* Format defined by 'MeasurementType' field */ + /* Only used for 'Continuous' sensor types. Value used as the starting point for calculating the values of all */ + /* measurements in the package. */ + /* Format defined by 'MeasurementType' field */ sint32 start_point = 4; - /* 'Continuous' sensor types */ - /* Value of the offset from the 'start_point' for each measurement in the package. The oldest sample first ([0]). */ - /* 'sample_offsets' format defined by 'MeasurementType' field. */ - /* Example: MeasurementType = 1 (temperature), start_point = 100, sample_offsets[0] = 15, sample_offsets[1] = 20 */ - /* 1st sample in the package temperature value = 11.5 °C, 2nd sample in the package temperature value = 12 °C */ - /* Calculating timestamps of the measurements: timestamp = 1606391700, measurement_period_base = 60, */ - /* measurement_period_factor = 1. Timestamp of the 1st sample = 1606391700, timestamp of the 2nd sample = 1606391760 */ - /* 'Binary' sensor types: */ - /* Absolute value of the 'sample_offsets' field indicates the offset in seconds from 'timestamp' field. */ - /* Sign (- or +) indicates the state of measurements depend of sensor type. */ - /* Value of this field equals to '1' or '-1' indicates the state at the 'timestamp'. Other values */ - /* indicate the state of the relay at the time (in seconds) equal to 'timestamp' + value. */ - /* Values of this field are incremented starting from 1 (1->0: state at the time */ - /* of 'timestamp', 2->1: state at the time equal to 'timestamp' + 1 s, 3->2 : */ - /* state at the time equal to 'timestamp' + 2 s, etc.). The first and the last sample define the time range of the */ - /* measurements. Only state changes in the time range are included in the 'sample_offsets' field */ - /* Examples: if 'timestamp' value is 1553518060 and 'sample_offsets' equals '1', it means that at 1553518060 the state */ - /* was high, if 'timestamp' value is 1553518060 and 'sample_offsets' equals '-9', it means at 1553518068 the state was low */ + /* 'Continuous' sensor types */ + /* Value of the offset from the 'start_point' for each measurement in the package. The oldest sample first ([0]). */ + /* 'sample_offsets' format defined by 'MeasurementType' field. */ + /* If the 'sample_offset' has a value from the range [8355840: 8388607], it should be interpreted as a sensor error code. */ + /* In that case value of the 'start_point' field should not be added to this 'sample_offset'. See ES6-264 for error codes. */ + /* Example: MeasurementType = 1 (temperature), start_point = 100, sample_offsets[0] = 15, sample_offsets[1] = 20, */ + /* sample_offset[2] = 8388605 */ + /* 1st sample in the package temperature value = 11.5 °C, 2nd sample in the package temperature value = 12 °C */ + /* 3rd sample in the package has no temperature value. It has information about failure of MCP9808 (temperature) sensor. */ + /* Calculating timestamps of the measurements: timestamp = 1606391700, measurement_period_base = 60, */ + /* measurement_period_factor = 1. Timestamp of the 1st sample = 1606391700, timestamp of the 2nd sample = 1606391760, */ + /* timestamp of the 3rd sample 1606391820 */ + + /* 'Binary' sensor types: */ + /* Absolute value of the 'sample_offsets' field indicates the offset in seconds from 'timestamp' field. */ + /* Sign (- or +) indicates the state of measurements depending on the sensor type. */ + /* Value of this field equals to '1' or '-1' indicates the state at the 'timestamp'. Other values */ + /* indicate the state of the relay at the time (in seconds) equal to 'timestamp' + absolute value -1. */ + /* Values of this field are incremented starting from 1 (1->0: state at the time */ + /* of 'timestamp', 2->1: state at the time equal to 'timestamp' + 1 s, 3->2 : */ + /* state at the time equal to 'timestamp' + 2 s, etc.). The first and the last sample define the time range of the */ + /* measurements. Only state changes in the time range are included in the 'sample_offsets' field */ + /* Examples: if 'timestamp' value is 1553518060 and 'sample_offsets' equals '1', it means that at 1553518060 the state */ + /* was high, if 'timestamp' value is 1553518060 and 'sample_offsets' equals '-9', it means at 1553518068 the state was low */ repeated sint32 sample_offsets = 5 [packed=true]; - /* Deprecated - configuration is sent to endpoint 'c' */ - //int32 lo_threshold = 6; + /* Deprecated - configuration is sent to endpoint 'c' */ + /* int32 lo_threshold = 6; */ + reserved 6; - /* Deprecated - configuration is sent to endpoint 'c' */ - //int32 hi_threshold = 7; + /* Deprecated - configuration is sent to endpoint 'c' */ + /* int32 hi_threshold = 7; */ + reserved 7; - /* Deprecated - configurationis sent to endpoint 'c' */ - //int32 diff_threshold = 8; - } + /* Deprecated - configurations sent to endpoint 'c' */ + /* int32 diff_threshold = 8; */ + reserved 8; +} message ProtoMeasurements { - /* serial number of the device */ + /* Serial number of the device */ bytes serial_num = 1; - /* true - battery ok, false - battery low */ + /* Battery status: true - battery ok, false - battery low */ bool battery_status = 2; - /* 'Measurement_period_base' and 'measurement_period_factor' define how often the measurements are taken. */ - /* Sensors of 'Continuous' type take measurement each Measurement_period_base * measurement_period_factor. */ - /* Sensors of 'Binary' type take measurement each Measurement_period_base. */ - /* For backward compatibility with versions 5.x in case of binary/mixed sensors, if the 'measurement_period_factor' is */ - /* not sent (equal to 0), then the default value '14' shall be used for period calculation. */ - /* For backward compatibility with versions 5.x in case of continues sensors, if the measurement_period_factor is */ - /* not sent (equal to 0), then the default value '1' shall be used for period calculation. */ - /* measurement period base in seconds */ + /* 'Measurement_period_base' and 'measurement_period_factor' define how often the measurements are taken. */ + /* Sensors of 'Continuous' type take measurement each Measurement_period_base * measurement_period_factor. */ + /* Sensors of 'Binary' type take measurement each Measurement_period_base. */ + /* For backward compatibility with versions 5.x in case of binary/mixed sensors, if the 'measurement_period_factor' is */ + /* not sent (equal to 0), then the default value '14' shall be used for period calculation. */ + /* For backward compatibility with versions 5.x in case of continues sensors, if the measurement_period_factor is */ + /* not sent (equal to 0), then the default value '1' shall be used for period calculation. */ + /* measurement period base in seconds */ uint32 measurement_period_base = 3; - /* Measurement period factor */ + /* Measurement period factor */ uint32 measurement_period_factor = 8; repeated ProtoChannel channels = 4; - /* Timestamp of the next scheduled transmission. If the device will not send data until this time, */ - /* it should be considered as 'lost' */ + /* Timestamp of the next scheduled transmission. If the device will not send data until this time, */ + /* it should be considered as 'lost' */ uint32 next_transmission_at = 5; - /* reason of transmission - unsigned integer where each bit indicates different */ - /* possible communication reason. Can be more than one */ - /* - bit 0: first message after sensor reset */ - /* - bit 1: user button triggered */ - /* - bit 2: user BLE triggered */ - /* - bit 3-7: number of retries -> incremented after each unsuccessful transmission. Max value 4. */ - /* Set to 0 after a successful transmission. */ - /* - bit 8: channel 1 lower threshold exceeded */ - /* - bit 9: channel 1 lower threshold returned */ - /* - bit 10: channel 1 higher threshold exceeded */ - /* - bit 11: channel 1 higher threshold returned */ - /* - bit 12: channel 1 differential threshold crossed */ - /* - bits 13-17: channel 2 thresholds (same as for channel 1) */ - /* - bits 18-22: channel 3 thresholds (same as for channel 1) */ - /* - bits 23-27: channel 4 or 5 or 6 thresholds (same as for channel 1) */ + /* Reason of transmission - unsigned integer where each bit indicates different possible communication reason. */ + /* Can be more than one: */ + /* - bit 0: first message after sensor reset */ + /* - bit 1: user button triggered */ + /* - bit 2: user BLE triggered */ + /* - bit 3-7: number of retries -> incremented after each unsuccessful transmission. Max value 31. */ + /* Set to 0 after a successful transmission. */ + /* - bit 8...19: rule 1...12 was met */ + /* - bit 20: triggered after the end of the limit */ uint32 transfer_reason = 6; - /* Signal strength level mapped from RSSI */ - /* - 0 : 113 dBm or less */ - /* - 1 : 111 dBm */ - /* - 2...30 : 109...-53 dBm */ - /* - 31 : -51 dBm or greater */ - /* - 99 : Not known or not detectable */ + /* Signal strength level mapped from RSSI: */ + /* - 0: RSSI < -110 dBm */ + /* - 1: -110 dBm <= RSSI < -109 dBm */ + /* - 2...61: -109 <= RSSI < -108 dBm ... -50 dBm <= RSSI < -49 dBm */ + /* - 62: -49 dBm <= RSSI < -48 dBm */ + /* - 63: RSSI >= -48 dBm */ + /* - 99: Not known or not detectable */ uint32 signal = 7; - /* Hash of the current configuration. Hash value changes each time a device receives a new configuration */ + /* Hash of the current configuration. Hash value changes each time a device receives a new configuration */ uint32 hash = 9; - /* Optional string up to 36 bytes long. Can be set to any user define value or hold device's IMEI */ + /* Optional string up to 36 bytes long. Can be set to any user define value or hold device's IMEI */ string cloud_token = 16; } \ No newline at end of file diff --git a/common/transport/coap/src/main/proto/efento/proto_rule.proto b/common/transport/coap/src/main/proto/efento/proto_rule.proto index 4079683a3f..12550426b1 100644 --- a/common/transport/coap/src/main/proto/efento/proto_rule.proto +++ b/common/transport/coap/src/main/proto/efento/proto_rule.proto @@ -50,11 +50,19 @@ option java_outer_classname = "ProtoRuleProtos"; /* - CO2_EQUIVALENT - [ppm] - Parts per million. Resolution 1ppm. Range [0:1000000]. Carbon dioxide equivalent. */ /* - BREATH_VOC - [ppm] - Parts per million. Resolution 1ppm. Range [0:100000]. Breath VOC estimate. */ /* - PERCENTAGE - [%] - Percentage. Resolution 0.01%. Range [0.00:100.00]. */ +/* - VOLTAGE - [mV] - Milivolt. Resolution 0.1mV. Range [0.0:100000.0]. */ +/* - CURRENT - [mA] - Miliampere. Resolution 0.01mA. Range [0.00:10000.00]. */ /* - PULSE_CNT_ACC_MINOR - [NB] - Number of pulses. Resolution 1 pulse. Range [0:1000000]. Accumulative pulse counter (minor). */ /* - PULSE_CNT_ACC_MAJOR - [kNB] - Number of kilopulses. Resolution 1 kilopulse. Range [0:1000000]. */ /* Accumulative pulse counter (major). */ /* - ELEC_METER_ACC_MINOR - [Wh] - Watt-hour. Resolution 1Wh. Range [0:1000000]. Accumulative electricity meter (minor). */ /* - ELEC_METER_ACC_MAJOR - [kWh] - Kilowatt-hour. Resolution 1kWh. Range [0:1000000]. Accumulative electricity meter (major). */ +/* - PULSE_CNT_ACC_WIDE_MINOR - [NB] - Number of pulses. Resolution 1 pulse. Range [0:999999]. */ +/* Accumulative pulse counter wide range (minor). */ +/* - PULSE_CNT_ACC_WIDE_MAJOR - [MNB] - Number of megapulses. Resolution 1 megapulse. Range [0:999999]. */ +/* Accumulative pulse counter wide range (major). */ +/* - CURRENT_PRECISE - [mA] - Miliampere. Resolution 0.001mA. Range [-4 000.000:4 000.000]. */ +/* - OUTPUT_CONTROL - Not applicable */ /* Encoding R: used to set relative values in the Rules (e.g. differential threshold and hysteresis) */ /* - TEMPERATURE - [°C] - Celsius degree. Resolution 0.1°C. Range [0.1:4273.2]. */ @@ -88,11 +96,19 @@ option java_outer_classname = "ProtoRuleProtos"; /* - CO2_EQUIVALENT - [ppm] - Parts per million. Resolution 1ppm. Range [1:1000000]. Carbon dioxide equivalent. */ /* - BREATH_VOC - [ppm] - Parts per million. Resolution 1ppm. Range [1:100000]. Breath VOC estimate. */ /* - PERCENTAGE - [%] - Percentage. Resolution 0.01%. Range [0.01:100.00]. */ +/* - VOLTAGE - [mV] - Milivolt. Resolution 0.1mV. Range [0.1:100000.0]. */ +/* - CURRENT - [mA] - Miliampere. Resolution 0.01mA. Range [0.01:10000.00]. */ /* - PULSE_CNT_ACC_MINOR - [NB] - Number of pulses. Resolution 1 pulse. Range [1:1000000]. Accumulative pulse counter (minor). */ /* - PULSE_CNT_ACC_MAJOR - [kNB] - Number of kilopulses. Resolution 1 kilopulse. Range [1:1000000]. */ /* Accumulative pulse counter (major). */ /* - ELEC_METER_ACC_MINOR - [Wh] - Watt-hour. Resolution 1Wh. Range [1:1000000]. Accumulative electricity meter (minor). */ /* - ELEC_METER_ACC_MAJOR - [kWh] - Kilowatt-hour. Resolution 1kWh. Range [1:1000000]. Accumulative electricity meter (major). */ +/* - PULSE_CNT_ACC_WIDE_MINOR - [NB] - Number of pulses. Resolution 1 pulse. Range [1:999999]. */ +/* Accumulative pulse counter wide range (minor). */ +/* - PULSE_CNT_ACC_WIDE_MAJOR - [MNB] - Number of megapulses. Resolution 1 megapulse. Range [1:999999]. */ +/* Accumulative pulse counter wide range (major). */ +/* - CURRENT_PRECISE - [mA] - Miliampere. Resolution 0.001mA. Range [0.001:8 000.000]. */ +/* - OUTPUT_CONTROL - Not applicable */ /* Condition to be checked by the device. If the condition is true, an action is triggered */ enum Condition { @@ -202,6 +218,10 @@ enum Action { /* To trigger the transmission with ACK */ ACTION_TRIGGER_TRANSMISSION_WITH_ACK = 3; + + /* To change BLE advertising period mode to fast (with lower user-configured advertising interval). */ + /* Once the rule is deactived avertising period mode returns to previously configured value. */ + ACTION_FAST_ADVERTISING_MODE = 4; } /* Type of a rule calendars. */ From 8119e26369fccaaea96e0a224209e14cffc73746 Mon Sep 17 00:00:00 2001 From: mpetrov Date: Fri, 26 Jul 2024 19:10:08 +0300 Subject: [PATCH 05/48] Added Connector tables Row Action buttons descriptions tooltips --- .../mapping-table.component.html | 18 +++++------ .../modbus-master-table.component.html | 32 +++++++++---------- .../modbus-master-table.component.ts | 2 +- 3 files changed, 26 insertions(+), 26 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-table/mapping-table.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-table/mapping-table.component.html index 9f0566b566..76e7294403 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-table/mapping-table.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-table/mapping-table.component.html @@ -75,15 +75,22 @@ -
+ + +
+
- - +
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-master-table/modbus-master-table.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-master-table/modbus-master-table.component.html index e16aa0c51f..29e2fb6ccf 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-master-table/modbus-master-table.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-master-table/modbus-master-table.component.html @@ -63,33 +63,40 @@ {{ 'gateway.name' | translate }} - - {{ mapping['name'] }} + + {{ slave['name'] }} {{ 'gateway.client-communication-type' | translate }} - - {{ ModbusProtocolLabelsMap.get(mapping['type']) }} + + {{ ModbusProtocolLabelsMap.get(slave['type']) }} - -
+ + +
+
- - +
- +
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-master-table/modbus-master-table.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-master-table/modbus-master-table.component.ts index 2cb66f6062..915de21c32 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-master-table/modbus-master-table.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-master-table/modbus-master-table.component.ts @@ -185,7 +185,7 @@ export class ModbusMasterTableComponent implements ControlValueAccessor, Validat }); } - deleteMapping($event: Event, index: number): void { + deleteSlave($event: Event, index: number): void { if ($event) { $event.stopPropagation(); } From fe857b34b5f931ec1ed81b04c21440fb2a0b77bf Mon Sep 17 00:00:00 2001 From: mpetrov Date: Mon, 29 Jul 2024 12:01:40 +0300 Subject: [PATCH 06/48] Fixed Modbus Slave Config Values visibility --- .../modbus/modbus-values/modbus-values.component.scss | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-values/modbus-values.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-values/modbus-values.component.scss index 2d0782e6a5..e80998b88a 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-values/modbus-values.component.scss +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-values/modbus-values.component.scss @@ -13,10 +13,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -:host { - .mat-mdc-tab-body-wrapper { - min-height: 320px; - } + +:host ::ng-deep .mat-mdc-tab-body-wrapper { + min-height: 320px; } ::ng-deep .mdc-evolution-chip-set__chips { From d283db3bb5e824878056b796e7eddd2afa06f7d0 Mon Sep 17 00:00:00 2001 From: mpetrov Date: Mon, 29 Jul 2024 17:37:57 +0300 Subject: [PATCH 07/48] Updated Form for Mobdus RPC templates --- .../modbus-data-keys-panel.component.html | 2 +- .../modbus-data-keys-panel.component.ts | 5 +- .../modbus-rpc-parameters.component.html | 75 ++++++++ .../modbus-rpc-parameters.component.ts | 166 ++++++++++++++++++ ...teway-service-rpc-connector.component.html | 40 ----- ...gateway-service-rpc-connector.component.ts | 27 +-- .../gateway-service-rpc.component.html | 28 ++- .../gateway-service-rpc.component.scss | 4 + .../gateway/gateway-service-rpc.component.ts | 4 + .../lib/gateway/gateway-widget.models.ts | 53 ++---- .../widget/widget-components.module.ts | 4 + .../assets/locale/locale.constant-en_US.json | 22 ++- 12 files changed, 305 insertions(+), 125 deletions(-) create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-rpc-parameters/modbus-rpc-parameters.component.html create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-rpc-parameters/modbus-rpc-parameters.component.ts diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-data-keys-panel/modbus-data-keys-panel.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-data-keys-panel/modbus-data-keys-panel.component.html index 02d2d1491d..238c6d92af 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-data-keys-panel/modbus-data-keys-panel.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-data-keys-panel/modbus-data-keys-panel.component.html @@ -98,7 +98,7 @@ name="value" formControlName="objectsCount" placeholder="{{ 'gateway.set' | translate }}" - [readonly]="!editableDataTypes.includes(keyControl.get('type').value)" + [readonly]="!ModbusEditableDataTypes.includes(keyControl.get('type').value)" />
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-data-keys-panel/modbus-data-keys-panel.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-data-keys-panel/modbus-data-keys-panel.component.ts index 5b312f52c8..d645b4f30b 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-data-keys-panel/modbus-data-keys-panel.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-data-keys-panel/modbus-data-keys-panel.component.ts @@ -27,6 +27,7 @@ import { import { TbPopoverComponent } from '@shared/components/popover.component'; import { ModbusDataType, + ModbusEditableDataTypes, ModbusFunctionCodeTranslationsMap, ModbusObjectCountByDataType, ModbusValue, @@ -72,7 +73,7 @@ export class ModbusDataKeysPanelComponent implements OnInit, OnDestroy { functionCodesMap = new Map(); defaultFunctionCodes = []; - readonly editableDataTypes = [ModbusDataType.BYTES, ModbusDataType.BITS, ModbusDataType.STRING]; + readonly ModbusEditableDataTypes = ModbusEditableDataTypes; readonly ModbusFunctionCodeTranslationsMap = ModbusFunctionCodeTranslationsMap; private destroy$ = new Subject(); @@ -161,7 +162,7 @@ export class ModbusDataKeysPanelComponent implements OnInit, OnDestroy { private observeKeyDataType(keyFormGroup: FormGroup): void { keyFormGroup.get('type').valueChanges.pipe(takeUntil(this.destroy$)).subscribe(dataType => { - if (!this.editableDataTypes.includes(dataType)) { + if (!this.ModbusEditableDataTypes.includes(dataType)) { keyFormGroup.get('objectsCount').patchValue(ModbusObjectCountByDataType[dataType], {emitEvent: false}); } this.updateFunctionCodes(keyFormGroup, dataType); diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-rpc-parameters/modbus-rpc-parameters.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-rpc-parameters/modbus-rpc-parameters.component.html new file mode 100644 index 0000000000..426121c223 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-rpc-parameters/modbus-rpc-parameters.component.html @@ -0,0 +1,75 @@ + +
+ + {{ 'gateway.key' | translate }} + + + warning + + +
+
+ + {{ 'gateway.rpc.type' | translate }} + + {{ type }} + + + + {{ 'gateway.rpc.functionCode' | translate }} + + {{ ModbusFunctionCodeTranslationsMap.get(code) | translate}} + + +
+
+ + {{ 'gateway.rpc.value' | translate }} + + + warning + + +
+
+ + {{ 'gateway.rpc.address' | translate }} + + + warning + + + + {{ 'gateway.rpc.objectsCount' | translate }} + + +
+
+ diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-rpc-parameters/modbus-rpc-parameters.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-rpc-parameters/modbus-rpc-parameters.component.ts new file mode 100644 index 0000000000..17c48cc595 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-rpc-parameters/modbus-rpc-parameters.component.ts @@ -0,0 +1,166 @@ +/// +/// 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. +/// + +import { + ChangeDetectionStrategy, + Component, + forwardRef, + OnDestroy, +} from '@angular/core'; +import { + ControlValueAccessor, + FormBuilder, + NG_VALIDATORS, + NG_VALUE_ACCESSOR, + UntypedFormGroup, + ValidationErrors, + Validator, + Validators +} from '@angular/forms'; +import { SharedModule } from '@shared/shared.module'; +import { CommonModule } from '@angular/common'; +import { Subject } from 'rxjs'; +import { takeUntil } from 'rxjs/operators'; +import { + ModbusDataType, + ModbusEditableDataTypes, + ModbusFunctionCodeTranslationsMap, + ModbusObjectCountByDataType, + ModbusValue, + noLeadTrailSpacesRegex, +} from '@home/components/widget/lib/gateway/gateway-widget.models'; + +@Component({ + selector: 'tb-modbus-rpc-parameters', + templateUrl: './modbus-rpc-parameters.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => ModbusRpcParametersComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => ModbusRpcParametersComponent), + multi: true + } + ], + standalone: true, + imports: [ + CommonModule, + SharedModule, + ], +}) +export class ModbusRpcParametersComponent implements ControlValueAccessor, Validator, OnDestroy { + + rpcParametersFormGroup: UntypedFormGroup; + functionCodes: Array; + + readonly ModbusEditableDataTypes = ModbusEditableDataTypes; + readonly ModbusFunctionCodeTranslationsMap = ModbusFunctionCodeTranslationsMap; + + readonly modbusDataTypes = Object.values(ModbusDataType) as ModbusDataType[]; + readonly writeFunctionCodes = [5, 6, 15, 16]; + + private readonly defaultFunctionCodes = [3, 4, 6, 16]; + private readonly readFunctionCodes = [1, 2, 3, 4]; + private readonly bitsFunctionCodes = [...this.readFunctionCodes, ...this.writeFunctionCodes]; + + private onChange: (value: ModbusValue) => void; + private onTouched: () => void; + + private destroy$ = new Subject(); + + constructor(private fb: FormBuilder) { + this.rpcParametersFormGroup = this.fb.group({ + tag: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]], + type: [ModbusDataType.BYTES, [Validators.required]], + functionCode: [this.defaultFunctionCodes[0], [Validators.required]], + value: [{value: '', disabled: true}, [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]], + address: [null, [Validators.required]], + objectsCount: [1, [Validators.required]], + }); + + this.updateFunctionCodes(this.rpcParametersFormGroup.get('type').value); + this.observeValueChanges(); + this.observeKeyDataType(); + this.observeFunctionCode(); + } + + ngOnDestroy(): void { + this.destroy$.next(); + this.destroy$.complete(); + } + + registerOnChange(fn: (value: ModbusValue) => void): void { + this.onChange = fn; + } + + registerOnTouched(fn: () => void): void { + this.onTouched = fn; + } + + validate(): ValidationErrors | null { + return this.rpcParametersFormGroup.valid ? null : { + rpcParametersFormGroup: { valid: false } + }; + } + + writeValue(value: ModbusValue): void { + this.rpcParametersFormGroup.patchValue(value, {emitEvent: false}); + } + + private observeValueChanges(): void { + this.rpcParametersFormGroup.valueChanges.pipe( + takeUntil(this.destroy$) + ).subscribe((value) => { + this.onChange(value); + this.onTouched(); + }); + } + + private observeKeyDataType(): void { + this.rpcParametersFormGroup.get('type').valueChanges.pipe(takeUntil(this.destroy$)).subscribe(dataType => { + if (!this.ModbusEditableDataTypes.includes(dataType)) { + this.rpcParametersFormGroup.get('objectsCount').patchValue(ModbusObjectCountByDataType[dataType], {emitEvent: false}); + } + this.updateFunctionCodes(dataType); + }); + } + + private observeFunctionCode(): void { + this.rpcParametersFormGroup.get('functionCode').valueChanges + .pipe(takeUntil(this.destroy$)) + .subscribe(code => this.updateValueEnabling(code)); + } + + private updateValueEnabling(code: number): void { + if (this.writeFunctionCodes.includes(code)) { + this.rpcParametersFormGroup.get('value').enable({emitEvent: false}); + } else { + this.rpcParametersFormGroup.get('value').setValue(null); + this.rpcParametersFormGroup.get('value').disable({emitEvent: false}); + } + } + + private updateFunctionCodes(dataType: ModbusDataType): void { + this.functionCodes = dataType === ModbusDataType.BITS ? this.bitsFunctionCodes : this.defaultFunctionCodes; + if (!this.functionCodes.includes(this.rpcParametersFormGroup.get('functionCode').value)) { + this.rpcParametersFormGroup.get('functionCode').patchValue(this.functionCodes[0], {emitEvent: false}); + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-service-rpc-connector.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-service-rpc-connector.component.html index eb428bebdc..3e94ae96d7 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-service-rpc-connector.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-service-rpc-connector.component.html @@ -50,46 +50,6 @@ placeholder="${params}"/> - - - {{ 'gateway.rpc.tag' | translate }} - - -
- - {{ 'gateway.rpc.type' | translate }} - - - {{ type }} - - - - - {{ 'gateway.rpc.functionCode' | translate }} - - - {{ modbusCodesTranslate.get(code) | translate}} - - - -
- - {{ 'gateway.rpc.value' | translate }} - - -
- - {{ 'gateway.rpc.address' | translate }} - - - - {{ 'gateway.rpc.objectsCount' | translate }} - - -
-
{{ 'gateway.rpc.methodRPC' | translate }} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-service-rpc-connector.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-service-rpc-connector.component.ts index 3027cafa8d..989f9daeed 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-service-rpc-connector.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-service-rpc-connector.component.ts @@ -35,8 +35,6 @@ import { ConnectorType, GatewayConnectorDefaultTypesTranslatesMap, HTTPMethods, - ModbusCodesTranslate, - ModbusCommandTypes, noLeadTrailSpacesRegex, RPCCommand, RPCTemplateConfig, @@ -53,7 +51,7 @@ import { } from '@shared/components/dialog/json-object-edit-dialog.component'; import { jsonRequired } from '@shared/components/json-object-edit.component'; import { deepClone } from '@core/utils'; -import { filter, takeUntil, tap } from "rxjs/operators"; +import { takeUntil, tap } from "rxjs/operators"; import { Subject } from "rxjs"; @Component({ @@ -80,9 +78,7 @@ export class GatewayServiceRPCConnectorComponent implements OnInit, OnDestroy, C saveTemplate: EventEmitter = new EventEmitter(); commandForm: FormGroup; - codesArray: Array = [1, 2, 3, 4, 5, 6, 15, 16]; ConnectorType = ConnectorType; - modbusCommandTypes = Object.values(ModbusCommandTypes) as ModbusCommandTypes[]; bACnetRequestTypes = Object.values(BACnetRequestTypes) as BACnetRequestTypes[]; bACnetObjectTypes = Object.values(BACnetObjectTypes) as BACnetObjectTypes[]; bLEMethods = Object.values(BLEMethods) as BLEMethods[]; @@ -98,7 +94,6 @@ export class GatewayServiceRPCConnectorComponent implements OnInit, OnDestroy, C SocketMethodProcessingsTranslates = SocketMethodProcessingsTranslates; SNMPMethodsTranslations = SNMPMethodsTranslations; gatewayConnectorDefaultTypesTranslates = GatewayConnectorDefaultTypesTranslatesMap; - modbusCodesTranslate = ModbusCodesTranslate; urlPattern = /^[-a-zA-Zd_$:{}?~+=\/.0-9-]*$/; numbersOnlyPattern = /^[0-9]*$/; @@ -156,26 +151,6 @@ export class GatewayServiceRPCConnectorComponent implements OnInit, OnDestroy, C withResponse: [false, []], }); break; - case ConnectorType.MODBUS: - formGroup = this.fb.group({ - tag: [null, [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]], - type: [null, [Validators.required]], - functionCode: [null, [Validators.required]], - value: [null, []], - address: [null, [Validators.required, Validators.min(0), Validators.pattern(this.numbersOnlyPattern)]], - objectsCount: [null, [Validators.required, Validators.min(0), Validators.pattern(this.numbersOnlyPattern)]] - }) - const valueForm = formGroup.get('value'); - formGroup.get('functionCode').valueChanges.subscribe(value => { - if (value > 4) { - valueForm.addValidators([Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]); - } else { - valueForm.clearValidators(); - valueForm.setValue(null); - } - valueForm.updateValueAndValidity(); - }) - break; case ConnectorType.BACNET: formGroup = this.fb.group({ method: [null, [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]], diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-service-rpc.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-service-rpc.component.html index 8368698087..41dea0b656 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-service-rpc.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-service-rpc.component.html @@ -41,8 +41,32 @@
- + + +
+
{{ 'gateway.rpc.title' | translate: {type: gatewayConnectorDefaultTypesTranslates.get(connectorType)} }}
+ +
+ + +
+
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-service-rpc.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-service-rpc.component.scss index f74097bbba..b8424dc068 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-service-rpc.component.scss +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-service-rpc.component.scss @@ -40,6 +40,10 @@ } } + .rpc-parameters { + width: 100%; + } + .result-block { padding: 0 5px; display: flex; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-service-rpc.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-service-rpc.component.ts index 1f3327ec06..d6b20d0137 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-service-rpc.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-service-rpc.component.ts @@ -22,6 +22,7 @@ import { ContentType } from '@shared/models/constants'; import { jsonRequired } from '@shared/components/json-object-edit.component'; import { ConnectorType, + GatewayConnectorDefaultTypesTranslatesMap, RPCCommand, RPCTemplate, RPCTemplateConfig, @@ -71,6 +72,9 @@ export class GatewayServiceRPCComponent implements OnInit { public connectorType: ConnectorType; public templates: Array = []; + readonly ConnectorType = ConnectorType; + readonly gatewayConnectorDefaultTypesTranslates = GatewayConnectorDefaultTypesTranslatesMap; + private subscription: IWidgetSubscription; private subscriptionOptions: WidgetSubscriptionOptions = { callbacks: { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-widget.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-widget.models.ts index 8df3994ea2..0cafb185f4 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-widget.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-widget.models.ts @@ -311,35 +311,15 @@ export interface RPCCommand { time: number; } - -export enum ModbusCommandTypes { - Bits = 'bits', - Bit = 'bit', - // eslint-disable-next-line id-blacklist - String = 'string', - Bytes = 'bytes', - Int8 = '8int', - Uint8 = '8uint', - Int16 = '16int', - Uint16 = '16uint', - Float16 = '16float', - Int32 = '32int', - Uint32 = '32uint', - Float32 = '32float', - Int64 = '64int', - Uint64 = '64uint', - Float64 = '64float' -} - -export const ModbusCodesTranslate = new Map([ - [1, 'gateway.rpc.read-coils'], - [2, 'gateway.rpc.read-discrete-inputs'], - [3, 'gateway.rpc.read-multiple-holding-registers'], - [4, 'gateway.rpc.read-input-registers'], - [5, 'gateway.rpc.write-single-coil'], - [6, 'gateway.rpc.write-single-holding-register'], - [15, 'gateway.rpc.write-multiple-coils'], - [16, 'gateway.rpc.write-multiple-holding-registers'] +export const ModbusFunctionCodeTranslationsMap = new Map([ + [1, 'gateway.function-codes.read-coils'], + [2, 'gateway.function-codes.read-discrete-inputs'], + [3, 'gateway.function-codes.read-multiple-holding-registers'], + [4, 'gateway.function-codes.read-input-registers'], + [5, 'gateway.function-codes.write-single-coil'], + [6, 'gateway.function-codes.write-single-holding-register'], + [15, 'gateway.function-codes.write-multiple-coils'], + [16, 'gateway.function-codes.write-multiple-holding-registers'] ]); export enum BACnetRequestTypes { @@ -862,6 +842,8 @@ export enum ModbusDataType { FLOAT64 = '64float' } +export const ModbusEditableDataTypes = [ModbusDataType.BYTES, ModbusDataType.BITS, ModbusDataType.STRING]; + export enum ModbusObjectCountByDataType { '8int' = 1, '8uint' = 1, @@ -920,19 +902,6 @@ export const ModbusKeysNoKeysTextTranslationsMap = new Map( - [ - [1, 'gateway.read-coils'], - [2, 'gateway.read-discrete-inputs'], - [3, 'gateway.read-multiple-holding-registers'], - [4, 'gateway.read-input-registers'], - [5, 'gateway.write-coil'], - [6, 'gateway.write-register'], - [15, 'gateway.write-coils'], - [16, 'gateway.write-registers'], - ] -); - export interface ModbusMasterConfig { slaves: SlaveConfig[]; } diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts b/ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts index 51d82dece9..db6b7543a4 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts +++ b/ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts @@ -141,6 +141,9 @@ import { import { TypeValuePanelComponent } from '@home/components/widget/lib/gateway/connectors-configuration/type-value-panel/type-value-panel.component'; +import { + ModbusRpcParametersComponent +} from '@home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-rpc-parameters/modbus-rpc-parameters.component'; @NgModule({ declarations: [ @@ -227,6 +230,7 @@ import { KeyValueIsNotEmptyPipe, ModbusBasicConfigComponent, EllipsisChipListDirective, + ModbusRpcParametersComponent, ], exports: [ EntitiesTableWidgetComponent, diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index 14eb272910..e99c652721 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -2914,6 +2914,16 @@ "from-device-request-settings": "Input request parsing", "from-device-request-settings-hint": "These fields support JSONPath expressions to extract a name from incoming message.", "function-code": "Function code", + "function-codes": { + "read-coils": "01 - Read Coils", + "read-discrete-inputs": "02 - Read Discrete Inputs", + "read-multiple-holding-registers": "03 - Read Multiple Holding Registers", + "read-input-registers": "04 - Read Input Registers", + "write-single-coil": "05 - Write Single Coil", + "write-single-holding-register": "06 - Write Single Holding Register", + "write-multiple-coils": "15 - Write Multiple Coils", + "write-multiple-holding-registers": "16 - Write Multiple Holding Registers" + }, "to-device-response-settings": "Output request processing", "to-device-response-settings-hint": "For these fields you can use the following variables and they will be replaced with actual values: ${deviceName}, ${attributeKey}, ${attributeValue}", "gateway": "Gateway", @@ -3120,14 +3130,6 @@ "template-name-duplicate": "Template with such name already exists, it will be updated.", "command": "Command", "params": "Params", - "read-coils": "01: Read Coils", - "read-discrete-inputs": "02: Read Discrete Inputs", - "read-multiple-holding-registers": "03: Read Multiple Holding Registers", - "read-input-registers": "04: Read Input Registers", - "write-single-coil": "05: Write Single Coil", - "write-single-holding-register": "06: Write Single Holding Register", - "write-multiple-coils": "15: Write Multiple Coils", - "write-multiple-holding-registers": "16: Write Multiple Holding Registers", "json-value-invalid": "JSON value has an invalid format" }, "rpc-methods": "RPC methods", @@ -3317,10 +3319,6 @@ "username": "Username", "username-required": "Username is required.", "unit-id-required": "Unit ID is required.", - "read-coils": "Read Coils", - "read-discrete-inputs": "Read Discrete Inputs", - "read-multiple-holding-registers": "Read Multiple Holding Register", - "read-input-registers": "Read Input Registers", "write-coil": "Write Coil", "write-coils": "Write Coils", "write-register": "Write Register", From 8ca608cdb90c01ab2457604f483e5568272b3be3 Mon Sep 17 00:00:00 2001 From: mpetrov Date: Mon, 29 Jul 2024 17:43:34 +0300 Subject: [PATCH 08/48] license --- .../modbus-rpc-parameters.component.html | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-rpc-parameters/modbus-rpc-parameters.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-rpc-parameters/modbus-rpc-parameters.component.html index 426121c223..c2dead1abc 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-rpc-parameters/modbus-rpc-parameters.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-rpc-parameters/modbus-rpc-parameters.component.html @@ -1,3 +1,20 @@ +
From 635f48d3434f93acdb523025dd2ac92dffb41e3e Mon Sep 17 00:00:00 2001 From: mpetrov Date: Tue, 30 Jul 2024 12:10:27 +0300 Subject: [PATCH 09/48] Reworked validation of Mosbus tabs --- .../modbus-basic-config.component.ts | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-basic-config/modbus-basic-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-basic-config/modbus-basic-config.component.ts index 1fca768980..bfd085a4c4 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-basic-config/modbus-basic-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-basic-config/modbus-basic-config.component.ts @@ -24,7 +24,7 @@ import { ValidationErrors, Validator, } from '@angular/forms'; -import { ConnectorType, ModbusBasicConfig } from '@home/components/widget/lib/gateway/gateway-widget.models'; +import { ModbusBasicConfig } from '@home/components/widget/lib/gateway/gateway-widget.models'; import { SharedModule } from '@shared/shared.module'; import { CommonModule } from '@angular/common'; import { takeUntil } from 'rxjs/operators'; @@ -109,8 +109,14 @@ export class ModbusBasicConfigComponent implements ControlValueAccessor, Validat } validate(): ValidationErrors | null { - return this.basicFormGroup.valid ? null : { - basicFormGroup: {valid: false} - }; + const masterHasSlaves = !!this.basicFormGroup.get('master').value.slaves?.length; + const slaveEnabled = this.basicFormGroup.get('slave').value.sendDataToThingsBoard; + const slaveIsValid = this.basicFormGroup.get('slave').valid; + + if ((slaveEnabled && slaveIsValid) || (masterHasSlaves && !slaveEnabled)) { + return null; + } + + return { basicFormGroup: { valid: false } }; } } From a4ee0f6d4c9c60f3b6e3b5de69cb4addb7697378 Mon Sep 17 00:00:00 2001 From: mpetrov Date: Tue, 30 Jul 2024 12:59:18 +0300 Subject: [PATCH 10/48] refactoring --- .../modbus-basic-config/modbus-basic-config.component.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-basic-config/modbus-basic-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-basic-config/modbus-basic-config.component.ts index bfd085a4c4..61e5677e9f 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-basic-config/modbus-basic-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-basic-config/modbus-basic-config.component.ts @@ -21,6 +21,7 @@ import { FormGroup, NG_VALIDATORS, NG_VALUE_ACCESSOR, + UntypedFormControl, ValidationErrors, Validator, } from '@angular/forms'; @@ -108,9 +109,9 @@ export class ModbusBasicConfigComponent implements ControlValueAccessor, Validat this.basicFormGroup.setValue(editedBase, {emitEvent: false}); } - validate(): ValidationErrors | null { - const masterHasSlaves = !!this.basicFormGroup.get('master').value.slaves?.length; - const slaveEnabled = this.basicFormGroup.get('slave').value.sendDataToThingsBoard; + validate(basicConfigControl: UntypedFormControl): ValidationErrors | null { + const masterHasSlaves = !!basicConfigControl.value.master?.slaves?.length; + const slaveEnabled = basicConfigControl.value.slave?.sendDataToThingsBoard; const slaveIsValid = this.basicFormGroup.get('slave').valid; if ((slaveEnabled && slaveIsValid) || (masterHasSlaves && !slaveEnabled)) { From 322e90610d388190ab3c27275457867abbfc8cb5 Mon Sep 17 00:00:00 2001 From: mpetrov Date: Tue, 30 Jul 2024 15:46:47 +0300 Subject: [PATCH 11/48] Fixed saving validation on creating empty OPC connector --- .../broker-config-control.component.ts | 13 ++++---- .../opc-server-config.component.ts | 31 ++++++++++++------- .../lib/gateway/gateway-widget.models.ts | 1 + 3 files changed, 28 insertions(+), 17 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/broker-config-control/broker-config-control.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/broker-config-control/broker-config-control.component.ts index f7d37b167e..d4bfdd4694 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/broker-config-control/broker-config-control.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/broker-config-control/broker-config-control.component.ts @@ -109,12 +109,13 @@ export class BrokerConfigControlComponent implements ControlValueAccessor, Valid } writeValue(brokerConfig: BrokerConfig): void { - const brokerConfigState = { - ...brokerConfig, - version: brokerConfig.version || 5, - clientId: brokerConfig.clientId || 'tb_gw_' + generateSecret(5), - }; - this.brokerConfigFormGroup.reset(brokerConfigState, {emitEvent: false}); + const { + version = 5, + clientId = `tb_gw_${generateSecret(5)}`, + security = {}, + } = brokerConfig; + + this.brokerConfigFormGroup.reset({ ...brokerConfig, version, clientId, security }, { emitEvent: false }); this.cdr.markForCheck(); } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/opc-server-config/opc-server-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/opc-server-config/opc-server-config.component.ts index eddad255a3..ff50480b92 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/opc-server-config/opc-server-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/opc-server-config/opc-server-config.component.ts @@ -85,7 +85,7 @@ export class OpcServerConfigComponent implements ControlValueAccessor, Validator subCheckPeriodInMillis: [10, [Validators.required, Validators.min(10)]], showMap: [false, []], security: [SecurityPolicy.BASIC128, []], - identity: [{}, [Validators.required]] + identity: [] }); this.serverConfigFormGroup.valueChanges.pipe( @@ -116,16 +116,25 @@ export class OpcServerConfigComponent implements ControlValueAccessor, Validator } writeValue(serverConfig: ServerConfig): void { - const { timeoutInMillis, scanPeriodInMillis, enableSubscriptions, subCheckPeriodInMillis, showMap, security } = serverConfig; - const serverConfigState = { + const { + timeoutInMillis = 1000, + scanPeriodInMillis = 1000, + enableSubscriptions = true, + subCheckPeriodInMillis = 10, + showMap = false, + security = SecurityPolicy.BASIC128, + identity = {}, + } = serverConfig; + + this.serverConfigFormGroup.reset({ ...serverConfig, - timeoutInMillis: timeoutInMillis || 1000, - scanPeriodInMillis: scanPeriodInMillis || 1000, - enableSubscriptions: enableSubscriptions || true, - subCheckPeriodInMillis: subCheckPeriodInMillis || 10, - showMap: showMap || false, - security: security || SecurityPolicy.BASIC128, - }; - this.serverConfigFormGroup.reset(serverConfigState, {emitEvent: false}); + timeoutInMillis, + scanPeriodInMillis, + enableSubscriptions, + subCheckPeriodInMillis, + showMap, + security, + identity, + }, { emitEvent: false }); } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-widget.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-widget.models.ts index 8df3994ea2..18e4d31496 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-widget.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-widget.models.ts @@ -170,6 +170,7 @@ export interface ConnectorSecurity { pathToCACert?: string; pathToPrivateKey?: string; pathToClientCert?: string; + mode?: ModeType; } export type ConnectorMapping = DeviceConnectorMapping | RequestMappingData | ConverterConnectorMapping; From 1dddce5171055222203e08b9fd0885e70d9be365 Mon Sep 17 00:00:00 2001 From: mpetrov Date: Tue, 30 Jul 2024 17:45:42 +0300 Subject: [PATCH 12/48] Changed approach of enable button --- .../modbus-basic-config.component.html | 10 +++++ .../modbus-basic-config.component.scss | 6 --- .../modbus-basic-config.component.ts | 22 ++++++++++- .../modbus-master-table.component.ts | 6 +-- .../modbus-slave-config.component.html | 17 ++++----- .../modbus-slave-config.component.scss | 27 -------------- .../modbus-slave-config.component.ts | 37 ++++++++----------- 7 files changed, 55 insertions(+), 70 deletions(-) delete mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-config/modbus-slave-config.component.scss diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-basic-config/modbus-basic-config.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-basic-config/modbus-basic-config.component.html index 17f5185bc6..105a3c0e8e 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-basic-config/modbus-basic-config.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-basic-config/modbus-basic-config.component.html @@ -23,6 +23,16 @@ +
+
{{ 'gateway.hints.modbus-server' | translate }}
+
+ + + {{ 'gateway.enable' | translate }} + + +
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-basic-config/modbus-basic-config.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-basic-config/modbus-basic-config.component.scss index b70fe42401..3b7e7288c8 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-basic-config/modbus-basic-config.component.scss +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-basic-config/modbus-basic-config.component.scss @@ -16,9 +16,3 @@ :host { height: 100%; } - -:host ::ng-deep { - .mat-mdc-tab-body-content { - overflow: hidden !important; - } -} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-basic-config/modbus-basic-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-basic-config/modbus-basic-config.component.ts index 61e5677e9f..d5465f7b5c 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-basic-config/modbus-basic-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-basic-config/modbus-basic-config.component.ts @@ -18,6 +18,7 @@ import { ChangeDetectionStrategy, Component, forwardRef, Input, OnDestroy, Templ import { ControlValueAccessor, FormBuilder, + FormControl, FormGroup, NG_VALIDATORS, NG_VALUE_ACCESSOR, @@ -34,6 +35,7 @@ import { Subject } from 'rxjs'; import { EllipsisChipListDirective } from '@shared/directives/ellipsis-chip-list.directive'; import { ModbusSlaveConfigComponent } from '../modbus-slave-config/modbus-slave-config.component'; import { ModbusMasterTableComponent } from '../modbus-master-table/modbus-master-table.component'; +import { isEqual } from '@core/utils'; @Component({ selector: 'tb-modbus-basic-config', @@ -67,6 +69,7 @@ export class ModbusBasicConfigComponent implements ControlValueAccessor, Validat @Input() generalTabContent: TemplateRef; basicFormGroup: FormGroup; + enableSlaveControl: FormControl; onChange: (value: ModbusBasicConfig) => void; onTouched: () => void; @@ -78,6 +81,7 @@ export class ModbusBasicConfigComponent implements ControlValueAccessor, Validat master: [], slave: [], }); + this.enableSlaveControl = new FormControl(false); this.basicFormGroup.valueChanges .pipe(takeUntil(this.destroy$)) @@ -85,6 +89,13 @@ export class ModbusBasicConfigComponent implements ControlValueAccessor, Validat this.onChange(value); this.onTouched(); }); + + this.enableSlaveControl.valueChanges + .pipe(takeUntil(this.destroy$)) + .subscribe(enable => { + this.updateSlaveEnabling(enable); + this.basicFormGroup.get('slave').updateValueAndValidity(); + }); } ngOnDestroy(): void { @@ -107,11 +118,12 @@ export class ModbusBasicConfigComponent implements ControlValueAccessor, Validat }; this.basicFormGroup.setValue(editedBase, {emitEvent: false}); + this.enableSlaveControl.setValue(!!basicConfig.slave && !isEqual(basicConfig.slave, {})); } validate(basicConfigControl: UntypedFormControl): ValidationErrors | null { const masterHasSlaves = !!basicConfigControl.value.master?.slaves?.length; - const slaveEnabled = basicConfigControl.value.slave?.sendDataToThingsBoard; + const slaveEnabled = this.enableSlaveControl.value; const slaveIsValid = this.basicFormGroup.get('slave').valid; if ((slaveEnabled && slaveIsValid) || (masterHasSlaves && !slaveEnabled)) { @@ -120,4 +132,12 @@ export class ModbusBasicConfigComponent implements ControlValueAccessor, Validat return { basicFormGroup: { valid: false } }; } + + private updateSlaveEnabling(isEnabled: boolean): void { + if (isEnabled) { + this.basicFormGroup.get('slave').enable({emitEvent: false}); + } else { + this.basicFormGroup.get('slave').disable({emitEvent: false}); + } + } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-master-table/modbus-master-table.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-master-table/modbus-master-table.component.ts index 2cb66f6062..6a36f9a829 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-master-table/modbus-master-table.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-master-table/modbus-master-table.component.ts @@ -45,7 +45,7 @@ import { ModbusProtocolLabelsMap, SlaveConfig } from '@home/components/widget/lib/gateway/gateway-widget.models'; -import { isDefinedAndNotNull, isUndefinedOrNull } from '@core/utils'; +import { isDefinedAndNotNull } from '@core/utils'; import { SharedModule } from '@shared/shared.module'; import { CommonModule } from '@angular/common'; import { ModbusSlaveDialogComponent } from '../modbus-slave-dialog/modbus-slave-dialog.component'; @@ -139,9 +139,7 @@ export class ModbusMasterTableComponent implements ControlValueAccessor, Validat } validate(): ValidationErrors | null { - return this.slaves.controls.length ? null : { - slavesFormGroup: {valid: false} - }; + return null; } enterFilterMode(): void { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-config/modbus-slave-config.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-config/modbus-slave-config.component.html index d37c8cc9ee..a46dd68edc 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-config/modbus-slave-config.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-config/modbus-slave-config.component.html @@ -16,16 +16,6 @@ -->
-
-
{{ 'gateway.hints.modbus-server' | translate }}
-
- - - {{ 'gateway.enable' | translate }} - - -
-
gateway.server-slave-config
@@ -178,6 +168,13 @@
+
+ + + {{ 'gateway.send-data-TB' | translate }} + + +
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-config/modbus-slave-config.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-config/modbus-slave-config.component.scss deleted file mode 100644 index a464832202..0000000000 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-config/modbus-slave-config.component.scss +++ /dev/null @@ -1,27 +0,0 @@ -/** - * 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. - */ -$server-config-header-height: 132px; - -:host { - .slave-content { - height: calc(100% - #{$server-config-header-height}); - overflow: auto; - } - - .slave-container { - display: inherit; - } -} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-config/modbus-slave-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-config/modbus-slave-config.component.ts index 2a8488a2d4..46771c6937 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-config/modbus-slave-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-config/modbus-slave-config.component.ts @@ -43,7 +43,7 @@ import { import { SharedModule } from '@shared/shared.module'; import { CommonModule } from '@angular/common'; import { Subject } from 'rxjs'; -import { startWith, takeUntil } from 'rxjs/operators'; +import { takeUntil } from 'rxjs/operators'; import { GatewayPortTooltipPipe } from '@home/components/widget/lib/gateway/pipes/gateway-port-tooltip.pipe'; import { ModbusSecurityConfigComponent } from '../modbus-security-config/modbus-security-config.component'; import { ModbusValuesComponent, } from '../modbus-values/modbus-values.component'; @@ -73,7 +73,6 @@ import { isEqual } from '@core/utils'; ModbusSecurityConfigComponent, GatewayPortTooltipPipe, ], - styleUrls: ['./modbus-slave-config.component.scss'], }) export class ModbusSlaveConfigComponent implements ControlValueAccessor, Validator, OnDestroy { @@ -90,6 +89,7 @@ export class ModbusSlaveConfigComponent implements ControlValueAccessor, Validat readonly ModbusProtocolType = ModbusProtocolType; readonly modbusBaudrates = ModbusBaudrates; + private isSlaveEnabled = false; private readonly serialSpecificControlKeys = ['serialPort', 'baudrate']; private readonly tcpUdpSpecificControlKeys = ['port', 'security', 'host']; @@ -126,14 +126,9 @@ export class ModbusSlaveConfigComponent implements ControlValueAccessor, Validat this.observeValueChanges(); this.observeTypeChange(); - this.observeFormEnable(); this.observeShowSecurity(); } - get isSlaveEnabled(): boolean { - return this.slaveConfigFormGroup.get('sendDataToThingsBoard').value; - } - get protocolType(): ModbusProtocolType { return this.slaveConfigFormGroup.get('type').value; } @@ -160,7 +155,11 @@ export class ModbusSlaveConfigComponent implements ControlValueAccessor, Validat writeValue(slaveConfig: ModbusSlave): void { this.showSecurityControl.patchValue(!!slaveConfig.security && !isEqual(slaveConfig.security, {})); this.updateSlaveConfig(slaveConfig); - this.updateFormEnableState(slaveConfig.sendDataToThingsBoard); + } + + setDisabledState(isDisabled: boolean): void { + this.isSlaveEnabled = !isDisabled; + this.updateFormEnableState(); } private observeValueChanges(): void { @@ -180,7 +179,7 @@ export class ModbusSlaveConfigComponent implements ControlValueAccessor, Validat this.slaveConfigFormGroup.get('type').valueChanges .pipe(takeUntil(this.destroy$)) .subscribe(type => { - this.updateFormEnableState(this.isSlaveEnabled); + this.updateFormEnableState(); this.updateMethodType(type); }); } @@ -196,22 +195,15 @@ export class ModbusSlaveConfigComponent implements ControlValueAccessor, Validat } } - private observeFormEnable(): void { - this.slaveConfigFormGroup.get('sendDataToThingsBoard').valueChanges - .pipe(startWith(this.isSlaveEnabled), takeUntil(this.destroy$)) - .subscribe(value => this.updateFormEnableState(value)); - } - - private updateFormEnableState(enabled: boolean): void { - if (enabled) { + private updateFormEnableState(): void { + if (this.isSlaveEnabled) { this.slaveConfigFormGroup.enable({emitEvent: false}); this.showSecurityControl.enable({emitEvent: false}); } else { this.slaveConfigFormGroup.disable({emitEvent: false}); this.showSecurityControl.disable({emitEvent: false}); - this.slaveConfigFormGroup.get('sendDataToThingsBoard').enable({emitEvent: false}); } - this.updateEnablingByProtocol(this.protocolType); + this.updateEnablingByProtocol(); this.updateSecurityEnable(this.showSecurityControl.value); } @@ -229,9 +221,10 @@ export class ModbusSlaveConfigComponent implements ControlValueAccessor, Validat } } - private updateEnablingByProtocol(type: ModbusProtocolType): void { - const enableKeys = type === ModbusProtocolType.Serial ? this.serialSpecificControlKeys : this.tcpUdpSpecificControlKeys; - const disableKeys = type === ModbusProtocolType.Serial ? this.tcpUdpSpecificControlKeys : this.serialSpecificControlKeys; + private updateEnablingByProtocol(): void { + const isSerial = this.protocolType === ModbusProtocolType.Serial; + const enableKeys = isSerial ? this.serialSpecificControlKeys : this.tcpUdpSpecificControlKeys; + const disableKeys = isSerial ? this.tcpUdpSpecificControlKeys : this.serialSpecificControlKeys; if (this.isSlaveEnabled) { enableKeys.forEach(key => this.slaveConfigFormGroup.get(key)?.enable({ emitEvent: false })); From d9a733c1840ddb3cfe7f3f5584572d7796dc33a0 Mon Sep 17 00:00:00 2001 From: mpetrov Date: Tue, 30 Jul 2024 17:49:11 +0300 Subject: [PATCH 13/48] refactoring --- .../modbus-master-table.component.ts | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-master-table/modbus-master-table.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-master-table/modbus-master-table.component.ts index 6a36f9a829..5838540d65 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-master-table/modbus-master-table.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-master-table/modbus-master-table.component.ts @@ -34,11 +34,8 @@ import { ControlValueAccessor, FormArray, FormBuilder, - NG_VALIDATORS, NG_VALUE_ACCESSOR, UntypedFormGroup, - ValidationErrors, - Validator, } from '@angular/forms'; import { ModbusMasterConfig, @@ -61,17 +58,12 @@ import { TbTableDatasource } from '@shared/components/table/table-datasource.abs provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => ModbusMasterTableComponent), multi: true - }, - { - provide: NG_VALIDATORS, - useExisting: forwardRef(() => ModbusMasterTableComponent), - multi: true } ], standalone: true, imports: [CommonModule, SharedModule] }) -export class ModbusMasterTableComponent implements ControlValueAccessor, Validator, AfterViewInit, OnInit, OnDestroy { +export class ModbusMasterTableComponent implements ControlValueAccessor, AfterViewInit, OnInit, OnDestroy { @ViewChild('searchInput') searchInputField: ElementRef; @@ -138,10 +130,6 @@ export class ModbusMasterTableComponent implements ControlValueAccessor, Validat this.pushDataAsFormArrays(master.slaves); } - validate(): ValidationErrors | null { - return null; - } - enterFilterMode(): void { this.textSearchMode = true; this.cdr.detectChanges(); From a8f6e2c4d2a7dec9cb1fc01d5f659f1661c35e65 Mon Sep 17 00:00:00 2001 From: mpetrov Date: Tue, 30 Jul 2024 18:18:33 +0300 Subject: [PATCH 14/48] refactoring --- .../modbus/modbus-basic-config/modbus-basic-config.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-basic-config/modbus-basic-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-basic-config/modbus-basic-config.component.ts index d5465f7b5c..87aef8d43e 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-basic-config/modbus-basic-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-basic-config/modbus-basic-config.component.ts @@ -94,7 +94,7 @@ export class ModbusBasicConfigComponent implements ControlValueAccessor, Validat .pipe(takeUntil(this.destroy$)) .subscribe(enable => { this.updateSlaveEnabling(enable); - this.basicFormGroup.get('slave').updateValueAndValidity(); + this.basicFormGroup.get('slave').updateValueAndValidity({emitEvent: !!this.onChange}); }); } From c1d3ac8f437de1fb83eb4edc97aeea5e09d8983c Mon Sep 17 00:00:00 2001 From: mpetrov Date: Tue, 30 Jul 2024 18:24:44 +0300 Subject: [PATCH 15/48] refactoring --- .../modbus/modbus-basic-config/modbus-basic-config.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-basic-config/modbus-basic-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-basic-config/modbus-basic-config.component.ts index 87aef8d43e..e85d9d31c0 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-basic-config/modbus-basic-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-basic-config/modbus-basic-config.component.ts @@ -124,7 +124,7 @@ export class ModbusBasicConfigComponent implements ControlValueAccessor, Validat validate(basicConfigControl: UntypedFormControl): ValidationErrors | null { const masterHasSlaves = !!basicConfigControl.value.master?.slaves?.length; const slaveEnabled = this.enableSlaveControl.value; - const slaveIsValid = this.basicFormGroup.get('slave').valid; + const slaveIsValid = this.basicFormGroup.valid; if ((slaveEnabled && slaveIsValid) || (masterHasSlaves && !slaveEnabled)) { return null; From 4ff73d3777bdf31c8005500aa6cc58e66c86beff Mon Sep 17 00:00:00 2001 From: mpetrov Date: Wed, 31 Jul 2024 11:12:40 +0300 Subject: [PATCH 16/48] Modbus minor UI adjustments --- .../modbus-data-keys-panel.component.html | 14 +++++++---- .../modbus-data-keys-panel.component.scss | 5 +--- .../modbus-data-keys-panel.component.ts | 23 +++++++++++-------- .../modbus-master-table.component.html | 3 +++ .../modbus-master-table.component.ts | 2 +- .../modbus-slave-dialog.component.html | 2 +- .../modbus-slave-dialog.component.scss | 5 ++++ .../assets/locale/locale.constant-en_US.json | 3 ++- 8 files changed, 35 insertions(+), 22 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-data-keys-panel/modbus-data-keys-panel.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-data-keys-panel/modbus-data-keys-panel.component.html index 02d2d1491d..4f71ebc19c 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-data-keys-panel/modbus-data-keys-panel.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-data-keys-panel/modbus-data-keys-panel.component.html @@ -26,12 +26,16 @@ -
- - {{ keyControl.get('tag').value }}{{ '-' }}{{ keyControl.get('value').value }} - - {{ keyControl.get('tag').value }} +
+ {{ keyControl.get('tag').value }}{{ '-' }}{{ keyControl.get('value').value }}
+ +
+
{{ 'gateway.key' | translate }}: {{ keyControl.get('tag').value }}
+
{{ 'gateway.address' | translate }}: {{ keyControl.get('address').value }}
+
{{ 'gateway.type' | translate }}: {{ keyControl.get('type').value }}
+
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-data-keys-panel/modbus-data-keys-panel.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-data-keys-panel/modbus-data-keys-panel.component.scss index 0e9f9a432f..d9ef45d66b 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-data-keys-panel/modbus-data-keys-panel.component.scss +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-data-keys-panel/modbus-data-keys-panel.component.scss @@ -20,10 +20,7 @@ max-width: 700px; .title-container { - max-width: 11vw; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap + width: 12vw; } .key-panel { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-data-keys-panel/modbus-data-keys-panel.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-data-keys-panel/modbus-data-keys-panel.component.ts index 5b312f52c8..304495e8d9 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-data-keys-panel/modbus-data-keys-panel.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-data-keys-panel/modbus-data-keys-panel.component.ts @@ -40,6 +40,7 @@ import { generateSecret } from '@core/utils'; import { coerceBoolean } from '@shared/decorators/coercion'; import { takeUntil } from 'rxjs/operators'; import { Subject } from 'rxjs'; +import { TruncateWithTooltipDirective } from '@shared/directives/truncate-with-tooltip.directive'; @Component({ selector: 'tb-modbus-data-keys-panel', @@ -50,6 +51,7 @@ import { Subject } from 'rxjs'; CommonModule, SharedModule, GatewayHelpLinkPipe, + TruncateWithTooltipDirective, ] }) export class ModbusDataKeysPanelComponent implements OnInit, OnDestroy { @@ -78,8 +80,9 @@ export class ModbusDataKeysPanelComponent implements OnInit, OnDestroy { private destroy$ = new Subject(); private readonly defaultReadFunctionCodes = [3, 4]; - private readonly defaultWriteFunctionCodes = [5, 6, 15, 16]; - private readonly stringAttrUpdatesWriteFunctionCodes = [6, 16]; + private readonly bitsReadFunctionCodes = [1, 2]; + private readonly defaultWriteFunctionCodes = [6, 16]; + private readonly bitsWriteFunctionCodes = [5, 15]; constructor(private fb: UntypedFormBuilder) {} @@ -177,23 +180,23 @@ export class ModbusDataKeysPanelComponent implements OnInit, OnDestroy { } private getFunctionCodes(dataType: ModbusDataType): number[] { + const writeFunctionCodes = [ + ...(dataType === ModbusDataType.BITS ? this.bitsWriteFunctionCodes : []), ...this.defaultWriteFunctionCodes + ]; + if (this.keysType === ModbusValueKey.ATTRIBUTES_UPDATES) { - return dataType === ModbusDataType.STRING - ? this.stringAttrUpdatesWriteFunctionCodes - : this.defaultWriteFunctionCodes; + return writeFunctionCodes.sort((a, b) => a - b); } const functionCodes = [...this.defaultReadFunctionCodes]; if (dataType === ModbusDataType.BITS) { - const bitsFunctionCodes = [1, 2]; - functionCodes.push(...bitsFunctionCodes); - functionCodes.sort(); + functionCodes.push(...this.bitsReadFunctionCodes); } if (this.keysType === ModbusValueKey.RPC_REQUESTS) { - functionCodes.push(...this.defaultWriteFunctionCodes); + functionCodes.push(...writeFunctionCodes); } - return functionCodes; + return functionCodes.sort((a, b) => a - b); } private getDefaultFunctionCodes(): number[] { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-master-table/modbus-master-table.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-master-table/modbus-master-table.component.html index e16aa0c51f..0da32e8338 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-master-table/modbus-master-table.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-master-table/modbus-master-table.component.html @@ -16,6 +16,9 @@ -->
+
+
{{ 'gateway.hints.modbus-master' | translate }}
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-master-table/modbus-master-table.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-master-table/modbus-master-table.component.ts index 2cb66f6062..14f3635b98 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-master-table/modbus-master-table.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-master-table/modbus-master-table.component.ts @@ -169,7 +169,7 @@ export class ModbusMasterTableComponent implements ControlValueAccessor, Validat panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], data: { value, - buttonTitle: withIndex ? 'action.add' : 'action.apply' + buttonTitle: withIndex ? 'action.apply' : 'action.add' } }).afterClosed() .pipe(take(1), takeUntil(this.destroy$)) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-dialog/modbus-slave-dialog.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-dialog/modbus-slave-dialog.component.html index 1e47479f03..c7fb50f146 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-dialog/modbus-slave-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-dialog/modbus-slave-dialog.component.html @@ -28,7 +28,7 @@
-
gateway.name
+
gateway.name
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-dialog/modbus-slave-dialog.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-dialog/modbus-slave-dialog.component.scss index 8cea184957..8900741a93 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-dialog/modbus-slave-dialog.component.scss +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-dialog/modbus-slave-dialog.component.scss @@ -18,4 +18,9 @@ width: 80vw; max-width: 900px; } + + .slave-name-label { + margin-right: 16px; + color: rgba(0, 0, 0, 0.87); + } } diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index 14eb272910..2c6459582b 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -3326,7 +3326,8 @@ "write-register": "Write Register", "write-registers": "Write Registers", "hints": { - "modbus-server": "Starting with version 3.0, Gateway can run as a Modbus slave.", + "modbus-master": "Configuration sections for connecting to Modbus servers and reading data from them.", + "modbus-server": "Configuration section for the Modbus server, storing data and sending updates to the platform when changes occur or at fixed intervals.", "remote-configuration": "Enables remote configuration and management of the gateway", "remote-shell": "Enables remote control of the operating system with the gateway from the Remote Shell widget", "host": "Hostname or IP address of platform server", From 27bd1b2fa0ded735b05e6083ebc64474f5f0663d Mon Sep 17 00:00:00 2001 From: mpetrov Date: Wed, 31 Jul 2024 11:50:39 +0300 Subject: [PATCH 17/48] refactoring --- .../modbus-basic-config.component.ts | 16 +++++----------- .../modbus-master-table.component.ts | 17 ++++++++++++++++- 2 files changed, 21 insertions(+), 12 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-basic-config/modbus-basic-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-basic-config/modbus-basic-config.component.ts index e85d9d31c0..8291558b4d 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-basic-config/modbus-basic-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-basic-config/modbus-basic-config.component.ts @@ -22,7 +22,6 @@ import { FormGroup, NG_VALIDATORS, NG_VALUE_ACCESSOR, - UntypedFormControl, ValidationErrors, Validator, } from '@angular/forms'; @@ -95,6 +94,7 @@ export class ModbusBasicConfigComponent implements ControlValueAccessor, Validat .subscribe(enable => { this.updateSlaveEnabling(enable); this.basicFormGroup.get('slave').updateValueAndValidity({emitEvent: !!this.onChange}); + this.basicFormGroup.get('master').updateValueAndValidity({emitEvent: !!this.onChange}); }); } @@ -121,16 +121,10 @@ export class ModbusBasicConfigComponent implements ControlValueAccessor, Validat this.enableSlaveControl.setValue(!!basicConfig.slave && !isEqual(basicConfig.slave, {})); } - validate(basicConfigControl: UntypedFormControl): ValidationErrors | null { - const masterHasSlaves = !!basicConfigControl.value.master?.slaves?.length; - const slaveEnabled = this.enableSlaveControl.value; - const slaveIsValid = this.basicFormGroup.valid; - - if ((slaveEnabled && slaveIsValid) || (masterHasSlaves && !slaveEnabled)) { - return null; - } - - return { basicFormGroup: { valid: false } }; + validate(): ValidationErrors | null { + return this.basicFormGroup.valid ? null : { + basicFormGroup: {valid: false} + }; } private updateSlaveEnabling(isEnabled: boolean): void { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-master-table/modbus-master-table.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-master-table/modbus-master-table.component.ts index 5838540d65..5f51f59284 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-master-table/modbus-master-table.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-master-table/modbus-master-table.component.ts @@ -34,8 +34,12 @@ import { ControlValueAccessor, FormArray, FormBuilder, + NG_VALIDATORS, NG_VALUE_ACCESSOR, + UntypedFormControl, UntypedFormGroup, + ValidationErrors, + Validator, } from '@angular/forms'; import { ModbusMasterConfig, @@ -58,12 +62,17 @@ import { TbTableDatasource } from '@shared/components/table/table-datasource.abs provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => ModbusMasterTableComponent), multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => ModbusMasterTableComponent), + multi: true } ], standalone: true, imports: [CommonModule, SharedModule] }) -export class ModbusMasterTableComponent implements ControlValueAccessor, AfterViewInit, OnInit, OnDestroy { +export class ModbusMasterTableComponent implements ControlValueAccessor, Validator, AfterViewInit, OnInit, OnDestroy { @ViewChild('searchInput') searchInputField: ElementRef; @@ -130,6 +139,12 @@ export class ModbusMasterTableComponent implements ControlValueAccessor, AfterVi this.pushDataAsFormArrays(master.slaves); } + validate(masterControl: UntypedFormControl): ValidationErrors | null { + return masterControl.parent.get('slave').enabled || this.slaves.controls.length ? null : { + slavesFormGroup: {valid: false} + }; + } + enterFilterMode(): void { this.textSearchMode = true; this.cdr.detectChanges(); From 8a263b08b84b477f212f6ef82ec32f70213c5881 Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Wed, 31 Jul 2024 13:03:40 +0300 Subject: [PATCH 18/48] UI: Add handler to call when mobile service is ready --- ui-ngx/src/app/core/services/mobile.service.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ui-ngx/src/app/core/services/mobile.service.ts b/ui-ngx/src/app/core/services/mobile.service.ts index 03772ce167..7b11b817b4 100644 --- a/ui-ngx/src/app/core/services/mobile.service.ts +++ b/ui-ngx/src/app/core/services/mobile.service.ts @@ -30,6 +30,7 @@ const dashboardLoadedHandler = 'tbMobileDashboardLoadedHandler'; const dashboardLayoutHandler = 'tbMobileDashboardLayoutHandler'; const navigationHandler = 'tbMobileNavigationHandler'; const mobileHandler = 'tbMobileHandler'; +const mobileReadyHandler = 'tbMobileReadyHandler'; // @dynamic @Injectable({ @@ -54,6 +55,7 @@ export class MobileService { this.mobileApp = isDefined(this.mobileChannel); if (this.mobileApp) { window.addEventListener('message', this.onWindowMessageListener); + this.mobileChannel.callHandler(mobileReadyHandler); } } From 87415c0df6bbf5170a88f48cd80721fa5aad9888 Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Wed, 31 Jul 2024 14:46:20 +0300 Subject: [PATCH 19/48] UI: Fixed load platform in iOs 16 or later (fixed noLeadTrailSpacesRegex) --- .../home/components/widget/lib/gateway/gateway-widget.models.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-widget.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-widget.models.ts index 0e143cff36..125d14fd3a 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-widget.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-widget.models.ts @@ -19,7 +19,7 @@ import { Observable } from 'rxjs'; import { ValueTypeData } from '@shared/models/constants'; import { Validators } from '@angular/forms'; -export const noLeadTrailSpacesRegex: RegExp = /^(?! )[\S\s]*(? Date: Wed, 31 Jul 2024 17:52:38 +0300 Subject: [PATCH 20/48] UI: Added missing audit log action type and improved audit log table handler translation --- .../audit-log/audit-log-table-config.ts | 38 ++++++++++++++++--- .../src/app/shared/models/audit-log.models.ts | 2 + .../assets/locale/locale.constant-en_US.json | 1 + 3 files changed, 36 insertions(+), 5 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/audit-log/audit-log-table-config.ts b/ui-ngx/src/app/modules/home/components/audit-log/audit-log-table-config.ts index b5c94f9aeb..fa4e8bcc71 100644 --- a/ui-ngx/src/app/modules/home/components/audit-log/audit-log-table-config.ts +++ b/ui-ngx/src/app/modules/home/components/audit-log/audit-log-table-config.ts @@ -20,12 +20,19 @@ import { EntityTableConfig } from '@home/models/entity/entities-table-config.models'; import { + ActionStatus, actionStatusTranslations, + ActionType, actionTypeTranslations, AuditLog, AuditLogMode } from '@shared/models/audit-log.models'; -import { EntityTypeResource, entityTypeTranslations } from '@shared/models/entity-type.models'; +import { + AliasEntityType, + EntityType, + EntityTypeResource, + entityTypeTranslations +} from '@shared/models/entity-type.models'; import { AuditLogService } from '@core/http/audit-log.service'; import { TranslateService } from '@ngx-translate/core'; import { DatePipe } from '@angular/common'; @@ -82,7 +89,7 @@ export class AuditLogTableConfig extends EntityTableConfig('entityType', 'audit-log.entity-type', '20%', - (entity) => translate.instant(entityTypeTranslations.get(entity.entityId.entityType).type)), + (entity) => this.getEntityTypeTranslation(entity.entityId.entityType)), new EntityTableColumn('entityName', 'audit-log.entity-name', '20%'), ); } @@ -95,9 +102,9 @@ export class AuditLogTableConfig extends EntityTableConfig('actionType', 'audit-log.type', '33%', - (entity) => translate.instant(actionTypeTranslations.get(entity.actionType))), + (entity) => this.getActionTypeTranslation(entity.actionType)), new EntityTableColumn('actionStatus', 'audit-log.status', '33%', - (entity) => translate.instant(actionStatusTranslations.get(entity.actionStatus))) + (entity) => this.getActionStatusTranslation(entity.actionStatus)) ); this.cellActionDescriptors.push( @@ -105,11 +112,32 @@ export class AuditLogTableConfig extends EntityTableConfig true, - onAction: ($event, entity) => this.showAuditLogDetails(entity) + onAction: (_, entity) => this.showAuditLogDetails(entity) } ); } + private getEntityTypeTranslation(entityType: EntityType | AliasEntityType): string { + if (entityTypeTranslations.has(entityType) && entityTypeTranslations.get(entityType).type) { + return this.translate.instant(entityTypeTranslations.get(entityType).type); + } + return entityType; + } + + private getActionTypeTranslation(actionType: ActionType): string { + if (actionTypeTranslations.has(actionType)) { + return this.translate.instant(actionTypeTranslations.get(actionType)); + } + return actionType; + } + + private getActionStatusTranslation(actionStatus: ActionStatus): string { + if (actionStatusTranslations.has(actionStatus)) { + return this.translate.instant(actionStatusTranslations.get(actionStatus)); + } + return actionStatus; + } + fetchAuditLogs(pageLink: TimePageLink): Observable> { switch (this.auditLogMode) { case AuditLogMode.TENANT: diff --git a/ui-ngx/src/app/shared/models/audit-log.models.ts b/ui-ngx/src/app/shared/models/audit-log.models.ts index 70e8a0be6b..a399a458c2 100644 --- a/ui-ngx/src/app/shared/models/audit-log.models.ts +++ b/ui-ngx/src/app/shared/models/audit-log.models.ts @@ -48,6 +48,7 @@ export enum ActionType { ALARM_ACK = 'ALARM_ACK', ALARM_CLEAR = 'ALARM_CLEAR', ALARM_ASSIGNED = 'ALARM_ASSIGNED', + ALARM_DELETE = 'ALARM_DELETE', ALARM_UNASSIGNED = 'ALARM_UNASSIGNED', ADDED_COMMENT = 'ADDED_COMMENT', UPDATED_COMMENT = 'UPDATED_COMMENT', @@ -91,6 +92,7 @@ export const actionTypeTranslations = new Map( [ActionType.RELATIONS_DELETED, 'audit-log.type-relations-delete'], [ActionType.ALARM_ACK, 'audit-log.type-alarm-ack'], [ActionType.ALARM_CLEAR, 'audit-log.type-alarm-clear'], + [ActionType.ALARM_DELETE, 'audit-log.type-alarm-delete'], [ActionType.ALARM_ASSIGNED, 'audit-log.type-alarm-assign'], [ActionType.ALARM_UNASSIGNED, 'audit-log.type-alarm-unassign'], [ActionType.ADDED_COMMENT, 'audit-log.type-added-comment'], diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index 14eb272910..1101270d49 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -936,6 +936,7 @@ "type-relations-delete": "All relation deleted", "type-alarm-ack": "Acknowledged", "type-alarm-clear": "Cleared", + "type-alarm-delete": "Delete", "type-alarm-assign": "Assigned", "type-alarm-unassign": "Unassigned", "type-added-comment": "Added comment", From eb39f36007ef44a3c399cbfc1c2363492935719c Mon Sep 17 00:00:00 2001 From: mpetrov Date: Thu, 1 Aug 2024 11:37:58 +0300 Subject: [PATCH 21/48] refactoring --- .../modbus-basic-config.component.html | 10 ------ .../modbus-basic-config.component.ts | 32 ++++++------------- .../modbus-master-table.component.ts | 17 +--------- .../modbus-slave-config.component.html | 10 ++++++ .../modbus-slave-config.component.ts | 29 ++++++++++------- 5 files changed, 38 insertions(+), 60 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-basic-config/modbus-basic-config.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-basic-config/modbus-basic-config.component.html index 105a3c0e8e..17f5185bc6 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-basic-config/modbus-basic-config.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-basic-config/modbus-basic-config.component.html @@ -23,16 +23,6 @@ -
-
{{ 'gateway.hints.modbus-server' | translate }}
-
- - - {{ 'gateway.enable' | translate }} - - -
-
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-basic-config/modbus-basic-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-basic-config/modbus-basic-config.component.ts index 8291558b4d..22430ae77c 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-basic-config/modbus-basic-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-basic-config/modbus-basic-config.component.ts @@ -18,10 +18,10 @@ import { ChangeDetectionStrategy, Component, forwardRef, Input, OnDestroy, Templ import { ControlValueAccessor, FormBuilder, - FormControl, FormGroup, NG_VALIDATORS, NG_VALUE_ACCESSOR, + UntypedFormControl, ValidationErrors, Validator, } from '@angular/forms'; @@ -68,7 +68,6 @@ export class ModbusBasicConfigComponent implements ControlValueAccessor, Validat @Input() generalTabContent: TemplateRef; basicFormGroup: FormGroup; - enableSlaveControl: FormControl; onChange: (value: ModbusBasicConfig) => void; onTouched: () => void; @@ -80,7 +79,6 @@ export class ModbusBasicConfigComponent implements ControlValueAccessor, Validat master: [], slave: [], }); - this.enableSlaveControl = new FormControl(false); this.basicFormGroup.valueChanges .pipe(takeUntil(this.destroy$)) @@ -88,14 +86,6 @@ export class ModbusBasicConfigComponent implements ControlValueAccessor, Validat this.onChange(value); this.onTouched(); }); - - this.enableSlaveControl.valueChanges - .pipe(takeUntil(this.destroy$)) - .subscribe(enable => { - this.updateSlaveEnabling(enable); - this.basicFormGroup.get('slave').updateValueAndValidity({emitEvent: !!this.onChange}); - this.basicFormGroup.get('master').updateValueAndValidity({emitEvent: !!this.onChange}); - }); } ngOnDestroy(): void { @@ -118,20 +108,16 @@ export class ModbusBasicConfigComponent implements ControlValueAccessor, Validat }; this.basicFormGroup.setValue(editedBase, {emitEvent: false}); - this.enableSlaveControl.setValue(!!basicConfig.slave && !isEqual(basicConfig.slave, {})); - } - - validate(): ValidationErrors | null { - return this.basicFormGroup.valid ? null : { - basicFormGroup: {valid: false} - }; } - private updateSlaveEnabling(isEnabled: boolean): void { - if (isEnabled) { - this.basicFormGroup.get('slave').enable({emitEvent: false}); - } else { - this.basicFormGroup.get('slave').disable({emitEvent: false}); + validate(basicFormControl: UntypedFormControl): ValidationErrors | null { + const { master, slave } = basicFormControl.value; + const isEmpty = !master?.slaves?.length && (isEqual(slave, {}) || !slave); + if (!this.basicFormGroup.valid || isEmpty) { + return { + basicFormGroup: {valid: false} + }; } + return null; } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-master-table/modbus-master-table.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-master-table/modbus-master-table.component.ts index 5f51f59284..75fc62ed5e 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-master-table/modbus-master-table.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-master-table/modbus-master-table.component.ts @@ -34,12 +34,8 @@ import { ControlValueAccessor, FormArray, FormBuilder, - NG_VALIDATORS, NG_VALUE_ACCESSOR, - UntypedFormControl, UntypedFormGroup, - ValidationErrors, - Validator, } from '@angular/forms'; import { ModbusMasterConfig, @@ -63,16 +59,11 @@ import { TbTableDatasource } from '@shared/components/table/table-datasource.abs useExisting: forwardRef(() => ModbusMasterTableComponent), multi: true }, - { - provide: NG_VALIDATORS, - useExisting: forwardRef(() => ModbusMasterTableComponent), - multi: true - } ], standalone: true, imports: [CommonModule, SharedModule] }) -export class ModbusMasterTableComponent implements ControlValueAccessor, Validator, AfterViewInit, OnInit, OnDestroy { +export class ModbusMasterTableComponent implements ControlValueAccessor, AfterViewInit, OnInit, OnDestroy { @ViewChild('searchInput') searchInputField: ElementRef; @@ -139,12 +130,6 @@ export class ModbusMasterTableComponent implements ControlValueAccessor, Validat this.pushDataAsFormArrays(master.slaves); } - validate(masterControl: UntypedFormControl): ValidationErrors | null { - return masterControl.parent.get('slave').enabled || this.slaves.controls.length ? null : { - slavesFormGroup: {valid: false} - }; - } - enterFilterMode(): void { this.textSearchMode = true; this.cdr.detectChanges(); diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-config/modbus-slave-config.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-config/modbus-slave-config.component.html index a46dd68edc..c4986eefce 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-config/modbus-slave-config.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-config/modbus-slave-config.component.html @@ -16,6 +16,16 @@ -->
+
+
{{ 'gateway.hints.modbus-server' | translate }}
+
+ + + {{ 'gateway.enable' | translate }} + + +
+
gateway.server-slave-config
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-config/modbus-slave-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-config/modbus-slave-config.component.ts index 46771c6937..86b601de53 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-config/modbus-slave-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-config/modbus-slave-config.component.ts @@ -77,6 +77,7 @@ import { isEqual } from '@core/utils'; export class ModbusSlaveConfigComponent implements ControlValueAccessor, Validator, OnDestroy { slaveConfigFormGroup: UntypedFormGroup; + enableSlaveControl: FormControl; showSecurityControl: FormControl; ModbusProtocolLabelsMap = ModbusProtocolLabelsMap; ModbusMethodLabelsMap = ModbusMethodLabelsMap; @@ -89,7 +90,6 @@ export class ModbusSlaveConfigComponent implements ControlValueAccessor, Validat readonly ModbusProtocolType = ModbusProtocolType; readonly modbusBaudrates = ModbusBaudrates; - private isSlaveEnabled = false; private readonly serialSpecificControlKeys = ['serialPort', 'baudrate']; private readonly tcpUdpSpecificControlKeys = ['port', 'security', 'host']; @@ -100,6 +100,7 @@ export class ModbusSlaveConfigComponent implements ControlValueAccessor, Validat constructor(private fb: FormBuilder) { this.showSecurityControl = this.fb.control(false); + this.enableSlaveControl = this.fb.control(false); this.slaveConfigFormGroup = this.fb.group({ type: [ModbusProtocolType.TCP], host: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]], @@ -127,6 +128,7 @@ export class ModbusSlaveConfigComponent implements ControlValueAccessor, Validat this.observeValueChanges(); this.observeTypeChange(); this.observeShowSecurity(); + this.observeFormEnable(); } get protocolType(): ModbusProtocolType { @@ -153,15 +155,11 @@ export class ModbusSlaveConfigComponent implements ControlValueAccessor, Validat } writeValue(slaveConfig: ModbusSlave): void { + this.enableSlaveControl.patchValue(!!slaveConfig && !isEqual(slaveConfig, {})); this.showSecurityControl.patchValue(!!slaveConfig.security && !isEqual(slaveConfig.security, {})); this.updateSlaveConfig(slaveConfig); } - setDisabledState(isDisabled: boolean): void { - this.isSlaveEnabled = !isDisabled; - this.updateFormEnableState(); - } - private observeValueChanges(): void { this.slaveConfigFormGroup.valueChanges.pipe( takeUntil(this.destroy$) @@ -175,11 +173,20 @@ export class ModbusSlaveConfigComponent implements ControlValueAccessor, Validat }); } + private observeFormEnable(): void { + this.enableSlaveControl.valueChanges + .pipe(takeUntil(this.destroy$)) + .subscribe(value => { + this.updateFormEnableState(value); + this.slaveConfigFormGroup.updateValueAndValidity({emitEvent: !!this.onChange}); + }); + } + private observeTypeChange(): void { this.slaveConfigFormGroup.get('type').valueChanges .pipe(takeUntil(this.destroy$)) .subscribe(type => { - this.updateFormEnableState(); + this.updateFormEnableState(this.enableSlaveControl.value); this.updateMethodType(type); }); } @@ -195,8 +202,8 @@ export class ModbusSlaveConfigComponent implements ControlValueAccessor, Validat } } - private updateFormEnableState(): void { - if (this.isSlaveEnabled) { + private updateFormEnableState(enabled: boolean): void { + if (enabled) { this.slaveConfigFormGroup.enable({emitEvent: false}); this.showSecurityControl.enable({emitEvent: false}); } else { @@ -214,7 +221,7 @@ export class ModbusSlaveConfigComponent implements ControlValueAccessor, Validat } private updateSecurityEnable(securityEnabled: boolean): void { - if (securityEnabled && this.isSlaveEnabled && this.protocolType !== ModbusProtocolType.Serial) { + if (securityEnabled && this.enableSlaveControl.value && this.protocolType !== ModbusProtocolType.Serial) { this.slaveConfigFormGroup.get('security').enable({emitEvent: false}); } else { this.slaveConfigFormGroup.get('security').disable({emitEvent: false}); @@ -226,7 +233,7 @@ export class ModbusSlaveConfigComponent implements ControlValueAccessor, Validat const enableKeys = isSerial ? this.serialSpecificControlKeys : this.tcpUdpSpecificControlKeys; const disableKeys = isSerial ? this.tcpUdpSpecificControlKeys : this.serialSpecificControlKeys; - if (this.isSlaveEnabled) { + if (this.enableSlaveControl.value) { enableKeys.forEach(key => this.slaveConfigFormGroup.get(key)?.enable({ emitEvent: false })); } From c2c54e5104d4f414721915e1931095cf1bed9887 Mon Sep 17 00:00:00 2001 From: mpetrov Date: Thu, 1 Aug 2024 11:42:19 +0300 Subject: [PATCH 22/48] refactoring --- .../modbus-slave-config.component.ts | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-config/modbus-slave-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-config/modbus-slave-config.component.ts index 86b601de53..bf1acfbf94 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-config/modbus-slave-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-config/modbus-slave-config.component.ts @@ -127,8 +127,8 @@ export class ModbusSlaveConfigComponent implements ControlValueAccessor, Validat this.observeValueChanges(); this.observeTypeChange(); - this.observeShowSecurity(); this.observeFormEnable(); + this.observeShowSecurity(); } get protocolType(): ModbusProtocolType { @@ -173,15 +173,6 @@ export class ModbusSlaveConfigComponent implements ControlValueAccessor, Validat }); } - private observeFormEnable(): void { - this.enableSlaveControl.valueChanges - .pipe(takeUntil(this.destroy$)) - .subscribe(value => { - this.updateFormEnableState(value); - this.slaveConfigFormGroup.updateValueAndValidity({emitEvent: !!this.onChange}); - }); - } - private observeTypeChange(): void { this.slaveConfigFormGroup.get('type').valueChanges .pipe(takeUntil(this.destroy$)) @@ -191,6 +182,15 @@ export class ModbusSlaveConfigComponent implements ControlValueAccessor, Validat }); } + private observeFormEnable(): void { + this.enableSlaveControl.valueChanges + .pipe(takeUntil(this.destroy$)) + .subscribe(value => { + this.updateFormEnableState(value); + this.slaveConfigFormGroup.updateValueAndValidity({emitEvent: !!this.onChange}); + }); + } + private updateMethodType(type: ModbusProtocolType): void { if (this.slaveConfigFormGroup.get('method').value !== ModbusMethodType.RTU) { this.slaveConfigFormGroup.get('method').patchValue( From ff3420890e0b4407aa34a16c864fd8406d6e3294 Mon Sep 17 00:00:00 2001 From: IrynaMatveieva Date: Thu, 1 Aug 2024 11:52:43 +0300 Subject: [PATCH 23/48] removed dataformat xml exclusion --- ...appingJackson2XmlHttpMessageConverter.java | 64 +++++++++++++++++++ .../system/RestTemplateConvertersTest.java | 36 ----------- pom.xml | 4 -- 3 files changed, 64 insertions(+), 40 deletions(-) create mode 100644 application/src/main/java/org/springframework/http/converter/xml/MappingJackson2XmlHttpMessageConverter.java delete mode 100644 application/src/test/java/org/thingsboard/server/system/RestTemplateConvertersTest.java diff --git a/application/src/main/java/org/springframework/http/converter/xml/MappingJackson2XmlHttpMessageConverter.java b/application/src/main/java/org/springframework/http/converter/xml/MappingJackson2XmlHttpMessageConverter.java new file mode 100644 index 0000000000..0074526ebc --- /dev/null +++ b/application/src/main/java/org/springframework/http/converter/xml/MappingJackson2XmlHttpMessageConverter.java @@ -0,0 +1,64 @@ +/** + * 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.springframework.http.converter.xml; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.dataformat.xml.XmlMapper; +import org.springframework.http.MediaType; +import org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter; +import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder; +import org.springframework.util.Assert; + +import java.lang.reflect.Type; +import java.nio.charset.StandardCharsets; +import java.util.Collections; +import java.util.List; + +public class MappingJackson2XmlHttpMessageConverter extends AbstractJackson2HttpMessageConverter { + private static final List problemDetailMediaTypes; + + public MappingJackson2XmlHttpMessageConverter() { + this(Jackson2ObjectMapperBuilder.xml().build()); + } + + public MappingJackson2XmlHttpMessageConverter(ObjectMapper objectMapper) { + super(objectMapper, new MediaType[]{new MediaType("application", "xml", StandardCharsets.UTF_8), new MediaType("text", "xml", StandardCharsets.UTF_8), new MediaType("application", "*+xml", StandardCharsets.UTF_8)}); + Assert.isInstanceOf(XmlMapper.class, objectMapper, "XmlMapper required"); + } + + public void setObjectMapper(ObjectMapper objectMapper) { + Assert.isInstanceOf(XmlMapper.class, objectMapper, "XmlMapper required"); + super.setObjectMapper(objectMapper); + } + + protected List getMediaTypesForProblemDetail() { + return problemDetailMediaTypes; + } + + static { + problemDetailMediaTypes = Collections.singletonList(MediaType.APPLICATION_PROBLEM_XML); + } + + @Override + public boolean canRead(Type type, Class contextClass, MediaType mediaType) { + return false; + } + + @Override + public boolean canWrite(Class clazz, MediaType mediaType) { + return false; + } +} diff --git a/application/src/test/java/org/thingsboard/server/system/RestTemplateConvertersTest.java b/application/src/test/java/org/thingsboard/server/system/RestTemplateConvertersTest.java deleted file mode 100644 index 74c2dfda37..0000000000 --- a/application/src/test/java/org/thingsboard/server/system/RestTemplateConvertersTest.java +++ /dev/null @@ -1,36 +0,0 @@ -/** - * 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.system; - -import lombok.extern.slf4j.Slf4j; -import org.junit.Test; -import org.junit.jupiter.api.Assertions; -import org.springframework.util.ClassUtils; -import org.springframework.web.client.RestTemplate; - - -@Slf4j -public class RestTemplateConvertersTest { - - @Test - public void testJacksonXmlConverter() { - ClassLoader classLoader = RestTemplate.class.getClassLoader(); - boolean jackson2XmlPresent = ClassUtils.isPresent("com.fasterxml.jackson.dataformat.xml.XmlMapper", classLoader); - Assertions.assertFalse(jackson2XmlPresent, "XmlMapper must not be present in classpath, please, exclude \"jackson-dataformat-xml\" dependency!"); - //If this xml mapper will be present in classpath then we will get "Unsupported Media Type" in RestTemplate - } - -} diff --git a/pom.xml b/pom.xml index 9b8943cff3..833028b1a8 100755 --- a/pom.xml +++ b/pom.xml @@ -2092,10 +2092,6 @@ io.jsonwebtoken jjwt-impl - - com.fasterxml.jackson.dataformat - jackson-dataformat-xml - From 0a42624ba46e45a04d52417586d12337186d949c Mon Sep 17 00:00:00 2001 From: mpetrov Date: Thu, 1 Aug 2024 12:49:07 +0300 Subject: [PATCH 24/48] Revert "refactoring" This reverts commit c2c54e5104d4f414721915e1931095cf1bed9887. --- .../modbus-slave-config.component.ts | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-config/modbus-slave-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-config/modbus-slave-config.component.ts index bf1acfbf94..86b601de53 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-config/modbus-slave-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-config/modbus-slave-config.component.ts @@ -127,8 +127,8 @@ export class ModbusSlaveConfigComponent implements ControlValueAccessor, Validat this.observeValueChanges(); this.observeTypeChange(); - this.observeFormEnable(); this.observeShowSecurity(); + this.observeFormEnable(); } get protocolType(): ModbusProtocolType { @@ -173,15 +173,6 @@ export class ModbusSlaveConfigComponent implements ControlValueAccessor, Validat }); } - private observeTypeChange(): void { - this.slaveConfigFormGroup.get('type').valueChanges - .pipe(takeUntil(this.destroy$)) - .subscribe(type => { - this.updateFormEnableState(this.enableSlaveControl.value); - this.updateMethodType(type); - }); - } - private observeFormEnable(): void { this.enableSlaveControl.valueChanges .pipe(takeUntil(this.destroy$)) @@ -191,6 +182,15 @@ export class ModbusSlaveConfigComponent implements ControlValueAccessor, Validat }); } + private observeTypeChange(): void { + this.slaveConfigFormGroup.get('type').valueChanges + .pipe(takeUntil(this.destroy$)) + .subscribe(type => { + this.updateFormEnableState(this.enableSlaveControl.value); + this.updateMethodType(type); + }); + } + private updateMethodType(type: ModbusProtocolType): void { if (this.slaveConfigFormGroup.get('method').value !== ModbusMethodType.RTU) { this.slaveConfigFormGroup.get('method').patchValue( From 9c0a33237aa02e12cf22a6b3c8c895dec73723ce Mon Sep 17 00:00:00 2001 From: mpetrov Date: Thu, 1 Aug 2024 12:49:07 +0300 Subject: [PATCH 25/48] Revert "refactoring" This reverts commit eb39f36007ef44a3c399cbfc1c2363492935719c. --- .../modbus-basic-config.component.html | 10 ++++++ .../modbus-basic-config.component.ts | 32 +++++++++++++------ .../modbus-master-table.component.ts | 17 +++++++++- .../modbus-slave-config.component.html | 10 ------ .../modbus-slave-config.component.ts | 29 +++++++---------- 5 files changed, 60 insertions(+), 38 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-basic-config/modbus-basic-config.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-basic-config/modbus-basic-config.component.html index 17f5185bc6..105a3c0e8e 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-basic-config/modbus-basic-config.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-basic-config/modbus-basic-config.component.html @@ -23,6 +23,16 @@ +
+
{{ 'gateway.hints.modbus-server' | translate }}
+
+ + + {{ 'gateway.enable' | translate }} + + +
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-basic-config/modbus-basic-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-basic-config/modbus-basic-config.component.ts index 22430ae77c..8291558b4d 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-basic-config/modbus-basic-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-basic-config/modbus-basic-config.component.ts @@ -18,10 +18,10 @@ import { ChangeDetectionStrategy, Component, forwardRef, Input, OnDestroy, Templ import { ControlValueAccessor, FormBuilder, + FormControl, FormGroup, NG_VALIDATORS, NG_VALUE_ACCESSOR, - UntypedFormControl, ValidationErrors, Validator, } from '@angular/forms'; @@ -68,6 +68,7 @@ export class ModbusBasicConfigComponent implements ControlValueAccessor, Validat @Input() generalTabContent: TemplateRef; basicFormGroup: FormGroup; + enableSlaveControl: FormControl; onChange: (value: ModbusBasicConfig) => void; onTouched: () => void; @@ -79,6 +80,7 @@ export class ModbusBasicConfigComponent implements ControlValueAccessor, Validat master: [], slave: [], }); + this.enableSlaveControl = new FormControl(false); this.basicFormGroup.valueChanges .pipe(takeUntil(this.destroy$)) @@ -86,6 +88,14 @@ export class ModbusBasicConfigComponent implements ControlValueAccessor, Validat this.onChange(value); this.onTouched(); }); + + this.enableSlaveControl.valueChanges + .pipe(takeUntil(this.destroy$)) + .subscribe(enable => { + this.updateSlaveEnabling(enable); + this.basicFormGroup.get('slave').updateValueAndValidity({emitEvent: !!this.onChange}); + this.basicFormGroup.get('master').updateValueAndValidity({emitEvent: !!this.onChange}); + }); } ngOnDestroy(): void { @@ -108,16 +118,20 @@ export class ModbusBasicConfigComponent implements ControlValueAccessor, Validat }; this.basicFormGroup.setValue(editedBase, {emitEvent: false}); + this.enableSlaveControl.setValue(!!basicConfig.slave && !isEqual(basicConfig.slave, {})); + } + + validate(): ValidationErrors | null { + return this.basicFormGroup.valid ? null : { + basicFormGroup: {valid: false} + }; } - validate(basicFormControl: UntypedFormControl): ValidationErrors | null { - const { master, slave } = basicFormControl.value; - const isEmpty = !master?.slaves?.length && (isEqual(slave, {}) || !slave); - if (!this.basicFormGroup.valid || isEmpty) { - return { - basicFormGroup: {valid: false} - }; + private updateSlaveEnabling(isEnabled: boolean): void { + if (isEnabled) { + this.basicFormGroup.get('slave').enable({emitEvent: false}); + } else { + this.basicFormGroup.get('slave').disable({emitEvent: false}); } - return null; } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-master-table/modbus-master-table.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-master-table/modbus-master-table.component.ts index 75fc62ed5e..5f51f59284 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-master-table/modbus-master-table.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-master-table/modbus-master-table.component.ts @@ -34,8 +34,12 @@ import { ControlValueAccessor, FormArray, FormBuilder, + NG_VALIDATORS, NG_VALUE_ACCESSOR, + UntypedFormControl, UntypedFormGroup, + ValidationErrors, + Validator, } from '@angular/forms'; import { ModbusMasterConfig, @@ -59,11 +63,16 @@ import { TbTableDatasource } from '@shared/components/table/table-datasource.abs useExisting: forwardRef(() => ModbusMasterTableComponent), multi: true }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => ModbusMasterTableComponent), + multi: true + } ], standalone: true, imports: [CommonModule, SharedModule] }) -export class ModbusMasterTableComponent implements ControlValueAccessor, AfterViewInit, OnInit, OnDestroy { +export class ModbusMasterTableComponent implements ControlValueAccessor, Validator, AfterViewInit, OnInit, OnDestroy { @ViewChild('searchInput') searchInputField: ElementRef; @@ -130,6 +139,12 @@ export class ModbusMasterTableComponent implements ControlValueAccessor, AfterVi this.pushDataAsFormArrays(master.slaves); } + validate(masterControl: UntypedFormControl): ValidationErrors | null { + return masterControl.parent.get('slave').enabled || this.slaves.controls.length ? null : { + slavesFormGroup: {valid: false} + }; + } + enterFilterMode(): void { this.textSearchMode = true; this.cdr.detectChanges(); diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-config/modbus-slave-config.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-config/modbus-slave-config.component.html index c4986eefce..a46dd68edc 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-config/modbus-slave-config.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-config/modbus-slave-config.component.html @@ -16,16 +16,6 @@ -->
-
-
{{ 'gateway.hints.modbus-server' | translate }}
-
- - - {{ 'gateway.enable' | translate }} - - -
-
gateway.server-slave-config
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-config/modbus-slave-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-config/modbus-slave-config.component.ts index 86b601de53..46771c6937 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-config/modbus-slave-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-config/modbus-slave-config.component.ts @@ -77,7 +77,6 @@ import { isEqual } from '@core/utils'; export class ModbusSlaveConfigComponent implements ControlValueAccessor, Validator, OnDestroy { slaveConfigFormGroup: UntypedFormGroup; - enableSlaveControl: FormControl; showSecurityControl: FormControl; ModbusProtocolLabelsMap = ModbusProtocolLabelsMap; ModbusMethodLabelsMap = ModbusMethodLabelsMap; @@ -90,6 +89,7 @@ export class ModbusSlaveConfigComponent implements ControlValueAccessor, Validat readonly ModbusProtocolType = ModbusProtocolType; readonly modbusBaudrates = ModbusBaudrates; + private isSlaveEnabled = false; private readonly serialSpecificControlKeys = ['serialPort', 'baudrate']; private readonly tcpUdpSpecificControlKeys = ['port', 'security', 'host']; @@ -100,7 +100,6 @@ export class ModbusSlaveConfigComponent implements ControlValueAccessor, Validat constructor(private fb: FormBuilder) { this.showSecurityControl = this.fb.control(false); - this.enableSlaveControl = this.fb.control(false); this.slaveConfigFormGroup = this.fb.group({ type: [ModbusProtocolType.TCP], host: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]], @@ -128,7 +127,6 @@ export class ModbusSlaveConfigComponent implements ControlValueAccessor, Validat this.observeValueChanges(); this.observeTypeChange(); this.observeShowSecurity(); - this.observeFormEnable(); } get protocolType(): ModbusProtocolType { @@ -155,11 +153,15 @@ export class ModbusSlaveConfigComponent implements ControlValueAccessor, Validat } writeValue(slaveConfig: ModbusSlave): void { - this.enableSlaveControl.patchValue(!!slaveConfig && !isEqual(slaveConfig, {})); this.showSecurityControl.patchValue(!!slaveConfig.security && !isEqual(slaveConfig.security, {})); this.updateSlaveConfig(slaveConfig); } + setDisabledState(isDisabled: boolean): void { + this.isSlaveEnabled = !isDisabled; + this.updateFormEnableState(); + } + private observeValueChanges(): void { this.slaveConfigFormGroup.valueChanges.pipe( takeUntil(this.destroy$) @@ -173,20 +175,11 @@ export class ModbusSlaveConfigComponent implements ControlValueAccessor, Validat }); } - private observeFormEnable(): void { - this.enableSlaveControl.valueChanges - .pipe(takeUntil(this.destroy$)) - .subscribe(value => { - this.updateFormEnableState(value); - this.slaveConfigFormGroup.updateValueAndValidity({emitEvent: !!this.onChange}); - }); - } - private observeTypeChange(): void { this.slaveConfigFormGroup.get('type').valueChanges .pipe(takeUntil(this.destroy$)) .subscribe(type => { - this.updateFormEnableState(this.enableSlaveControl.value); + this.updateFormEnableState(); this.updateMethodType(type); }); } @@ -202,8 +195,8 @@ export class ModbusSlaveConfigComponent implements ControlValueAccessor, Validat } } - private updateFormEnableState(enabled: boolean): void { - if (enabled) { + private updateFormEnableState(): void { + if (this.isSlaveEnabled) { this.slaveConfigFormGroup.enable({emitEvent: false}); this.showSecurityControl.enable({emitEvent: false}); } else { @@ -221,7 +214,7 @@ export class ModbusSlaveConfigComponent implements ControlValueAccessor, Validat } private updateSecurityEnable(securityEnabled: boolean): void { - if (securityEnabled && this.enableSlaveControl.value && this.protocolType !== ModbusProtocolType.Serial) { + if (securityEnabled && this.isSlaveEnabled && this.protocolType !== ModbusProtocolType.Serial) { this.slaveConfigFormGroup.get('security').enable({emitEvent: false}); } else { this.slaveConfigFormGroup.get('security').disable({emitEvent: false}); @@ -233,7 +226,7 @@ export class ModbusSlaveConfigComponent implements ControlValueAccessor, Validat const enableKeys = isSerial ? this.serialSpecificControlKeys : this.tcpUdpSpecificControlKeys; const disableKeys = isSerial ? this.tcpUdpSpecificControlKeys : this.serialSpecificControlKeys; - if (this.enableSlaveControl.value) { + if (this.isSlaveEnabled) { enableKeys.forEach(key => this.slaveConfigFormGroup.get(key)?.enable({ emitEvent: false })); } From 17dfc0bdb0adc5c8656f12e7d36704158a97312f Mon Sep 17 00:00:00 2001 From: mpetrov Date: Thu, 1 Aug 2024 12:52:15 +0300 Subject: [PATCH 26/48] refactoring --- .../modbus-basic-config.component.ts | 14 ++++++++++---- .../modbus-master-table.component.ts | 17 +---------------- 2 files changed, 11 insertions(+), 20 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-basic-config/modbus-basic-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-basic-config/modbus-basic-config.component.ts index 8291558b4d..3ed0ebf124 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-basic-config/modbus-basic-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-basic-config/modbus-basic-config.component.ts @@ -22,6 +22,7 @@ import { FormGroup, NG_VALIDATORS, NG_VALUE_ACCESSOR, + UntypedFormControl, ValidationErrors, Validator, } from '@angular/forms'; @@ -121,10 +122,15 @@ export class ModbusBasicConfigComponent implements ControlValueAccessor, Validat this.enableSlaveControl.setValue(!!basicConfig.slave && !isEqual(basicConfig.slave, {})); } - validate(): ValidationErrors | null { - return this.basicFormGroup.valid ? null : { - basicFormGroup: {valid: false} - }; + validate(basicFormControl: UntypedFormControl): ValidationErrors | null { + const { master, slave } = basicFormControl.value; + const isEmpty = !master?.slaves?.length && (isEqual(slave, {}) || !slave); + if (!this.basicFormGroup.valid || isEmpty) { + return { + basicFormGroup: {valid: false} + }; + } + return null; } private updateSlaveEnabling(isEnabled: boolean): void { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-master-table/modbus-master-table.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-master-table/modbus-master-table.component.ts index 5f51f59284..75fc62ed5e 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-master-table/modbus-master-table.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-master-table/modbus-master-table.component.ts @@ -34,12 +34,8 @@ import { ControlValueAccessor, FormArray, FormBuilder, - NG_VALIDATORS, NG_VALUE_ACCESSOR, - UntypedFormControl, UntypedFormGroup, - ValidationErrors, - Validator, } from '@angular/forms'; import { ModbusMasterConfig, @@ -63,16 +59,11 @@ import { TbTableDatasource } from '@shared/components/table/table-datasource.abs useExisting: forwardRef(() => ModbusMasterTableComponent), multi: true }, - { - provide: NG_VALIDATORS, - useExisting: forwardRef(() => ModbusMasterTableComponent), - multi: true - } ], standalone: true, imports: [CommonModule, SharedModule] }) -export class ModbusMasterTableComponent implements ControlValueAccessor, Validator, AfterViewInit, OnInit, OnDestroy { +export class ModbusMasterTableComponent implements ControlValueAccessor, AfterViewInit, OnInit, OnDestroy { @ViewChild('searchInput') searchInputField: ElementRef; @@ -139,12 +130,6 @@ export class ModbusMasterTableComponent implements ControlValueAccessor, Validat this.pushDataAsFormArrays(master.slaves); } - validate(masterControl: UntypedFormControl): ValidationErrors | null { - return masterControl.parent.get('slave').enabled || this.slaves.controls.length ? null : { - slavesFormGroup: {valid: false} - }; - } - enterFilterMode(): void { this.textSearchMode = true; this.cdr.detectChanges(); From a0656dc5b842258cbc55182b2f8ea90db234de06 Mon Sep 17 00:00:00 2001 From: mpetrov Date: Thu, 1 Aug 2024 12:58:00 +0300 Subject: [PATCH 27/48] refactoring --- .../modbus/modbus-basic-config/modbus-basic-config.component.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-basic-config/modbus-basic-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-basic-config/modbus-basic-config.component.ts index 3ed0ebf124..015fc97f1b 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-basic-config/modbus-basic-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-basic-config/modbus-basic-config.component.ts @@ -95,7 +95,6 @@ export class ModbusBasicConfigComponent implements ControlValueAccessor, Validat .subscribe(enable => { this.updateSlaveEnabling(enable); this.basicFormGroup.get('slave').updateValueAndValidity({emitEvent: !!this.onChange}); - this.basicFormGroup.get('master').updateValueAndValidity({emitEvent: !!this.onChange}); }); } From d203d819f8b2c73d8634c62e1ab13468e8514360 Mon Sep 17 00:00:00 2001 From: mpetrov Date: Thu, 1 Aug 2024 15:28:49 +0300 Subject: [PATCH 28/48] small typing adjustment --- .../modbus-master-table/modbus-master-table.component.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-master-table/modbus-master-table.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-master-table/modbus-master-table.component.ts index 14f3635b98..b3d316be23 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-master-table/modbus-master-table.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-master-table/modbus-master-table.component.ts @@ -43,6 +43,8 @@ import { import { ModbusMasterConfig, ModbusProtocolLabelsMap, + ModbusSlaveInfo, + ModbusValues, SlaveConfig } from '@home/components/widget/lib/gateway/gateway-widget.models'; import { isDefinedAndNotNull, isUndefinedOrNull } from '@core/utils'; @@ -164,7 +166,7 @@ export class ModbusMasterTableComponent implements ControlValueAccessor, Validat } const withIndex = isDefinedAndNotNull(index); const value = withIndex ? this.slaves.at(index).value : {}; - this.dialog.open(ModbusSlaveDialogComponent, { + this.dialog.open(ModbusSlaveDialogComponent, { disableClose: true, panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], data: { From e5484dd94177197ac13ee20088573e04c7c611d2 Mon Sep 17 00:00:00 2001 From: IrynaMatveieva Date: Thu, 1 Aug 2024 18:21:52 +0300 Subject: [PATCH 29/48] added test to verify usage of correct converter --- ...appingJackson2XmlHttpMessageConverter.java | 4 ++ .../system/RestTemplateConvertersTest.java | 61 +++++++++++++++++++ 2 files changed, 65 insertions(+) create mode 100644 application/src/test/java/org/thingsboard/server/system/RestTemplateConvertersTest.java diff --git a/application/src/main/java/org/springframework/http/converter/xml/MappingJackson2XmlHttpMessageConverter.java b/application/src/main/java/org/springframework/http/converter/xml/MappingJackson2XmlHttpMessageConverter.java index 0074526ebc..158f29a03a 100644 --- a/application/src/main/java/org/springframework/http/converter/xml/MappingJackson2XmlHttpMessageConverter.java +++ b/application/src/main/java/org/springframework/http/converter/xml/MappingJackson2XmlHttpMessageConverter.java @@ -27,6 +27,10 @@ import java.nio.charset.StandardCharsets; import java.util.Collections; import java.util.List; +/** + * RestTemplate firstly uses MappingJackson2XmlHttpMessageConverter converter instead of MappingJackson2HttpMessageConverter. + * It produces error UnsupportedMediaType, so this converter had to be shadowed for read and write operations to use the correct converter + */ public class MappingJackson2XmlHttpMessageConverter extends AbstractJackson2HttpMessageConverter { private static final List problemDetailMediaTypes; diff --git a/application/src/test/java/org/thingsboard/server/system/RestTemplateConvertersTest.java b/application/src/test/java/org/thingsboard/server/system/RestTemplateConvertersTest.java new file mode 100644 index 0000000000..5fde907bf1 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/system/RestTemplateConvertersTest.java @@ -0,0 +1,61 @@ +/** + * 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.system; + +import lombok.extern.slf4j.Slf4j; +import org.junit.Test; +import org.springframework.http.MediaType; +import org.springframework.mock.http.client.MockClientHttpRequest; +import org.springframework.test.web.client.MockRestServiceServer; +import org.springframework.util.ClassUtils; +import org.springframework.web.client.RestTemplate; + +import java.nio.charset.StandardCharsets; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.test.web.client.match.MockRestRequestMatchers.requestTo; +import static org.springframework.test.web.client.response.MockRestResponseCreators.withSuccess; + + +@Slf4j +public class RestTemplateConvertersTest { + + @Test + public void testMappingJackson2HttpMessageConverterIsUsedInsteadOfMappingJackson2XmlHttpMessageConverter() { + ClassLoader classLoader = RestTemplate.class.getClassLoader(); + boolean jackson2XmlPresent = ClassUtils.isPresent("com.fasterxml.jackson.dataformat.xml.XmlMapper", classLoader); + assertThat(jackson2XmlPresent).isTrue(); + + RestTemplate restTemplate = new RestTemplate(); + MockRestServiceServer mockServer = MockRestServiceServer.createServer(restTemplate); + mockServer.expect(requestTo("/test")) + .andExpect(request -> { + MockClientHttpRequest mockRequest = (MockClientHttpRequest) request; + byte[] body = mockRequest.getBodyAsBytes(); + String requestBody = new String(body, StandardCharsets.UTF_8); + assertThat(requestBody).contains("{\"name\":\"test\",\"value\":1}"); + }) + .andRespond(withSuccess("{\"name\":\"test\",\"value\":1}", MediaType.APPLICATION_JSON)); + + TestObject requestObject = new TestObject("test", 1); + TestObject actualObject = restTemplate.postForObject("/test", requestObject, TestObject.class); + assertThat(actualObject).isEqualTo(requestObject); + mockServer.verify(); + } + + record TestObject(String name, int value) {} + +} From cfca80defe71af0188e20046d7866697d1a84659 Mon Sep 17 00:00:00 2001 From: Volodymyr Babak Date: Fri, 2 Aug 2024 12:48:13 +0300 Subject: [PATCH 30/48] Remove Edge request messages - send multiple data messages in single rpc message --- .../server/controller/EdgeController.java | 20 +++++++++++++- .../service/edge/rpc/EdgeGrpcService.java | 26 ++++++++++++++----- .../service/edge/rpc/EdgeGrpcSession.java | 17 +++++++++--- .../service/edge/rpc/EdgeRpcService.java | 2 ++ .../service/edge/rpc/EdgeSyncCursor.java | 19 ++++++-------- .../edge/rpc/processor/BaseEdgeProcessor.java | 2 +- .../processor/device/DeviceEdgeProcessor.java | 24 +++++------------ .../rule/RuleChainEdgeProcessor.java | 15 ++++++++--- .../rpc/processor/user/UserEdgeProcessor.java | 12 ++++++--- .../common/data/edge/EdgeEventActionType.java | 2 +- .../common/msg/edge/FromEdgeSyncResponse.java | 3 ++- .../server/common/util/ProtoUtils.java | 3 ++- common/proto/src/main/proto/queue.proto | 1 + .../server/common/util/ProtoUtilsTest.java | 2 +- .../thingsboard/rest/client/RestClient.java | 4 +++ 15 files changed, 102 insertions(+), 50 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/controller/EdgeController.java b/application/src/main/java/org/thingsboard/server/controller/EdgeController.java index 33b6045248..011f2d6614 100644 --- a/application/src/main/java/org/thingsboard/server/controller/EdgeController.java +++ b/application/src/main/java/org/thingsboard/server/controller/EdgeController.java @@ -490,7 +490,25 @@ public class EdgeController extends BaseController { if (fromEdgeSyncResponse.isSuccess()) { response.setResult(new ResponseEntity<>(HttpStatus.OK)); } else { - response.setErrorResult(new ThingsboardException("Edge is not connected", ThingsboardErrorCode.GENERAL)); + response.setErrorResult(new ThingsboardException(fromEdgeSyncResponse.getError(), ThingsboardErrorCode.GENERAL)); + } + } + + @ApiOperation(value = "Is edge sync process is active (isEdgeSyncProcessActive)", + notes = "Returns 'true' if edge is currently in sync process, 'false' - otherwise." + TENANT_AUTHORITY_PARAGRAPH) + @PreAuthorize("hasAuthority('TENANT_ADMIN')") + @GetMapping(value = "/edge/sync/{edgeId}/active") + public Boolean isEdgeSyncProcessActive( + @Parameter(description = EDGE_ID_PARAM_DESCRIPTION, required = true) + @PathVariable("edgeId") String strEdgeId) throws ThingsboardException { + checkParameter("edgeId", strEdgeId); + if (isEdgesEnabled() && edgeRpcServiceOpt.isPresent()) { + EdgeId edgeId = new EdgeId(toUUID(strEdgeId)); + edgeId = checkNotNull(edgeId); + Edge edge = checkEdgeId(edgeId, Operation.READ); + return edgeRpcServiceOpt.get().isEdgeSyncProcessActive(edge.getTenantId(), edge.getId()); + } else { + throw new ThingsboardException("Edges support disabled", ThingsboardErrorCode.GENERAL); } } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcService.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcService.java index 04e6fe2185..19b70f5e87 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcService.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcService.java @@ -292,17 +292,31 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i session.startSyncProcess(true); success = true; } - clusterService.pushEdgeSyncResponseToCore(new FromEdgeSyncResponse(requestId, tenantId, edgeId, success)); + clusterService.pushEdgeSyncResponseToCore(new FromEdgeSyncResponse(requestId, tenantId, edgeId, success, null)); } } @Override public void processSyncRequest(ToEdgeSyncRequest request, Consumer responseConsumer) { - log.trace("[{}][{}] Processing sync edge request [{}]", request.getTenantId(), request.getId(), request.getEdgeId()); UUID requestId = request.getId(); - localSyncEdgeRequests.put(requestId, responseConsumer); - clusterService.pushEdgeSyncRequestToCore(request); - scheduleSyncRequestTimeout(request, requestId); + EdgeGrpcSession session = sessions.get(request.getEdgeId()); + if (!session.isSyncCompleted()) { + responseConsumer.accept(new FromEdgeSyncResponse(requestId, request.getTenantId(), request.getEdgeId(), false, "Sync process is active at the moment")); + } else { + log.trace("[{}][{}] Processing sync edge request [{}]", request.getTenantId(), request.getId(), request.getEdgeId()); + localSyncEdgeRequests.put(requestId, responseConsumer); + clusterService.pushEdgeSyncRequestToCore(request); + scheduleSyncRequestTimeout(request, requestId); + } + } + + @Override + public Boolean isEdgeSyncProcessActive(TenantId tenantId, EdgeId edgeId) { + EdgeGrpcSession session = sessions.get(edgeId); + if (session == null) { + return false; + } + return !session.isSyncCompleted(); } private void scheduleSyncRequestTimeout(ToEdgeSyncRequest request, UUID requestId) { @@ -312,7 +326,7 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i Consumer consumer = localSyncEdgeRequests.remove(requestId); if (consumer != null) { log.trace("[{}] timeout for processing sync edge request.", requestId); - consumer.accept(new FromEdgeSyncResponse(requestId, request.getTenantId(), request.getEdgeId(), false)); + consumer.accept(new FromEdgeSyncResponse(requestId, request.getTenantId(), request.getEdgeId(), false, "Edge is not connected")); } }, 20, TimeUnit.SECONDS); } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java index 55e8e051ab..1e84caeadf 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java @@ -247,6 +247,7 @@ public final class EdgeGrpcSession implements Closeable { } }, ctx.getGrpcCallbackExecutorService()); } else { + log.info("[{}][{}] sync process completed", this.tenantId, edge.getId()); DownlinkMsg syncCompleteDownlinkMsg = DownlinkMsg.newBuilder() .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) .setSyncCompletedMsg(SyncCompletedMsg.newBuilder().build()) @@ -325,7 +326,11 @@ public final class EdgeGrpcSession implements Closeable { } private void sendDownlinkMsg(ResponseMsg downlinkMsg) { - log.trace("[{}][{}] Sending downlink msg [{}]", this.tenantId, this.sessionId, downlinkMsg); + if (downlinkMsg.getDownlinkMsg().getWidgetTypeUpdateMsgCount() > 0) { + log.trace("[{}][{}] Sending downlink widgetTypeUpdateMsg, downlinkMsgId = {}", this.tenantId, this.sessionId, downlinkMsg.getDownlinkMsg().getDownlinkMsgId()); + } else { + log.trace("[{}][{}] Sending downlink msg [{}]", this.tenantId, this.sessionId, downlinkMsg); + } if (isConnected()) { downlinkMsgLock.lock(); try { @@ -337,7 +342,7 @@ public final class EdgeGrpcSession implements Closeable { } finally { downlinkMsgLock.unlock(); } - log.trace("[{}][{}] Response msg successfully sent [{}]", this.tenantId, this.sessionId, downlinkMsg); + log.trace("[{}][{}] Response msg successfully sent. downlinkMsgId = {}", this.tenantId, this.sessionId, downlinkMsg.getDownlinkMsg().getDownlinkMsgId()); } } @@ -551,9 +556,13 @@ public final class EdgeGrpcSession implements Closeable { DownlinkMsg downlinkMsg = null; try { switch (edgeEvent.getAction()) { - case UPDATED, ADDED, DELETED, ASSIGNED_TO_EDGE, UNASSIGNED_FROM_EDGE, ALARM_ACK, ALARM_CLEAR, ALARM_DELETE, CREDENTIALS_UPDATED, RELATION_ADD_OR_UPDATE, RELATION_DELETED, CREDENTIALS_REQUEST, RPC_CALL, ASSIGNED_TO_CUSTOMER, UNASSIGNED_FROM_CUSTOMER, ADDED_COMMENT, UPDATED_COMMENT, DELETED_COMMENT -> { + case UPDATED, ADDED, DELETED, ASSIGNED_TO_EDGE, UNASSIGNED_FROM_EDGE, ALARM_ACK, ALARM_CLEAR, ALARM_DELETE, CREDENTIALS_UPDATED, RELATION_ADD_OR_UPDATE, RELATION_DELETED, RPC_CALL, ASSIGNED_TO_CUSTOMER, UNASSIGNED_FROM_CUSTOMER, ADDED_COMMENT, UPDATED_COMMENT, DELETED_COMMENT -> { downlinkMsg = convertEntityEventToDownlink(edgeEvent); - log.trace("[{}][{}] entity message processed [{}]", this.tenantId, this.sessionId, downlinkMsg); + if (downlinkMsg != null && downlinkMsg.getWidgetTypeUpdateMsgCount() > 0) { + log.trace("[{}][{}] widgetTypeUpdateMsg message processed, downlinkMsgId = {}", this.tenantId, this.sessionId, downlinkMsg.getDownlinkMsgId()); + } else { + log.trace("[{}][{}] entity message processed [{}]", this.tenantId, this.sessionId, downlinkMsg); + } } case ATTRIBUTES_UPDATED, POST_ATTRIBUTES, ATTRIBUTES_DELETED, TIMESERIES_UPDATED -> downlinkMsg = ctx.getTelemetryProcessor().convertTelemetryEventToDownlink(edge, edgeEvent); diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeRpcService.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeRpcService.java index 12203db0c2..733d2e95cb 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeRpcService.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeRpcService.java @@ -33,4 +33,6 @@ public interface EdgeRpcService { void deleteEdge(TenantId tenantId, EdgeId edgeId); void processSyncRequest(ToEdgeSyncRequest request, Consumer responseConsumer); + + Boolean isEdgeSyncProcessActive(TenantId tenantId, EdgeId edgeId); } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeSyncCursor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeSyncCursor.java index 6321991586..125fd48463 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeSyncCursor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeSyncCursor.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.service.edge.rpc; +import lombok.Getter; import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.id.EntityId; @@ -53,6 +54,7 @@ public class EdgeSyncCursor { private final List fetchers = new LinkedList<>(); + @Getter private int currentIdx = 0; public EdgeSyncCursor(EdgeContextComponent ctx, Edge edge, boolean fullSync) { @@ -62,12 +64,12 @@ public class EdgeSyncCursor { fetchers.add(new RuleChainsEdgeEventFetcher(ctx.getRuleChainService())); fetchers.add(new AdminSettingsEdgeEventFetcher(ctx.getAdminSettingsService())); fetchers.add(new TenantAdminUsersEdgeEventFetcher(ctx.getUserService())); - Customer publicCustomer = ctx.getCustomerService().findOrCreatePublicCustomer(edge.getTenantId()); - fetchers.add(new CustomerEdgeEventFetcher(publicCustomer.getId())); - if (edge.getCustomerId() != null && !EntityId.NULL_UUID.equals(edge.getCustomerId().getId())) { - fetchers.add(new CustomerEdgeEventFetcher(edge.getCustomerId())); - fetchers.add(new CustomerUsersEdgeEventFetcher(ctx.getUserService(), edge.getCustomerId())); - } + } + Customer publicCustomer = ctx.getCustomerService().findOrCreatePublicCustomer(edge.getTenantId()); + fetchers.add(new CustomerEdgeEventFetcher(publicCustomer.getId())); + if (edge.getCustomerId() != null && !EntityId.NULL_UUID.equals(edge.getCustomerId().getId())) { + fetchers.add(new CustomerEdgeEventFetcher(edge.getCustomerId())); + fetchers.add(new CustomerUsersEdgeEventFetcher(ctx.getUserService(), edge.getCustomerId())); } fetchers.add(new DashboardsEdgeEventFetcher(ctx.getDashboardService())); fetchers.add(new DefaultProfilesEdgeEventFetcher(ctx.getDeviceProfileService(), ctx.getAssetProfileService())); @@ -102,9 +104,4 @@ public class EdgeSyncCursor { currentIdx++; return edgeEventFetcher; } - - public int getCurrentIdx() { - return currentIdx; - } - } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessor.java index f46c07ab5a..b1c1fc63c5 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessor.java @@ -373,7 +373,7 @@ public abstract class BaseEdgeProcessor { private boolean doSaveIfEdgeIsOffline(EdgeEventType type, EdgeEventActionType action) { return switch (action) { - case TIMESERIES_UPDATED, ALARM_ACK, ALARM_CLEAR, ALARM_ASSIGNED, ALARM_UNASSIGNED, CREDENTIALS_REQUEST, ADDED_COMMENT, UPDATED_COMMENT -> + case TIMESERIES_UPDATED, ALARM_ACK, ALARM_CLEAR, ALARM_ASSIGNED, ALARM_UNASSIGNED, ADDED_COMMENT, UPDATED_COMMENT -> true; default -> switch (type) { case ALARM, ALARM_COMMENT, RULE_CHAIN, RULE_CHAIN_METADATA, USER, CUSTOMER, TENANT, TENANT_PROFILE, WIDGETS_BUNDLE, WIDGET_TYPE, diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/DeviceEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/DeviceEdgeProcessor.java index 5e73034b1c..34a3a79acf 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/DeviceEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/DeviceEdgeProcessor.java @@ -43,7 +43,6 @@ import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.common.msg.rpc.FromDeviceRpcResponse; import org.thingsboard.server.common.msg.rpc.FromDeviceRpcResponseActorMsg; import org.thingsboard.server.dao.exception.DataValidationException; -import org.thingsboard.server.gen.edge.v1.DeviceCredentialsRequestMsg; import org.thingsboard.server.gen.edge.v1.DeviceCredentialsUpdateMsg; import org.thingsboard.server.gen.edge.v1.DeviceRpcCallMsg; import org.thingsboard.server.gen.edge.v1.DeviceUpdateMsg; @@ -70,7 +69,7 @@ public abstract class DeviceEdgeProcessor extends BaseDeviceProcessor implements case ENTITY_CREATED_RPC_MESSAGE: case ENTITY_UPDATED_RPC_MESSAGE: saveOrUpdateDevice(tenantId, deviceId, deviceUpdateMsg, edge); - return saveEdgeEvent(tenantId, edge.getId(), EdgeEventType.DEVICE, EdgeEventActionType.CREDENTIALS_REQUEST, deviceId, null); + return Futures.immediateFuture(null); case ENTITY_DELETED_RPC_MESSAGE: Device deviceToDelete = deviceService.findDeviceById(tenantId, deviceId); if (deviceToDelete != null) { @@ -232,6 +231,12 @@ public abstract class DeviceEdgeProcessor extends BaseDeviceProcessor implements DownlinkMsg.Builder builder = DownlinkMsg.newBuilder() .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) .addDeviceUpdateMsg(deviceUpdateMsg); + + DeviceCredentials deviceCredentials = deviceCredentialsService.findDeviceCredentialsByDeviceId(edgeEvent.getTenantId(), deviceId); + DeviceCredentialsUpdateMsg deviceCredentialsUpdateMsg = ((DeviceMsgConstructor) + deviceMsgConstructorFactory.getMsgConstructorByEdgeVersion(edgeVersion)).constructDeviceCredentialsUpdatedMsg(deviceCredentials); + builder.addDeviceCredentialsUpdateMsg(deviceCredentialsUpdateMsg).build(); + if (UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE.equals(msgType)) { DeviceProfile deviceProfile = deviceProfileService.findDeviceProfileById(edgeEvent.getTenantId(), device.getDeviceProfileId()); deviceProfile = checkIfDeviceProfileDefaultFieldsAssignedToEdge(edgeEvent.getTenantId(), edgeId, deviceProfile, edgeVersion); @@ -269,22 +274,7 @@ public abstract class DeviceEdgeProcessor extends BaseDeviceProcessor implements deviceMsgConstructorFactory.getMsgConstructorByEdgeVersion(edgeVersion)) .constructDeviceRpcCallMsg(edgeEvent.getEntityId(), edgeEvent.getBody())) .build(); - case CREDENTIALS_REQUEST: - return convertCredentialsRequestEventToDownlink(edgeEvent); } return downlinkMsg; } - - private DownlinkMsg convertCredentialsRequestEventToDownlink(EdgeEvent edgeEvent) { - DeviceId deviceId = new DeviceId(edgeEvent.getEntityId()); - DeviceCredentialsRequestMsg deviceCredentialsRequestMsg = DeviceCredentialsRequestMsg.newBuilder() - .setDeviceIdMSB(deviceId.getId().getMostSignificantBits()) - .setDeviceIdLSB(deviceId.getId().getLeastSignificantBits()) - .build(); - DownlinkMsg.Builder builder = DownlinkMsg.newBuilder() - .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) - .addDeviceCredentialsRequestMsg(deviceCredentialsRequestMsg); - return builder.build(); - } - } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/rule/RuleChainEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/rule/RuleChainEdgeProcessor.java index 30f74ae42d..f63aa8b829 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/rule/RuleChainEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/rule/RuleChainEdgeProcessor.java @@ -61,10 +61,19 @@ public class RuleChainEdgeProcessor extends BaseEdgeProcessor { RuleChainUpdateMsg ruleChainUpdateMsg = ((RuleChainMsgConstructor) ruleChainMsgConstructorFactory.getMsgConstructorByEdgeVersion(edgeVersion)) .constructRuleChainUpdatedMsg(msgType, ruleChain, isRoot); - downlinkMsg = DownlinkMsg.newBuilder() + + DownlinkMsg.Builder builder = DownlinkMsg.newBuilder() .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) - .addRuleChainUpdateMsg(ruleChainUpdateMsg) - .build(); + .addRuleChainUpdateMsg(ruleChainUpdateMsg); + + RuleChainMetaData ruleChainMetaData = ruleChainService.loadRuleChainMetaData(edgeEvent.getTenantId(), ruleChainId); + RuleChainMetadataUpdateMsg ruleChainMetadataUpdateMsg = ((RuleChainMsgConstructor) + ruleChainMsgConstructorFactory.getMsgConstructorByEdgeVersion(edgeVersion)) + .constructRuleChainMetadataUpdatedMsg(edgeEvent.getTenantId(), msgType, ruleChainMetaData, edgeVersion); + if (ruleChainMetadataUpdateMsg != null) { + builder.addRuleChainMetadataUpdateMsg(ruleChainMetadataUpdateMsg); + } + downlinkMsg = builder.build(); } } case DELETED, UNASSIGNED_FROM_EDGE -> downlinkMsg = DownlinkMsg.newBuilder() diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/user/UserEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/user/UserEdgeProcessor.java index 607c5e665c..cd072b4a1e 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/user/UserEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/user/UserEdgeProcessor.java @@ -43,10 +43,16 @@ public class UserEdgeProcessor extends BaseEdgeProcessor { User user = userService.findUserById(edgeEvent.getTenantId(), userId); if (user != null) { UpdateMsgType msgType = getUpdateMsgType(edgeEvent.getAction()); - downlinkMsg = DownlinkMsg.newBuilder() + DownlinkMsg.Builder builder = DownlinkMsg.newBuilder() .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) - .addUserUpdateMsg(((UserMsgConstructor) userMsgConstructorFactory.getMsgConstructorByEdgeVersion(edgeVersion)).constructUserUpdatedMsg(msgType, user)) - .build(); + .addUserUpdateMsg(((UserMsgConstructor) userMsgConstructorFactory.getMsgConstructorByEdgeVersion(edgeVersion)).constructUserUpdatedMsg(msgType, user)); + UserCredentials userCredentialsByUserId = userService.findUserCredentialsByUserId(edgeEvent.getTenantId(), userId); + if (userCredentialsByUserId != null && userCredentialsByUserId.isEnabled()) { + UserCredentialsUpdateMsg userCredentialsUpdateMsg = + ((UserMsgConstructor) userMsgConstructorFactory.getMsgConstructorByEdgeVersion(edgeVersion)).constructUserCredentialsUpdatedMsg(userCredentialsByUserId); + builder.addUserCredentialsUpdateMsg(userCredentialsUpdateMsg); + } + downlinkMsg = builder.build(); } } case DELETED -> downlinkMsg = DownlinkMsg.newBuilder() diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/edge/EdgeEventActionType.java b/common/data/src/main/java/org/thingsboard/server/common/data/edge/EdgeEventActionType.java index 7bee0c6094..176de62f69 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/edge/EdgeEventActionType.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/edge/EdgeEventActionType.java @@ -43,7 +43,7 @@ public enum EdgeEventActionType { DELETED_COMMENT(ActionType.DELETED_COMMENT), ASSIGNED_TO_EDGE(ActionType.ASSIGNED_TO_EDGE), UNASSIGNED_FROM_EDGE(ActionType.UNASSIGNED_FROM_EDGE), - CREDENTIALS_REQUEST(null), + CREDENTIALS_REQUEST(null), // deprecated ENTITY_MERGE_REQUEST(null); // deprecated private final ActionType actionType; diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/edge/FromEdgeSyncResponse.java b/common/message/src/main/java/org/thingsboard/server/common/msg/edge/FromEdgeSyncResponse.java index 733129b4de..baf483310c 100644 --- a/common/message/src/main/java/org/thingsboard/server/common/msg/edge/FromEdgeSyncResponse.java +++ b/common/message/src/main/java/org/thingsboard/server/common/msg/edge/FromEdgeSyncResponse.java @@ -25,12 +25,13 @@ import java.util.UUID; @Data public class FromEdgeSyncResponse implements EdgeSessionMsg { - private static final long serialVersionUID = -6360890886315347486L; + private static final long serialVersionUID = -6360890556315667486L; private final UUID id; private final TenantId tenantId; private final EdgeId edgeId; private final boolean success; + private final String error; @Override public MsgType getMsgType() { diff --git a/common/proto/src/main/java/org/thingsboard/server/common/util/ProtoUtils.java b/common/proto/src/main/java/org/thingsboard/server/common/util/ProtoUtils.java index 3c4aefd4f3..a5e0712936 100644 --- a/common/proto/src/main/java/org/thingsboard/server/common/util/ProtoUtils.java +++ b/common/proto/src/main/java/org/thingsboard/server/common/util/ProtoUtils.java @@ -167,7 +167,8 @@ public class ProtoUtils { new UUID(proto.getResponseIdMSB(), proto.getResponseIdLSB()), TenantId.fromUUID(new UUID(proto.getTenantIdMSB(), proto.getTenantIdLSB())), new EdgeId(new UUID(proto.getEdgeIdMSB(), proto.getEdgeIdLSB())), - proto.getSuccess() + proto.getSuccess(), + proto.getError() ); } diff --git a/common/proto/src/main/proto/queue.proto b/common/proto/src/main/proto/queue.proto index 50a6140d8b..0ebbc6ea03 100644 --- a/common/proto/src/main/proto/queue.proto +++ b/common/proto/src/main/proto/queue.proto @@ -1148,6 +1148,7 @@ message FromEdgeSyncResponseMsgProto { int64 edgeIdMSB = 5; int64 edgeIdLSB = 6; bool success = 7; + string error = 8; } message DeviceEdgeUpdateMsgProto { diff --git a/common/proto/src/test/java/org/thingsboard/server/common/util/ProtoUtilsTest.java b/common/proto/src/test/java/org/thingsboard/server/common/util/ProtoUtilsTest.java index 904e85f702..7852457277 100644 --- a/common/proto/src/test/java/org/thingsboard/server/common/util/ProtoUtilsTest.java +++ b/common/proto/src/test/java/org/thingsboard/server/common/util/ProtoUtilsTest.java @@ -124,7 +124,7 @@ class ProtoUtilsTest { @Test void protoFromEdgeSyncResponseSerialization() { - FromEdgeSyncResponse msg = new FromEdgeSyncResponse(id, tenantId, edgeId, true); + FromEdgeSyncResponse msg = new FromEdgeSyncResponse(id, tenantId, edgeId, true, null); assertThat(ProtoUtils.fromProto(ProtoUtils.toProto(msg))).as("deserialized").isEqualTo(msg); } diff --git a/rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java b/rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java index 8783770be3..e650db9608 100644 --- a/rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java +++ b/rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java @@ -2950,6 +2950,10 @@ public class RestClient implements Closeable { return restTemplate.getForEntity(baseURL + "/api/edges/enabled", Boolean.class).getBody(); } + public Boolean isEdgeSyncProcessActive(EdgeId edgeId) { + return restTemplate.getForEntity(baseURL + "/api/edge/sync/" + edgeId.getId() + "/active", Boolean.class).getBody(); + } + public Edge saveEdge(Edge edge) { return restTemplate.postForEntity(baseURL + "/api/edge", edge, Edge.class).getBody(); } From 61b7bfd849d0dd0503ed9e096c1860f9e8ecb5d2 Mon Sep 17 00:00:00 2001 From: Volodymyr Babak Date: Fri, 2 Aug 2024 14:41:36 +0300 Subject: [PATCH 31/48] Fixed FromEdgeSyncResponse in ProtoUtils --- .../thingsboard/server/service/edge/rpc/EdgeGrpcService.java | 2 +- .../java/org/thingsboard/server/common/util/ProtoUtils.java | 1 + .../java/org/thingsboard/server/common/util/ProtoUtilsTest.java | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcService.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcService.java index 19b70f5e87..69495f19bc 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcService.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcService.java @@ -292,7 +292,7 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i session.startSyncProcess(true); success = true; } - clusterService.pushEdgeSyncResponseToCore(new FromEdgeSyncResponse(requestId, tenantId, edgeId, success, null)); + clusterService.pushEdgeSyncResponseToCore(new FromEdgeSyncResponse(requestId, tenantId, edgeId, success, "")); } } diff --git a/common/proto/src/main/java/org/thingsboard/server/common/util/ProtoUtils.java b/common/proto/src/main/java/org/thingsboard/server/common/util/ProtoUtils.java index a5e0712936..f9d4237fdc 100644 --- a/common/proto/src/main/java/org/thingsboard/server/common/util/ProtoUtils.java +++ b/common/proto/src/main/java/org/thingsboard/server/common/util/ProtoUtils.java @@ -159,6 +159,7 @@ public class ProtoUtils { .setEdgeIdMSB(response.getEdgeId().getId().getMostSignificantBits()) .setEdgeIdLSB(response.getEdgeId().getId().getLeastSignificantBits()) .setSuccess(response.isSuccess()) + .setError(response.getError()) .build(); } diff --git a/common/proto/src/test/java/org/thingsboard/server/common/util/ProtoUtilsTest.java b/common/proto/src/test/java/org/thingsboard/server/common/util/ProtoUtilsTest.java index 7852457277..a513d3e311 100644 --- a/common/proto/src/test/java/org/thingsboard/server/common/util/ProtoUtilsTest.java +++ b/common/proto/src/test/java/org/thingsboard/server/common/util/ProtoUtilsTest.java @@ -124,7 +124,7 @@ class ProtoUtilsTest { @Test void protoFromEdgeSyncResponseSerialization() { - FromEdgeSyncResponse msg = new FromEdgeSyncResponse(id, tenantId, edgeId, true, null); + FromEdgeSyncResponse msg = new FromEdgeSyncResponse(id, tenantId, edgeId, true, "Error Msg"); assertThat(ProtoUtils.fromProto(ProtoUtils.toProto(msg))).as("deserialized").isEqualTo(msg); } From 62f0ba573925f39206568148a2a65d039b2a63f1 Mon Sep 17 00:00:00 2001 From: Volodymyr Babak Date: Fri, 2 Aug 2024 15:20:46 +0300 Subject: [PATCH 32/48] Fixed DeviceEdgeProcessorTest --- .../rpc/processor/device/AbstractDeviceProcessorTest.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/application/src/test/java/org/thingsboard/server/service/edge/rpc/processor/device/AbstractDeviceProcessorTest.java b/application/src/test/java/org/thingsboard/server/service/edge/rpc/processor/device/AbstractDeviceProcessorTest.java index 0ec40fb908..31524e467a 100644 --- a/application/src/test/java/org/thingsboard/server/service/edge/rpc/processor/device/AbstractDeviceProcessorTest.java +++ b/application/src/test/java/org/thingsboard/server/service/edge/rpc/processor/device/AbstractDeviceProcessorTest.java @@ -30,6 +30,7 @@ import org.thingsboard.server.common.data.id.DeviceProfileId; import org.thingsboard.server.common.data.id.EdgeId; import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.security.DeviceCredentials; import org.thingsboard.server.gen.edge.v1.DeviceProfileUpdateMsg; import org.thingsboard.server.gen.edge.v1.DownlinkMsg; import org.thingsboard.server.service.edge.rpc.processor.BaseEdgeProcessorTest; @@ -61,6 +62,9 @@ public abstract class AbstractDeviceProcessorTest extends BaseEdgeProcessorTest deviceProfile.setProfileData(deviceProfileData); deviceProfile.setTransportType(DeviceTransportType.DEFAULT); + DeviceCredentials deviceCredentials = new DeviceCredentials(); + deviceCredentials.setDeviceId(deviceId); + Device device = new Device(); device.setDeviceProfileId(deviceProfileId); device.setId(deviceId); @@ -71,9 +75,9 @@ public abstract class AbstractDeviceProcessorTest extends BaseEdgeProcessorTest edgeEvent.setTenantId(tenantId); edgeEvent.setAction(EdgeEventActionType.ADDED); - willReturn(device).given(deviceService).findDeviceById(tenantId, deviceId); willReturn(deviceProfile).given(deviceProfileService).findDeviceProfileById(tenantId, deviceProfileId); + willReturn(deviceCredentials).given(deviceCredentialsService).findDeviceCredentialsByDeviceId(tenantId, deviceId); } protected void updateDeviceProfileDefaultFields(long expectedDashboardIdMSB, long expectedDashboardIdLSB, From 779cfea5ca26481a9c2b8be2aba40029c3ba6ce9 Mon Sep 17 00:00:00 2001 From: mpetrov Date: Fri, 2 Aug 2024 17:16:37 +0300 Subject: [PATCH 33/48] Added gateway connectors synced save adjustments --- .../mqtt-basic-config.component.ts | 18 ++++++++-- .../gateway/gateway-connectors.component.ts | 33 ++++++++----------- .../lib/gateway/gateway-widget.models.ts | 2 +- 3 files changed, 30 insertions(+), 23 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mqtt-basic-config/mqtt-basic-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mqtt-basic-config/mqtt-basic-config.component.ts index 8ae9fc6163..a70bf956a2 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mqtt-basic-config/mqtt-basic-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mqtt-basic-config/mqtt-basic-config.component.ts @@ -84,7 +84,7 @@ export class MqttBasicConfigComponent implements ControlValueAccessor, Validator mappingTypes = MappingType; basicFormGroup: FormGroup; - private onChange: (value: string) => void; + private onChange: (value: MQTTBasicConfig) => void; private onTouched: () => void; private destroy$ = new Subject(); @@ -100,7 +100,7 @@ export class MqttBasicConfigComponent implements ControlValueAccessor, Validator this.basicFormGroup.valueChanges .pipe(takeUntil(this.destroy$)) .subscribe(value => { - this.onChange(value); + this.onChange(this.getMappedMQTTConfig(value)); this.onTouched(); }); } @@ -110,7 +110,7 @@ export class MqttBasicConfigComponent implements ControlValueAccessor, Validator this.destroy$.complete(); } - registerOnChange(fn: (value: string) => void): void { + registerOnChange(fn: (value: MQTTBasicConfig) => void): void { this.onChange = fn; } @@ -135,6 +135,18 @@ export class MqttBasicConfigComponent implements ControlValueAccessor, Validator this.basicFormGroup.setValue(editedBase, {emitEvent: false}); } + private getMappedMQTTConfig(basicConfig: MQTTBasicConfig): MQTTBasicConfig { + const { broker, workers, dataMapping, requestsMapping } = basicConfig || {}; + return workers ? { + dataMapping, + requestsMapping, + broker: { + ...broker, + ...workers, + } + } : basicConfig; + } + validate(): ValidationErrors | null { return this.basicFormGroup.valid ? null : { basicFormGroup: {valid: false} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.ts index e4c5360cdf..ebe5ebd646 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.ts @@ -60,7 +60,7 @@ import { } from './gateway-widget.models'; import { MatDialog } from '@angular/material/dialog'; import { AddConnectorDialogComponent } from '@home/components/widget/lib/gateway/dialog/add-connector-dialog.component'; -import { debounceTime, take, takeUntil, tap } from 'rxjs/operators'; +import { debounceTime, filter, take, takeUntil, tap } from 'rxjs/operators'; import { ErrorStateMatcher } from '@angular/material/core'; import { PageData } from '@shared/models/page/page-data'; @@ -133,6 +133,8 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie private basicConfigSub: Subscription; + private connectorUpdateInProgress = false; + private subscriptionOptions: WidgetSubscriptionOptions = { callbacks: { onDataUpdated: () => this.ctx.ngZone.run(() => { @@ -210,6 +212,7 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie }); this.connectorForm.get('configurationJson').valueChanges.pipe( + filter(() => !this.connectorUpdateInProgress), takeUntil(this.destroy$) ).subscribe((config) => { const basicConfig = this.connectorForm.get('basicConfig'); @@ -275,7 +278,7 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie } saveConnector(): void { - const value = this.connectorForm.get('type').value === ConnectorType.MQTT ? this.getMappedMQTTValue() : this.connectorForm.value; + const value = { ...this.connectorForm.value }; value.configuration = camelCase(value.name) + '.json'; delete value.basicConfig; if (value.type !== ConnectorType.GRPC) { @@ -336,20 +339,6 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie }); } - private getMappedMQTTValue(): GatewayConnector { - const value = this.connectorForm.value; - return { - ...value, - configurationJson: { - ...value.configurationJson, - broker: { - ...value.configurationJson.broker, - ...value.configurationJson.workers, - } - } - }; - } - private updateData(reload: boolean = false): void { this.pageLink.sortOrder.property = this.sort.active; this.pageLink.sortOrder.direction = Direction[this.sort.direction.toUpperCase()]; @@ -383,7 +372,9 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie } const sharedIndex = this.sharedAttributeData.findIndex(data => { const sharedData = data.value; - return sharedData.name === connectorData.name && sharedData.ts && sharedData.ts <= connectorData.ts; + return sharedData.name === connectorData.name + && sharedData.ts && sharedData.ts <= connectorData.ts + && isEqual(sharedData.value, connectorData.value); }); return sharedIndex !== -1; } @@ -684,6 +675,7 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie this.basicConfigSub.unsubscribe(); } this.basicConfigSub = this.connectorForm.get('basicConfig').valueChanges.pipe( + filter(() => !this.connectorUpdateInProgress), takeUntil(this.destroy$) ).subscribe((config) => { const configJson = this.connectorForm.get('configurationJson'); @@ -734,13 +726,16 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie case ConnectorType.MQTT: case ConnectorType.OPCUA: case ConnectorType.MODBUS: - this.connectorForm.get('type').patchValue(connector.type, {emitValue: false, onlySelf: true}); - this.connectorForm.get('basicConfig').setValue({}, {emitEvent: false}); + this.connectorUpdateInProgress = true; + if (connector.name !== this.connectorForm.value.name) { + this.connectorForm.get('basicConfig').setValue({}, {emitEvent: false}); + } setTimeout(() => { this.connectorForm.patchValue({...connector, mode: connector.mode || ConnectorConfigurationModes.BASIC}); this.createBasicConfigWatcher(); this.connectorForm.markAsPristine(); + this.connectorUpdateInProgress = false; }); break; default: diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-widget.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-widget.models.ts index 8df3994ea2..7878296039 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-widget.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-widget.models.ts @@ -182,7 +182,7 @@ export interface MQTTBasicConfig { dataMapping: ConverterConnectorMapping[]; requestsMapping: Record | RequestMappingData[]; broker: BrokerConfig; - workers: WorkersConfig; + workers?: WorkersConfig; } export interface OPCBasicConfig { From 560708328ee0376d43865b2a3121def076981a63 Mon Sep 17 00:00:00 2001 From: dashevchenko Date: Fri, 2 Aug 2024 17:28:26 +0300 Subject: [PATCH 34/48] extracted some methods to CoapEfentoUtils, added tests --- .../efento/CoapEfentoTransportResource.java | 64 ++++---- .../coap/efento/utils/CoapEfentoUtils.java | 13 ++ .../CoapEfentTransportResourceTest.java | 144 ++++++++++++++++++ 3 files changed, 183 insertions(+), 38 deletions(-) create mode 100644 common/transport/coap/src/test/java/org/thingsboard/server/transport/coap/efento/CoapEfentTransportResourceTest.java diff --git a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/efento/CoapEfentoTransportResource.java b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/efento/CoapEfentoTransportResource.java index 6e481cc21d..562a633920 100644 --- a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/efento/CoapEfentoTransportResource.java +++ b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/efento/CoapEfentoTransportResource.java @@ -28,18 +28,17 @@ import org.eclipse.californium.core.network.Exchange; import org.eclipse.californium.core.server.resources.CoapExchange; import org.eclipse.californium.core.server.resources.Resource; import org.springframework.util.CollectionUtils; +import org.thingsboard.server.common.adaptor.AdaptorException; import org.thingsboard.server.common.adaptor.ProtoConverter; import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.DeviceTransportType; import org.thingsboard.server.common.data.device.profile.CoapDeviceProfileTransportConfiguration; import org.thingsboard.server.common.data.device.profile.DeviceProfileTransportConfiguration; import org.thingsboard.server.common.data.device.profile.EfentoCoapDeviceTypeConfiguration; -import org.thingsboard.server.common.adaptor.AdaptorException; import org.thingsboard.server.common.transport.auth.SessionInfoCreator; import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.gen.transport.coap.ConfigProtos; import org.thingsboard.server.gen.transport.coap.DeviceInfoProtos; -import org.thingsboard.server.gen.transport.coap.MeasurementTypeProtos; import org.thingsboard.server.gen.transport.coap.MeasurementsProtos; import org.thingsboard.server.gen.transport.coap.MeasurementsProtos.ProtoChannel; import org.thingsboard.server.transport.coap.AbstractCoapTransportResource; @@ -59,13 +58,12 @@ import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; import static com.google.gson.JsonParser.parseString; -import static org.thingsboard.server.gen.transport.coap.MeasurementTypeProtos.MeasurementType.MEASUREMENT_TYPE_FLOODING; -import static org.thingsboard.server.gen.transport.coap.MeasurementTypeProtos.MeasurementType.MEASUREMENT_TYPE_OK_ALARM; -import static org.thingsboard.server.gen.transport.coap.MeasurementTypeProtos.MeasurementType.MEASUREMENT_TYPE_OUTPUT_CONTROL; import static org.thingsboard.server.transport.coap.CoapTransportService.CONFIGURATION; import static org.thingsboard.server.transport.coap.CoapTransportService.CURRENT_TIMESTAMP; import static org.thingsboard.server.transport.coap.CoapTransportService.DEVICE_INFO; import static org.thingsboard.server.transport.coap.CoapTransportService.MEASUREMENTS; +import static org.thingsboard.server.transport.coap.efento.utils.CoapEfentoUtils.isBinarySensor; +import static org.thingsboard.server.transport.coap.efento.utils.CoapEfentoUtils.isSensorError; @Slf4j public class CoapEfentoTransportResource extends AbstractCoapTransportResource { @@ -224,7 +222,7 @@ public class CoapEfentoTransportResource extends AbstractCoapTransportResource { } } - private List getEfentoMeasurements(MeasurementsProtos.ProtoMeasurements protoMeasurements, UUID sessionId) { + public List getEfentoMeasurements(MeasurementsProtos.ProtoMeasurements protoMeasurements, UUID sessionId) { String serialNumber = CoapEfentoUtils.convertByteArrayToString(protoMeasurements.getSerialNum().toByteArray()); boolean batteryStatus = protoMeasurements.getBatteryStatus(); int measurementPeriodBase = protoMeasurements.getMeasurementPeriodBase(); @@ -240,17 +238,16 @@ public class CoapEfentoTransportResource extends AbstractCoapTransportResource { Map valuesMap = new TreeMap<>(); for (int channel = 0; channel < channelsList.size(); channel++) { ProtoChannel protoChannel = channelsList.get(channel); - boolean isBinarySensor = isBinarySensor(protoChannel.getType()); - int channelPeriodFactor = (measurementPeriodFactor == 0 ? (isBinarySensor ? 14 : 1) : measurementPeriodFactor); - int measurementPeriod = measurementPeriodBase * channelPeriodFactor; - long measurementPeriodMillis = TimeUnit.SECONDS.toMillis(measurementPeriod); - long startTimestampMillis = TimeUnit.SECONDS.toMillis(protoChannel.getTimestamp()); List sampleOffsetsList = protoChannel.getSampleOffsetsList(); - if (CollectionUtils.isEmpty(sampleOffsetsList)) { log.trace("[{}][{}] sampleOffsetsList list is empty!", sessionId, protoChannel.getType().name()); continue; } + boolean isBinarySensor = isBinarySensor(protoChannel.getType()); + int channelPeriodFactor = (measurementPeriodFactor == 0 ? (isBinarySensor ? 14 : 1) : measurementPeriodFactor); + int measurementPeriod = measurementPeriodBase * channelPeriodFactor; + long measurementPeriodMillis = TimeUnit.SECONDS.toMillis(measurementPeriod); + long startTimestampMillis = TimeUnit.SECONDS.toMillis(protoChannel.getTimestamp()); for (int i = 0; i < sampleOffsetsList.size(); i++) { int sampleOffset = sampleOffsetsList.get(i); @@ -261,17 +258,19 @@ public class CoapEfentoTransportResource extends AbstractCoapTransportResource { JsonObject values; if (isBinarySensor) { + boolean currentIsOk = sampleOffset < 0; + Integer previousSampleOffset = i > 0 ? sampleOffsetsList.get(i - 1) : null; + if (previousSampleOffset != null) { //compare with previous value + boolean previousIsOk = previousSampleOffset < 0; + if (currentIsOk == previousIsOk) { + break; + } + } long sampleOffsetMillis = TimeUnit.SECONDS.toMillis(sampleOffset); - long measurementTimestamp = startTimestampMillis + Math.abs(sampleOffsetMillis) - 1; + long measurementTimestamp = startTimestampMillis + Math.abs(sampleOffsetMillis); values = valuesMap.computeIfAbsent(measurementTimestamp - 1000, k -> CoapEfentoUtils.setDefaultMeasurements(serialNumber, batteryStatus, measurementPeriod, nextTransmissionAtMillis, signal, k)); - Integer previousSampleOffset = i > 0 ? sampleOffsetsList.get(i - 1) : null; - boolean previousValueIsOk = previousSampleOffset != null && previousSampleOffset < 0; - boolean valueIsOk = sampleOffset < 0; - if (binaryValueNotChanged(valueIsOk, previousValueIsOk)) { - break; - } - addBinarySample(protoChannel, valueIsOk, values, channel + 1); + addBinarySample(protoChannel, currentIsOk, values, channel + 1, sessionId); } else { long timestampMillis = startTimestampMillis + i * measurementPeriodMillis; values = valuesMap.computeIfAbsent(timestampMillis, k -> CoapEfentoUtils.setDefaultMeasurements( @@ -290,14 +289,6 @@ public class CoapEfentoTransportResource extends AbstractCoapTransportResource { .collect(Collectors.toList()); } - private boolean isBinarySensor(MeasurementTypeProtos.MeasurementType type) { - return type == MEASUREMENT_TYPE_OK_ALARM || type == MEASUREMENT_TYPE_FLOODING || type == MEASUREMENT_TYPE_OUTPUT_CONTROL; - } - - private boolean isSensorError(int sampleOffset) { - return sampleOffset >= 8355840 && sampleOffset <= 8388607; - } - private void addContinuesSample(ProtoChannel protoChannel, int sampleOffset, JsonObject values, int channelNumber, UUID sessionId) { int startPoint = protoChannel.getStartPoint(); @@ -396,26 +387,23 @@ public class CoapEfentoTransportResource extends AbstractCoapTransportResource { } } - private void addBinarySample(ProtoChannel protoChannel, boolean valueIsOk, JsonObject values, int channel) { + private void addBinarySample(ProtoChannel protoChannel, boolean valueIsOk, JsonObject values, int channel, UUID sessionId) { switch (protoChannel.getType()) { case MEASUREMENT_TYPE_OK_ALARM: values.addProperty("ok_alarm_" + channel, valueIsOk ? "OK" : "ALARM"); + break; case MEASUREMENT_TYPE_FLOODING: values.addProperty("flooding_" + channel, valueIsOk ? "OK" : "WATER_DETECTED"); + break; case MEASUREMENT_TYPE_OUTPUT_CONTROL: values.addProperty("output_control_" + channel, valueIsOk ? "OFF" : "ON"); + break; + default: + log.trace("[{}],[{}] Unsupported binary measurementType! Ignoring.", sessionId, protoChannel.getType().name()); + break; } } - private static boolean binaryValueNotChanged(boolean currentIsOk, boolean previousIsOk) { - boolean isOk = previousIsOk && currentIsOk; - boolean isAlarm = !previousIsOk && !currentIsOk; - if (isOk || isAlarm) { - return true; - } - return false; - } - private EfentoTelemetry getEfentoDeviceInfo(DeviceInfoProtos.ProtoDeviceInfo protoDeviceInfo) { JsonObject values = new JsonObject(); values.addProperty("sw_version", protoDeviceInfo.getSwVersion()); diff --git a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/efento/utils/CoapEfentoUtils.java b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/efento/utils/CoapEfentoUtils.java index d87c97543e..96c1805be3 100644 --- a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/efento/utils/CoapEfentoUtils.java +++ b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/efento/utils/CoapEfentoUtils.java @@ -16,11 +16,16 @@ package org.thingsboard.server.transport.coap.efento.utils; import com.google.gson.JsonObject; +import org.thingsboard.server.gen.transport.coap.MeasurementTypeProtos.MeasurementType; import java.text.SimpleDateFormat; import java.util.Date; import java.util.TimeZone; +import static org.thingsboard.server.gen.transport.coap.MeasurementTypeProtos.MeasurementType.MEASUREMENT_TYPE_FLOODING; +import static org.thingsboard.server.gen.transport.coap.MeasurementTypeProtos.MeasurementType.MEASUREMENT_TYPE_OK_ALARM; +import static org.thingsboard.server.gen.transport.coap.MeasurementTypeProtos.MeasurementType.MEASUREMENT_TYPE_OUTPUT_CONTROL; + public class CoapEfentoUtils { public static String convertByteArrayToString(byte[] a) { @@ -50,4 +55,12 @@ public class CoapEfentoUtils { return values; } + public static boolean isBinarySensor(MeasurementType type) { + return type == MEASUREMENT_TYPE_OK_ALARM || type == MEASUREMENT_TYPE_FLOODING || type == MEASUREMENT_TYPE_OUTPUT_CONTROL; + } + + public static boolean isSensorError(int sampleOffset) { + return sampleOffset >= 8355840 && sampleOffset <= 8388607; + } + } diff --git a/common/transport/coap/src/test/java/org/thingsboard/server/transport/coap/efento/CoapEfentTransportResourceTest.java b/common/transport/coap/src/test/java/org/thingsboard/server/transport/coap/efento/CoapEfentTransportResourceTest.java new file mode 100644 index 0000000000..5faaaee1d4 --- /dev/null +++ b/common/transport/coap/src/test/java/org/thingsboard/server/transport/coap/efento/CoapEfentTransportResourceTest.java @@ -0,0 +1,144 @@ +/** + * 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.transport.coap.efento; + +import com.google.protobuf.ByteString; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.thingsboard.server.gen.transport.coap.MeasurementTypeProtos; +import org.thingsboard.server.gen.transport.coap.MeasurementsProtos; +import org.thingsboard.server.transport.coap.CoapTransportContext; + +import java.nio.ByteBuffer; +import java.time.Instant; +import java.util.List; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; + +class CoapEfentTransportResourceTest { + + private static CoapEfentoTransportResource coapEfentoTransportResource; + + @BeforeAll + static void setUp() { + var ctxMock = mock(CoapTransportContext.class); + coapEfentoTransportResource = new CoapEfentoTransportResource(ctxMock, "testName"); + } + + @Test + void checkContinuousSensor() { + long tsInSec = Instant.now().getEpochSecond(); + MeasurementsProtos.ProtoMeasurements measurements = MeasurementsProtos.ProtoMeasurements.newBuilder() + .setSerialNum(integerToByteString(1234)) + .setCloudToken("test_token") + .setMeasurementPeriodBase(180) + .setMeasurementPeriodFactor(1) + .setBatteryStatus(true) + .setSignal(0) + .setNextTransmissionAt(1000) + .setTransferReason(0) + .setHash(0) + .addAllChannels(List.of(MeasurementsProtos.ProtoChannel.newBuilder() + .setType(MeasurementTypeProtos.MeasurementType.MEASUREMENT_TYPE_TEMPERATURE) + .setTimestamp(Math.toIntExact(tsInSec)) + .addAllSampleOffsets(List.of(223, 224)) + .build(), + MeasurementsProtos.ProtoChannel.newBuilder() + .setType(MeasurementTypeProtos.MeasurementType.MEASUREMENT_TYPE_HUMIDITY) + .setTimestamp(Math.toIntExact(tsInSec)) + .addAllSampleOffsets(List.of(20, 30)) + .build() + )) + .build(); + List efentoMeasurements = coapEfentoTransportResource.getEfentoMeasurements(measurements, UUID.randomUUID()); + assertThat(efentoMeasurements).hasSize(2); + assertThat(efentoMeasurements.get(0).getTs()).isEqualTo(tsInSec * 1000); + assertThat(efentoMeasurements.get(0).getValues().getAsJsonObject().get("temperature_1").getAsDouble()).isEqualTo(22.3); + assertThat(efentoMeasurements.get(0).getValues().getAsJsonObject().get("humidity_2").getAsDouble()).isEqualTo(20); + assertThat(efentoMeasurements.get(1).getTs()).isEqualTo((tsInSec + 180) * 1000); + assertThat(efentoMeasurements.get(1).getValues().getAsJsonObject().get("temperature_1").getAsDouble()).isEqualTo(22.4); + assertThat(efentoMeasurements.get(1).getValues().getAsJsonObject().get("humidity_2").getAsDouble()).isEqualTo(30); + } + + @Test + void checkBinarySensor() { + long tsInSec = Instant.now().getEpochSecond(); + MeasurementsProtos.ProtoMeasurements measurements = MeasurementsProtos.ProtoMeasurements.newBuilder() + .setSerialNum(integerToByteString(1234)) + .setCloudToken("test_token") + .setMeasurementPeriodBase(180) + .setMeasurementPeriodFactor(1) + .setBatteryStatus(true) + .setSignal(0) + .setNextTransmissionAt(1000) + .setTransferReason(0) + .setHash(0) + .addChannels(MeasurementsProtos.ProtoChannel.newBuilder() + .setType(MeasurementTypeProtos.MeasurementType.MEASUREMENT_TYPE_OK_ALARM) + .setTimestamp(Math.toIntExact(tsInSec)) + .addAllSampleOffsets(List.of(1, 1)) + .build()) + .build(); + List efentoMeasurements = coapEfentoTransportResource.getEfentoMeasurements(measurements, UUID.randomUUID()); + assertThat(efentoMeasurements).hasSize(1); + assertThat(efentoMeasurements.get(0).getTs()).isEqualTo(tsInSec * 1000); + assertThat(efentoMeasurements.get(0).getValues().getAsJsonObject().get("ok_alarm_1").getAsString()).isEqualTo("ALARM"); + } + + @Test + void checkBinarySensorWhenValueIsVarying() { + long tsInSec = Instant.now().getEpochSecond(); + MeasurementsProtos.ProtoMeasurements measurements = MeasurementsProtos.ProtoMeasurements.newBuilder() + .setSerialNum(integerToByteString(1234)) + .setCloudToken("test_token") + .setMeasurementPeriodBase(180) + .setMeasurementPeriodFactor(1) + .setBatteryStatus(true) + .setSignal(0) + .setNextTransmissionAt(1000) + .setTransferReason(0) + .setHash(0) + .addChannels(MeasurementsProtos.ProtoChannel.newBuilder() + .setType(MeasurementTypeProtos.MeasurementType.MEASUREMENT_TYPE_OK_ALARM) + .setTimestamp(Math.toIntExact(tsInSec)) + .addAllSampleOffsets(List.of(1, -10)) + .build()) + .build(); + List efentoMeasurements = coapEfentoTransportResource.getEfentoMeasurements(measurements, UUID.randomUUID()); + assertThat(efentoMeasurements).hasSize(2); + assertThat(efentoMeasurements.get(0).getTs()).isEqualTo(tsInSec * 1000); + assertThat(efentoMeasurements.get(0).getValues().getAsJsonObject().get("ok_alarm_1").getAsString()).isEqualTo("ALARM"); + assertThat(efentoMeasurements.get(1).getTs()).isEqualTo((tsInSec + 9) * 1000); + assertThat(efentoMeasurements.get(1).getValues().getAsJsonObject().get("ok_alarm_1").getAsString()).isEqualTo("OK"); + } + + public static ByteString integerToByteString(Integer intValue) { + // Allocate a ByteBuffer with the size of an integer (4 bytes) + ByteBuffer buffer = ByteBuffer.allocate(Integer.BYTES); + + // Put the integer value into the ByteBuffer + buffer.putInt(intValue); + + // Convert the ByteBuffer to a byte array + byte[] byteArray = buffer.array(); + + // Create a ByteString from the byte array + return ByteString.copyFrom(byteArray); + } + +} From 9a239f2c2e132a82db18a7e5e4b459c6d6719047 Mon Sep 17 00:00:00 2001 From: mpetrov Date: Fri, 2 Aug 2024 17:51:13 +0300 Subject: [PATCH 35/48] isCisConnectorSynced adjustments --- .../widget/lib/gateway/gateway-connectors.component.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.ts index ebe5ebd646..0434e9e853 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.ts @@ -372,9 +372,11 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie } const sharedIndex = this.sharedAttributeData.findIndex(data => { const sharedData = data.value; - return sharedData.name === connectorData.name - && sharedData.ts && sharedData.ts <= connectorData.ts - && isEqual(sharedData.value, connectorData.value); + const hasSameName = sharedData.name === connectorData.name; + const hasEmptyConfig = isEqual(sharedData.configurationJson, {}) && hasSameName; + const hasSameConfig =isEqual(sharedData.configurationJson, connectorData.configurationJson); + const isRecentlyCreated = sharedData.ts && sharedData.ts <= connectorData.ts; + return hasSameName && isRecentlyCreated && (hasSameConfig || hasEmptyConfig); }); return sharedIndex !== -1; } From 4bbfb09ab41929fa6f07a06b8c143163da03ec84 Mon Sep 17 00:00:00 2001 From: mpetrov Date: Fri, 2 Aug 2024 18:34:23 +0300 Subject: [PATCH 36/48] shared directives adjustments --- ui-ngx/src/app/modules/common/modules-map.ts | 6 ++++++ ui-ngx/src/app/shared/directives/public-api.ts | 2 ++ .../shared/directives/truncate-with-tooltip.directive.ts | 2 -- ui-ngx/src/app/shared/public-api.ts | 1 + 4 files changed, 9 insertions(+), 2 deletions(-) create mode 100644 ui-ngx/src/app/shared/directives/public-api.ts diff --git a/ui-ngx/src/app/modules/common/modules-map.ts b/ui-ngx/src/app/modules/common/modules-map.ts index fcb4e0b411..c06863c6e8 100644 --- a/ui-ngx/src/app/modules/common/modules-map.ts +++ b/ui-ngx/src/app/modules/common/modules-map.ts @@ -99,6 +99,9 @@ import * as TbJsonPipe from '@shared/pipe/tbJson.pipe'; import * as TruncatePipe from '@shared/pipe/truncate.pipe'; import * as ImagePipe from '@shared/pipe/image.pipe'; +import * as EllipsisChipListDirective from '@shared/directives/ellipsis-chip-list.directive'; +import * as TruncateWithTooltipDirective from '@shared/directives/truncate-with-tooltip.directive'; + import * as coercion from '@shared/decorators/coercion'; import * as enumerable from '@shared/decorators/enumerable'; import * as TbInject from '@shared/decorators/tb-inject'; @@ -422,6 +425,9 @@ class ModulesMap implements IModulesMap { '@shared/pipe/truncate.pipe': TruncatePipe, '@shared/pipe/image.pipe': ImagePipe, + '@shared/directives/ellipsis-chip-list.directive': EllipsisChipListDirective, + '@shared/directives/truncate-with-tooltip.directive': TruncateWithTooltipDirective, + '@shared/decorators/coercion': coercion, '@shared/decorators/enumerable': enumerable, '@shared/decorators/tb-inject': TbInject, diff --git a/ui-ngx/src/app/shared/directives/public-api.ts b/ui-ngx/src/app/shared/directives/public-api.ts new file mode 100644 index 0000000000..08431abb28 --- /dev/null +++ b/ui-ngx/src/app/shared/directives/public-api.ts @@ -0,0 +1,2 @@ +export * from './truncate-with-tooltip.directive'; +export * from './ellipsis-chip-list.directive'; diff --git a/ui-ngx/src/app/shared/directives/truncate-with-tooltip.directive.ts b/ui-ngx/src/app/shared/directives/truncate-with-tooltip.directive.ts index 1281b86040..ad4e4cbbc1 100644 --- a/ui-ngx/src/app/shared/directives/truncate-with-tooltip.directive.ts +++ b/ui-ngx/src/app/shared/directives/truncate-with-tooltip.directive.ts @@ -105,8 +105,6 @@ export class TruncateWithTooltipDirective implements OnInit, AfterViewInit, OnDe private showTooltip(): void { this.tooltip.message = this.text; - - this.renderer.setAttribute(this.elementRef.nativeElement, 'matTooltip', this.text); this.tooltip.show(); } diff --git a/ui-ngx/src/app/shared/public-api.ts b/ui-ngx/src/app/shared/public-api.ts index bd3d565da3..1f38b7f70c 100644 --- a/ui-ngx/src/app/shared/public-api.ts +++ b/ui-ngx/src/app/shared/public-api.ts @@ -19,3 +19,4 @@ export * from './decorators/public-api'; export * from './models/public-api'; export * from './pipe/public-api'; export * from './shared.module'; +export * from './directives/public-api'; From 4391d0b474af635c3282f9c4465878eefb52b96d Mon Sep 17 00:00:00 2001 From: dashevchenko Date: Mon, 5 Aug 2024 13:03:37 +0300 Subject: [PATCH 37/48] changed access modifier from public to package-private --- .../transport/coap/efento/CoapEfentoTransportResource.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/efento/CoapEfentoTransportResource.java b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/efento/CoapEfentoTransportResource.java index 562a633920..1ba7c9aff1 100644 --- a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/efento/CoapEfentoTransportResource.java +++ b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/efento/CoapEfentoTransportResource.java @@ -222,7 +222,7 @@ public class CoapEfentoTransportResource extends AbstractCoapTransportResource { } } - public List getEfentoMeasurements(MeasurementsProtos.ProtoMeasurements protoMeasurements, UUID sessionId) { + List getEfentoMeasurements(MeasurementsProtos.ProtoMeasurements protoMeasurements, UUID sessionId) { String serialNumber = CoapEfentoUtils.convertByteArrayToString(protoMeasurements.getSerialNum().toByteArray()); boolean batteryStatus = protoMeasurements.getBatteryStatus(); int measurementPeriodBase = protoMeasurements.getMeasurementPeriodBase(); From 4e71093aca9073e5eeae091240a2a0520b1dfdad Mon Sep 17 00:00:00 2001 From: mpetrov Date: Mon, 5 Aug 2024 13:05:07 +0300 Subject: [PATCH 38/48] changed connector update approach --- .../mqtt-basic-config.component.ts | 38 +++++++++--- .../gateway/gateway-connectors.component.ts | 60 ++++++++++++------- .../lib/gateway/gateway-widget.models.ts | 11 +++- 3 files changed, 76 insertions(+), 33 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mqtt-basic-config/mqtt-basic-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mqtt-basic-config/mqtt-basic-config.component.ts index a70bf956a2..f70139eb1e 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mqtt-basic-config/mqtt-basic-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mqtt-basic-config/mqtt-basic-config.component.ts @@ -47,6 +47,7 @@ import { import { MappingTableComponent } from '@home/components/widget/lib/gateway/connectors-configuration/mapping-table/mapping-table.component'; +import { isDefinedAndNotNull } from '@core/utils'; @Component({ selector: 'tb-mqtt-basic-config', @@ -136,15 +137,20 @@ export class MqttBasicConfigComponent implements ControlValueAccessor, Validator } private getMappedMQTTConfig(basicConfig: MQTTBasicConfig): MQTTBasicConfig { - const { broker, workers, dataMapping, requestsMapping } = basicConfig || {}; - return workers ? { - dataMapping, - requestsMapping, - broker: { + let { broker, workers, dataMapping, requestsMapping } = basicConfig || {}; + + if (isDefinedAndNotNull(workers.maxNumberOfWorkers) || isDefinedAndNotNull(workers.maxMessageNumberPerWorker)) { + broker = { ...broker, ...workers, - } - } : basicConfig; + }; + } + + if ((requestsMapping as RequestMappingData[])?.length) { + requestsMapping = this.getRequestDataObject(requestsMapping as RequestMappingData[]); + } + + return { broker, workers, dataMapping, requestsMapping }; } validate(): ValidationErrors | null { @@ -153,7 +159,7 @@ export class MqttBasicConfigComponent implements ControlValueAccessor, Validator }; } - private getRequestDataArray(value: Record): RequestMappingData[] { + private getRequestDataArray(value: Record): RequestMappingData[] { const mappingConfigs = []; if (isObject(value)) { @@ -169,4 +175,20 @@ export class MqttBasicConfigComponent implements ControlValueAccessor, Validator return mappingConfigs; } + + private getRequestDataObject(array: RequestMappingData[]): Record { + const result = { + connectRequests: [], + disconnectRequests: [], + attributeRequests: [], + attributeUpdates: [], + serverSideRpc: [], + }; + + array.forEach(({ requestType, requestValue }) => { + result[requestType].push(requestValue); + }); + + return result as Record; + } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.ts index 0434e9e853..a91a7c1b9f 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.ts @@ -51,6 +51,7 @@ import { EntityType } from '@shared/models/entity-type.models'; import { AddConnectorConfigData, ConnectorBaseConfig, + ConnectorBaseInfo, ConnectorConfigurationModes, ConnectorType, GatewayConnector, @@ -133,7 +134,7 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie private basicConfigSub: Subscription; - private connectorUpdateInProgress = false; + private jsonConfigSub: Subscription; private subscriptionOptions: WidgetSubscriptionOptions = { callbacks: { @@ -211,18 +212,6 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie } }); - this.connectorForm.get('configurationJson').valueChanges.pipe( - filter(() => !this.connectorUpdateInProgress), - takeUntil(this.destroy$) - ).subscribe((config) => { - const basicConfig = this.connectorForm.get('basicConfig'); - const type = this.connectorForm.get('type').value; - const mode = this.connectorForm.get('mode').value; - if (!isEqual(config, basicConfig?.value) && this.allowBasicConfig.has(type) && mode === ConnectorConfigurationModes.ADVANCED) { - this.connectorForm.get('basicConfig').patchValue(config, {emitEvent: false}); - } - }); - this.dataSource.sort = this.sort; this.dataSource.sortingDataAccessor = (data: AttributeData, sortHeaderId: string) => { switch (sortHeaderId) { @@ -374,13 +363,26 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie const sharedData = data.value; const hasSameName = sharedData.name === connectorData.name; const hasEmptyConfig = isEqual(sharedData.configurationJson, {}) && hasSameName; - const hasSameConfig =isEqual(sharedData.configurationJson, connectorData.configurationJson); + const hasSameConfig = this.hasSameConfig(sharedData.configurationJson, connectorData.configurationJson); const isRecentlyCreated = sharedData.ts && sharedData.ts <= connectorData.ts; return hasSameName && isRecentlyCreated && (hasSameConfig || hasEmptyConfig); }); return sharedIndex !== -1; } + private hasSameConfig(sharedDataConfigJson: ConnectorBaseInfo, connectorDataConfigJson: ConnectorBaseInfo): boolean { + const { name, id, enableRemoteLogging, logLevel, ...sharedDataConfig } = sharedDataConfigJson; + const { + name: connectorName, + id: connectorId, + enableRemoteLogging: connectorEnableRemoteLogging, + logLevel: connectorLogLevel, + ...connectorConfig + } = connectorDataConfigJson; + + return isEqual(sharedDataConfig, connectorConfig); + } + private combineData(): void { this.dataSource.data = [...this.activeData, ...this.inactiveData, ...this.sharedAttributeData].filter((item, index, self) => index === self.findIndex((t) => t.key === item.key) @@ -677,7 +679,7 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie this.basicConfigSub.unsubscribe(); } this.basicConfigSub = this.connectorForm.get('basicConfig').valueChanges.pipe( - filter(() => !this.connectorUpdateInProgress), + filter(() => !!this.initialConnector), takeUntil(this.destroy$) ).subscribe((config) => { const configJson = this.connectorForm.get('configurationJson'); @@ -690,6 +692,22 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie }); } + private createJsonConfigWatcher(): void { + if (this.jsonConfigSub) { + this.jsonConfigSub.unsubscribe(); + } + this.jsonConfigSub = this.connectorForm.get('configurationJson').valueChanges.pipe( + takeUntil(this.destroy$) + ).subscribe((config) => { + const basicConfig = this.connectorForm.get('basicConfig'); + const type = this.connectorForm.get('type').value; + const mode = this.connectorForm.get('mode').value; + if (!isEqual(config, basicConfig?.value) && this.allowBasicConfig.has(type) && mode === ConnectorConfigurationModes.ADVANCED) { + this.connectorForm.get('basicConfig').patchValue(config, {emitEvent: false}); + } + }); + } + private confirmConnectorChange(): Observable { if (this.initialConnector && this.connectorForm.dirty) { return this.dialogService.confirm( @@ -728,22 +746,18 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie case ConnectorType.MQTT: case ConnectorType.OPCUA: case ConnectorType.MODBUS: - this.connectorUpdateInProgress = true; - if (connector.name !== this.connectorForm.value.name) { - this.connectorForm.get('basicConfig').setValue({}, {emitEvent: false}); - } - + this.connectorForm.get('mode').setValue(connector.mode || ConnectorConfigurationModes.BASIC, {emitEvent: false}); setTimeout(() => { - this.connectorForm.patchValue({...connector, mode: connector.mode || ConnectorConfigurationModes.BASIC}); - this.createBasicConfigWatcher(); + this.connectorForm.patchValue(connector, {emitEvent: false}); this.connectorForm.markAsPristine(); - this.connectorUpdateInProgress = false; + this.createBasicConfigWatcher(); }); break; default: this.connectorForm.patchValue({...connector, mode: null}); this.connectorForm.markAsPristine(); } + this.createJsonConfigWatcher(); } private setClientData(data: PageData): void { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-widget.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-widget.models.ts index 7878296039..22915ad9eb 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-widget.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-widget.models.ts @@ -176,11 +176,18 @@ export type ConnectorMapping = DeviceConnectorMapping | RequestMappingData | Con export type ConnectorMappingFormValue = DeviceConnectorMapping | RequestMappingFormValue | ConverterMappingFormValue; -export type ConnectorBaseConfig = MQTTBasicConfig | OPCBasicConfig | ModbusBasicConfig; +export type ConnectorBaseConfig = ConnectorBaseInfo | MQTTBasicConfig | OPCBasicConfig | ModbusBasicConfig; + +export interface ConnectorBaseInfo { + name: string; + id: string; + enableRemoteLogging: boolean; + logLevel: GatewayLogLevel; +} export interface MQTTBasicConfig { dataMapping: ConverterConnectorMapping[]; - requestsMapping: Record | RequestMappingData[]; + requestsMapping: Record | RequestMappingData[]; broker: BrokerConfig; workers?: WorkersConfig; } From 915929f79320a32c93f6095690f8d3146eb1f838 Mon Sep 17 00:00:00 2001 From: mpetrov Date: Mon, 5 Aug 2024 14:24:34 +0300 Subject: [PATCH 39/48] refactoring --- .../mqtt-basic-config/mqtt-basic-config.component.ts | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mqtt-basic-config/mqtt-basic-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mqtt-basic-config/mqtt-basic-config.component.ts index f70139eb1e..25f8671201 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mqtt-basic-config/mqtt-basic-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mqtt-basic-config/mqtt-basic-config.component.ts @@ -177,18 +177,15 @@ export class MqttBasicConfigComponent implements ControlValueAccessor, Validator } private getRequestDataObject(array: RequestMappingData[]): Record { - const result = { + return array.reduce((result, { requestType, requestValue }) => { + result[requestType].push(requestValue); + return result; + }, { connectRequests: [], disconnectRequests: [], attributeRequests: [], attributeUpdates: [], serverSideRpc: [], - }; - - array.forEach(({ requestType, requestValue }) => { - result[requestType].push(requestValue); }); - - return result as Record; } } From 1e89a62d3fce69fcb0714104beef7da89fdc8843 Mon Sep 17 00:00:00 2001 From: mpetrov Date: Mon, 5 Aug 2024 14:42:59 +0300 Subject: [PATCH 40/48] format headers --- ui-ngx/src/app/shared/directives/public-api.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/ui-ngx/src/app/shared/directives/public-api.ts b/ui-ngx/src/app/shared/directives/public-api.ts index 08431abb28..6f25320d90 100644 --- a/ui-ngx/src/app/shared/directives/public-api.ts +++ b/ui-ngx/src/app/shared/directives/public-api.ts @@ -1,2 +1,18 @@ +/// +/// 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. +/// + export * from './truncate-with-tooltip.directive'; export * from './ellipsis-chip-list.directive'; From 5759610340a724cb09e76b03c927f58c73b3c28d Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Mon, 5 Aug 2024 17:22:58 +0300 Subject: [PATCH 41/48] Rate limit for WS subscriptions --- ...efaultTbEntityDataSubscriptionService.java | 8 +++- .../DefaultTbLocalSubscriptionService.java | 41 ++++++++++++++--- .../subscription/TbAbstractDataSubCtx.java | 5 ++- .../subscription/TbAbstractSubCtx.java | 2 +- .../subscription/TbAlarmDataSubCtx.java | 5 ++- .../subscription/TbEntityDataSubCtx.java | 3 +- .../TbLocalSubscriptionService.java | 3 +- .../service/ws/DefaultWebSocketService.java | 21 ++++----- .../DefaultNotificationCommandsHandler.java | 4 +- .../src/main/resources/thingsboard.yml | 5 +++ .../server/common/data/limit/LimitedApi.java | 3 +- .../msg/tools/TbRateLimitsException.java | 6 +++ .../common/util/DeduplicationUtil.java | 44 +++++++++++++++++++ 13 files changed, 123 insertions(+), 27 deletions(-) create mode 100644 common/util/src/main/java/org/thingsboard/common/util/DeduplicationUtil.java diff --git a/application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbEntityDataSubscriptionService.java b/application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbEntityDataSubscriptionService.java index 984af019b4..632ea44940 100644 --- a/application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbEntityDataSubscriptionService.java +++ b/application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbEntityDataSubscriptionService.java @@ -19,6 +19,8 @@ import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.MoreExecutors; +import jakarta.annotation.PostConstruct; +import jakarta.annotation.PreDestroy; import lombok.Getter; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.ArrayUtils; @@ -42,6 +44,7 @@ import org.thingsboard.server.common.data.query.EntityDataQuery; import org.thingsboard.server.common.data.query.EntityKey; import org.thingsboard.server.common.data.query.EntityKeyType; import org.thingsboard.server.common.data.query.TsValue; +import org.thingsboard.server.common.msg.tools.TbRateLimitsException; import org.thingsboard.server.dao.alarm.AlarmService; import org.thingsboard.server.dao.attributes.AttributesService; import org.thingsboard.server.dao.entity.EntityService; @@ -66,8 +69,6 @@ import org.thingsboard.server.service.ws.telemetry.cmd.v2.LatestValueCmd; import org.thingsboard.server.service.ws.telemetry.cmd.v2.TimeSeriesCmd; import org.thingsboard.server.service.ws.telemetry.cmd.v2.UnsubscribeCmd; -import jakarta.annotation.PostConstruct; -import jakarta.annotation.PreDestroy; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; @@ -355,6 +356,9 @@ public class DefaultTbEntityDataSubscriptionService implements TbEntityDataSubsc private void handleWsCmdRuntimeException(String sessionId, RuntimeException e, EntityDataCmd cmd) { log.debug("[{}] Failed to process ws cmd: {}", sessionId, cmd, e); + if (e instanceof TbRateLimitsException) { + return; + } wsService.close(sessionId, CloseStatus.SERVICE_RESTARTED); } diff --git a/application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbLocalSubscriptionService.java b/application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbLocalSubscriptionService.java index 8abb78d9c1..6e99731305 100644 --- a/application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbLocalSubscriptionService.java +++ b/application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbLocalSubscriptionService.java @@ -15,16 +15,20 @@ */ package org.thingsboard.server.service.subscription; +import jakarta.annotation.PostConstruct; +import jakarta.annotation.PreDestroy; import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Lazy; import org.springframework.context.event.EventListener; import org.springframework.stereotype.Service; +import org.thingsboard.common.util.DeduplicationUtil; import org.thingsboard.common.util.DonAsynchron; import org.thingsboard.common.util.ThingsBoardExecutors; import org.thingsboard.common.util.ThingsBoardThreadFactory; +import org.thingsboard.server.cache.limits.RateLimitService; import org.thingsboard.server.cluster.TbClusterService; import org.thingsboard.server.common.data.AttributeScope; -import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.alarm.AlarmInfo; import org.thingsboard.server.common.data.id.EntityId; @@ -34,10 +38,12 @@ import org.thingsboard.server.common.data.kv.BaseReadTsKvQuery; import org.thingsboard.server.common.data.kv.BasicTsKvEntry; import org.thingsboard.server.common.data.kv.ReadTsKvQuery; import org.thingsboard.server.common.data.kv.TsKvEntry; +import org.thingsboard.server.common.data.limit.LimitedApi; import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent; import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.msg.queue.TbCallback; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; +import org.thingsboard.server.common.msg.tools.TbRateLimitsException; import org.thingsboard.server.dao.attributes.AttributesService; import org.thingsboard.server.dao.timeseries.TimeseriesService; import org.thingsboard.server.gen.transport.TransportProtos; @@ -46,13 +52,12 @@ import org.thingsboard.server.queue.discovery.TbServiceInfoProvider; import org.thingsboard.server.queue.discovery.event.ClusterTopologyChangeEvent; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.ws.WebSocketService; +import org.thingsboard.server.service.ws.WebSocketSessionRef; import org.thingsboard.server.service.ws.notification.sub.NotificationRequestUpdate; import org.thingsboard.server.service.ws.notification.sub.NotificationsSubscriptionUpdate; import org.thingsboard.server.service.ws.telemetry.sub.AlarmSubscriptionUpdate; import org.thingsboard.server.service.ws.telemetry.sub.TelemetrySubscriptionUpdate; -import jakarta.annotation.PostConstruct; -import jakarta.annotation.PreDestroy; import java.util.ArrayList; import java.util.HashSet; import java.util.List; @@ -88,13 +93,20 @@ public class DefaultTbLocalSubscriptionService implements TbLocalSubscriptionSer private final TbClusterService clusterService; private final SubscriptionManagerService subscriptionManagerService; private final WebSocketService webSocketService; + private final RateLimitService rateLimitService; private ExecutorService tsCallBackExecutor; private ScheduledExecutorService staleSessionCleanupExecutor; + @Value("${server.ws.rate_limits.subscriptions_per_tenant:2000:60}") + private String subscriptionsPerTenantRateLimit; + @Value("${server.ws.rate_limits.subscriptions_per_user:500:60}") + private String subscriptionsPerUserRateLimit; + public DefaultTbLocalSubscriptionService(AttributesService attrService, TimeseriesService tsService, TbServiceInfoProvider serviceInfoProvider, PartitionService partitionService, TbClusterService clusterService, - @Lazy SubscriptionManagerService subscriptionManagerService, @Lazy WebSocketService webSocketService) { + @Lazy SubscriptionManagerService subscriptionManagerService, @Lazy WebSocketService webSocketService, + RateLimitService rateLimitService) { this.attrService = attrService; this.tsService = tsService; this.serviceInfoProvider = serviceInfoProvider; @@ -102,6 +114,7 @@ public class DefaultTbLocalSubscriptionService implements TbLocalSubscriptionSer this.clusterService = clusterService; this.subscriptionManagerService = subscriptionManagerService; this.webSocketService = webSocketService; + this.rateLimitService = rateLimitService; } private String serviceId; @@ -164,9 +177,18 @@ public class DefaultTbLocalSubscriptionService implements TbLocalSubscriptionSer } @Override - public void addSubscription(TbSubscription subscription) { + public void addSubscription(TbSubscription subscription, WebSocketSessionRef sessionRef) { TenantId tenantId = subscription.getTenantId(); EntityId entityId = subscription.getEntityId(); + if (!rateLimitService.checkRateLimit(LimitedApi.WS_SUBSCRIPTIONS, (Object) tenantId, subscriptionsPerTenantRateLimit)) { + handleRateLimitError(subscription, sessionRef, "Exceeded rate limit for WS subscriptions per tenant"); + return; + } + if (sessionRef.getSecurityCtx() != null && !rateLimitService.checkRateLimit(LimitedApi.WS_SUBSCRIPTIONS, sessionRef.getSecurityCtx().getId(), subscriptionsPerUserRateLimit)) { + handleRateLimitError(subscription, sessionRef, "Exceeded rate limit for WS subscriptions per user"); + return; + } + log.debug("[{}][{}] Register subscription: {}", tenantId, entityId, subscription); SubscriptionModificationResult result; subsLock.lock(); @@ -563,4 +585,13 @@ public class DefaultTbLocalSubscriptionService implements TbLocalSubscriptionSer subscriptionsBySessionId.keySet().forEach(webSocketService::cleanupIfStale); } + private void handleRateLimitError(TbSubscription subscription, WebSocketSessionRef sessionRef, String message) { + String deduplicationKey = sessionRef.getSessionId() + message; + if (!DeduplicationUtil.alreadyProcessed(deduplicationKey, TimeUnit.SECONDS.toMillis(15))) { + log.info("{} {}", sessionRef, message); + webSocketService.sendError(sessionRef, subscription.getSubscriptionId(), SubscriptionErrorCode.BAD_REQUEST, message); + } + throw new TbRateLimitsException(message); + } + } diff --git a/application/src/main/java/org/thingsboard/server/service/subscription/TbAbstractDataSubCtx.java b/application/src/main/java/org/thingsboard/server/service/subscription/TbAbstractDataSubCtx.java index f65eec8453..77ef960d72 100644 --- a/application/src/main/java/org/thingsboard/server/service/subscription/TbAbstractDataSubCtx.java +++ b/application/src/main/java/org/thingsboard/server/service/subscription/TbAbstractDataSubCtx.java @@ -132,7 +132,7 @@ public abstract class TbAbstractDataSubCtx> keysByType = getEntityKeyByTypeMap(keys); for (EntityData entityData : data.getData()) { List entitySubscriptions = addSubscriptions(entityData, keysByType, latestValues, startTs, endTs); - entitySubscriptions.forEach(localSubscriptionService::addSubscription); + entitySubscriptions.forEach(subscription -> localSubscriptionService.addSubscription(subscription, sessionRef)); } } @@ -254,4 +254,5 @@ public abstract class TbAbstractDataSubCtx { .scope(TbAttributeSubscriptionScope.SERVER_SCOPE) .build(); subToDynamicValueKeySet.add(subIdx); - localSubscriptionService.addSubscription(sub); + localSubscriptionService.addSubscription(sub, sessionRef); } } catch (InterruptedException | ExecutionException e) { log.info("[{}][{}][{}] Failed to resolve dynamic values: {}", tenantId, customerId, userId, dynamicValues.keySet()); diff --git a/application/src/main/java/org/thingsboard/server/service/subscription/TbAlarmDataSubCtx.java b/application/src/main/java/org/thingsboard/server/service/subscription/TbAlarmDataSubCtx.java index 0d1f2f94b2..828da388fb 100644 --- a/application/src/main/java/org/thingsboard/server/service/subscription/TbAlarmDataSubCtx.java +++ b/application/src/main/java/org/thingsboard/server/service/subscription/TbAlarmDataSubCtx.java @@ -177,7 +177,7 @@ public class TbAlarmDataSubCtx extends TbAbstractDataSubCtx { .updateProcessor((sub, update) -> sendWsMsg(sub.getSessionId(), update)) .ts(startTs) .build(); - localSubscriptionService.addSubscription(subscription); + localSubscriptionService.addSubscription(subscription, sessionRef); } @Override @@ -342,7 +342,7 @@ public class TbAlarmDataSubCtx extends TbAbstractDataSubCtx { newSubsList.forEach(entity -> createAlarmSubscriptionForEntity(query.getPageLink(), startTs, entity)); } subIdsToCancel.forEach(subId -> localSubscriptionService.cancelSubscription(getSessionId(), subId)); - subsToAdd.forEach(localSubscriptionService::addSubscription); + subsToAdd.forEach(subscription -> localSubscriptionService.addSubscription(subscription, sessionRef)); } private void resetInvocationCounter() { @@ -361,4 +361,5 @@ public class TbAlarmDataSubCtx extends TbAbstractDataSubCtx { EntityDataPageLink edpl = new EntityDataPageLink(maxEntitiesPerAlarmSubscription, 0, null, entitiesSortOrder); return new EntityDataQuery(query.getEntityFilter(), edpl, query.getEntityFields(), query.getLatestValues(), query.getKeyFilters()); } + } diff --git a/application/src/main/java/org/thingsboard/server/service/subscription/TbEntityDataSubCtx.java b/application/src/main/java/org/thingsboard/server/service/subscription/TbEntityDataSubCtx.java index 98ec81b798..48458ca6fd 100644 --- a/application/src/main/java/org/thingsboard/server/service/subscription/TbEntityDataSubCtx.java +++ b/application/src/main/java/org/thingsboard/server/service/subscription/TbEntityDataSubCtx.java @@ -226,7 +226,7 @@ public class TbEntityDataSubCtx extends TbAbstractDataSubCtx { } } subIdsToCancel.forEach(subId -> localSubscriptionService.cancelSubscription(getSessionId(), subId)); - subsToAdd.forEach(localSubscriptionService::addSubscription); + subsToAdd.forEach(subscription -> localSubscriptionService.addSubscription(subscription, sessionRef)); sendWsMsg(new EntityDataUpdate(cmdId, data, null, maxEntitiesPerDataSubscription)); } @@ -239,4 +239,5 @@ public class TbEntityDataSubCtx extends TbAbstractDataSubCtx { protected EntityDataQuery buildEntityDataQuery() { return query; } + } diff --git a/application/src/main/java/org/thingsboard/server/service/subscription/TbLocalSubscriptionService.java b/application/src/main/java/org/thingsboard/server/service/subscription/TbLocalSubscriptionService.java index ddb2a1b590..59e7ad532d 100644 --- a/application/src/main/java/org/thingsboard/server/service/subscription/TbLocalSubscriptionService.java +++ b/application/src/main/java/org/thingsboard/server/service/subscription/TbLocalSubscriptionService.java @@ -22,6 +22,7 @@ import org.thingsboard.server.common.data.kv.TsKvEntry; import org.thingsboard.server.common.msg.queue.TbCallback; import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.queue.discovery.event.ClusterTopologyChangeEvent; +import org.thingsboard.server.service.ws.WebSocketSessionRef; import org.thingsboard.server.service.ws.notification.sub.NotificationRequestUpdate; import org.thingsboard.server.service.ws.notification.sub.NotificationsSubscriptionUpdate; @@ -29,7 +30,7 @@ import java.util.List; public interface TbLocalSubscriptionService { - void addSubscription(TbSubscription subscription); + void addSubscription(TbSubscription subscription, WebSocketSessionRef sessionRef); void onSubEventCallback(TransportProtos.TbEntitySubEventCallbackProto subEventCallback, TbCallback callback); diff --git a/application/src/main/java/org/thingsboard/server/service/ws/DefaultWebSocketService.java b/application/src/main/java/org/thingsboard/server/service/ws/DefaultWebSocketService.java index d7091bbad2..33b011cc0a 100644 --- a/application/src/main/java/org/thingsboard/server/service/ws/DefaultWebSocketService.java +++ b/application/src/main/java/org/thingsboard/server/service/ws/DefaultWebSocketService.java @@ -21,6 +21,9 @@ import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.MoreExecutors; +import jakarta.annotation.Nullable; +import jakarta.annotation.PostConstruct; +import jakarta.annotation.PreDestroy; import lombok.Getter; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -46,6 +49,7 @@ import org.thingsboard.server.common.data.kv.BasicTsKvEntry; import org.thingsboard.server.common.data.kv.ReadTsKvQuery; import org.thingsboard.server.common.data.kv.TsKvEntry; import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; +import org.thingsboard.server.common.msg.tools.TbRateLimitsException; import org.thingsboard.server.dao.attributes.AttributesService; import org.thingsboard.server.dao.tenant.TbTenantProfileCache; import org.thingsboard.server.dao.timeseries.TimeseriesService; @@ -80,10 +84,6 @@ import org.thingsboard.server.service.ws.telemetry.cmd.v2.EntityDataUpdate; import org.thingsboard.server.service.ws.telemetry.cmd.v2.UnsubscribeCmd; import org.thingsboard.server.service.ws.telemetry.sub.TelemetrySubscriptionUpdate; -import jakarta.annotation.Nullable; -import jakarta.annotation.PostConstruct; -import jakarta.annotation.PreDestroy; - import java.io.IOException; import java.util.ArrayList; import java.util.Collections; @@ -224,9 +224,10 @@ public class DefaultWebSocketService implements WebSocketService { try { Optional.ofNullable(cmdsHandlers.get(cmd.getType())) .ifPresent(cmdHandler -> cmdHandler.handle(sessionRef, cmd)); + } catch (TbRateLimitsException e) { + log.debug("{} Failed to handle WS cmd: {}", sessionRef, cmd, e); } catch (Exception e) { - log.error("[sessionId: {}, tenantId: {}, userId: {}] Failed to handle WS cmd: {}", sessionId, - sessionRef.getSecurityCtx().getTenantId(), sessionRef.getSecurityCtx().getId(), cmd, e); + log.error("{} Failed to handle WS cmd: {}", sessionRef, cmd, e); } } } @@ -468,7 +469,7 @@ public class DefaultWebSocketService implements WebSocketService { subLock.lock(); try { - oldSubService.addSubscription(sub); + oldSubService.addSubscription(sub, sessionRef); sendUpdate(sessionRef, new TelemetrySubscriptionUpdate(cmd.getCmdId(), attributesData)); } finally { subLock.unlock(); @@ -581,7 +582,7 @@ public class DefaultWebSocketService implements WebSocketService { subLock.lock(); try { - oldSubService.addSubscription(sub); + oldSubService.addSubscription(sub, sessionRef); sendUpdate(sessionRef, new TelemetrySubscriptionUpdate(cmd.getCmdId(), attributesData)); } finally { subLock.unlock(); @@ -678,7 +679,7 @@ public class DefaultWebSocketService implements WebSocketService { subLock.lock(); try { - oldSubService.addSubscription(sub); + oldSubService.addSubscription(sub, sessionRef); sendUpdate(sessionRef, new TelemetrySubscriptionUpdate(cmd.getCmdId(), data)); } finally { subLock.unlock(); @@ -733,7 +734,7 @@ public class DefaultWebSocketService implements WebSocketService { subLock.lock(); try { - oldSubService.addSubscription(sub); + oldSubService.addSubscription(sub, sessionRef); sendUpdate(sessionRef, new TelemetrySubscriptionUpdate(cmd.getCmdId(), data)); } finally { subLock.unlock(); diff --git a/application/src/main/java/org/thingsboard/server/service/ws/notification/DefaultNotificationCommandsHandler.java b/application/src/main/java/org/thingsboard/server/service/ws/notification/DefaultNotificationCommandsHandler.java index 6fa9111c3d..5bfe976194 100644 --- a/application/src/main/java/org/thingsboard/server/service/ws/notification/DefaultNotificationCommandsHandler.java +++ b/application/src/main/java/org/thingsboard/server/service/ws/notification/DefaultNotificationCommandsHandler.java @@ -79,7 +79,7 @@ public class DefaultNotificationCommandsHandler implements NotificationCommandsH .updateProcessor(this::handleNotificationsSubscriptionUpdate) .limit(cmd.getLimit()) .build(); - localSubscriptionService.addSubscription(subscription); + localSubscriptionService.addSubscription(subscription, sessionRef); fetchUnreadNotifications(subscription); sendUpdate(sessionRef.getSessionId(), subscription.createFullUpdate()); @@ -97,7 +97,7 @@ public class DefaultNotificationCommandsHandler implements NotificationCommandsH .entityId(securityCtx.getId()) .updateProcessor(this::handleNotificationsCountSubscriptionUpdate) .build(); - localSubscriptionService.addSubscription(subscription); + localSubscriptionService.addSubscription(subscription, sessionRef); fetchUnreadNotificationsCount(subscription); sendUpdate(sessionRef.getSessionId(), subscription.createUpdate()); diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index 3db77d50c5..a9dc32963d 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -78,6 +78,11 @@ server: max_queue_messages_per_session: "${TB_SERVER_WS_DEFAULT_QUEUE_MESSAGES_PER_SESSION:1000}" # Maximum time between WS session opening and sending auth command auth_timeout_ms: "${TB_SERVER_WS_AUTH_TIMEOUT_MS:10000}" + rate_limits: + # Per-tenant rate limit for WS subscriptions + subscriptions_per_tenant: "${TB_SERVER_WS_SUBSCRIPTIONS_PER_TENANT_RATE_LIMIT:2000:60}" + # Per-user rate limit for WS subscriptions + subscriptions_per_user: "${TB_SERVER_WS_SUBSCRIPTIONS_PER_USER_RATE_LIMIT:500:60}" rest: server_side_rpc: # Minimum value of the server-side RPC timeout. May override value provided in the REST API call. diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/limit/LimitedApi.java b/common/data/src/main/java/org/thingsboard/server/common/data/limit/LimitedApi.java index 7aa472bea1..6532a4fe0d 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/limit/LimitedApi.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/limit/LimitedApi.java @@ -42,7 +42,8 @@ public enum LimitedApi { TRANSPORT_MESSAGES_PER_DEVICE("transport messages per device", false), TRANSPORT_MESSAGES_PER_GATEWAY("transport messages per gateway", false), TRANSPORT_MESSAGES_PER_GATEWAY_DEVICE("transport messages per gateway device", false), - EMAILS("emails sending", true); + EMAILS("emails sending", true), + WS_SUBSCRIPTIONS("WS subscriptions", false); private Function configExtractor; @Getter diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/tools/TbRateLimitsException.java b/common/message/src/main/java/org/thingsboard/server/common/msg/tools/TbRateLimitsException.java index b39477fa38..32d55048ae 100644 --- a/common/message/src/main/java/org/thingsboard/server/common/msg/tools/TbRateLimitsException.java +++ b/common/message/src/main/java/org/thingsboard/server/common/msg/tools/TbRateLimitsException.java @@ -30,4 +30,10 @@ public class TbRateLimitsException extends AbstractRateLimitException { super(entityType.name() + " rate limits reached!"); this.entityType = entityType; } + + public TbRateLimitsException(String message) { + super(message); + this.entityType = null; + } + } diff --git a/common/util/src/main/java/org/thingsboard/common/util/DeduplicationUtil.java b/common/util/src/main/java/org/thingsboard/common/util/DeduplicationUtil.java new file mode 100644 index 0000000000..fd25e8b924 --- /dev/null +++ b/common/util/src/main/java/org/thingsboard/common/util/DeduplicationUtil.java @@ -0,0 +1,44 @@ +/** + * 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.common.util; + +import org.springframework.util.ConcurrentReferenceHashMap; + +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.atomic.AtomicBoolean; + +import static org.springframework.util.ConcurrentReferenceHashMap.ReferenceType.SOFT; + +public class DeduplicationUtil { + + private static final ConcurrentMap cache = new ConcurrentReferenceHashMap<>(16, SOFT); + + public static boolean alreadyProcessed(Object deduplicationKey, long deduplicationDuration) { + AtomicBoolean alreadyProcessed = new AtomicBoolean(false); + cache.compute(deduplicationKey, (key, lastProcessedTs) -> { + if (lastProcessedTs != null) { + long passed = System.currentTimeMillis() - lastProcessedTs; + if (passed <= deduplicationDuration) { + alreadyProcessed.set(true); + return lastProcessedTs; + } + } + return System.currentTimeMillis(); + }); + return alreadyProcessed.get(); + } + +} From 8e8aa5c920f76a973e835c96f57ec31c397aaf67 Mon Sep 17 00:00:00 2001 From: Volodymyr Babak Date: Mon, 5 Aug 2024 18:12:23 +0300 Subject: [PATCH 42/48] Remove check if edge is in sync. Fixed misc test according to new changes --- .../server/controller/EdgeController.java | 18 ----- .../service/edge/rpc/EdgeGrpcService.java | 25 +++---- .../service/edge/rpc/EdgeRpcService.java | 2 - .../processor/device/DeviceEdgeProcessor.java | 10 +-- .../server/controller/EdgeControllerTest.java | 66 +++++++++++++++++-- .../server/edge/AbstractEdgeTest.java | 10 ++- .../server/edge/DeviceEdgeTest.java | 52 ++++----------- .../server/edge/RuleChainEdgeTest.java | 4 +- .../server/edge/TelemetryEdgeTest.java | 6 +- .../thingsboard/server/edge/UserEdgeTest.java | 20 +++--- .../thingsboard/rest/client/RestClient.java | 4 -- 11 files changed, 115 insertions(+), 102 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/controller/EdgeController.java b/application/src/main/java/org/thingsboard/server/controller/EdgeController.java index 011f2d6614..26c30f85d1 100644 --- a/application/src/main/java/org/thingsboard/server/controller/EdgeController.java +++ b/application/src/main/java/org/thingsboard/server/controller/EdgeController.java @@ -494,24 +494,6 @@ public class EdgeController extends BaseController { } } - @ApiOperation(value = "Is edge sync process is active (isEdgeSyncProcessActive)", - notes = "Returns 'true' if edge is currently in sync process, 'false' - otherwise." + TENANT_AUTHORITY_PARAGRAPH) - @PreAuthorize("hasAuthority('TENANT_ADMIN')") - @GetMapping(value = "/edge/sync/{edgeId}/active") - public Boolean isEdgeSyncProcessActive( - @Parameter(description = EDGE_ID_PARAM_DESCRIPTION, required = true) - @PathVariable("edgeId") String strEdgeId) throws ThingsboardException { - checkParameter("edgeId", strEdgeId); - if (isEdgesEnabled() && edgeRpcServiceOpt.isPresent()) { - EdgeId edgeId = new EdgeId(toUUID(strEdgeId)); - edgeId = checkNotNull(edgeId); - Edge edge = checkEdgeId(edgeId, Operation.READ); - return edgeRpcServiceOpt.get().isEdgeSyncProcessActive(edge.getTenantId(), edge.getId()); - } else { - throw new ThingsboardException("Edges support disabled", ThingsboardErrorCode.GENERAL); - } - } - @ApiOperation(value = "Find missing rule chains (findMissingToRelatedRuleChains)", notes = "Returns list of rule chains ids that are not assigned to particular edge, but these rule chains are present in the already assigned rule chains to edge." + TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('TENANT_ADMIN')") diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcService.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcService.java index 69495f19bc..46bf3f69a4 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcService.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcService.java @@ -287,12 +287,16 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i private void startSyncProcess(TenantId tenantId, EdgeId edgeId, UUID requestId) { EdgeGrpcSession session = sessions.get(edgeId); if (session != null) { - boolean success = false; - if (session.isConnected()) { - session.startSyncProcess(true); - success = true; + if (!session.isSyncCompleted()) { + clusterService.pushEdgeSyncResponseToCore(new FromEdgeSyncResponse(requestId, tenantId, edgeId, false, "Sync process is active at the moment")); + } else { + boolean success = false; + if (session.isConnected()) { + session.startSyncProcess(true); + success = true; + } + clusterService.pushEdgeSyncResponseToCore(new FromEdgeSyncResponse(requestId, tenantId, edgeId, success, "")); } - clusterService.pushEdgeSyncResponseToCore(new FromEdgeSyncResponse(requestId, tenantId, edgeId, success, "")); } } @@ -300,7 +304,7 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i public void processSyncRequest(ToEdgeSyncRequest request, Consumer responseConsumer) { UUID requestId = request.getId(); EdgeGrpcSession session = sessions.get(request.getEdgeId()); - if (!session.isSyncCompleted()) { + if (session != null && !session.isSyncCompleted()) { responseConsumer.accept(new FromEdgeSyncResponse(requestId, request.getTenantId(), request.getEdgeId(), false, "Sync process is active at the moment")); } else { log.trace("[{}][{}] Processing sync edge request [{}]", request.getTenantId(), request.getId(), request.getEdgeId()); @@ -310,15 +314,6 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i } } - @Override - public Boolean isEdgeSyncProcessActive(TenantId tenantId, EdgeId edgeId) { - EdgeGrpcSession session = sessions.get(edgeId); - if (session == null) { - return false; - } - return !session.isSyncCompleted(); - } - private void scheduleSyncRequestTimeout(ToEdgeSyncRequest request, UUID requestId) { log.trace("[{}] scheduling sync edge request", requestId); executorService.schedule(() -> { diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeRpcService.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeRpcService.java index 733d2e95cb..12203db0c2 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeRpcService.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeRpcService.java @@ -33,6 +33,4 @@ public interface EdgeRpcService { void deleteEdge(TenantId tenantId, EdgeId edgeId); void processSyncRequest(ToEdgeSyncRequest request, Consumer responseConsumer); - - Boolean isEdgeSyncProcessActive(TenantId tenantId, EdgeId edgeId); } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/DeviceEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/DeviceEdgeProcessor.java index 34a3a79acf..f448766994 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/DeviceEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/DeviceEdgeProcessor.java @@ -231,12 +231,12 @@ public abstract class DeviceEdgeProcessor extends BaseDeviceProcessor implements DownlinkMsg.Builder builder = DownlinkMsg.newBuilder() .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) .addDeviceUpdateMsg(deviceUpdateMsg); - DeviceCredentials deviceCredentials = deviceCredentialsService.findDeviceCredentialsByDeviceId(edgeEvent.getTenantId(), deviceId); - DeviceCredentialsUpdateMsg deviceCredentialsUpdateMsg = ((DeviceMsgConstructor) - deviceMsgConstructorFactory.getMsgConstructorByEdgeVersion(edgeVersion)).constructDeviceCredentialsUpdatedMsg(deviceCredentials); - builder.addDeviceCredentialsUpdateMsg(deviceCredentialsUpdateMsg).build(); - + if (deviceCredentials != null) { + DeviceCredentialsUpdateMsg deviceCredentialsUpdateMsg = ((DeviceMsgConstructor) + deviceMsgConstructorFactory.getMsgConstructorByEdgeVersion(edgeVersion)).constructDeviceCredentialsUpdatedMsg(deviceCredentials); + builder.addDeviceCredentialsUpdateMsg(deviceCredentialsUpdateMsg).build(); + } if (UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE.equals(msgType)) { DeviceProfile deviceProfile = deviceProfileService.findDeviceProfileById(edgeEvent.getTenantId(), device.getDeviceProfileId()); deviceProfile = checkIfDeviceProfileDefaultFieldsAssignedToEdge(edgeEvent.getTenantId(), edgeId, deviceProfile, edgeVersion); diff --git a/application/src/test/java/org/thingsboard/server/controller/EdgeControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/EdgeControllerTest.java index 49dfa71223..de530b5268 100644 --- a/application/src/test/java/org/thingsboard/server/controller/EdgeControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/EdgeControllerTest.java @@ -55,14 +55,18 @@ import org.thingsboard.server.common.data.asset.AssetProfile; import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.id.CustomerId; +import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.EdgeId; +import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantProfileId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.queue.Queue; import org.thingsboard.server.common.data.rule.RuleChain; +import org.thingsboard.server.common.data.rule.RuleChainMetaData; import org.thingsboard.server.common.data.security.Authority; +import org.thingsboard.server.common.data.security.DeviceCredentials; import org.thingsboard.server.common.data.security.model.JwtSettings; import org.thingsboard.server.dao.edge.EdgeDao; import org.thingsboard.server.dao.exception.DataValidationException; @@ -73,11 +77,13 @@ import org.thingsboard.server.gen.edge.v1.AdminSettingsUpdateMsg; import org.thingsboard.server.gen.edge.v1.AssetProfileUpdateMsg; import org.thingsboard.server.gen.edge.v1.AssetUpdateMsg; import org.thingsboard.server.gen.edge.v1.CustomerUpdateMsg; +import org.thingsboard.server.gen.edge.v1.DeviceCredentialsUpdateMsg; import org.thingsboard.server.gen.edge.v1.DeviceProfileUpdateMsg; import org.thingsboard.server.gen.edge.v1.DeviceUpdateMsg; import org.thingsboard.server.gen.edge.v1.EdgeVersion; import org.thingsboard.server.gen.edge.v1.OAuth2UpdateMsg; import org.thingsboard.server.gen.edge.v1.QueueUpdateMsg; +import org.thingsboard.server.gen.edge.v1.RuleChainMetadataUpdateMsg; import org.thingsboard.server.gen.edge.v1.RuleChainUpdateMsg; import org.thingsboard.server.gen.edge.v1.SyncCompletedMsg; import org.thingsboard.server.gen.edge.v1.TenantProfileUpdateMsg; @@ -93,6 +99,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.UUID; import java.util.concurrent.TimeUnit; import static org.hamcrest.Matchers.containsString; @@ -887,23 +894,24 @@ public class EdgeControllerTest extends AbstractControllerTest { edgeImitator.ignoreType(UserCredentialsUpdateMsg.class); edgeImitator.ignoreType(OAuth2UpdateMsg.class); - edgeImitator.expectMessageAmount(24); + edgeImitator.expectMessageAmount(27); edgeImitator.connect(); waitForMessages(edgeImitator); - verifyFetchersMsgs(edgeImitator); + verifyFetchersMsgs(edgeImitator, savedDevice); // verify queue msgs Assert.assertTrue(popDeviceProfileMsg(edgeImitator.getDownlinkMsgs(), UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, "default")); Assert.assertTrue(popDeviceMsg(edgeImitator.getDownlinkMsgs(), UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, "Test Sync Edge Device 1")); + Assert.assertTrue(popDeviceCredentialsMsg(edgeImitator.getDownlinkMsgs(), savedDevice.getId())); Assert.assertTrue(popAssetProfileMsg(edgeImitator.getDownlinkMsgs(), UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, "test")); Assert.assertTrue(popAssetMsg(edgeImitator.getDownlinkMsgs(), UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, "Test Sync Edge Asset 1")); Assert.assertTrue(edgeImitator.getDownlinkMsgs().isEmpty()); - edgeImitator.expectMessageAmount(20); + edgeImitator.expectMessageAmount(22); doPost("/api/edge/sync/" + edge.getId()); waitForMessages(edgeImitator); - verifyFetchersMsgs(edgeImitator); + verifyFetchersMsgs(edgeImitator, savedDevice); Assert.assertTrue(edgeImitator.getDownlinkMsgs().isEmpty()); edgeImitator.allowIgnoredTypes(); @@ -920,6 +928,23 @@ public class EdgeControllerTest extends AbstractControllerTest { .andExpect(status().isOk()); } + private RuleChainId getEdgeRootRuleChainId(EdgeImitator edgeImitator) { + try { + EdgeId edgeId = new EdgeId(new UUID(edgeImitator.getConfiguration().getEdgeIdMSB(), edgeImitator.getConfiguration().getEdgeIdLSB())); + List edgeRuleChains = doGetTypedWithPageLink("/api/edge/" + edgeId.getId() + "/ruleChains?", + new TypeReference>() { + }, new PageLink(100)).getData(); + for (RuleChain edgeRuleChain : edgeRuleChains) { + if (edgeRuleChain.isRoot()) { + return edgeRuleChain.getId(); + } + } + } catch (Exception e) { + throw new RuntimeException(e); + } + throw new RuntimeException("Root rule chain not found"); + } + private void simulateEdgeActivation(Edge edge) throws Exception { ObjectNode attributes = JacksonUtil.newObjectNode(); attributes.put("active", true); @@ -949,9 +974,10 @@ public class EdgeControllerTest extends AbstractControllerTest { } } - private void verifyFetchersMsgs(EdgeImitator edgeImitator) { + private void verifyFetchersMsgs(EdgeImitator edgeImitator, Device savedDevice) { Assert.assertTrue(popQueueMsg(edgeImitator.getDownlinkMsgs(), UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, "Main")); Assert.assertTrue(popRuleChainMsg(edgeImitator.getDownlinkMsgs(), UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, "Edge Root Rule Chain")); + Assert.assertTrue(popRuleChainMetadataMsg(edgeImitator.getDownlinkMsgs(), UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, getEdgeRootRuleChainId(edgeImitator))); Assert.assertTrue(popAdminSettingsMsg(edgeImitator.getDownlinkMsgs(), "general")); Assert.assertTrue(popAdminSettingsMsg(edgeImitator.getDownlinkMsgs(), "mail")); Assert.assertTrue(popAdminSettingsMsg(edgeImitator.getDownlinkMsgs(), "connectivity")); @@ -965,6 +991,7 @@ public class EdgeControllerTest extends AbstractControllerTest { Assert.assertTrue(popCustomerMsg(edgeImitator.getDownlinkMsgs(), UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, "Public")); Assert.assertTrue(popDeviceProfileMsg(edgeImitator.getDownlinkMsgs(), UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, "default")); Assert.assertTrue(popDeviceMsg(edgeImitator.getDownlinkMsgs(), UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, "Test Sync Edge Device 1")); + Assert.assertTrue(popDeviceCredentialsMsg(edgeImitator.getDownlinkMsgs(), savedDevice.getId())); Assert.assertTrue(popAssetProfileMsg(edgeImitator.getDownlinkMsgs(), UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, "test")); Assert.assertTrue(popAssetMsg(edgeImitator.getDownlinkMsgs(), UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, "Test Sync Edge Asset 1")); Assert.assertTrue(popTenantMsg(edgeImitator.getDownlinkMsgs(), tenantId)); @@ -1002,6 +1029,21 @@ public class EdgeControllerTest extends AbstractControllerTest { return false; } + private boolean popRuleChainMetadataMsg(List messages, UpdateMsgType msgType, RuleChainId ruleChainId) { + for (AbstractMessage message : messages) { + if (message instanceof RuleChainMetadataUpdateMsg ruleChainMetadataUpdateMsg) { + RuleChainMetaData ruleChainMetaData = JacksonUtil.fromString(ruleChainMetadataUpdateMsg.getEntity(), RuleChainMetaData.class, true); + Assert.assertNotNull(ruleChainMetaData); + if (msgType.equals(ruleChainMetadataUpdateMsg.getMsgType()) + && ruleChainId.equals(ruleChainMetaData.getRuleChainId())) { + messages.remove(message); + return true; + } + } + } + return false; + } + private boolean popAdminSettingsMsg(List messages, String key) { for (AbstractMessage message : messages) { if (message instanceof AdminSettingsUpdateMsg adminSettingsUpdateMsg) { @@ -1046,6 +1088,20 @@ public class EdgeControllerTest extends AbstractControllerTest { return false; } + private boolean popDeviceCredentialsMsg(List messages, DeviceId deviceId) { + for (AbstractMessage message : messages) { + if (message instanceof DeviceCredentialsUpdateMsg deviceCredentialsUpdateMsg) { + DeviceCredentials deviceCredentials = JacksonUtil.fromString(deviceCredentialsUpdateMsg.getEntity(), DeviceCredentials.class, true); + Assert.assertNotNull(deviceCredentials); + if (deviceId.equals(deviceCredentials.getDeviceId())) { + messages.remove(message); + return true; + } + } + } + return false; + } + private boolean popAssetProfileMsg(List messages, UpdateMsgType msgType, String name) { for (AbstractMessage message : messages) { if (message instanceof AssetProfileUpdateMsg assetProfileUpdateMsg) { diff --git a/application/src/test/java/org/thingsboard/server/edge/AbstractEdgeTest.java b/application/src/test/java/org/thingsboard/server/edge/AbstractEdgeTest.java index e7f664477c..f7a066f7e0 100644 --- a/application/src/test/java/org/thingsboard/server/edge/AbstractEdgeTest.java +++ b/application/src/test/java/org/thingsboard/server/edge/AbstractEdgeTest.java @@ -83,6 +83,7 @@ import org.thingsboard.server.gen.edge.v1.AdminSettingsUpdateMsg; import org.thingsboard.server.gen.edge.v1.AssetProfileUpdateMsg; import org.thingsboard.server.gen.edge.v1.AssetUpdateMsg; import org.thingsboard.server.gen.edge.v1.CustomerUpdateMsg; +import org.thingsboard.server.gen.edge.v1.DeviceCredentialsUpdateMsg; import org.thingsboard.server.gen.edge.v1.DeviceProfileUpdateMsg; import org.thingsboard.server.gen.edge.v1.DeviceUpdateMsg; import org.thingsboard.server.gen.edge.v1.EdgeConfiguration; @@ -96,6 +97,7 @@ import org.thingsboard.server.gen.edge.v1.TenantProfileUpdateMsg; import org.thingsboard.server.gen.edge.v1.TenantUpdateMsg; import org.thingsboard.server.gen.edge.v1.UpdateMsgType; import org.thingsboard.server.gen.edge.v1.UplinkMsg; +import org.thingsboard.server.gen.edge.v1.UserCredentialsUpdateMsg; import org.thingsboard.server.gen.edge.v1.UserUpdateMsg; import java.util.ArrayList; @@ -143,7 +145,7 @@ abstract public class AbstractEdgeTest extends AbstractControllerTest { edgeImitator = new EdgeImitator("localhost", 7070, edge.getRoutingKey(), edge.getSecret()); edgeImitator.ignoreType(OAuth2UpdateMsg.class); - edgeImitator.expectMessageAmount(21); + edgeImitator.expectMessageAmount(24); edgeImitator.connect(); requestEdgeRuleChainMetadata(); @@ -250,7 +252,7 @@ abstract public class AbstractEdgeTest extends AbstractControllerTest { UUID ruleChainUUID = validateRuleChains(); // 1 from request message - validateMsgsCnt(RuleChainMetadataUpdateMsg.class, 1); + validateMsgsCnt(RuleChainMetadataUpdateMsg.class, 2); validateRuleChainMetadataUpdates(ruleChainUUID); // 4 messages ('general', 'mail', 'connectivity', 'jwt') @@ -275,6 +277,8 @@ abstract public class AbstractEdgeTest extends AbstractControllerTest { validateMsgsCnt(DeviceUpdateMsg.class, 1); validateDevices(); + validateMsgsCnt(DeviceCredentialsUpdateMsg.class, 1); + // 1 from asset fetcher validateMsgsCnt(AssetUpdateMsg.class, 1); validateAssets(); @@ -287,6 +291,8 @@ abstract public class AbstractEdgeTest extends AbstractControllerTest { validateMsgsCnt(UserUpdateMsg.class, 1); validateUsers(); + validateMsgsCnt(UserCredentialsUpdateMsg.class, 1); + // 1 from tenant fetcher validateMsgsCnt(TenantUpdateMsg.class, 1); validateTenant(); diff --git a/application/src/test/java/org/thingsboard/server/edge/DeviceEdgeTest.java b/application/src/test/java/org/thingsboard/server/edge/DeviceEdgeTest.java index 9e09a16fd9..c5f34e4f47 100644 --- a/application/src/test/java/org/thingsboard/server/edge/DeviceEdgeTest.java +++ b/application/src/test/java/org/thingsboard/server/edge/DeviceEdgeTest.java @@ -150,25 +150,25 @@ public class DeviceEdgeTest extends AbstractEdgeTest { + "/edge/" + edge.getUuidId(), Edge.class); Assert.assertTrue(edgeImitator.waitForMessages()); - edgeImitator.expectMessageAmount(1); + edgeImitator.expectMessageAmount(2); doPost("/api/customer/" + savedCustomer.getUuidId() + "/device/" + savedDevice.getUuidId(), Device.class); Assert.assertTrue(edgeImitator.waitForMessages()); - latestMessage = edgeImitator.getLatestMessage(); - Assert.assertTrue(latestMessage instanceof DeviceUpdateMsg); - deviceUpdateMsg = (DeviceUpdateMsg) latestMessage; + deviceUpdateMsgOpt = edgeImitator.findMessageByType(DeviceUpdateMsg.class); + Assert.assertTrue(deviceUpdateMsgOpt.isPresent()); + deviceUpdateMsg = deviceUpdateMsgOpt.get(); deviceFromMsg = JacksonUtil.fromString(deviceUpdateMsg.getEntity(), Device.class, true); Assert.assertNotNull(deviceFromMsg); Assert.assertEquals(UpdateMsgType.ENTITY_UPDATED_RPC_MESSAGE, deviceUpdateMsg.getMsgType()); Assert.assertEquals(savedCustomer.getId(), deviceFromMsg.getCustomerId()); // unassign device #2 from customer - edgeImitator.expectMessageAmount(1); + edgeImitator.expectMessageAmount(2); doDelete("/api/customer/device/" + savedDevice.getUuidId(), Device.class); Assert.assertTrue(edgeImitator.waitForMessages()); - latestMessage = edgeImitator.getLatestMessage(); - Assert.assertTrue(latestMessage instanceof DeviceUpdateMsg); - deviceUpdateMsg = (DeviceUpdateMsg) latestMessage; + deviceUpdateMsgOpt = edgeImitator.findMessageByType(DeviceUpdateMsg.class); + Assert.assertTrue(deviceUpdateMsgOpt.isPresent()); + deviceUpdateMsg = deviceUpdateMsgOpt.get(); deviceFromMsg = JacksonUtil.fromString(deviceUpdateMsg.getEntity(), Device.class, true); Assert.assertNotNull(deviceFromMsg); Assert.assertEquals(UpdateMsgType.ENTITY_UPDATED_RPC_MESSAGE, deviceUpdateMsg.getMsgType()); @@ -243,7 +243,7 @@ public class DeviceEdgeTest extends AbstractEdgeTest { Assert.assertTrue(edgeImitator.waitForMessages()); // update device - edgeImitator.expectMessageAmount(1); + edgeImitator.expectMessageAmount(2); savedDevice.setFirmwareId(firmwareOtaPackageInfo.getId()); savedDevice.setSoftwareId(softwareOtaPackageInfo.getId()); @@ -256,9 +256,9 @@ public class DeviceEdgeTest extends AbstractEdgeTest { savedDevice = doPost("/api/device", savedDevice, Device.class); Assert.assertTrue(edgeImitator.waitForMessages()); - AbstractMessage latestMessage = edgeImitator.getLatestMessage(); - Assert.assertTrue(latestMessage instanceof DeviceUpdateMsg); - DeviceUpdateMsg deviceUpdateMsg = (DeviceUpdateMsg) latestMessage; + Optional deviceUpdateMsgOpt = edgeImitator.findMessageByType(DeviceUpdateMsg.class); + Assert.assertTrue(deviceUpdateMsgOpt.isPresent()); + DeviceUpdateMsg deviceUpdateMsg = deviceUpdateMsgOpt.get(); Device deviceMsg = JacksonUtil.fromString(deviceUpdateMsg.getEntity(), Device.class, true); Assert.assertNotNull(deviceMsg); Assert.assertEquals(UpdateMsgType.ENTITY_UPDATED_RPC_MESSAGE, deviceUpdateMsg.getMsgType()); @@ -504,7 +504,7 @@ public class DeviceEdgeTest extends AbstractEdgeTest { uplinkMsgBuilder.addDeviceUpdateMsg(deviceUpdateMsgBuilder.build()); edgeImitator.expectResponsesAmount(1); - edgeImitator.expectMessageAmount(2); + edgeImitator.expectMessageAmount(1); testAutoGeneratedCodeByProtobuf(uplinkMsgBuilder); edgeImitator.sendUplinkMsg(uplinkMsgBuilder.build()); @@ -526,18 +526,6 @@ public class DeviceEdgeTest extends AbstractEdgeTest { Device device = doGet("/api/device/" + newDeviceId, Device.class); Assert.assertNotNull(device); Assert.assertNotEquals(deviceOnCloudName, device.getName()); - - Optional deviceCredentialsUpdateMsgOpt = edgeImitator.findMessageByType(DeviceCredentialsRequestMsg.class); - Assert.assertTrue(deviceCredentialsUpdateMsgOpt.isPresent()); - DeviceCredentialsRequestMsg latestDeviceCredentialsRequestMsg = deviceCredentialsUpdateMsgOpt.get(); - Assert.assertEquals(deviceMsg.getUuidId().getMostSignificantBits(), latestDeviceCredentialsRequestMsg.getDeviceIdMSB()); - Assert.assertEquals(device.getUuidId().getLeastSignificantBits(), latestDeviceCredentialsRequestMsg.getDeviceIdLSB()); - - newDeviceId = new UUID(latestDeviceCredentialsRequestMsg.getDeviceIdMSB(), latestDeviceCredentialsRequestMsg.getDeviceIdLSB()); - - device = doGet("/api/device/" + newDeviceId, Device.class); - Assert.assertNotNull(device); - Assert.assertNotEquals(deviceOnCloudName, device.getName()); } @Test @@ -553,22 +541,10 @@ public class DeviceEdgeTest extends AbstractEdgeTest { uplinkMsgBuilder.addDeviceUpdateMsg(deviceUpdateMsgBuilder.build()); edgeImitator.expectResponsesAmount(1); - edgeImitator.expectMessageAmount(1); - edgeImitator.sendUplinkMsg(uplinkMsgBuilder.build()); - Assert.assertTrue(edgeImitator.waitForResponses()); - Assert.assertTrue(edgeImitator.waitForMessages()); - AbstractMessage latestMessage = edgeImitator.getLatestMessage(); - Assert.assertTrue(latestMessage instanceof DeviceCredentialsRequestMsg); - DeviceCredentialsRequestMsg latestDeviceCredentialsRequestMsg = (DeviceCredentialsRequestMsg) latestMessage; - Assert.assertEquals(deviceMsg.getUuidId().getMostSignificantBits(), latestDeviceCredentialsRequestMsg.getDeviceIdMSB()); - Assert.assertEquals(deviceMsg.getUuidId().getLeastSignificantBits(), latestDeviceCredentialsRequestMsg.getDeviceIdLSB()); - - UUID newDeviceId = new UUID(latestDeviceCredentialsRequestMsg.getDeviceIdMSB(), latestDeviceCredentialsRequestMsg.getDeviceIdLSB()); - - Device device = doGet("/api/device/" + newDeviceId, Device.class); + Device device = doGet("/api/device/" + deviceMsg.getId().getId(), Device.class); Assert.assertNotNull(device); Assert.assertEquals("Edge Device 2", device.getName()); } diff --git a/application/src/test/java/org/thingsboard/server/edge/RuleChainEdgeTest.java b/application/src/test/java/org/thingsboard/server/edge/RuleChainEdgeTest.java index 62d2c5eade..c694df84f8 100644 --- a/application/src/test/java/org/thingsboard/server/edge/RuleChainEdgeTest.java +++ b/application/src/test/java/org/thingsboard/server/edge/RuleChainEdgeTest.java @@ -193,7 +193,7 @@ public class RuleChainEdgeTest extends AbstractEdgeTest { ruleChain.setType(RuleChainType.EDGE); RuleChain savedRuleChain = doPost("/api/ruleChain", ruleChain, RuleChain.class); - edgeImitator.expectMessageAmount(2); + edgeImitator.expectMessageAmount(4); doPost("/api/edge/" + edge.getUuidId() + "/ruleChain/" + savedRuleChain.getUuidId(), RuleChain.class); RuleChainMetaData metaData = createRuleChainMetadata(savedRuleChain); @@ -201,7 +201,7 @@ public class RuleChainEdgeTest extends AbstractEdgeTest { // set new rule chain as root RuleChainId currentRootRuleChainId = edge.getRootRuleChainId(); - edgeImitator.expectMessageAmount(1); + edgeImitator.expectMessageAmount(2); doPost("/api/edge/" + edge.getUuidId() + "/" + savedRuleChain.getUuidId() + "/root", Edge.class); Assert.assertTrue(edgeImitator.waitForMessages()); diff --git a/application/src/test/java/org/thingsboard/server/edge/TelemetryEdgeTest.java b/application/src/test/java/org/thingsboard/server/edge/TelemetryEdgeTest.java index 00b8cc4b74..b77e0b5ccc 100644 --- a/application/src/test/java/org/thingsboard/server/edge/TelemetryEdgeTest.java +++ b/application/src/test/java/org/thingsboard/server/edge/TelemetryEdgeTest.java @@ -32,6 +32,7 @@ import org.thingsboard.server.common.data.edge.EdgeEventType; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.dao.service.DaoSqlTest; import org.thingsboard.server.gen.edge.v1.AttributeDeleteMsg; +import org.thingsboard.server.gen.edge.v1.DeviceCredentialsUpdateMsg; import org.thingsboard.server.gen.edge.v1.DeviceUpdateMsg; import org.thingsboard.server.gen.edge.v1.EntityDataProto; import org.thingsboard.server.gen.edge.v1.UplinkMsg; @@ -183,7 +184,7 @@ public class TelemetryEdgeTest extends AbstractEdgeTest { edgeImitator.setRandomFailuresOnTimeseriesDownlink(true); // imitator will generate failure in 100% of timeseries cases edgeImitator.setFailureProbability(100); - edgeImitator.expectMessageAmount(numberOfMsgsToSend); + edgeImitator.expectMessageAmount(numberOfMsgsToSend * 2); for (int idx = 1; idx <= numberOfMsgsToSend; idx++) { String timeseriesData = "{\"data\":{\"idx\":" + idx + "},\"ts\":" + System.currentTimeMillis() + "}"; JsonNode timeseriesEntityData = JacksonUtil.toJsonNode(timeseriesData); @@ -204,6 +205,9 @@ public class TelemetryEdgeTest extends AbstractEdgeTest { List deviceUpdateMsgs = edgeImitator.findAllMessagesByType(DeviceUpdateMsg.class); Assert.assertEquals(numberOfMsgsToSend, deviceUpdateMsgs.size()); + List deviceCredentialsUpdateMsgs = edgeImitator.findAllMessagesByType(DeviceCredentialsUpdateMsg.class); + Assert.assertEquals(numberOfMsgsToSend, deviceCredentialsUpdateMsgs.size()); + edgeImitator.setRandomFailuresOnTimeseriesDownlink(false); } diff --git a/application/src/test/java/org/thingsboard/server/edge/UserEdgeTest.java b/application/src/test/java/org/thingsboard/server/edge/UserEdgeTest.java index 6314cf53b9..503d87cf6f 100644 --- a/application/src/test/java/org/thingsboard/server/edge/UserEdgeTest.java +++ b/application/src/test/java/org/thingsboard/server/edge/UserEdgeTest.java @@ -71,13 +71,13 @@ public class UserEdgeTest extends AbstractEdgeTest { Assert.assertTrue(userCredentialsUpdateMsgOpt.isPresent()); // update user - edgeImitator.expectMessageAmount(1); + edgeImitator.expectMessageAmount(2); savedTenantAdmin.setLastName("Borisov"); savedTenantAdmin = doPost("/api/user", savedTenantAdmin, User.class); Assert.assertTrue(edgeImitator.waitForMessages()); - AbstractMessage latestMessage = edgeImitator.getLatestMessage(); - Assert.assertTrue(latestMessage instanceof UserUpdateMsg); - userUpdateMsg = (UserUpdateMsg) latestMessage; + userUpdateMsgOpt = edgeImitator.findMessageByType(UserUpdateMsg.class); + Assert.assertTrue(userUpdateMsgOpt.isPresent()); + userUpdateMsg = userUpdateMsgOpt.get(); userMsg = JacksonUtil.fromString(userUpdateMsg.getEntity(), User.class, true); Assert.assertNotNull(userMsg); Assert.assertEquals(UpdateMsgType.ENTITY_UPDATED_RPC_MESSAGE, userUpdateMsg.getMsgType()); @@ -92,7 +92,7 @@ public class UserEdgeTest extends AbstractEdgeTest { changePasswordRequest.setNewPassword("newTenant"); doPost("/api/auth/changePassword", changePasswordRequest); Assert.assertTrue(edgeImitator.waitForMessages()); - latestMessage = edgeImitator.getLatestMessage(); + AbstractMessage latestMessage = edgeImitator.getLatestMessage(); Assert.assertTrue(latestMessage instanceof UserCredentialsUpdateMsg); UserCredentialsUpdateMsg userCredentialsUpdateMsg = (UserCredentialsUpdateMsg) latestMessage; UserCredentials userCredentialsMsg = JacksonUtil.fromString(userCredentialsUpdateMsg.getEntity(), UserCredentials.class, true); @@ -155,13 +155,13 @@ public class UserEdgeTest extends AbstractEdgeTest { Assert.assertEquals(savedCustomerUser.getLastName(), userMsg.getLastName()); // update user - edgeImitator.expectMessageAmount(1); + edgeImitator.expectMessageAmount(2); savedCustomerUser.setLastName("Addams"); savedCustomerUser = doPost("/api/user", savedCustomerUser, User.class); Assert.assertTrue(edgeImitator.waitForMessages()); - AbstractMessage latestMessage = edgeImitator.getLatestMessage(); - Assert.assertTrue(latestMessage instanceof UserUpdateMsg); - userUpdateMsg = (UserUpdateMsg) latestMessage; + userUpdateMsgOpt = edgeImitator.findMessageByType(UserUpdateMsg.class); + Assert.assertTrue(userUpdateMsgOpt.isPresent()); + userUpdateMsg = userUpdateMsgOpt.get(); userMsg = JacksonUtil.fromString(userUpdateMsg.getEntity(), User.class, true); Assert.assertNotNull(userMsg); Assert.assertEquals(UpdateMsgType.ENTITY_UPDATED_RPC_MESSAGE, userUpdateMsg.getMsgType()); @@ -176,7 +176,7 @@ public class UserEdgeTest extends AbstractEdgeTest { changePasswordRequest.setNewPassword("newCustomer"); doPost("/api/auth/changePassword", changePasswordRequest); Assert.assertTrue(edgeImitator.waitForMessages()); - latestMessage = edgeImitator.getLatestMessage(); + AbstractMessage latestMessage = edgeImitator.getLatestMessage(); Assert.assertTrue(latestMessage instanceof UserCredentialsUpdateMsg); UserCredentialsUpdateMsg userCredentialsUpdateMsg = (UserCredentialsUpdateMsg) latestMessage; UserCredentials userCredentialsMsg = JacksonUtil.fromString(userCredentialsUpdateMsg.getEntity(), UserCredentials.class, true); diff --git a/rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java b/rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java index 40aab430da..c116eddc74 100644 --- a/rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java +++ b/rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java @@ -2950,10 +2950,6 @@ public class RestClient implements Closeable { return restTemplate.getForEntity(baseURL + "/api/edges/enabled", Boolean.class).getBody(); } - public Boolean isEdgeSyncProcessActive(EdgeId edgeId) { - return restTemplate.getForEntity(baseURL + "/api/edge/sync/" + edgeId.getId() + "/active", Boolean.class).getBody(); - } - public Edge saveEdge(Edge edge) { return restTemplate.postForEntity(baseURL + "/api/edge", edge, Edge.class).getBody(); } From 7dd9e47065acf1710cd55ed8b8d626a91d178c6f Mon Sep 17 00:00:00 2001 From: mpetrov Date: Tue, 6 Aug 2024 15:13:18 +0300 Subject: [PATCH 43/48] minor fixes --- .../modbus-data-keys-panel.component.scss | 2 +- .../modbus-slave-config.component.html | 2 +- .../modbus-slave-config.component.ts | 4 ++-- .../modbus-slave-dialog.component.html | 8 ++++---- .../modbus-slave-dialog.component.ts | 10 +++++----- 5 files changed, 13 insertions(+), 13 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-data-keys-panel/modbus-data-keys-panel.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-data-keys-panel/modbus-data-keys-panel.component.scss index d9ef45d66b..445ac6c0ae 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-data-keys-panel/modbus-data-keys-panel.component.scss +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-data-keys-panel/modbus-data-keys-panel.component.scss @@ -20,7 +20,7 @@ max-width: 700px; .title-container { - width: 12vw; + width: 180px; } .key-panel { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-config/modbus-slave-config.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-config/modbus-slave-config.component.html index d37c8cc9ee..12ddb479ca 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-config/modbus-slave-config.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-config/modbus-slave-config.component.html @@ -161,7 +161,7 @@
-
gateway.poll-period
+
gateway.poll-period
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-config/modbus-slave-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-config/modbus-slave-config.component.ts index 2a8488a2d4..d0e8ecdd4e 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-config/modbus-slave-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-config/modbus-slave-config.component.ts @@ -106,11 +106,11 @@ export class ModbusSlaveConfigComponent implements ControlValueAccessor, Validat port: [null, [Validators.required, Validators.min(PortLimits.MIN), Validators.max(PortLimits.MAX)]], serialPort: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]], method: [ModbusMethodType.SOCKET], - unitId: [0, [Validators.required]], + unitId: [null, [Validators.required]], baudrate: [this.modbusBaudrates[0]], deviceName: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]], deviceType: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]], - pollPeriod: [5000], + pollPeriod: [5000, [Validators.required]], sendDataToThingsBoard: [false], byteOrder:[ModbusOrderType.BIG], security: [], diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-dialog/modbus-slave-dialog.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-dialog/modbus-slave-dialog.component.html index c7fb50f146..088b385623 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-dialog/modbus-slave-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-dialog/modbus-slave-dialog.component.html @@ -306,7 +306,7 @@
-
gateway.poll-period
+
gateway.poll-period
@@ -314,7 +314,7 @@
-
gateway.connect-attempt-time
+
gateway.connect-attempt-time
@@ -322,7 +322,7 @@
-
gateway.connect-attempt-count
+
gateway.connect-attempt-count
@@ -330,7 +330,7 @@
-
gateway.wait-after-failed-attempts
+
gateway.wait-after-failed-attempts
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-dialog/modbus-slave-dialog.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-dialog/modbus-slave-dialog.component.ts index a00a488aa0..e21a9921a7 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-dialog/modbus-slave-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-dialog/modbus-slave-dialog.component.ts @@ -163,7 +163,7 @@ export class ModbusSlaveDialogComponent extends DialogComponent Date: Tue, 6 Aug 2024 16:47:35 +0300 Subject: [PATCH 44/48] disable slave fix --- .../modbus-basic-config/modbus-basic-config.component.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-basic-config/modbus-basic-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-basic-config/modbus-basic-config.component.ts index 1fca768980..3aadc74fbd 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-basic-config/modbus-basic-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-basic-config/modbus-basic-config.component.ts @@ -80,8 +80,8 @@ export class ModbusBasicConfigComponent implements ControlValueAccessor, Validat this.basicFormGroup.valueChanges .pipe(takeUntil(this.destroy$)) - .subscribe(value => { - this.onChange(value); + .subscribe(({ master, slave }) => { + this.onChange({ master, slave: slave ?? {} }); this.onTouched(); }); } From 09238cada5569f06a67b93948061396f1d78457a Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Tue, 6 Aug 2024 17:46:42 +0300 Subject: [PATCH 45/48] Update locale.constant-en_US.json --- ui-ngx/src/assets/locale/locale.constant-en_US.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index 1101270d49..e899ef7ace 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -936,7 +936,7 @@ "type-relations-delete": "All relation deleted", "type-alarm-ack": "Acknowledged", "type-alarm-clear": "Cleared", - "type-alarm-delete": "Delete", + "type-alarm-delete": "Deleted", "type-alarm-assign": "Assigned", "type-alarm-unassign": "Unassigned", "type-added-comment": "Added comment", From 73214e9afe2269ca3d02f0d03c84a69f5027b478 Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Tue, 6 Aug 2024 19:24:34 +0300 Subject: [PATCH 46/48] Fix KeyDictionaryDao.getOrSaveKeyId returning zero --- .../dao/sqlts/dictionary/JpaKeyDictionaryDao.java | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/dao/src/main/java/org/thingsboard/server/dao/sqlts/dictionary/JpaKeyDictionaryDao.java b/dao/src/main/java/org/thingsboard/server/dao/sqlts/dictionary/JpaKeyDictionaryDao.java index 870c0bc505..b01d1c4ea0 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sqlts/dictionary/JpaKeyDictionaryDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sqlts/dictionary/JpaKeyDictionaryDao.java @@ -15,11 +15,13 @@ */ package org.thingsboard.server.dao.sqlts.dictionary; +import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.hibernate.exception.ConstraintViolationException; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.dao.DataIntegrityViolationException; import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; import org.thingsboard.server.dao.dictionary.KeyDictionaryDao; import org.thingsboard.server.dao.model.sqlts.dictionary.KeyDictionaryCompositeKey; import org.thingsboard.server.dao.model.sqlts.dictionary.KeyDictionaryEntry; @@ -34,14 +36,15 @@ import java.util.concurrent.locks.ReentrantLock; @Component @Slf4j @SqlDao +@RequiredArgsConstructor public class JpaKeyDictionaryDao extends JpaAbstractDaoListeningExecutorService implements KeyDictionaryDao { - private final ConcurrentMap keyDictionaryMap = new ConcurrentHashMap<>(); - protected static final ReentrantLock creationLock = new ReentrantLock(); + private final KeyDictionaryRepository keyDictionaryRepository; - @Autowired - private KeyDictionaryRepository keyDictionaryRepository; + private final ConcurrentMap keyDictionaryMap = new ConcurrentHashMap<>(); + private static final ReentrantLock creationLock = new ReentrantLock(); + @Transactional(propagation = Propagation.NOT_SUPPORTED) @Override public Integer getOrSaveKeyId(String strKey) { Integer keyId = keyDictionaryMap.get(strKey); From 8343c602968c5c6891ba37cf57222446bdb4fec1 Mon Sep 17 00:00:00 2001 From: Volodymyr Babak Date: Wed, 7 Aug 2024 12:57:35 +0300 Subject: [PATCH 47/48] DefaultEdgeRequestsService - logging attributes and entityData in case failure --- .../rpc/sync/DefaultEdgeRequestsService.java | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/sync/DefaultEdgeRequestsService.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/sync/DefaultEdgeRequestsService.java index 3f61734801..6e4117869b 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/sync/DefaultEdgeRequestsService.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/sync/DefaultEdgeRequestsService.java @@ -82,8 +82,6 @@ import java.util.UUID; @Slf4j public class DefaultEdgeRequestsService implements EdgeRequestsService { - private static final int DEFAULT_PAGE_SIZE = 1000; - @Autowired private EdgeEventService edgeEventService; @@ -142,8 +140,10 @@ public class DefaultEdgeRequestsService implements EdgeRequestsService { private ListenableFuture processEntityAttributesAndAddToEdgeQueue(TenantId tenantId, EntityId entityId, Edge edge, EdgeEventType entityType, String scope, List ssAttributes, AttributesRequestMsg attributesRequestMsg) { + Map entityData = null; + ObjectNode attributes = null; + ListenableFuture future; try { - ListenableFuture future; if (ssAttributes == null || ssAttributes.isEmpty()) { log.trace("[{}][{}] No attributes found for entity {} [{}]", tenantId, edge.getName(), @@ -151,8 +151,8 @@ public class DefaultEdgeRequestsService implements EdgeRequestsService { entityId.getId()); future = Futures.immediateFuture(null); } else { - Map entityData = new HashMap<>(); - ObjectNode attributes = JacksonUtil.newObjectNode(); + entityData = new HashMap<>(); + attributes = JacksonUtil.newObjectNode(); for (AttributeKvEntry attr : ssAttributes) { if (DefaultDeviceStateService.PERSISTENT_ATTRIBUTES.contains(attr.getKey()) && !DefaultDeviceStateService.INACTIVITY_TIMEOUT.equals(attr.getKey())) { @@ -170,7 +170,7 @@ public class DefaultEdgeRequestsService implements EdgeRequestsService { attributes.put(attr.getKey(), attr.getValueAsString()); } } - if (attributes.size() > 0) { + if (!attributes.isEmpty()) { entityData.put("kv", attributes); entityData.put("scope", scope); JsonNode body = JacksonUtil.valueToTree(entityData); @@ -182,12 +182,13 @@ public class DefaultEdgeRequestsService implements EdgeRequestsService { } return Futures.transformAsync(future, v -> processLatestTimeseriesAndAddToEdgeQueue(tenantId, entityId, edge, entityType), dbCallbackExecutorService); } catch (Exception e) { - String errMsg = String.format("[%s][%s] Failed to save attribute updates to the edge [%s]", tenantId, edge.getId(), attributesRequestMsg); + String errMsg = String.format("[%s][%s] Failed to save attribute updates to the edge [%s], scope = %s, entityData = %s, attributes = %s", + tenantId, edge.getId(), attributesRequestMsg, scope, entityData, attributes); log.error(errMsg, e); return Futures.immediateFailedFuture(new RuntimeException(errMsg, e)); } } - + private ListenableFuture processLatestTimeseriesAndAddToEdgeQueue(TenantId tenantId, EntityId entityId, Edge edge, EdgeEventType entityType) { ListenableFuture> getAllLatestFuture = timeseriesService.findAllLatest(tenantId, entityId); From 6efb2ab81f1e8a20aacb26e9c79c17dbc53ed4a0 Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Wed, 7 Aug 2024 13:48:36 +0300 Subject: [PATCH 48/48] Add WS subscribe latency monitoring --- .../main/java/org/thingsboard/monitoring/data/Latencies.java | 2 +- .../thingsboard/monitoring/service/BaseMonitoringService.java | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/monitoring/src/main/java/org/thingsboard/monitoring/data/Latencies.java b/monitoring/src/main/java/org/thingsboard/monitoring/data/Latencies.java index ee21b36c53..4ab9d3c457 100644 --- a/monitoring/src/main/java/org/thingsboard/monitoring/data/Latencies.java +++ b/monitoring/src/main/java/org/thingsboard/monitoring/data/Latencies.java @@ -17,8 +17,8 @@ package org.thingsboard.monitoring.data; public class Latencies { - public static final String WS_UPDATE = "wsUpdate"; public static final String WS_CONNECT = "wsConnect"; + public static final String WS_SUBSCRIBE = "wsSubscribe"; public static final String LOG_IN = "logIn"; public static String request(String key) { diff --git a/monitoring/src/main/java/org/thingsboard/monitoring/service/BaseMonitoringService.java b/monitoring/src/main/java/org/thingsboard/monitoring/service/BaseMonitoringService.java index 7df767caef..c667c5b7ac 100644 --- a/monitoring/src/main/java/org/thingsboard/monitoring/service/BaseMonitoringService.java +++ b/monitoring/src/main/java/org/thingsboard/monitoring/service/BaseMonitoringService.java @@ -100,7 +100,10 @@ public abstract class BaseMonitoringService, T ext reporter.reportLatency(Latencies.LOG_IN, stopWatch.getTime()); try (WsClient wsClient = wsClientFactory.createClient(accessToken)) { + stopWatch.start(); wsClient.subscribeForTelemetry(devices, TransportHealthChecker.TEST_TELEMETRY_KEY).waitForReply(); + reporter.reportLatency(Latencies.WS_SUBSCRIBE, stopWatch.getTime()); + for (BaseHealthChecker healthChecker : healthCheckers) { check(healthChecker, wsClient); }