From 0d0ca90cf96cd63a2da79ee28e93110434a18734 Mon Sep 17 00:00:00 2001 From: thingsboard648 Date: Tue, 5 Mar 2024 10:38:19 +0200 Subject: [PATCH 01/80] added property to ignore delta in output messages if it is zero --- .../engine/metadata/CalculateDeltaNode.java | 27 +++++++++- .../CalculateDeltaNodeConfiguration.java | 3 +- .../metadata/CalculateDeltaNodeTest.java | 54 ++++++++++++++++++- 3 files changed, 80 insertions(+), 4 deletions(-) diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/CalculateDeltaNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/CalculateDeltaNode.java index 609888db84..510d089372 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/CalculateDeltaNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/CalculateDeltaNode.java @@ -32,6 +32,7 @@ import org.thingsboard.server.common.data.kv.TsKvEntry; import org.thingsboard.server.common.data.msg.TbMsgType; import org.thingsboard.server.common.data.msg.TbNodeConnectionType; import org.thingsboard.server.common.data.plugin.ComponentType; +import org.thingsboard.server.common.data.util.TbPair; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.dao.timeseries.TimeseriesService; @@ -46,7 +47,9 @@ import static org.thingsboard.common.util.DonAsynchron.withCallback; @Slf4j @RuleNode(type = ComponentType.ENRICHMENT, - name = "calculate delta", relationTypes = {TbNodeConnectionType.SUCCESS, TbNodeConnectionType.FAILURE, TbNodeConnectionType.OTHER}, + name = "calculate delta", + version = 1, + relationTypes = {TbNodeConnectionType.SUCCESS, TbNodeConnectionType.FAILURE, TbNodeConnectionType.OTHER}, configClazz = CalculateDeltaNodeConfiguration.class, nodeDescription = "Calculates delta and amount of time passed between previous timeseries key reading " + "and current value for this key from the incoming message", @@ -96,6 +99,11 @@ public class CalculateDeltaNode implements TbNode { BigDecimal delta = BigDecimal.valueOf(previousData != null ? currentValue - previousData.value : 0.0); + if (config.isOnlyComputeTrueDeltas() && delta.doubleValue() == 0) { + ctx.tellSuccess(msg); + return; + } + if (config.isTellFailureIfDeltaIsNegative() && delta.doubleValue() < 0) { ctx.tellFailure(msg, new IllegalArgumentException("Delta value is negative!")); return; @@ -128,6 +136,23 @@ public class CalculateDeltaNode implements TbNode { } } + @Override + public TbPair upgrade(int fromVersion, JsonNode oldConfiguration) throws TbNodeException { + boolean hasChanges = false; + switch (fromVersion) { + case 0: + String onlyComputeTrueDeltas = "onlyComputeTrueDeltas"; + if (!oldConfiguration.has(onlyComputeTrueDeltas)) { + hasChanges = true; + ((ObjectNode) oldConfiguration).put(onlyComputeTrueDeltas, false); + } + break; + default: + break; + } + return new TbPair<>(hasChanges, oldConfiguration); + } + private ListenableFuture fetchLatestValueAsync(EntityId entityId) { return Futures.transform(timeseriesService.findLatest(ctx.getTenantId(), entityId, Collections.singletonList(config.getInputValueKey())), list -> extractValue(list.get(0)) diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/CalculateDeltaNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/CalculateDeltaNodeConfiguration.java index 0c4e6de556..9f30aae22b 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/CalculateDeltaNodeConfiguration.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/CalculateDeltaNodeConfiguration.java @@ -30,6 +30,7 @@ public class CalculateDeltaNodeConfiguration implements NodeConfiguration givenFromVersionAndConfig_whenUpgrade_thenVerifyHasChangesAndConfig() { + return Stream.of( + // default config for version 0 + Arguments.of(0, + "{\"inputValueKey\":\"pulseCounter\",\"outputValueKey\":\"delta\",\"useCache\":true,\"addPeriodBetweenMsgs\":false, \"periodValueKey\":\"periodInMs\", \"round\":null,\"tellFailureIfDeltaIsNegative\":true}", + true, + "{\"inputValueKey\":\"pulseCounter\",\"outputValueKey\":\"delta\",\"useCache\":true,\"addPeriodBetweenMsgs\":false, \"periodValueKey\":\"periodInMs\", \"round\":null,\"tellFailureIfDeltaIsNegative\":true, \"onlyComputeTrueDeltas\":false}"), + // default config for version 1 with upgrade from version 0 + Arguments.of(1, + "{\"inputValueKey\":\"pulseCounter\",\"outputValueKey\":\"delta\",\"useCache\":true,\"addPeriodBetweenMsgs\":false, \"periodValueKey\":\"periodInMs\", \"round\":null,\"tellFailureIfDeltaIsNegative\":true, \"onlyComputeTrueDeltas\":false}", + false, + "{\"inputValueKey\":\"pulseCounter\",\"outputValueKey\":\"delta\",\"useCache\":true,\"addPeriodBetweenMsgs\":false, \"periodValueKey\":\"periodInMs\", \"round\":null,\"tellFailureIfDeltaIsNegative\":true, \"onlyComputeTrueDeltas\":false}") + ); + + } + + @Override + protected TbNode getTestNode() { + return node; + } } From 96a71ad8dc07ba4f08eebe878ea52be699532327 Mon Sep 17 00:00:00 2001 From: thingsboard648 Date: Tue, 5 Mar 2024 16:03:00 +0200 Subject: [PATCH 02/80] changed the check to exclude only 0 deltas from the message --- .../engine/metadata/CalculateDeltaNode.java | 14 +-- .../CalculateDeltaNodeConfiguration.java | 5 +- .../metadata/CalculateDeltaNodeTest.java | 90 ++++++++++++++++--- 3 files changed, 88 insertions(+), 21 deletions(-) diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/CalculateDeltaNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/CalculateDeltaNode.java index 510d089372..ce3a318fe6 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/CalculateDeltaNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/CalculateDeltaNode.java @@ -99,13 +99,13 @@ public class CalculateDeltaNode implements TbNode { BigDecimal delta = BigDecimal.valueOf(previousData != null ? currentValue - previousData.value : 0.0); - if (config.isOnlyComputeTrueDeltas() && delta.doubleValue() == 0) { - ctx.tellSuccess(msg); + if (config.isTellFailureIfDeltaIsNegative() && delta.doubleValue() < 0) { + ctx.tellFailure(msg, new IllegalArgumentException("Delta value is negative!")); return; } - if (config.isTellFailureIfDeltaIsNegative() && delta.doubleValue() < 0) { - ctx.tellFailure(msg, new IllegalArgumentException("Delta value is negative!")); + if (config.isExcludeZeroDeltasFromOutboundMessage() && delta.doubleValue() == 0) { + ctx.tellSuccess(msg); return; } @@ -141,10 +141,10 @@ public class CalculateDeltaNode implements TbNode { boolean hasChanges = false; switch (fromVersion) { case 0: - String onlyComputeTrueDeltas = "onlyComputeTrueDeltas"; - if (!oldConfiguration.has(onlyComputeTrueDeltas)) { + String excludeZeroDeltasFromOutboundMessage = "excludeZeroDeltasFromOutboundMessage"; + if (!oldConfiguration.has(excludeZeroDeltasFromOutboundMessage)) { hasChanges = true; - ((ObjectNode) oldConfiguration).put(onlyComputeTrueDeltas, false); + ((ObjectNode) oldConfiguration).put(excludeZeroDeltasFromOutboundMessage, false); } break; default: diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/CalculateDeltaNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/CalculateDeltaNodeConfiguration.java index 9f30aae22b..82a2eb85b0 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/CalculateDeltaNodeConfiguration.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/CalculateDeltaNodeConfiguration.java @@ -30,7 +30,7 @@ public class CalculateDeltaNodeConfiguration implements NodeConfiguration CalculateDeltaTestConfig() { + return Stream.of( + // delta = 0, tell failure if delta is negative is set to true and exclude zero deltas from outbound message is set to true so delta should filter out the message. + new CalculateDeltaTestConfig(true, true, 40, 40, (ctx, msg) -> { + verify(ctx).tellSuccess(eq(msg)); + verify(ctx).getDbCallbackExecutor(); + verifyNoMoreInteractions(ctx); + }), + // delta < 0, tell failure if delta is negative is set to true so it should throw exception. + new CalculateDeltaTestConfig(true, true, 40, 41, (ctx, msg) -> { + var errorCaptor = ArgumentCaptor.forClass(Throwable.class); + verify(ctx).tellFailure(eq(msg), errorCaptor.capture()); + verify(ctx).getDbCallbackExecutor(); + verifyNoMoreInteractions(ctx); + assertThat(errorCaptor.getValue()).isInstanceOf(IllegalArgumentException.class).hasMessage("Delta value is negative!"); + }), + // delta < 0, exclude zero deltas from outbound message is set to true so it should return message with delta if delta is negative is set to false. + new CalculateDeltaTestConfig(false, true, 40, 41, (ctx, msg) -> { + var actualMsgCaptor = ArgumentCaptor.forClass(TbMsg.class); + verify(ctx).tellSuccess(actualMsgCaptor.capture()); + verify(ctx).getDbCallbackExecutor(); + verifyNoMoreInteractions(ctx); + String expectedMsgData = "{\"temperature\":40.0,\"airPressure\":123,\"delta\":-1}"; + assertEquals(expectedMsgData, actualMsgCaptor.getValue().getData()); + }), + // delta = 0, tell failure if delta is negative is set to false and exclude zero deltas from outbound message is set to true so delta should filter out the message. + new CalculateDeltaTestConfig(false, true, 40, 40, (ctx, msg) -> { + verify(ctx).tellSuccess(eq(msg)); + verify(ctx).getDbCallbackExecutor(); + verifyNoMoreInteractions(ctx); + }), + // delta > 0, exclude zero deltas from outbound message is set to true so it should return message with delta. + new CalculateDeltaTestConfig(false, true, 40, 39, (ctx, msg) -> { + var actualMsgCaptor = ArgumentCaptor.forClass(TbMsg.class); + verify(ctx).tellSuccess(actualMsgCaptor.capture()); + verify(ctx).getDbCallbackExecutor(); + verifyNoMoreInteractions(ctx); + String expectedMsgData = "{\"temperature\":40.0,\"airPressure\":123,\"delta\":1}"; + assertEquals(expectedMsgData, actualMsgCaptor.getValue().getData()); + }), + // delta > 0, exclude zero deltas from outbound message is set to false so it should return message with delta. + new CalculateDeltaTestConfig(false, false, 40, 39, (ctx, msg) -> { + var actualMsgCaptor = ArgumentCaptor.forClass(TbMsg.class); + verify(ctx).tellSuccess(actualMsgCaptor.capture()); + verify(ctx).getDbCallbackExecutor(); + verifyNoMoreInteractions(ctx); + String expectedMsgData = "{\"temperature\":40.0,\"airPressure\":123,\"delta\":1}"; + assertEquals(expectedMsgData, actualMsgCaptor.getValue().getData()); + }) + ); + } + + @Data + @RequiredArgsConstructor + static class CalculateDeltaTestConfig { + private final boolean tellFailureIfDeltaIsNegative; + private final boolean computeOnlyTrueDeltas; + private final double currentValue; + private final double prevValue; + private final BiConsumer verificationMethod; } private void mockFindLatest(TsKvEntry tsKvEntry) { @@ -493,12 +559,12 @@ public class CalculateDeltaNodeTest extends AbstractRuleNodeUpgradeTest { Arguments.of(0, "{\"inputValueKey\":\"pulseCounter\",\"outputValueKey\":\"delta\",\"useCache\":true,\"addPeriodBetweenMsgs\":false, \"periodValueKey\":\"periodInMs\", \"round\":null,\"tellFailureIfDeltaIsNegative\":true}", true, - "{\"inputValueKey\":\"pulseCounter\",\"outputValueKey\":\"delta\",\"useCache\":true,\"addPeriodBetweenMsgs\":false, \"periodValueKey\":\"periodInMs\", \"round\":null,\"tellFailureIfDeltaIsNegative\":true, \"onlyComputeTrueDeltas\":false}"), + "{\"inputValueKey\":\"pulseCounter\",\"outputValueKey\":\"delta\",\"useCache\":true,\"addPeriodBetweenMsgs\":false, \"periodValueKey\":\"periodInMs\", \"round\":null,\"tellFailureIfDeltaIsNegative\":true, \"excludeZeroDeltasFromOutboundMessage\":false}"), // default config for version 1 with upgrade from version 0 Arguments.of(1, - "{\"inputValueKey\":\"pulseCounter\",\"outputValueKey\":\"delta\",\"useCache\":true,\"addPeriodBetweenMsgs\":false, \"periodValueKey\":\"periodInMs\", \"round\":null,\"tellFailureIfDeltaIsNegative\":true, \"onlyComputeTrueDeltas\":false}", + "{\"inputValueKey\":\"pulseCounter\",\"outputValueKey\":\"delta\",\"useCache\":true,\"addPeriodBetweenMsgs\":false, \"periodValueKey\":\"periodInMs\", \"round\":null,\"tellFailureIfDeltaIsNegative\":true, \"excludeZeroDeltasFromOutboundMessage\":false}", false, - "{\"inputValueKey\":\"pulseCounter\",\"outputValueKey\":\"delta\",\"useCache\":true,\"addPeriodBetweenMsgs\":false, \"periodValueKey\":\"periodInMs\", \"round\":null,\"tellFailureIfDeltaIsNegative\":true, \"onlyComputeTrueDeltas\":false}") + "{\"inputValueKey\":\"pulseCounter\",\"outputValueKey\":\"delta\",\"useCache\":true,\"addPeriodBetweenMsgs\":false, \"periodValueKey\":\"periodInMs\", \"round\":null,\"tellFailureIfDeltaIsNegative\":true, \"excludeZeroDeltasFromOutboundMessage\":false}") ); } From c18cef5eda3e5caf8857ffa32978acaa0119857a Mon Sep 17 00:00:00 2001 From: IrynaMatveieva Date: Tue, 5 Mar 2024 16:37:53 +0200 Subject: [PATCH 03/80] changed variable names --- .../engine/metadata/CalculateDeltaNode.java | 8 ++++---- .../CalculateDeltaNodeConfiguration.java | 4 ++-- .../metadata/CalculateDeltaNodeTest.java | 18 +++++++++--------- 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/CalculateDeltaNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/CalculateDeltaNode.java index ce3a318fe6..7141d66f42 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/CalculateDeltaNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/CalculateDeltaNode.java @@ -104,7 +104,7 @@ public class CalculateDeltaNode implements TbNode { return; } - if (config.isExcludeZeroDeltasFromOutboundMessage() && delta.doubleValue() == 0) { + if (config.isExcludeZeroDeltas() && delta.doubleValue() == 0) { ctx.tellSuccess(msg); return; } @@ -141,10 +141,10 @@ public class CalculateDeltaNode implements TbNode { boolean hasChanges = false; switch (fromVersion) { case 0: - String excludeZeroDeltasFromOutboundMessage = "excludeZeroDeltasFromOutboundMessage"; - if (!oldConfiguration.has(excludeZeroDeltasFromOutboundMessage)) { + String excludeZeroDeltas = "excludeZeroDeltas"; + if (!oldConfiguration.has(excludeZeroDeltas)) { hasChanges = true; - ((ObjectNode) oldConfiguration).put(excludeZeroDeltasFromOutboundMessage, false); + ((ObjectNode) oldConfiguration).put(excludeZeroDeltas, false); } break; default: diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/CalculateDeltaNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/CalculateDeltaNodeConfiguration.java index 82a2eb85b0..0ae558718b 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/CalculateDeltaNodeConfiguration.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/CalculateDeltaNodeConfiguration.java @@ -30,7 +30,7 @@ public class CalculateDeltaNodeConfiguration implements NodeConfiguration CalculateDeltaTestConfig() { return Stream.of( - // delta = 0, tell failure if delta is negative is set to true and exclude zero deltas from outbound message is set to true so delta should filter out the message. + // delta = 0, tell failure if delta is negative is set to true and exclude zero deltas is set to true so delta should filter out the message. new CalculateDeltaTestConfig(true, true, 40, 40, (ctx, msg) -> { verify(ctx).tellSuccess(eq(msg)); verify(ctx).getDbCallbackExecutor(); @@ -474,7 +474,7 @@ public class CalculateDeltaNodeTest extends AbstractRuleNodeUpgradeTest { verifyNoMoreInteractions(ctx); assertThat(errorCaptor.getValue()).isInstanceOf(IllegalArgumentException.class).hasMessage("Delta value is negative!"); }), - // delta < 0, exclude zero deltas from outbound message is set to true so it should return message with delta if delta is negative is set to false. + // delta < 0, exclude zero deltas is set to true so it should return message with delta if delta is negative is set to false. new CalculateDeltaTestConfig(false, true, 40, 41, (ctx, msg) -> { var actualMsgCaptor = ArgumentCaptor.forClass(TbMsg.class); verify(ctx).tellSuccess(actualMsgCaptor.capture()); @@ -483,13 +483,13 @@ public class CalculateDeltaNodeTest extends AbstractRuleNodeUpgradeTest { String expectedMsgData = "{\"temperature\":40.0,\"airPressure\":123,\"delta\":-1}"; assertEquals(expectedMsgData, actualMsgCaptor.getValue().getData()); }), - // delta = 0, tell failure if delta is negative is set to false and exclude zero deltas from outbound message is set to true so delta should filter out the message. + // delta = 0, tell failure if delta is negative is set to false and exclude zero deltas is set to true so delta should filter out the message. new CalculateDeltaTestConfig(false, true, 40, 40, (ctx, msg) -> { verify(ctx).tellSuccess(eq(msg)); verify(ctx).getDbCallbackExecutor(); verifyNoMoreInteractions(ctx); }), - // delta > 0, exclude zero deltas from outbound message is set to true so it should return message with delta. + // delta > 0, exclude zero deltas is set to true so it should return message with delta. new CalculateDeltaTestConfig(false, true, 40, 39, (ctx, msg) -> { var actualMsgCaptor = ArgumentCaptor.forClass(TbMsg.class); verify(ctx).tellSuccess(actualMsgCaptor.capture()); @@ -498,7 +498,7 @@ public class CalculateDeltaNodeTest extends AbstractRuleNodeUpgradeTest { String expectedMsgData = "{\"temperature\":40.0,\"airPressure\":123,\"delta\":1}"; assertEquals(expectedMsgData, actualMsgCaptor.getValue().getData()); }), - // delta > 0, exclude zero deltas from outbound message is set to false so it should return message with delta. + // delta > 0, exclude zero deltas is set to false so it should return message with delta. new CalculateDeltaTestConfig(false, false, 40, 39, (ctx, msg) -> { var actualMsgCaptor = ArgumentCaptor.forClass(TbMsg.class); verify(ctx).tellSuccess(actualMsgCaptor.capture()); @@ -559,12 +559,12 @@ public class CalculateDeltaNodeTest extends AbstractRuleNodeUpgradeTest { Arguments.of(0, "{\"inputValueKey\":\"pulseCounter\",\"outputValueKey\":\"delta\",\"useCache\":true,\"addPeriodBetweenMsgs\":false, \"periodValueKey\":\"periodInMs\", \"round\":null,\"tellFailureIfDeltaIsNegative\":true}", true, - "{\"inputValueKey\":\"pulseCounter\",\"outputValueKey\":\"delta\",\"useCache\":true,\"addPeriodBetweenMsgs\":false, \"periodValueKey\":\"periodInMs\", \"round\":null,\"tellFailureIfDeltaIsNegative\":true, \"excludeZeroDeltasFromOutboundMessage\":false}"), + "{\"inputValueKey\":\"pulseCounter\",\"outputValueKey\":\"delta\",\"useCache\":true,\"addPeriodBetweenMsgs\":false, \"periodValueKey\":\"periodInMs\", \"round\":null,\"tellFailureIfDeltaIsNegative\":true, \"excludeZeroDeltas\":false}"), // default config for version 1 with upgrade from version 0 Arguments.of(1, - "{\"inputValueKey\":\"pulseCounter\",\"outputValueKey\":\"delta\",\"useCache\":true,\"addPeriodBetweenMsgs\":false, \"periodValueKey\":\"periodInMs\", \"round\":null,\"tellFailureIfDeltaIsNegative\":true, \"excludeZeroDeltasFromOutboundMessage\":false}", + "{\"inputValueKey\":\"pulseCounter\",\"outputValueKey\":\"delta\",\"useCache\":true,\"addPeriodBetweenMsgs\":false, \"periodValueKey\":\"periodInMs\", \"round\":null,\"tellFailureIfDeltaIsNegative\":true, \"excludeZeroDeltas\":false}", false, - "{\"inputValueKey\":\"pulseCounter\",\"outputValueKey\":\"delta\",\"useCache\":true,\"addPeriodBetweenMsgs\":false, \"periodValueKey\":\"periodInMs\", \"round\":null,\"tellFailureIfDeltaIsNegative\":true, \"excludeZeroDeltasFromOutboundMessage\":false}") + "{\"inputValueKey\":\"pulseCounter\",\"outputValueKey\":\"delta\",\"useCache\":true,\"addPeriodBetweenMsgs\":false, \"periodValueKey\":\"periodInMs\", \"round\":null,\"tellFailureIfDeltaIsNegative\":true, \"excludeZeroDeltas\":false}") ); } From 7d4d7f97aa933299797f6f1355ba5cb64214a674 Mon Sep 17 00:00:00 2001 From: IrynaMatveieva Date: Wed, 6 Mar 2024 10:21:00 +0200 Subject: [PATCH 04/80] restricted access modifier for CalculateDeltaTestConfig to private --- .../metadata/CalculateDeltaNodeTest.java | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/CalculateDeltaNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/CalculateDeltaNodeTest.java index 404ee49287..74269b9be6 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/CalculateDeltaNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/CalculateDeltaNodeTest.java @@ -440,7 +440,7 @@ public class CalculateDeltaNodeTest extends AbstractRuleNodeUpgradeTest { public void givenCalculateDeltaConfig_whenOnMsg_thenVerify(CalculateDeltaTestConfig testConfig) throws TbNodeException { // GIVEN config.setTellFailureIfDeltaIsNegative(testConfig.isTellFailureIfDeltaIsNegative()); - config.setExcludeZeroDeltas(testConfig.isComputeOnlyTrueDeltas()); + config.setExcludeZeroDeltas(testConfig.isExcludeZeroDeltas()); config.setInputValueKey("temperature"); nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config)); node.init(ctxMock, nodeConfiguration); @@ -458,7 +458,7 @@ public class CalculateDeltaNodeTest extends AbstractRuleNodeUpgradeTest { testConfig.getVerificationMethod().accept(ctxMock, msg); } - static Stream CalculateDeltaTestConfig() { + private static Stream CalculateDeltaTestConfig() { return Stream.of( // delta = 0, tell failure if delta is negative is set to true and exclude zero deltas is set to true so delta should filter out the message. new CalculateDeltaTestConfig(true, true, 40, 40, (ctx, msg) -> { @@ -467,7 +467,7 @@ public class CalculateDeltaNodeTest extends AbstractRuleNodeUpgradeTest { verifyNoMoreInteractions(ctx); }), // delta < 0, tell failure if delta is negative is set to true so it should throw exception. - new CalculateDeltaTestConfig(true, true, 40, 41, (ctx, msg) -> { + new CalculateDeltaTestConfig(true, true, 41, 40, (ctx, msg) -> { var errorCaptor = ArgumentCaptor.forClass(Throwable.class); verify(ctx).tellFailure(eq(msg), errorCaptor.capture()); verify(ctx).getDbCallbackExecutor(); @@ -475,7 +475,7 @@ public class CalculateDeltaNodeTest extends AbstractRuleNodeUpgradeTest { assertThat(errorCaptor.getValue()).isInstanceOf(IllegalArgumentException.class).hasMessage("Delta value is negative!"); }), // delta < 0, exclude zero deltas is set to true so it should return message with delta if delta is negative is set to false. - new CalculateDeltaTestConfig(false, true, 40, 41, (ctx, msg) -> { + new CalculateDeltaTestConfig(false, true, 41, 40, (ctx, msg) -> { var actualMsgCaptor = ArgumentCaptor.forClass(TbMsg.class); verify(ctx).tellSuccess(actualMsgCaptor.capture()); verify(ctx).getDbCallbackExecutor(); @@ -490,7 +490,7 @@ public class CalculateDeltaNodeTest extends AbstractRuleNodeUpgradeTest { verifyNoMoreInteractions(ctx); }), // delta > 0, exclude zero deltas is set to true so it should return message with delta. - new CalculateDeltaTestConfig(false, true, 40, 39, (ctx, msg) -> { + new CalculateDeltaTestConfig(false, true, 39, 40, (ctx, msg) -> { var actualMsgCaptor = ArgumentCaptor.forClass(TbMsg.class); verify(ctx).tellSuccess(actualMsgCaptor.capture()); verify(ctx).getDbCallbackExecutor(); @@ -499,7 +499,7 @@ public class CalculateDeltaNodeTest extends AbstractRuleNodeUpgradeTest { assertEquals(expectedMsgData, actualMsgCaptor.getValue().getData()); }), // delta > 0, exclude zero deltas is set to false so it should return message with delta. - new CalculateDeltaTestConfig(false, false, 40, 39, (ctx, msg) -> { + new CalculateDeltaTestConfig(false, false, 39, 40, (ctx, msg) -> { var actualMsgCaptor = ArgumentCaptor.forClass(TbMsg.class); verify(ctx).tellSuccess(actualMsgCaptor.capture()); verify(ctx).getDbCallbackExecutor(); @@ -512,11 +512,11 @@ public class CalculateDeltaNodeTest extends AbstractRuleNodeUpgradeTest { @Data @RequiredArgsConstructor - static class CalculateDeltaTestConfig { + private static class CalculateDeltaTestConfig { private final boolean tellFailureIfDeltaIsNegative; - private final boolean computeOnlyTrueDeltas; - private final double currentValue; + private final boolean excludeZeroDeltas; private final double prevValue; + private final double currentValue; private final BiConsumer verificationMethod; } From 3e8f9e3242ec2b5fd34b859c2b7a07074df47750 Mon Sep 17 00:00:00 2001 From: IrynaMatveieva Date: Mon, 11 Mar 2024 15:27:25 +0200 Subject: [PATCH 05/80] added ability to send string without quotes --- .../rule/engine/mqtt/TbMqttNode.java | 42 +++++++++- .../engine/mqtt/TbMqttNodeConfiguration.java | 2 + .../rule/engine/mqtt/TbMqttNodeTest.java | 79 +++++++++++++++++++ 3 files changed, 122 insertions(+), 1 deletion(-) create mode 100644 rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/mqtt/TbMqttNodeTest.java diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/TbMqttNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/TbMqttNode.java index 6e80a1577b..7d8935f25e 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/TbMqttNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/TbMqttNode.java @@ -15,11 +15,14 @@ */ package org.thingsboard.rule.engine.mqtt; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; import io.netty.buffer.Unpooled; import io.netty.handler.codec.mqtt.MqttQoS; import io.netty.handler.ssl.SslContext; import io.netty.util.concurrent.Promise; import lombok.extern.slf4j.Slf4j; +import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.mqtt.MqttClient; import org.thingsboard.mqtt.MqttClientConfig; import org.thingsboard.mqtt.MqttConnectResult; @@ -35,6 +38,7 @@ import org.thingsboard.rule.engine.external.TbAbstractExternalNode; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.plugin.ComponentClusteringMode; import org.thingsboard.server.common.data.plugin.ComponentType; +import org.thingsboard.server.common.data.util.TbPair; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgMetaData; @@ -81,7 +85,8 @@ public class TbMqttNode extends TbAbstractExternalNode { public void onMsg(TbContext ctx, TbMsg msg) { String topic = TbNodeUtils.processPattern(this.mqttNodeConfiguration.getTopicPattern(), msg); var tbMsg = ackIfNeeded(ctx, msg); - this.mqttClient.publish(topic, Unpooled.wrappedBuffer(tbMsg.getData().getBytes(UTF8)), MqttQoS.AT_LEAST_ONCE, mqttNodeConfiguration.isRetainedMessage()) + this.mqttClient.publish(topic, Unpooled.wrappedBuffer(getData(tbMsg, mqttNodeConfiguration.isParseToPlainText()).getBytes(UTF8)), + MqttQoS.AT_LEAST_ONCE, mqttNodeConfiguration.isRetainedMessage()) .addListener(future -> { if (future.isSuccess()) { tellSuccess(ctx, tbMsg); @@ -153,4 +158,39 @@ public class TbMqttNode extends TbAbstractExternalNode { return this.mqttNodeConfiguration.isSsl() ? this.mqttNodeConfiguration.getCredentials().initSslContext() : null; } + private String getData(TbMsg tbMsg, boolean parseToPlainText) { + if (parseToPlainText) { + return parseJsonStringToPlainText(tbMsg.getData()); + } + return tbMsg.getData(); + } + + protected String parseJsonStringToPlainText(String data) { + if (data.startsWith("\"") && data.endsWith("\"") && data.length() >= 2) { + final String dataBefore = data; + try { + data = JacksonUtil.fromString(data, String.class); + } catch (Exception ignored) {} + log.trace("Trimming double quotes. Before trim: [{}], after trim: [{}]", dataBefore, data); + } + + return data; + } + + @Override + public TbPair upgrade(int fromVersion, JsonNode oldConfiguration) throws TbNodeException { + boolean hasChanges = false; + switch (fromVersion) { + case 0: + String parseToPlainText = "parseToPlainText"; + if (!oldConfiguration.has(parseToPlainText)) { + hasChanges = true; + ((ObjectNode) oldConfiguration).put(parseToPlainText, false); + } + break; + default: + break; + } + return new TbPair<>(hasChanges, oldConfiguration); + } } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/TbMqttNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/TbMqttNodeConfiguration.java index 5f13b0e677..edf3618631 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/TbMqttNodeConfiguration.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/TbMqttNodeConfiguration.java @@ -33,6 +33,7 @@ public class TbMqttNodeConfiguration implements NodeConfiguration givenFromVersionAndConfig_whenUpgrade_thenVerifyHasChangesAndConfig() { + return Stream.of( + // default config for version 0 + Arguments.of(0, + "{\"topicPattern\":\"my-topic\",\"port\":1883,\"connectTimeoutSec\":10,\"cleanSession\":true, \"ssl\":false, \"retainedMessage\":false,\"credentials\":{\"type\":\"anonymous\"}}", + true, + "{\"topicPattern\":\"my-topic\",\"port\":1883,\"connectTimeoutSec\":10,\"cleanSession\":true, \"ssl\":false, \"retainedMessage\":false,\"credentials\":{\"type\":\"anonymous\"},\"parseToPlainText\":false}"), + // default config for version 1 with upgrade from version 0 + Arguments.of(1, + "{\"topicPattern\":\"my-topic\",\"port\":1883,\"connectTimeoutSec\":10,\"cleanSession\":true, \"ssl\":false, \"retainedMessage\":false,\"credentials\":{\"type\":\"anonymous\"},\"parseToPlainText\":false}", + false, + "{\"topicPattern\":\"my-topic\",\"port\":1883,\"connectTimeoutSec\":10,\"cleanSession\":true, \"ssl\":false, \"retainedMessage\":false,\"credentials\":{\"type\":\"anonymous\"},\"parseToPlainText\":false}") + ); + + } + + @Override + protected TbNode getTestNode() { + return node; + } +} \ No newline at end of file From 79ba823c03829a56854f7f3b37a7091dcc516899 Mon Sep 17 00:00:00 2001 From: IrynaMatveieva Date: Mon, 11 Mar 2024 15:38:44 +0200 Subject: [PATCH 06/80] Revert "added ability to send string without quotes" This reverts commit 3e8f9e3242ec2b5fd34b859c2b7a07074df47750. --- .../rule/engine/mqtt/TbMqttNode.java | 42 +--------- .../engine/mqtt/TbMqttNodeConfiguration.java | 2 - .../rule/engine/mqtt/TbMqttNodeTest.java | 79 ------------------- 3 files changed, 1 insertion(+), 122 deletions(-) delete mode 100644 rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/mqtt/TbMqttNodeTest.java diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/TbMqttNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/TbMqttNode.java index 7d8935f25e..6e80a1577b 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/TbMqttNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/TbMqttNode.java @@ -15,14 +15,11 @@ */ package org.thingsboard.rule.engine.mqtt; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.node.ObjectNode; import io.netty.buffer.Unpooled; import io.netty.handler.codec.mqtt.MqttQoS; import io.netty.handler.ssl.SslContext; import io.netty.util.concurrent.Promise; import lombok.extern.slf4j.Slf4j; -import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.mqtt.MqttClient; import org.thingsboard.mqtt.MqttClientConfig; import org.thingsboard.mqtt.MqttConnectResult; @@ -38,7 +35,6 @@ import org.thingsboard.rule.engine.external.TbAbstractExternalNode; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.plugin.ComponentClusteringMode; import org.thingsboard.server.common.data.plugin.ComponentType; -import org.thingsboard.server.common.data.util.TbPair; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgMetaData; @@ -85,8 +81,7 @@ public class TbMqttNode extends TbAbstractExternalNode { public void onMsg(TbContext ctx, TbMsg msg) { String topic = TbNodeUtils.processPattern(this.mqttNodeConfiguration.getTopicPattern(), msg); var tbMsg = ackIfNeeded(ctx, msg); - this.mqttClient.publish(topic, Unpooled.wrappedBuffer(getData(tbMsg, mqttNodeConfiguration.isParseToPlainText()).getBytes(UTF8)), - MqttQoS.AT_LEAST_ONCE, mqttNodeConfiguration.isRetainedMessage()) + this.mqttClient.publish(topic, Unpooled.wrappedBuffer(tbMsg.getData().getBytes(UTF8)), MqttQoS.AT_LEAST_ONCE, mqttNodeConfiguration.isRetainedMessage()) .addListener(future -> { if (future.isSuccess()) { tellSuccess(ctx, tbMsg); @@ -158,39 +153,4 @@ public class TbMqttNode extends TbAbstractExternalNode { return this.mqttNodeConfiguration.isSsl() ? this.mqttNodeConfiguration.getCredentials().initSslContext() : null; } - private String getData(TbMsg tbMsg, boolean parseToPlainText) { - if (parseToPlainText) { - return parseJsonStringToPlainText(tbMsg.getData()); - } - return tbMsg.getData(); - } - - protected String parseJsonStringToPlainText(String data) { - if (data.startsWith("\"") && data.endsWith("\"") && data.length() >= 2) { - final String dataBefore = data; - try { - data = JacksonUtil.fromString(data, String.class); - } catch (Exception ignored) {} - log.trace("Trimming double quotes. Before trim: [{}], after trim: [{}]", dataBefore, data); - } - - return data; - } - - @Override - public TbPair upgrade(int fromVersion, JsonNode oldConfiguration) throws TbNodeException { - boolean hasChanges = false; - switch (fromVersion) { - case 0: - String parseToPlainText = "parseToPlainText"; - if (!oldConfiguration.has(parseToPlainText)) { - hasChanges = true; - ((ObjectNode) oldConfiguration).put(parseToPlainText, false); - } - break; - default: - break; - } - return new TbPair<>(hasChanges, oldConfiguration); - } } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/TbMqttNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/TbMqttNodeConfiguration.java index edf3618631..5f13b0e677 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/TbMqttNodeConfiguration.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/TbMqttNodeConfiguration.java @@ -33,7 +33,6 @@ public class TbMqttNodeConfiguration implements NodeConfiguration givenFromVersionAndConfig_whenUpgrade_thenVerifyHasChangesAndConfig() { - return Stream.of( - // default config for version 0 - Arguments.of(0, - "{\"topicPattern\":\"my-topic\",\"port\":1883,\"connectTimeoutSec\":10,\"cleanSession\":true, \"ssl\":false, \"retainedMessage\":false,\"credentials\":{\"type\":\"anonymous\"}}", - true, - "{\"topicPattern\":\"my-topic\",\"port\":1883,\"connectTimeoutSec\":10,\"cleanSession\":true, \"ssl\":false, \"retainedMessage\":false,\"credentials\":{\"type\":\"anonymous\"},\"parseToPlainText\":false}"), - // default config for version 1 with upgrade from version 0 - Arguments.of(1, - "{\"topicPattern\":\"my-topic\",\"port\":1883,\"connectTimeoutSec\":10,\"cleanSession\":true, \"ssl\":false, \"retainedMessage\":false,\"credentials\":{\"type\":\"anonymous\"},\"parseToPlainText\":false}", - false, - "{\"topicPattern\":\"my-topic\",\"port\":1883,\"connectTimeoutSec\":10,\"cleanSession\":true, \"ssl\":false, \"retainedMessage\":false,\"credentials\":{\"type\":\"anonymous\"},\"parseToPlainText\":false}") - ); - - } - - @Override - protected TbNode getTestNode() { - return node; - } -} \ No newline at end of file From 311ef1bb8b789bbb72032df3c757d101924c9b55 Mon Sep 17 00:00:00 2001 From: IrynaMatveieva Date: Mon, 11 Mar 2024 15:49:12 +0200 Subject: [PATCH 07/80] added a new line to the end of the file --- .../java/org/thingsboard/rule/engine/mqtt/TbMqttNodeTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/mqtt/TbMqttNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/mqtt/TbMqttNodeTest.java index 1f110ef11c..661d4d7791 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/mqtt/TbMqttNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/mqtt/TbMqttNodeTest.java @@ -76,4 +76,4 @@ class TbMqttNodeTest extends AbstractRuleNodeUpgradeTest { protected TbNode getTestNode() { return node; } -} \ No newline at end of file +} From d67fd993bb47078906d4abaf035f7f5c71856ba3 Mon Sep 17 00:00:00 2001 From: IrynaMatveieva Date: Tue, 12 Mar 2024 17:58:11 +0200 Subject: [PATCH 08/80] added a method to get only the last IN debug event --- .../controller/RuleChainController.java | 15 ++-------- .../server/dao/event/EventService.java | 2 ++ .../server/dao/event/BaseEventService.java | 9 ++++++ .../server/dao/event/EventDao.java | 9 ++++++ .../server/dao/sql/event/JpaBaseEventDao.java | 5 ++++ .../event/RuleNodeDebugEventRepository.java | 4 +++ .../service/event/BaseEventServiceTest.java | 27 +++++++++++++++++ .../dao/sql/event/JpaBaseEventDaoTest.java | 30 +++++++++++++++++++ 8 files changed, 88 insertions(+), 13 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/controller/RuleChainController.java b/application/src/main/java/org/thingsboard/server/controller/RuleChainController.java index 2d21986834..a65c575c08 100644 --- a/application/src/main/java/org/thingsboard/server/controller/RuleChainController.java +++ b/application/src/main/java/org/thingsboard/server/controller/RuleChainController.java @@ -45,7 +45,6 @@ import org.thingsboard.server.actors.tenant.DebugTbRateLimits; import org.thingsboard.server.common.data.EventInfo; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.edge.Edge; -import org.thingsboard.server.common.data.event.EventType; import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.EdgeId; import org.thingsboard.server.common.data.id.RuleChainId; @@ -340,18 +339,8 @@ public class RuleChainController extends BaseController { RuleNodeId ruleNodeId = new RuleNodeId(toUUID(strRuleNodeId)); checkRuleNode(ruleNodeId, Operation.READ); TenantId tenantId = getCurrentUser().getTenantId(); - List events = eventService.findLatestEvents(tenantId, ruleNodeId, EventType.DEBUG_RULE_NODE, 2); - JsonNode result = null; - if (events != null) { - for (EventInfo event : events) { - JsonNode body = event.getBody(); - if (body.has("type") && body.get("type").asText().equals("IN")) { - result = body; - break; - } - } - } - return result; + EventInfo eventInfo = eventService.findLatestDebugRuleNodeInEvent(tenantId, ruleNodeId); + return eventInfo.getBody(); } @ApiOperation(value = "Is TBEL script executor enabled", diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/event/EventService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/event/EventService.java index 9b0d5a280d..ef6ebdcdd7 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/event/EventService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/event/EventService.java @@ -35,6 +35,8 @@ public interface EventService { List findLatestEvents(TenantId tenantId, EntityId entityId, EventType eventType, int limit); + EventInfo findLatestDebugRuleNodeInEvent(TenantId tenantId, EntityId entityId); + PageData findEventsByFilter(TenantId tenantId, EntityId entityId, EventFilter eventFilter, TimePageLink pageLink); void removeEvents(TenantId tenantId, EntityId entityId); diff --git a/dao/src/main/java/org/thingsboard/server/dao/event/BaseEventService.java b/dao/src/main/java/org/thingsboard/server/dao/event/BaseEventService.java index 88834eecbc..fd5a55a167 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/event/BaseEventService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/event/BaseEventService.java @@ -106,6 +106,11 @@ public class BaseEventService implements EventService { return convert(entityId.getEntityType(), eventDao.findLatestEvents(tenantId.getId(), entityId.getId(), eventType, limit)); } + @Override + public EventInfo findLatestDebugRuleNodeInEvent(TenantId tenantId, EntityId entityId) { + return convert(entityId.getEntityType(), eventDao.findLatestDebugRuleNodeInEvent(tenantId.getId(), entityId.getId())); + } + @Override public PageData findEventsByFilter(TenantId tenantId, EntityId entityId, EventFilter eventFilter, TimePageLink pageLink) { return convert(entityId.getEntityType(), eventDao.findEventByFilter(tenantId.getId(), entityId.getId(), eventFilter, pageLink)); @@ -140,4 +145,8 @@ public class BaseEventService implements EventService { return list == null ? null : list.stream().map(e -> e.toInfo(entityType)).collect(Collectors.toList()); } + private EventInfo convert(EntityType entityType, Event event) { + return event == null ? null : event.toInfo(entityType); + } + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/event/EventDao.java b/dao/src/main/java/org/thingsboard/server/dao/event/EventDao.java index 2db5a7012d..6b3febf502 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/event/EventDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/event/EventDao.java @@ -62,6 +62,15 @@ public interface EventDao { */ List findLatestEvents(UUID tenantId, UUID entityId, EventType eventType, int limit); + /** + * Find latest debug IN event by tenantId, entityId. + * + * @param tenantId the tenantId + * @param entityId the entityId + * @return the latest debug IN event + */ + Event findLatestDebugRuleNodeInEvent(UUID tenantId, UUID entityId); + /** * Executes stored procedure to cleanup old events. Uses separate ttl for debug and other events. * @param regularEventExpTs the expiration time of the regular events diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/event/JpaBaseEventDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/event/JpaBaseEventDao.java index e19556be41..4e2b799f47 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/event/JpaBaseEventDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/event/JpaBaseEventDao.java @@ -392,6 +392,11 @@ public class JpaBaseEventDao implements EventDao { return DaoUtil.convertDataList(getEventRepository(eventType).findLatestEvents(tenantId, entityId, limit)); } + @Override + public Event findLatestDebugRuleNodeInEvent(UUID tenantId, UUID entityId) { + return DaoUtil.getData(ruleNodeDebugEventRepository.findLatestDebugRuleNodeInEvent(tenantId, entityId)); + } + @Override public void cleanupEvents(long regularEventExpTs, long debugEventExpTs, boolean cleanupDb) { if (regularEventExpTs > 0) { diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/event/RuleNodeDebugEventRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/event/RuleNodeDebugEventRepository.java index 7c13c923c8..05473d92f7 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/event/RuleNodeDebugEventRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/event/RuleNodeDebugEventRepository.java @@ -26,6 +26,7 @@ import org.thingsboard.server.common.data.event.RuleNodeDebugEvent; import org.thingsboard.server.dao.model.sql.RuleNodeDebugEventEntity; import java.util.List; +import java.util.Optional; import java.util.UUID; @@ -35,6 +36,9 @@ public interface RuleNodeDebugEventRepository extends EventRepository findLatestEvents(@Param("tenantId") UUID tenantId, @Param("entityId") UUID entityId, @Param("limit") int limit); + @Query(nativeQuery = true, value = "SELECT * FROM rule_node_debug_event e WHERE e.tenant_id = :tenantId AND e.entity_id = :entityId AND e.e_type = 'IN' ORDER BY e.ts DESC LIMIT 1") + Optional findLatestDebugRuleNodeInEvent(@Param("tenantId") UUID tenantId, @Param("entityId") UUID entityId); + @Override @Query("SELECT e FROM RuleNodeDebugEventEntity e WHERE " + "e.tenantId = :tenantId " + diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/event/BaseEventServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/event/BaseEventServiceTest.java index b73728d314..5331b291dd 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/event/BaseEventServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/event/BaseEventServiceTest.java @@ -132,6 +132,22 @@ public abstract class BaseEventServiceTest extends AbstractServiceTest { eventService.cleanupEvents(timeBeforeStartTime - 1, timeAfterEndTime + 1, true); } + @Test + public void findLatestDebugRuleNodeInEvent() { + CustomerId customerId = new CustomerId(Uuids.timeBased()); + TenantId tenantId = TenantId.fromUUID(Uuids.timeBased()); + + Event event1 = saveDebugEvent(customerId, tenantId); + Event event2 = saveDebugEvent(customerId, tenantId); + + EventInfo event = eventService.findLatestDebugRuleNodeInEvent(tenantId, customerId); + + Assert.assertNotNull(event); + Assert.assertEquals(event2.getUuidId(), event.getUuidId()); + + eventService.cleanupEvents(timeBeforeStartTime - 1, timeAfterEndTime + 1, true); + } + private Event saveEventWithProvidedTime(long time, EntityId entityId, TenantId tenantId) throws Exception { RuleNodeDebugEvent event = generateEvent(tenantId, entityId); event.setId(new EventId(Uuids.timeBased())); @@ -139,4 +155,15 @@ public abstract class BaseEventServiceTest extends AbstractServiceTest { eventService.saveAsync(event).get(); return event; } + + private Event saveDebugEvent(EntityId entityId, TenantId tenantId) { + Event event = RuleNodeDebugEvent.builder() + .tenantId(tenantId) + .entityId(entityId.getId()) + .serviceId("server A") + .eventType("IN") + .build(); + eventService.saveAsync(event); + return event; + } } diff --git a/dao/src/test/java/org/thingsboard/server/dao/sql/event/JpaBaseEventDaoTest.java b/dao/src/test/java/org/thingsboard/server/dao/sql/event/JpaBaseEventDaoTest.java index 5c908fa1b4..8add7471cd 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/sql/event/JpaBaseEventDaoTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/sql/event/JpaBaseEventDaoTest.java @@ -27,6 +27,7 @@ import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.TimePageLink; import org.thingsboard.server.dao.AbstractJpaDaoTest; import org.thingsboard.server.dao.event.EventDao; +import org.thingsboard.server.dao.model.sql.RuleNodeDebugEventEntity; import java.util.List; import java.util.UUID; @@ -42,6 +43,10 @@ public class JpaBaseEventDaoTest extends AbstractJpaDaoTest { @Autowired private EventDao eventDao; + + @Autowired + private RuleNodeDebugEventRepository ruleNodeDebugEventRepository; + UUID tenantId = Uuids.timeBased(); @@ -106,6 +111,22 @@ public class JpaBaseEventDaoTest extends AbstractJpaDaoTest { } + @Test + public void findLatestDebugRuleNodeInEvent() { + + UUID entityId = Uuids.timeBased(); + + RuleNodeDebugEventEntity event = getDebugEventEntity(Uuids.timeBased(), tenantId, entityId); + eventDao.saveAsync(event.toData()); + RuleNodeDebugEventEntity event2 = getDebugEventEntity(Uuids.timeBased(), tenantId, entityId); + eventDao.saveAsync(event2.toData()); + + RuleNodeDebugEventEntity foundEvent = ruleNodeDebugEventRepository.findLatestDebugRuleNodeInEvent(tenantId, entityId).get(); + assertNotNull("Events expected to be not null", foundEvent); + assertEquals(event2.getEventType(), foundEvent.getEventType()); + assertEquals(event2.getId(), foundEvent.getId()); + } + private Event getStatsEvent(UUID eventId, UUID tenantId, UUID entityId) { StatisticsEvent.StatisticsEventBuilder event = StatisticsEvent.builder(); event.id(eventId); @@ -117,4 +138,13 @@ public class JpaBaseEventDaoTest extends AbstractJpaDaoTest { event.errorsOccurred(0); return event.build(); } + + private RuleNodeDebugEventEntity getDebugEventEntity(UUID eventId, UUID tenantId, UUID entityId) { + RuleNodeDebugEventEntity event = new RuleNodeDebugEventEntity(); + event.setId(eventId); + event.setEventType("IN"); + event.setEntityId(entityId); + event.setTenantId(tenantId); + return event; + } } From 837933a2e02f7a558f8684cb2eb185ce34aed7e8 Mon Sep 17 00:00:00 2001 From: IrynaMatveieva Date: Tue, 12 Mar 2024 18:53:45 +0200 Subject: [PATCH 09/80] moved the static toPlainText() method to the JacksonUtil class --- .../thingsboard/common/util/JacksonUtil.java | 16 ++++++++++++++++ .../common/util/JacksonUtilTest.java | 14 ++++++++++++++ .../rule/engine/mqtt/TbMqttNode.java | 14 +------------- .../rule/engine/rest/TbHttpClient.java | 14 +------------- .../rule/engine/mqtt/TbMqttNodeTest.java | 19 ------------------- .../rule/engine/rest/TbHttpClientTest.java | 16 ---------------- 6 files changed, 32 insertions(+), 61 deletions(-) diff --git a/common/util/src/main/java/org/thingsboard/common/util/JacksonUtil.java b/common/util/src/main/java/org/thingsboard/common/util/JacksonUtil.java index 53c3860dde..dd20282d7e 100644 --- a/common/util/src/main/java/org/thingsboard/common/util/JacksonUtil.java +++ b/common/util/src/main/java/org/thingsboard/common/util/JacksonUtil.java @@ -30,6 +30,7 @@ import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.ObjectNode; import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import com.google.common.collect.Lists; +import lombok.extern.slf4j.Slf4j; import org.thingsboard.server.common.data.kv.DataType; import org.thingsboard.server.common.data.kv.KvEntry; @@ -50,6 +51,7 @@ import java.util.regex.Pattern; /** * Created by Valerii Sosliuk on 5/12/2017. */ +@Slf4j public class JacksonUtil { public static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); @@ -150,6 +152,20 @@ public class JacksonUtil { } } + public static String toPlainText(String data) { + if (data == null) { + return null; + } + if (data.startsWith("\"") && data.endsWith("\"") && data.length() >= 2) { + final String dataBefore = data; + try { + data = JacksonUtil.fromString(data, String.class); + } catch (Exception ignored) {} + log.trace("Trimming double quotes. Before trim: [{}], after trim: [{}]", dataBefore, data); + } + return data; + } + public static T treeToValue(JsonNode node, Class clazz) { try { return OBJECT_MAPPER.treeToValue(node, clazz); diff --git a/common/util/src/test/java/org/thingsboard/common/util/JacksonUtilTest.java b/common/util/src/test/java/org/thingsboard/common/util/JacksonUtilTest.java index 7e37d34509..dcddc6e07a 100644 --- a/common/util/src/test/java/org/thingsboard/common/util/JacksonUtilTest.java +++ b/common/util/src/test/java/org/thingsboard/common/util/JacksonUtilTest.java @@ -19,6 +19,9 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ObjectNode; import org.junit.Assert; import org.junit.Test; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; import org.thingsboard.server.common.data.asset.Asset; import org.thingsboard.server.common.data.id.AssetId; @@ -55,4 +58,15 @@ public class JacksonUtilTest { Assert.assertEquals(asset.getName(), result.getName()); Assert.assertEquals(asset.getType(), result.getType()); } + + @ParameterizedTest + @ValueSource(strings = { "", "false", "\"", "\"\"", "\"This is a string with double quotes\"", "Path: /home/developer/test.txt", + "First line\nSecond line\n\nFourth line", "Before\rAfter", "Tab\tSeparated\tValues", "Test\bbackspace", "[]", + "[1, 2, 3]", "{\"key\": \"value\"}", "{\n\"temperature\": 25.5,\n\"humidity\": 50.2\n\"}", "Expression: (a + b) * c", + "世界", "Україна", "\u1F1FA\u1F1E6", "🇺🇦"}) + public void toPlainTextTest(String original) { + String serialized = JacksonUtil.toString(original); + Assertions.assertNotNull(serialized); + Assertions.assertEquals(original, JacksonUtil.toPlainText(serialized)); + } } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/TbMqttNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/TbMqttNode.java index 7d8935f25e..5074024be5 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/TbMqttNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/TbMqttNode.java @@ -160,23 +160,11 @@ public class TbMqttNode extends TbAbstractExternalNode { private String getData(TbMsg tbMsg, boolean parseToPlainText) { if (parseToPlainText) { - return parseJsonStringToPlainText(tbMsg.getData()); + return JacksonUtil.toPlainText(tbMsg.getData()); } return tbMsg.getData(); } - protected String parseJsonStringToPlainText(String data) { - if (data.startsWith("\"") && data.endsWith("\"") && data.length() >= 2) { - final String dataBefore = data; - try { - data = JacksonUtil.fromString(data, String.class); - } catch (Exception ignored) {} - log.trace("Trimming double quotes. Before trim: [{}], after trim: [{}]", dataBefore, data); - } - - return data; - } - @Override public TbPair upgrade(int fromVersion, JsonNode oldConfiguration) throws TbNodeException { boolean hasChanges = false; diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbHttpClient.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbHttpClient.java index 73671d0e36..006afb83dc 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbHttpClient.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbHttpClient.java @@ -244,23 +244,11 @@ public class TbHttpClient { private String getData(TbMsg tbMsg, boolean ignoreBody, boolean parseToPlainText) { if (!ignoreBody && parseToPlainText) { - return parseJsonStringToPlainText(tbMsg.getData()); + return JacksonUtil.toPlainText(tbMsg.getData()); } return tbMsg.getData(); } - protected String parseJsonStringToPlainText(String data) { - if (data.startsWith("\"") && data.endsWith("\"") && data.length() >= 2) { - final String dataBefore = data; - try { - data = JacksonUtil.fromString(data, String.class); - } catch (Exception ignored) {} - log.trace("Trimming double quotes. Before trim: [{}], after trim: [{}]", dataBefore, data); - } - - return data; - } - private TbMsg processResponse(TbContext ctx, TbMsg origMsg, ResponseEntity response) { TbMsgMetaData metaData = origMsg.getMetaData(); metaData.putValue(STATUS, response.getStatusCode().name()); diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/mqtt/TbMqttNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/mqtt/TbMqttNodeTest.java index 661d4d7791..b46ecdf6fe 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/mqtt/TbMqttNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/mqtt/TbMqttNodeTest.java @@ -15,22 +15,16 @@ */ package org.thingsboard.rule.engine.mqtt; -import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.extension.ExtendWith; -import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; -import org.junit.jupiter.params.provider.ValueSource; -import org.mockito.Mockito; import org.mockito.Spy; import org.mockito.junit.jupiter.MockitoExtension; -import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.rule.engine.AbstractRuleNodeUpgradeTest; import org.thingsboard.rule.engine.api.TbNode; import java.util.stream.Stream; -import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.mock; @ExtendWith(MockitoExtension.class) @@ -43,19 +37,6 @@ class TbMqttNodeTest extends AbstractRuleNodeUpgradeTest { node = mock(TbMqttNode.class); } - @ParameterizedTest - @ValueSource(strings = { "false", "\"", "\"\"", "\"This is a string with double quotes\"", "Path: /home/developer/test.txt", - "First line\nSecond line\n\nFourth line", "Before\rAfter", "Tab\tSeparated\tValues", "Test\bbackspace", "[]", - "[1, 2, 3]", "{\"key\": \"value\"}", "{\n\"temperature\": 25.5,\n\"humidity\": 50.2\n\"}", "Expression: (a + b) * c", - "世界", "Україна", "\u1F1FA\u1F1E6", "🇺🇦"}) - public void testParseJsonStringToPlainText(String original) { - Mockito.when(node.parseJsonStringToPlainText(anyString())).thenCallRealMethod(); - - String serialized = JacksonUtil.toString(original); - Assertions.assertNotNull(serialized); - Assertions.assertEquals(original, node.parseJsonStringToPlainText(serialized)); - } - private static Stream givenFromVersionAndConfig_whenUpgrade_thenVerifyHasChangesAndConfig() { return Stream.of( // default config for version 0 diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/rest/TbHttpClientTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/rest/TbHttpClientTest.java index b8e9aa3e38..82dbc0e508 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/rest/TbHttpClientTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/rest/TbHttpClientTest.java @@ -22,14 +22,11 @@ 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.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.ValueSource; import org.mockito.ArgumentCaptor; import org.mockito.Mockito; import org.mockserver.integration.ClientAndServer; import org.springframework.util.LinkedMultiValueMap; import org.springframework.web.client.AsyncRestTemplate; -import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.EntityId; @@ -48,7 +45,6 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.instanceOf; import static org.hamcrest.Matchers.is; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.BDDMockito.willCallRealMethod; import static org.mockito.Mockito.mock; @@ -226,16 +222,4 @@ public class TbHttpClientTest { Assertions.assertEquals(data.get("Set-Cookie"), "[\"sap-context=sap-client=075; path=/\",\"sap-token=sap-client=075; path=/\"]"); } - @ParameterizedTest - @ValueSource(strings = { "false", "\"", "\"\"", "\"This is a string with double quotes\"", "Path: /home/developer/test.txt", - "First line\nSecond line\n\nFourth line", "Before\rAfter", "Tab\tSeparated\tValues", "Test\bbackspace", "[]", - "[1, 2, 3]", "{\"key\": \"value\"}", "{\n\"temperature\": 25.5,\n\"humidity\": 50.2\n\"}", "Expression: (a + b) * c", - "世界", "Україна", "\u1F1FA\u1F1E6", "🇺🇦"}) - public void testParseJsonStringToPlainText(String original) { - Mockito.when(client.parseJsonStringToPlainText(anyString())).thenCallRealMethod(); - - String serialized = JacksonUtil.toString(original); - Assertions.assertNotNull(serialized); - Assertions.assertEquals(original, client.parseJsonStringToPlainText(serialized)); - } } From 683fdfb5393ebc2ad943a998db1c3c8dd33dad59 Mon Sep 17 00:00:00 2001 From: IrynaMatveieva Date: Wed, 13 Mar 2024 09:18:36 +0200 Subject: [PATCH 10/80] added check for null --- .../org/thingsboard/server/controller/RuleChainController.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/application/src/main/java/org/thingsboard/server/controller/RuleChainController.java b/application/src/main/java/org/thingsboard/server/controller/RuleChainController.java index a65c575c08..29e1bc9172 100644 --- a/application/src/main/java/org/thingsboard/server/controller/RuleChainController.java +++ b/application/src/main/java/org/thingsboard/server/controller/RuleChainController.java @@ -340,7 +340,7 @@ public class RuleChainController extends BaseController { checkRuleNode(ruleNodeId, Operation.READ); TenantId tenantId = getCurrentUser().getTenantId(); EventInfo eventInfo = eventService.findLatestDebugRuleNodeInEvent(tenantId, ruleNodeId); - return eventInfo.getBody(); + return eventInfo == null ? null : eventInfo.getBody(); } @ApiOperation(value = "Is TBEL script executor enabled", From 8767b6d845a0566306ef2451a67524b69f21d45e Mon Sep 17 00:00:00 2001 From: IrynaMatveieva Date: Tue, 19 Mar 2024 13:32:59 +0200 Subject: [PATCH 11/80] changed method of generating event in tests --- .../dao/service/AbstractServiceTest.java | 5 ++++ .../service/event/BaseEventServiceTest.java | 23 +++++++------------ .../dao/sql/event/JpaBaseEventDaoTest.java | 13 ++++++----- 3 files changed, 20 insertions(+), 21 deletions(-) diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/AbstractServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/AbstractServiceTest.java index e8232af692..5eddc78661 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/AbstractServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/AbstractServiceTest.java @@ -98,6 +98,10 @@ public abstract class AbstractServiceTest { protected RuleNodeDebugEvent generateEvent(TenantId tenantId, EntityId entityId) throws IOException { + return generateEvent(tenantId, entityId, null); + } + + protected RuleNodeDebugEvent generateEvent(TenantId tenantId, EntityId entityId, String eventType) throws IOException { if (tenantId == null) { tenantId = TenantId.fromUUID(Uuids.timeBased()); } @@ -105,6 +109,7 @@ public abstract class AbstractServiceTest { .tenantId(tenantId) .entityId(entityId.getId()) .serviceId("server A") + .eventType(eventType) .data(JacksonUtil.toString(readFromResource("TestJsonData.json"))) .build(); } diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/event/BaseEventServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/event/BaseEventServiceTest.java index 5331b291dd..8aa943484d 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/event/BaseEventServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/event/BaseEventServiceTest.java @@ -133,12 +133,12 @@ public abstract class BaseEventServiceTest extends AbstractServiceTest { } @Test - public void findLatestDebugRuleNodeInEvent() { + public void findLatestDebugRuleNodeInEvent() throws Exception { CustomerId customerId = new CustomerId(Uuids.timeBased()); TenantId tenantId = TenantId.fromUUID(Uuids.timeBased()); - Event event1 = saveDebugEvent(customerId, tenantId); - Event event2 = saveDebugEvent(customerId, tenantId); + Event event1 = saveEventWithProvidedTimeAndEventType(eventTime, "IN", customerId, tenantId); + Event event2 = saveEventWithProvidedTimeAndEventType(eventTime + 1, "IN", customerId, tenantId); EventInfo event = eventService.findLatestDebugRuleNodeInEvent(tenantId, customerId); @@ -149,21 +149,14 @@ public abstract class BaseEventServiceTest extends AbstractServiceTest { } private Event saveEventWithProvidedTime(long time, EntityId entityId, TenantId tenantId) throws Exception { - RuleNodeDebugEvent event = generateEvent(tenantId, entityId); + return saveEventWithProvidedTimeAndEventType(time, null, entityId, tenantId); + } + + private Event saveEventWithProvidedTimeAndEventType(long time, String eventType, EntityId entityId, TenantId tenantId) throws Exception { + RuleNodeDebugEvent event = generateEvent(tenantId, entityId, eventType); event.setId(new EventId(Uuids.timeBased())); event.setCreatedTime(time); eventService.saveAsync(event).get(); return event; } - - private Event saveDebugEvent(EntityId entityId, TenantId tenantId) { - Event event = RuleNodeDebugEvent.builder() - .tenantId(tenantId) - .entityId(entityId.getId()) - .serviceId("server A") - .eventType("IN") - .build(); - eventService.saveAsync(event); - return event; - } } diff --git a/dao/src/test/java/org/thingsboard/server/dao/sql/event/JpaBaseEventDaoTest.java b/dao/src/test/java/org/thingsboard/server/dao/sql/event/JpaBaseEventDaoTest.java index 8add7471cd..b178d7c78f 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/sql/event/JpaBaseEventDaoTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/sql/event/JpaBaseEventDaoTest.java @@ -112,14 +112,15 @@ public class JpaBaseEventDaoTest extends AbstractJpaDaoTest { } @Test - public void findLatestDebugRuleNodeInEvent() { + public void findLatestDebugRuleNodeInEvent() throws Exception { UUID entityId = Uuids.timeBased(); - RuleNodeDebugEventEntity event = getDebugEventEntity(Uuids.timeBased(), tenantId, entityId); - eventDao.saveAsync(event.toData()); - RuleNodeDebugEventEntity event2 = getDebugEventEntity(Uuids.timeBased(), tenantId, entityId); - eventDao.saveAsync(event2.toData()); + RuleNodeDebugEventEntity event = getDebugInEventEntity(Uuids.timeBased(), tenantId, entityId); + eventDao.saveAsync(event.toData()).get(1, TimeUnit.MINUTES); + Thread.sleep(2); + RuleNodeDebugEventEntity event2 = getDebugInEventEntity(Uuids.timeBased(), tenantId, entityId); + eventDao.saveAsync(event2.toData()).get(1, TimeUnit.MINUTES); RuleNodeDebugEventEntity foundEvent = ruleNodeDebugEventRepository.findLatestDebugRuleNodeInEvent(tenantId, entityId).get(); assertNotNull("Events expected to be not null", foundEvent); @@ -139,7 +140,7 @@ public class JpaBaseEventDaoTest extends AbstractJpaDaoTest { return event.build(); } - private RuleNodeDebugEventEntity getDebugEventEntity(UUID eventId, UUID tenantId, UUID entityId) { + private RuleNodeDebugEventEntity getDebugInEventEntity(UUID eventId, UUID tenantId, UUID entityId) { RuleNodeDebugEventEntity event = new RuleNodeDebugEventEntity(); event.setId(eventId); event.setEventType("IN"); From e879f63376786fb1affd55fbcab7e00288f898bf Mon Sep 17 00:00:00 2001 From: IrynaMatveieva Date: Fri, 22 Mar 2024 10:50:21 +0200 Subject: [PATCH 12/80] fixed error message for problem with external id --- .../server/dao/rule/BaseRuleChainService.java | 22 ++-- .../dao/rule/BaseRuleChainServiceTest.java | 110 ++++++++++++++++++ 2 files changed, 123 insertions(+), 9 deletions(-) create mode 100644 dao/src/test/java/org/thingsboard/server/dao/rule/BaseRuleChainServiceTest.java diff --git a/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java b/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java index 2857436fc8..a636ccf879 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java @@ -118,16 +118,20 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC @Transactional public RuleChain saveRuleChain(RuleChain ruleChain, boolean publishSaveEvent) { ruleChainValidator.validate(ruleChain, RuleChain::getTenantId); + RuleChain savedRuleChain = saveRuleChainInternal(ruleChain); + if (ruleChain.getId() == null) { + entityCountService.publishCountEntityEvictEvent(ruleChain.getTenantId(), EntityType.RULE_CHAIN); + } + if (publishSaveEvent) { + eventPublisher.publishEvent(SaveEntityEvent.builder().tenantId(savedRuleChain.getTenantId()) + .entity(savedRuleChain).entityId(savedRuleChain.getId()).created(ruleChain.getId() == null).build()); + } + return savedRuleChain; + } + + private RuleChain saveRuleChainInternal(RuleChain ruleChain) { try { - RuleChain savedRuleChain = ruleChainDao.save(ruleChain.getTenantId(), ruleChain); - if (ruleChain.getId() == null) { - entityCountService.publishCountEntityEvictEvent(ruleChain.getTenantId(), EntityType.RULE_CHAIN); - } - if (publishSaveEvent) { - eventPublisher.publishEvent(SaveEntityEvent.builder().tenantId(savedRuleChain.getTenantId()) - .entity(savedRuleChain).entityId(savedRuleChain.getId()).created(ruleChain.getId() == null).build()); - } - return savedRuleChain; + return ruleChainDao.saveAndFlush(ruleChain.getTenantId(), ruleChain); } catch (Exception e) { checkConstraintViolation(e, "rule_chain_external_id_unq_key", "Rule Chain with such external id already exists!"); throw e; diff --git a/dao/src/test/java/org/thingsboard/server/dao/rule/BaseRuleChainServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/rule/BaseRuleChainServiceTest.java new file mode 100644 index 0000000000..c9a91853ac --- /dev/null +++ b/dao/src/test/java/org/thingsboard/server/dao/rule/BaseRuleChainServiceTest.java @@ -0,0 +1,110 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.rule; + +import org.junit.Assert; +import org.junit.Test; +import org.junit.jupiter.api.Assertions; +import org.springframework.beans.factory.annotation.Autowired; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.common.data.id.RuleChainId; +import org.thingsboard.server.common.data.rule.RuleChain; +import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.dao.service.AbstractServiceTest; +import org.thingsboard.server.dao.service.DaoSqlTest; + +import java.util.UUID; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThrows; + +@DaoSqlTest +public class BaseRuleChainServiceTest extends AbstractServiceTest { + + @Autowired + private BaseRuleChainService ruleChainService; + + @Test + public void givenRuleChain_whenSave_thenReturnsSavedRuleChain() { + RuleChain ruleChain = getRuleChain(ruleChainWithoutId); + ruleChain.setTenantId(tenantId); + RuleChain savedRuleChain = ruleChainService.saveRuleChain(ruleChain); + + Assert.assertNotNull(savedRuleChain); + Assert.assertNotNull(savedRuleChain.getId()); + Assert.assertTrue(savedRuleChain.getCreatedTime() > 0); + Assert.assertEquals(ruleChain.getTenantId(), savedRuleChain.getTenantId()); + + + RuleChain foundRuleChain = ruleChainService.findRuleChainById(tenantId, savedRuleChain.getId()); + Assertions.assertEquals(foundRuleChain.getName(), savedRuleChain.getName()); + + ruleChainService.deleteRuleChainsByTenantId(tenantId); + } + + @Test + public void givenRuleChainWithExistingExternalId_whenSave_thenThrowsException() { + RuleChainId externalRuleChainId = new RuleChainId(UUID.fromString("2675d180-e1e5-11ee-9f06-71b6c7dc2cbf")); + + RuleChain ruleChain = getRuleChain(ruleChainWithoutId); + ruleChain.setTenantId(tenantId); + ruleChain.setExternalId(externalRuleChainId); + RuleChain savedRuleChain = ruleChainService.saveRuleChain(ruleChain); + + RuleChain ruleChainForSave = getRuleChain(ruleChainWithExternalId); + ruleChainForSave.setTenantId(tenantId); + + String expectedMsg = "Rule Chain with such external id already exists!"; + + assertEquals(savedRuleChain.getExternalId(), ruleChainForSave.getExternalId()); + Exception exception = assertThrows(DataValidationException.class, () -> ruleChainService.saveRuleChain(ruleChainForSave)); + assertEquals(expectedMsg, exception.getMessage()); + + ruleChainService.deleteRuleChainsByTenantId(tenantId); + } + + private RuleChain getRuleChain(String ruleChainString) { + return JacksonUtil.fromString(ruleChainString, RuleChain.class); + } + + private final String ruleChainWithoutId = "{\n" + + " \"name\": \"Root Rule Chain\",\n" + + " \"type\": \"CORE\",\n" + + " \"firstRuleNodeId\": {\n" + + " \"entityType\": \"RULE_NODE\",\n" + + " \"id\": \"91ad0b00-e779-11ee-9cf0-15d8b6079fdb\"\n" + + " },\n" + + " \"debugMode\": false,\n" + + " \"configuration\": null,\n" + + " \"additionalInfo\": null\n" + + "}"; + + private final String ruleChainWithExternalId = "{\n" + + " \"name\": \"Root Rule Chain\",\n" + + " \"type\": \"CORE\",\n" + + " \"firstRuleNodeId\": {\n" + + " \"entityType\": \"RULE_NODE\",\n" + + " \"id\": \"91ad0b00-e779-11ee-9cf0-15d8b6079fdb\"\n" + + " },\n" + + " \"debugMode\": false,\n" + + " \"externalId\": {\n" + + " \"entityType\": \"RULE_CHAIN\",\n" + + " \"id\": \"2675d180-e1e5-11ee-9f06-71b6c7dc2cbf\"\n" + + " },\n" + + " \"configuration\": null,\n" + + " \"additionalInfo\": null\n" + + "}"; +} \ No newline at end of file From ed44db6d25e58bde7ea9d50c5cd112d97d9d064a Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Fri, 22 Mar 2024 14:20:51 +0200 Subject: [PATCH 13/80] Add delivery_method column to notification; persist mobile notifications --- .../main/data/upgrade/3.6.3/schema_update.sql | 27 +++++++ .../controller/NotificationController.java | 12 ++- .../DefaultNotificationCenter.java | 32 ++++---- .../MobileAppNotificationChannel.java | 30 +++++++- .../provider/DefaultFirebaseService.java | 23 ++++-- .../DefaultNotificationCommandsHandler.java | 8 +- .../AbstractNotificationApiTest.java | 10 ++- .../notification/NotificationApiTest.java | 77 +++++++++++-------- .../dao/notification/NotificationService.java | 9 ++- .../data/notification/Notification.java | 3 +- .../server/dao/model/ModelConstants.java | 1 + .../dao/model/sql/NotificationEntity.java | 7 ++ .../DefaultNotificationService.java | 19 ++--- .../dao/notification/NotificationDao.java | 11 ++- .../sql/notification/JpaNotificationDao.java | 24 +++--- .../notification/NotificationRepository.java | 33 ++++---- .../resources/sql/schema-entities-idx.sql | 4 +- .../main/resources/sql/schema-entities.sql | 1 + .../rule/engine/api/NotificationCenter.java | 2 +- .../api/notification/FirebaseService.java | 2 +- 20 files changed, 219 insertions(+), 116 deletions(-) create mode 100644 application/src/main/data/upgrade/3.6.3/schema_update.sql diff --git a/application/src/main/data/upgrade/3.6.3/schema_update.sql b/application/src/main/data/upgrade/3.6.3/schema_update.sql new file mode 100644 index 0000000000..3e0fdc3b4b --- /dev/null +++ b/application/src/main/data/upgrade/3.6.3/schema_update.sql @@ -0,0 +1,27 @@ +-- +-- 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. +-- + +-- NOTIFICATIONS UPDATE START + +ALTER TABLE notification ADD COLUMN IF NOT EXISTS delivery_method VARCHAR(50) NOT NULL default 'WEB'; + +DROP INDEX IF EXISTS idx_notification_recipient_id_created_time; +DROP INDEX IF EXISTS idx_notification_recipient_id_unread; + +CREATE INDEX IF NOT EXISTS idx_notification_delivery_method_recipient_id_created_time ON notification(delivery_method, recipient_id, created_time DESC); +CREATE INDEX IF NOT EXISTS idx_notification_delivery_method_recipient_id_unread ON notification(delivery_method, recipient_id) WHERE status <> 'READ'; + +-- NOTIFICATIONS UPDATE END diff --git a/application/src/main/java/org/thingsboard/server/controller/NotificationController.java b/application/src/main/java/org/thingsboard/server/controller/NotificationController.java index d2b095c7fa..8de3997a60 100644 --- a/application/src/main/java/org/thingsboard/server/controller/NotificationController.java +++ b/application/src/main/java/org/thingsboard/server/controller/NotificationController.java @@ -105,6 +105,8 @@ public class NotificationController extends BaseController { private final NotificationCenter notificationCenter; private final NotificationSettingsService notificationSettingsService; + private static final String DELIVERY_METHOD_ALLOWABLE_VALUES = "WEB,MOBILE_APP"; + @ApiOperation(value = "Get notifications (getNotifications)", notes = "Returns the page of notifications for current user." + NEW_LINE + PAGE_DATA_PARAMETERS + @@ -172,10 +174,12 @@ public class NotificationController extends BaseController { @RequestParam(required = false) String sortOrder, @ApiParam(value = "To search for unread notifications only") @RequestParam(defaultValue = "false") boolean unreadOnly, + @ApiParam(value = "Delivery method", allowableValues = DELIVERY_METHOD_ALLOWABLE_VALUES) + @RequestParam(defaultValue = "WEB") NotificationDeliveryMethod deliveryMethod, @AuthenticationPrincipal SecurityUser user) throws ThingsboardException { // no permissions PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); - return notificationService.findNotificationsByRecipientIdAndReadStatus(user.getTenantId(), user.getId(), unreadOnly, pageLink); + return notificationService.findNotificationsByRecipientIdAndReadStatus(user.getTenantId(), deliveryMethod, user.getId(), unreadOnly, pageLink); } @ApiOperation(value = "Mark notification as read (markNotificationAsRead)", @@ -195,9 +199,11 @@ public class NotificationController extends BaseController { AVAILABLE_FOR_ANY_AUTHORIZED_USER) @PutMapping("/notifications/read") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") - public void markAllNotificationsAsRead(@AuthenticationPrincipal SecurityUser user) { + public void markAllNotificationsAsRead(@ApiParam(value = "Delivery method", allowableValues = DELIVERY_METHOD_ALLOWABLE_VALUES) + @RequestParam(defaultValue = "WEB") NotificationDeliveryMethod deliveryMethod, + @AuthenticationPrincipal SecurityUser user) { // no permissions - notificationCenter.markAllNotificationsAsRead(user.getTenantId(), user.getId()); + notificationCenter.markAllNotificationsAsRead(user.getTenantId(), deliveryMethod, user.getId()); } @ApiOperation(value = "Delete notification (deleteNotification)", diff --git a/application/src/main/java/org/thingsboard/server/service/notification/DefaultNotificationCenter.java b/application/src/main/java/org/thingsboard/server/service/notification/DefaultNotificationCenter.java index 25a344b775..2ab7692dbc 100644 --- a/application/src/main/java/org/thingsboard/server/service/notification/DefaultNotificationCenter.java +++ b/application/src/main/java/org/thingsboard/server/service/notification/DefaultNotificationCenter.java @@ -82,6 +82,8 @@ import java.util.Set; import java.util.UUID; import java.util.stream.Collectors; +import static org.thingsboard.server.common.data.notification.NotificationDeliveryMethod.WEB; + @Service @Slf4j @RequiredArgsConstructor @@ -192,7 +194,7 @@ public class DefaultNotificationCenter extends AbstractSubscriptionService imple NotificationProcessingContext ctx = NotificationProcessingContext.builder() .tenantId(tenantId) .request(notificationRequest) - .deliveryMethods(Set.of(NotificationDeliveryMethod.WEB)) + .deliveryMethods(Set.of(WEB)) .template(template) .build(); @@ -323,6 +325,7 @@ public class DefaultNotificationCenter extends AbstractSubscriptionService imple .requestId(request.getId()) .recipientId(recipient.getId()) .type(ctx.getNotificationType()) + .deliveryMethod(WEB) .subject(processedTemplate.getSubject()) .text(processedTemplate.getBody()) .additionalConfig(processedTemplate.getAdditionalConfig()) @@ -348,19 +351,22 @@ public class DefaultNotificationCenter extends AbstractSubscriptionService imple boolean updated = notificationService.markNotificationAsRead(tenantId, recipientId, notificationId); if (updated) { log.trace("Marked notification {} as read (recipient id: {}, tenant id: {})", notificationId, recipientId, tenantId); - NotificationUpdate update = NotificationUpdate.builder() - .updated(true) - .notificationId(notificationId.getId()) - .newStatus(NotificationStatus.READ) - .build(); - onNotificationUpdate(tenantId, recipientId, update); + Notification notification = notificationService.findNotificationById(tenantId, notificationId); + if (notification.getDeliveryMethod() == WEB) { + NotificationUpdate update = NotificationUpdate.builder() + .updated(true) + .notificationId(notificationId.getId()) + .newStatus(NotificationStatus.READ) + .build(); + onNotificationUpdate(tenantId, recipientId, update); + } } } @Override - public void markAllNotificationsAsRead(TenantId tenantId, UserId recipientId) { - int updatedCount = notificationService.markAllNotificationsAsRead(tenantId, recipientId); - if (updatedCount > 0) { + public void markAllNotificationsAsRead(TenantId tenantId, NotificationDeliveryMethod deliveryMethod, UserId recipientId) { + int updatedCount = notificationService.markAllNotificationsAsRead(tenantId, deliveryMethod, recipientId); + if (updatedCount > 0 && deliveryMethod == WEB) { log.trace("Marked all notifications as read (recipient id: {}, tenant id: {})", recipientId, tenantId); NotificationUpdate update = NotificationUpdate.builder() .updated(true) @@ -375,7 +381,7 @@ public class DefaultNotificationCenter extends AbstractSubscriptionService imple public void deleteNotification(TenantId tenantId, UserId recipientId, NotificationId notificationId) { Notification notification = notificationService.findNotificationById(tenantId, notificationId); boolean deleted = notificationService.deleteNotification(tenantId, recipientId, notificationId); - if (deleted) { + if (deleted && notification.getDeliveryMethod() == WEB) { NotificationUpdate update = NotificationUpdate.builder() .deleted(true) .notification(notification) @@ -455,7 +461,7 @@ public class DefaultNotificationCenter extends AbstractSubscriptionService imple @Override public NotificationDeliveryMethod getDeliveryMethod() { - return NotificationDeliveryMethod.WEB; + return WEB; } @Override @@ -466,7 +472,7 @@ public class DefaultNotificationCenter extends AbstractSubscriptionService imple @Autowired public void setChannels(List channels, NotificationCenter webNotificationChannel) { this.channels = channels.stream().collect(Collectors.toMap(NotificationChannel::getDeliveryMethod, c -> c)); - this.channels.put(NotificationDeliveryMethod.WEB, (NotificationChannel) webNotificationChannel); + this.channels.put(WEB, (NotificationChannel) webNotificationChannel); } } diff --git a/application/src/main/java/org/thingsboard/server/service/notification/channels/MobileAppNotificationChannel.java b/application/src/main/java/org/thingsboard/server/service/notification/channels/MobileAppNotificationChannel.java index 646556372a..116e0237a9 100644 --- a/application/src/main/java/org/thingsboard/server/service/notification/channels/MobileAppNotificationChannel.java +++ b/application/src/main/java/org/thingsboard/server/service/notification/channels/MobileAppNotificationChannel.java @@ -26,11 +26,15 @@ import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.rule.engine.api.notification.FirebaseService; import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.notification.Notification; import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod; +import org.thingsboard.server.common.data.notification.NotificationRequest; +import org.thingsboard.server.common.data.notification.NotificationStatus; import org.thingsboard.server.common.data.notification.info.NotificationInfo; import org.thingsboard.server.common.data.notification.settings.MobileAppNotificationDeliveryMethodConfig; import org.thingsboard.server.common.data.notification.settings.NotificationSettings; import org.thingsboard.server.common.data.notification.template.MobileAppDeliveryMethodNotificationTemplate; +import org.thingsboard.server.dao.notification.NotificationService; import org.thingsboard.server.dao.notification.NotificationSettingsService; import org.thingsboard.server.dao.user.UserService; import org.thingsboard.server.service.notification.NotificationProcessingContext; @@ -41,6 +45,8 @@ import java.util.Map; import java.util.Optional; import java.util.Set; +import static org.thingsboard.server.common.data.notification.NotificationDeliveryMethod.MOBILE_APP; + @Component @RequiredArgsConstructor @Slf4j @@ -48,25 +54,41 @@ public class MobileAppNotificationChannel implements NotificationChannel validTokens = new HashSet<>(mobileSessions.keySet()); String subject = processedTemplate.getSubject(); String body = processedTemplate.getBody(); Map data = getNotificationData(processedTemplate, ctx); + int unreadCount = notificationService.countUnreadNotificationsByRecipientId(ctx.getTenantId(), MOBILE_APP, recipient.getId()); for (String token : mobileSessions.keySet()) { try { - firebaseService.sendMessage(ctx.getTenantId(), credentials, token, subject, body, data); + firebaseService.sendMessage(ctx.getTenantId(), credentials, token, subject, body, data, unreadCount); } catch (FirebaseMessagingException e) { MessagingErrorCode errorCode = e.getMessagingErrorCode(); if (errorCode == MessagingErrorCode.UNREGISTERED || errorCode == MessagingErrorCode.INVALID_ARGUMENT) { @@ -116,14 +138,14 @@ public class MobileAppNotificationChannel implements NotificationChannel data) throws FirebaseMessagingException { + public void sendMessage(TenantId tenantId, String credentials, String fcmToken, String title, String body, + Map data, Integer badge) throws FirebaseMessagingException { FirebaseContext firebaseContext = contexts.asMap().compute(tenantId.toString(), (key, context) -> { if (context == null) { return new FirebaseContext(key, credentials); @@ -64,6 +65,13 @@ public class DefaultFirebaseService implements FirebaseService { } }); + Aps.Builder apsConfig = Aps.builder() + .setContentAvailable(true) + .setSound("default"); + if (badge != null) { + apsConfig.setBadge(badge); + } + Message message = Message.builder() .setToken(fcmToken) .setNotification(Notification.builder() @@ -74,14 +82,17 @@ public class DefaultFirebaseService implements FirebaseService { .setPriority(AndroidConfig.Priority.HIGH) .build()) .setApnsConfig(ApnsConfig.builder() - .setAps(Aps.builder() - .setContentAvailable(true) - .build()) + .setAps(apsConfig.build()) .build()) .putAllData(data) .build(); - firebaseContext.getMessaging().send(message); - log.trace("[{}] Sent message for FCM token {}", tenantId, fcmToken); + try { + firebaseContext.getMessaging().send(message); + log.trace("[{}] Sent message for FCM token {}", tenantId, fcmToken); + } catch (Throwable t) { + log.debug("[{}] Failed to send message for FCM token {}", tenantId, fcmToken, t); + throw t; + } } public static class FirebaseContext { 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 f868a9d4f3..6fa9111c3d 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 @@ -51,6 +51,8 @@ import java.util.Set; import java.util.UUID; import java.util.stream.Collectors; +import static org.thingsboard.server.common.data.notification.NotificationDeliveryMethod.WEB; + @Service @TbCoreComponent @RequiredArgsConstructor @@ -104,7 +106,7 @@ public class DefaultNotificationCommandsHandler implements NotificationCommandsH private void fetchUnreadNotifications(NotificationsSubscription subscription) { log.trace("[{}, subId: {}] Fetching unread notifications from DB", subscription.getSessionId(), subscription.getSubscriptionId()); PageData notifications = notificationService.findLatestUnreadNotificationsByRecipientId(subscription.getTenantId(), - (UserId) subscription.getEntityId(), subscription.getLimit()); + WEB, (UserId) subscription.getEntityId(), subscription.getLimit()); subscription.getLatestUnreadNotifications().clear(); notifications.getData().forEach(notification -> { subscription.getLatestUnreadNotifications().put(notification.getUuidId(), notification); @@ -114,7 +116,7 @@ public class DefaultNotificationCommandsHandler implements NotificationCommandsH private void fetchUnreadNotificationsCount(NotificationsCountSubscription subscription) { log.trace("[{}, subId: {}] Fetching unread notifications count from DB", subscription.getSessionId(), subscription.getSubscriptionId()); - int unreadCount = notificationService.countUnreadNotificationsByRecipientId(subscription.getTenantId(), (UserId) subscription.getEntityId()); + int unreadCount = notificationService.countUnreadNotificationsByRecipientId(subscription.getTenantId(), WEB, (UserId) subscription.getEntityId()); subscription.getTotalUnreadCounter().set(unreadCount); } @@ -235,7 +237,7 @@ public class DefaultNotificationCommandsHandler implements NotificationCommandsH @Override public void handleMarkAllAsReadCmd(WebSocketSessionRef sessionRef, MarkAllNotificationsAsReadCmd cmd) { SecurityUser securityCtx = sessionRef.getSecurityCtx(); - notificationCenter.markAllNotificationsAsRead(securityCtx.getTenantId(), securityCtx.getId()); + notificationCenter.markAllNotificationsAsRead(securityCtx.getTenantId(), WEB, securityCtx.getId()); } @Override diff --git a/application/src/test/java/org/thingsboard/server/service/notification/AbstractNotificationApiTest.java b/application/src/test/java/org/thingsboard/server/service/notification/AbstractNotificationApiTest.java index b945c162df..ae9dbe92c4 100644 --- a/application/src/test/java/org/thingsboard/server/service/notification/AbstractNotificationApiTest.java +++ b/application/src/test/java/org/thingsboard/server/service/notification/AbstractNotificationApiTest.java @@ -145,7 +145,7 @@ public abstract class AbstractNotificationApiTest extends AbstractControllerTest protected NotificationRequest submitNotificationRequest(List targets, String text, int delayInSec, NotificationDeliveryMethod... deliveryMethods) { if (deliveryMethods.length == 0) { - deliveryMethods = new NotificationDeliveryMethod[]{NotificationDeliveryMethod.WEB}; + deliveryMethods = new NotificationDeliveryMethod[]{NotificationDeliveryMethod.WEB, NotificationDeliveryMethod.MOBILE_APP}; } NotificationTemplate notificationTemplate = createNotificationTemplate(DEFAULT_NOTIFICATION_TYPE, DEFAULT_NOTIFICATION_SUBJECT, text, deliveryMethods); return submitNotificationRequest(targets, notificationTemplate.getId(), delayInSec); @@ -247,8 +247,12 @@ public abstract class AbstractNotificationApiTest extends AbstractControllerTest } protected List getMyNotifications(boolean unreadOnly, int limit) throws Exception { - return doGetTypedWithPageLink("/api/notifications?unreadOnly={unreadOnly}&", new TypeReference>() {}, - new PageLink(limit, 0), unreadOnly).getData(); + return getMyNotifications(NotificationDeliveryMethod.WEB, unreadOnly, limit); + } + + protected List getMyNotifications(NotificationDeliveryMethod deliveryMethod, boolean unreadOnly, int limit) throws Exception { + return doGetTypedWithPageLink("/api/notifications?unreadOnly={unreadOnly}&deliveryMethod={deliveryMethod}&", new TypeReference>() {}, + new PageLink(limit, 0), unreadOnly, deliveryMethod).getData(); } protected NotificationRule createNotificationRule(NotificationRuleTriggerConfig triggerConfig, String subject, String text, NotificationTargetId... targets) { diff --git a/application/src/test/java/org/thingsboard/server/service/notification/NotificationApiTest.java b/application/src/test/java/org/thingsboard/server/service/notification/NotificationApiTest.java index d3b79029ba..3a6090ef21 100644 --- a/application/src/test/java/org/thingsboard/server/service/notification/NotificationApiTest.java +++ b/application/src/test/java/org/thingsboard/server/service/notification/NotificationApiTest.java @@ -37,6 +37,7 @@ import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.alarm.AlarmComment; import org.thingsboard.server.common.data.alarm.AlarmSeverity; import org.thingsboard.server.common.data.audit.ActionType; +import org.thingsboard.server.common.data.id.AlarmId; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.NotificationRequestId; import org.thingsboard.server.common.data.id.NotificationRuleId; @@ -52,6 +53,7 @@ import org.thingsboard.server.common.data.notification.NotificationRequestPrevie import org.thingsboard.server.common.data.notification.NotificationRequestStats; import org.thingsboard.server.common.data.notification.NotificationRequestStatus; import org.thingsboard.server.common.data.notification.NotificationType; +import org.thingsboard.server.common.data.notification.info.AlarmCommentNotificationInfo; import org.thingsboard.server.common.data.notification.info.EntityActionNotificationInfo; import org.thingsboard.server.common.data.notification.rule.trigger.config.AlarmCommentNotificationRuleTriggerConfig; import org.thingsboard.server.common.data.notification.settings.MobileAppNotificationDeliveryMethodConfig; @@ -81,7 +83,6 @@ import org.thingsboard.server.common.data.notification.template.WebDeliveryMetho import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.dao.notification.DefaultNotifications; -import org.thingsboard.server.dao.notification.NotificationDao; import org.thingsboard.server.dao.service.DaoSqlTest; import org.thingsboard.server.service.notification.channels.MicrosoftTeamsNotificationChannel; import org.thingsboard.server.service.ws.notification.cmd.UnreadNotificationsUpdate; @@ -116,17 +117,25 @@ public class NotificationApiTest extends AbstractNotificationApiTest { @Autowired private NotificationCenter notificationCenter; @Autowired - private NotificationDao notificationDao; - @Autowired private MicrosoftTeamsNotificationChannel microsoftTeamsNotificationChannel; @MockBean private FirebaseService firebaseService; + private static final String TEST_MOBILE_TOKEN = "tenantFcmToken"; + @Before public void beforeEach() throws Exception { loginCustomerUser(); wsClient = getWsClient(); + + loginSysAdmin(); + MobileAppNotificationDeliveryMethodConfig config = new MobileAppNotificationDeliveryMethodConfig(); + config.setFirebaseServiceAccountCredentials("testCredentials"); + saveNotificationSettings(config); + loginTenantAdmin(); + mobileToken = TEST_MOBILE_TOKEN; + doPost("/api/user/mobile/session", new MobileSessionInfo()).andExpect(status().isOk()); } @Test @@ -242,7 +251,7 @@ public class NotificationApiTest extends AbstractNotificationApiTest { int notificationsCount = 20; wsClient.registerWaitForUpdate(notificationsCount); for (int i = 1; i <= notificationsCount; i++) { - submitNotificationRequest(target.getId(), "Test " + i, NotificationDeliveryMethod.WEB); + submitNotificationRequest(target.getId(), "Test " + i, NotificationDeliveryMethod.WEB, NotificationDeliveryMethod.MOBILE_APP); } wsClient.waitForUpdate(true); assertThat(wsClient.getLastDataUpdate().getTotalUnreadCount()).isEqualTo(notificationsCount); @@ -306,7 +315,7 @@ public class NotificationApiTest extends AbstractNotificationApiTest { NotificationTarget target = createNotificationTarget(savedDifferentTenantUser.getId()); int notificationsCount = 20; for (int i = 0; i < notificationsCount; i++) { - NotificationRequest request = submitNotificationRequest(target.getId(), "Test " + i, NotificationDeliveryMethod.WEB); + NotificationRequest request = submitNotificationRequest(target.getId(), "Test " + i, NotificationDeliveryMethod.WEB, NotificationDeliveryMethod.MOBILE_APP); awaitNotificationRequest(request.getId()); } List requests = notificationRequestService.findNotificationRequestsByTenantIdAndOriginatorType(tenantId, EntityType.USER, new PageLink(100)).getData(); @@ -500,7 +509,7 @@ public class NotificationApiTest extends AbstractNotificationApiTest { @Test public void testNotificationRequestInfo() throws Exception { NotificationDeliveryMethod[] deliveryMethods = new NotificationDeliveryMethod[]{ - NotificationDeliveryMethod.WEB + NotificationDeliveryMethod.WEB, NotificationDeliveryMethod.MOBILE_APP }; NotificationTemplate template = createNotificationTemplate(NotificationType.GENERAL, "Test subject", "Test text", deliveryMethods); NotificationTarget target = createNotificationTarget(tenantAdminUserId); @@ -525,7 +534,6 @@ public class NotificationApiTest extends AbstractNotificationApiTest { await().atMost(2, TimeUnit.SECONDS) .until(() -> findNotificationRequest(notificationRequest.getId()).isSent()); NotificationRequestStats stats = getStats(notificationRequest.getId()); - assertThat(stats.getSent().get(NotificationDeliveryMethod.WEB)).hasValue(1); } @@ -727,17 +735,12 @@ public class NotificationApiTest extends AbstractNotificationApiTest { @Test public void testMobileAppNotifications() throws Exception { - loginSysAdmin(); - MobileAppNotificationDeliveryMethodConfig config = new MobileAppNotificationDeliveryMethodConfig(); - config.setFirebaseServiceAccountCredentials("testCredentials"); - saveNotificationSettings(config); - loginCustomerUser(); mobileToken = "customerFcmToken"; doPost("/api/user/mobile/session", new MobileSessionInfo()).andExpect(status().isOk()); loginTenantAdmin(); - mobileToken = "tenantFcmToken1"; + mobileToken = TEST_MOBILE_TOKEN; doPost("/api/user/mobile/session", new MobileSessionInfo()).andExpect(status().isOk()); mobileToken = "tenantFcmToken2"; doPost("/api/user/mobile/session", new MobileSessionInfo()).andExpect(status().isOk()); @@ -746,7 +749,7 @@ public class NotificationApiTest extends AbstractNotificationApiTest { loginTenantAdmin(); NotificationTarget target = createNotificationTarget(new AllUsersFilter()); - NotificationTemplate template = createNotificationTemplate(NotificationType.GENERAL, "Title", "Message", NotificationDeliveryMethod.MOBILE_APP); + NotificationTemplate template = createNotificationTemplate(NotificationType.GENERAL, "Title", "Message", NotificationDeliveryMethod.MOBILE_APP, NotificationDeliveryMethod.WEB); ((MobileAppDeliveryMethodNotificationTemplate) template.getConfiguration().getDeliveryMethodsTemplates().get(NotificationDeliveryMethod.MOBILE_APP)) .setAdditionalConfig(JacksonUtil.newObjectNode().set("test", JacksonUtil.newObjectNode().put("test", "test"))); saveNotificationTemplate(template); @@ -758,11 +761,19 @@ public class NotificationApiTest extends AbstractNotificationApiTest { .contains("doesn't use the mobile app"); verify(firebaseService).sendMessage(eq(tenantId), eq("testCredentials"), - eq("tenantFcmToken1"), eq("Title"), eq("Message"), argThat(data -> "test".equals(data.get("test.test")))); + eq(TEST_MOBILE_TOKEN), eq("Title"), eq("Message"), argThat(data -> "test".equals(data.get("test.test"))), eq(1)); verify(firebaseService).sendMessage(eq(tenantId), eq("testCredentials"), - eq("tenantFcmToken2"), eq("Title"), eq("Message"), argThat(data -> "test".equals(data.get("test.test")))); + eq("tenantFcmToken2"), eq("Title"), eq("Message"), argThat(data -> "test".equals(data.get("test.test"))), eq(1)); verify(firebaseService).sendMessage(eq(tenantId), eq("testCredentials"), - eq("customerFcmToken"), eq("Title"), eq("Message"), argThat(data -> "test".equals(data.get("test.test")))); + eq("customerFcmToken"), eq("Title"), eq("Message"), argThat(data -> "test".equals(data.get("test.test"))), eq(1)); + assertThat(getMyNotifications(NotificationDeliveryMethod.MOBILE_APP, true, 10)).singleElement().satisfies(notification -> { + assertThat(notification.getDeliveryMethod()).isEqualTo(NotificationDeliveryMethod.MOBILE_APP); + assertThat(notification.getText()).isEqualTo("Message"); + assertThat(notification.getSubject()).isEqualTo("Title"); + }); + assertThat(getMyNotifications(true, 10)).singleElement().satisfies(notification -> { + assertThat(notification.getDeliveryMethod()).isEqualTo(NotificationDeliveryMethod.WEB); + }); verifyNoMoreInteractions(firebaseService); clearInvocations(firebaseService); @@ -770,26 +781,21 @@ public class NotificationApiTest extends AbstractNotificationApiTest { request = submitNotificationRequest(List.of(target.getId()), template.getId(), 0); awaitNotificationRequest(request.getId()); verify(firebaseService).sendMessage(eq(tenantId), eq("testCredentials"), - eq("tenantFcmToken1"), eq("Title"), eq("Message"), anyMap()); + eq(TEST_MOBILE_TOKEN), eq("Title"), eq("Message"), anyMap(), eq(2)); verify(firebaseService).sendMessage(eq(tenantId), eq("testCredentials"), - eq("customerFcmToken"), eq("Title"), eq("Message"), anyMap()); + eq("customerFcmToken"), eq("Title"), eq("Message"), anyMap(), eq(2)); verifyNoMoreInteractions(firebaseService); } @Test public void testMobileAppNotifications_ruleBased() throws Exception { - loginSysAdmin(); - MobileAppNotificationDeliveryMethodConfig config = new MobileAppNotificationDeliveryMethodConfig(); - config.setFirebaseServiceAccountCredentials("testCredentials"); - saveNotificationSettings(config); - loginTenantAdmin(); - mobileToken = "tenantFcmToken"; + mobileToken = TEST_MOBILE_TOKEN; doPost("/api/user/mobile/session", new MobileSessionInfo()).andExpect(status().isOk()); createNotificationRule(AlarmCommentNotificationRuleTriggerConfig.builder().onlyUserComments(true).build(), DefaultNotifications.alarmComment.getSubject(), DefaultNotifications.alarmComment.getText(), - List.of(createNotificationTarget(tenantAdminUserId).getId()), NotificationDeliveryMethod.MOBILE_APP); + List.of(createNotificationTarget(tenantAdminUserId).getId()), NotificationDeliveryMethod.MOBILE_APP, NotificationDeliveryMethod.WEB); Device device = createDevice("test", "test"); UUID alarmDashboardId = UUID.randomUUID(); @@ -802,18 +808,21 @@ public class NotificationApiTest extends AbstractNotificationApiTest { .put("dashboardId", alarmDashboardId.toString())) .build(); alarm = doPost("/api/alarm", alarm, Alarm.class); + AlarmId alarmId = alarm.getId(); AlarmComment comment = new AlarmComment(); comment.setComment(JacksonUtil.newObjectNode() .put("text", "text")); - doPost("/api/alarm/" + alarm.getId() + "/comment", comment, AlarmComment.class); + doPost("/api/alarm/" + alarmId + "/comment", comment, AlarmComment.class); + String expectedSubject = "Comment on 'test' alarm"; + String expectedBody = TENANT_ADMIN_EMAIL + " added comment: text"; ArgumentCaptor> msgCaptor = ArgumentCaptor.forClass(Map.class); await().atMost(30, TimeUnit.SECONDS).untilAsserted(() -> { verify(firebaseService).sendMessage(eq(tenantId), eq("testCredentials"), - eq("tenantFcmToken"), eq("Comment on 'test' alarm"), - eq(TENANT_ADMIN_EMAIL + " added comment: text"), - msgCaptor.capture()); + eq(TEST_MOBILE_TOKEN), eq(expectedSubject), + eq(expectedBody), + msgCaptor.capture(), eq(1)); }); Map firebaseMessageData = msgCaptor.getValue(); assertThat(firebaseMessageData.keySet()).doesNotContainNull().doesNotContain(""); @@ -823,6 +832,14 @@ public class NotificationApiTest extends AbstractNotificationApiTest { assertThat(firebaseMessageData.get("onClick.enabled")).isEqualTo("true"); assertThat(firebaseMessageData.get("onClick.linkType")).isEqualTo("DASHBOARD"); assertThat(firebaseMessageData.get("onClick.dashboardId")).isEqualTo(alarmDashboardId.toString()); + + assertThat(getMyNotifications(NotificationDeliveryMethod.MOBILE_APP, true, 10)).singleElement().satisfies(notification -> { + assertThat(notification.getDeliveryMethod()).isEqualTo(NotificationDeliveryMethod.MOBILE_APP); + assertThat(notification.getSubject()).isEqualTo(expectedSubject); + assertThat(notification.getText()).isEqualTo(expectedBody); + assertThat(notification.getInfo()).asInstanceOf(type(AlarmCommentNotificationInfo.class)) + .matches(info -> info.getAlarmId().equals(alarmId.getId()) && info.getDashboardId().getId().equals(alarmDashboardId)); + }); } @Test diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/notification/NotificationService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/notification/NotificationService.java index 18c9d4a7fb..91f6f7a379 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/notification/NotificationService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/notification/NotificationService.java @@ -19,6 +19,7 @@ import org.thingsboard.server.common.data.id.NotificationId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.notification.Notification; +import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; @@ -30,13 +31,13 @@ public interface NotificationService { boolean markNotificationAsRead(TenantId tenantId, UserId recipientId, NotificationId notificationId); - int markAllNotificationsAsRead(TenantId tenantId, UserId recipientId); + int markAllNotificationsAsRead(TenantId tenantId, NotificationDeliveryMethod deliveryMethod, UserId recipientId); - PageData findNotificationsByRecipientIdAndReadStatus(TenantId tenantId, UserId recipientId, boolean unreadOnly, PageLink pageLink); + PageData findNotificationsByRecipientIdAndReadStatus(TenantId tenantId, NotificationDeliveryMethod deliveryMethod, UserId recipientId, boolean unreadOnly, PageLink pageLink); - PageData findLatestUnreadNotificationsByRecipientId(TenantId tenantId, UserId recipientId, int limit); + PageData findLatestUnreadNotificationsByRecipientId(TenantId tenantId, NotificationDeliveryMethod deliveryMethod, UserId recipientId, int limit); - int countUnreadNotificationsByRecipientId(TenantId tenantId, UserId recipientId); + int countUnreadNotificationsByRecipientId(TenantId tenantId, NotificationDeliveryMethod deliveryMethod, UserId recipientId); boolean deleteNotification(TenantId tenantId, UserId recipientId, NotificationId notificationId); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/Notification.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/Notification.java index 0f7095c755..6335cb671d 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/Notification.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/Notification.java @@ -36,13 +36,12 @@ public class Notification extends BaseData { private NotificationRequestId requestId; private UserId recipientId; - private NotificationType type; + private NotificationDeliveryMethod deliveryMethod; private String subject; private String text; private JsonNode additionalConfig; private NotificationInfo info; - private NotificationStatus status; } diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java b/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java index 2fe56f5d1f..d0e8c7c394 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java @@ -620,6 +620,7 @@ public class ModelConstants { public static final String NOTIFICATION_REQUEST_ID_PROPERTY = "request_id"; public static final String NOTIFICATION_RECIPIENT_ID_PROPERTY = "recipient_id"; public static final String NOTIFICATION_TYPE_PROPERTY = "type"; + public static final String NOTIFICATION_DELIVERY_METHOD_PROPERTY = "delivery_method"; public static final String NOTIFICATION_SUBJECT_PROPERTY = "subject"; public static final String NOTIFICATION_TEXT_PROPERTY = "body"; public static final String NOTIFICATION_ADDITIONAL_CONFIG_PROPERTY = "additional_config"; diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/NotificationEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/NotificationEntity.java index 82c0ee0ab9..bf818b0f64 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/NotificationEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/NotificationEntity.java @@ -25,6 +25,7 @@ import org.thingsboard.server.common.data.id.NotificationId; import org.thingsboard.server.common.data.id.NotificationRequestId; import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.notification.Notification; +import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod; import org.thingsboard.server.common.data.notification.NotificationStatus; import org.thingsboard.server.common.data.notification.NotificationType; import org.thingsboard.server.common.data.notification.info.NotificationInfo; @@ -56,6 +57,10 @@ public class NotificationEntity extends BaseSqlEntity { @Column(name = ModelConstants.NOTIFICATION_TYPE_PROPERTY, nullable = false) private NotificationType type; + @Enumerated(EnumType.STRING) + @Column(name = ModelConstants.NOTIFICATION_DELIVERY_METHOD_PROPERTY, nullable = false) + private NotificationDeliveryMethod deliveryMethod; + @Column(name = ModelConstants.NOTIFICATION_SUBJECT_PROPERTY) private String subject; @@ -82,6 +87,7 @@ public class NotificationEntity extends BaseSqlEntity { setRequestId(getUuid(notification.getRequestId())); setRecipientId(getUuid(notification.getRecipientId())); setType(notification.getType()); + setDeliveryMethod(notification.getDeliveryMethod()); setSubject(notification.getSubject()); setText(notification.getText()); setAdditionalConfig(notification.getAdditionalConfig()); @@ -97,6 +103,7 @@ public class NotificationEntity extends BaseSqlEntity { notification.setRequestId(getEntityId(requestId, NotificationRequestId::new)); notification.setRecipientId(getEntityId(recipientId, UserId::new)); notification.setType(type); + notification.setDeliveryMethod(deliveryMethod); notification.setSubject(subject); notification.setText(text); notification.setAdditionalConfig(additionalConfig); diff --git a/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationService.java b/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationService.java index b62d9a83ad..38a8cb4178 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationService.java @@ -25,6 +25,7 @@ import org.thingsboard.server.common.data.id.NotificationId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.notification.Notification; +import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod; import org.thingsboard.server.common.data.notification.NotificationStatus; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; @@ -57,29 +58,29 @@ public class DefaultNotificationService implements NotificationService, EntityDa } @Override - public int markAllNotificationsAsRead(TenantId tenantId, UserId recipientId) { - return notificationDao.updateStatusByRecipientId(tenantId, recipientId, NotificationStatus.READ); + public int markAllNotificationsAsRead(TenantId tenantId, NotificationDeliveryMethod deliveryMethod, UserId recipientId) { + return notificationDao.updateStatusByDeliveryMethodAndRecipientId(tenantId, deliveryMethod, recipientId, NotificationStatus.READ); } @Override - public PageData findNotificationsByRecipientIdAndReadStatus(TenantId tenantId, UserId recipientId, boolean unreadOnly, PageLink pageLink) { + public PageData findNotificationsByRecipientIdAndReadStatus(TenantId tenantId, NotificationDeliveryMethod deliveryMethod, UserId recipientId, boolean unreadOnly, PageLink pageLink) { if (unreadOnly) { - return notificationDao.findUnreadByRecipientIdAndPageLink(tenantId, recipientId, pageLink); + return notificationDao.findUnreadByDeliveryMethodAndRecipientIdAndPageLink(tenantId, deliveryMethod, recipientId, pageLink); } else { - return notificationDao.findByRecipientIdAndPageLink(tenantId, recipientId, pageLink); + return notificationDao.findByDeliveryMethodAndRecipientIdAndPageLink(tenantId, deliveryMethod, recipientId, pageLink); } } @Override - public PageData findLatestUnreadNotificationsByRecipientId(TenantId tenantId, UserId recipientId, int limit) { + public PageData findLatestUnreadNotificationsByRecipientId(TenantId tenantId, NotificationDeliveryMethod deliveryMethod, UserId recipientId, int limit) { SortOrder sortOrder = new SortOrder(EntityKeyMapping.CREATED_TIME, SortOrder.Direction.DESC); PageLink pageLink = new PageLink(limit, 0, null, sortOrder); - return findNotificationsByRecipientIdAndReadStatus(tenantId, recipientId, true, pageLink); + return findNotificationsByRecipientIdAndReadStatus(tenantId, deliveryMethod, recipientId, true, pageLink); } @Override - public int countUnreadNotificationsByRecipientId(TenantId tenantId, UserId recipientId) { - return notificationDao.countUnreadByRecipientId(tenantId, recipientId); + public int countUnreadNotificationsByRecipientId(TenantId tenantId, NotificationDeliveryMethod deliveryMethod, UserId recipientId) { + return notificationDao.countUnreadByDeliveryMethodAndRecipientId(tenantId, deliveryMethod, recipientId); } @Override diff --git a/dao/src/main/java/org/thingsboard/server/dao/notification/NotificationDao.java b/dao/src/main/java/org/thingsboard/server/dao/notification/NotificationDao.java index 02bfe11fde..2233840058 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/notification/NotificationDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/notification/NotificationDao.java @@ -20,6 +20,7 @@ import org.thingsboard.server.common.data.id.NotificationRequestId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.notification.Notification; +import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod; import org.thingsboard.server.common.data.notification.NotificationStatus; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; @@ -27,19 +28,17 @@ import org.thingsboard.server.dao.Dao; public interface NotificationDao extends Dao { - PageData findUnreadByRecipientIdAndPageLink(TenantId tenantId, UserId recipientId, PageLink pageLink); + PageData findUnreadByDeliveryMethodAndRecipientIdAndPageLink(TenantId tenantId, NotificationDeliveryMethod deliveryMethod, UserId recipientId, PageLink pageLink); - PageData findByRecipientIdAndPageLink(TenantId tenantId, UserId recipientId, PageLink pageLink); + PageData findByDeliveryMethodAndRecipientIdAndPageLink(TenantId tenantId, NotificationDeliveryMethod deliveryMethod, UserId recipientId, PageLink pageLink); boolean updateStatusByIdAndRecipientId(TenantId tenantId, UserId recipientId, NotificationId notificationId, NotificationStatus status); - int countUnreadByRecipientId(TenantId tenantId, UserId recipientId); - - PageData findByRequestId(TenantId tenantId, NotificationRequestId notificationRequestId, PageLink pageLink); + int countUnreadByDeliveryMethodAndRecipientId(TenantId tenantId, NotificationDeliveryMethod deliveryMethod, UserId recipientId); boolean deleteByIdAndRecipientId(TenantId tenantId, UserId recipientId, NotificationId notificationId); - int updateStatusByRecipientId(TenantId tenantId, UserId recipientId, NotificationStatus status); + int updateStatusByDeliveryMethodAndRecipientId(TenantId tenantId, NotificationDeliveryMethod deliveryMethod, UserId recipientId, NotificationStatus status); void deleteByRequestId(TenantId tenantId, NotificationRequestId requestId); diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/notification/JpaNotificationDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/notification/JpaNotificationDao.java index f095aad1ae..64d663b397 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/notification/JpaNotificationDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/notification/JpaNotificationDao.java @@ -25,6 +25,7 @@ import org.thingsboard.server.common.data.id.NotificationRequestId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.notification.Notification; +import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod; import org.thingsboard.server.common.data.notification.NotificationStatus; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; @@ -51,14 +52,14 @@ public class JpaNotificationDao extends JpaPartitionedAbstractDao findUnreadByRecipientIdAndPageLink(TenantId tenantId, UserId recipientId, PageLink pageLink) { - return DaoUtil.toPageData(notificationRepository.findByRecipientIdAndStatusNot(recipientId.getId(), NotificationStatus.READ, - pageLink.getTextSearch(), DaoUtil.toPageable(pageLink))); + public PageData findUnreadByDeliveryMethodAndRecipientIdAndPageLink(TenantId tenantId, NotificationDeliveryMethod deliveryMethod, UserId recipientId, PageLink pageLink) { + return DaoUtil.toPageData(notificationRepository.findByDeliveryMethodAndRecipientIdAndStatusNot(deliveryMethod, + recipientId.getId(), NotificationStatus.READ, pageLink.getTextSearch(), DaoUtil.toPageable(pageLink))); } @Override - public PageData findByRecipientIdAndPageLink(TenantId tenantId, UserId recipientId, PageLink pageLink) { - return DaoUtil.toPageData(notificationRepository.findByRecipientId(recipientId.getId(), + public PageData findByDeliveryMethodAndRecipientIdAndPageLink(TenantId tenantId, NotificationDeliveryMethod deliveryMethod, UserId recipientId, PageLink pageLink) { + return DaoUtil.toPageData(notificationRepository.findByDeliveryMethodAndRecipientId(deliveryMethod, recipientId.getId(), pageLink.getTextSearch(), DaoUtil.toPageable(pageLink))); } @@ -71,13 +72,8 @@ public class JpaNotificationDao extends JpaPartitionedAbstractDao findByRequestId(TenantId tenantId, NotificationRequestId notificationRequestId, PageLink pageLink) { - return DaoUtil.toPageData(notificationRepository.findByRequestId(notificationRequestId.getId(), DaoUtil.toPageable(pageLink))); + public int countUnreadByDeliveryMethodAndRecipientId(TenantId tenantId, NotificationDeliveryMethod deliveryMethod, UserId recipientId) { + return notificationRepository.countByDeliveryMethodAndRecipientIdAndStatusNot(deliveryMethod, recipientId.getId(), NotificationStatus.READ); } @Override @@ -86,8 +82,8 @@ public class JpaNotificationDao extends JpaPartitionedAbstractDao { - @Query("SELECT n FROM NotificationEntity n WHERE n.recipientId = :recipientId AND n.status <> :status " + + @Query("SELECT n FROM NotificationEntity n WHERE n.deliveryMethod = :deliveryMethod " + + "AND n.recipientId = :recipientId AND n.status <> :status " + "AND (:searchText is NULL OR ilike(n.subject, concat('%', :searchText, '%')) = true " + "OR ilike(n.text, concat('%', :searchText, '%')) = true)") - Page findByRecipientIdAndStatusNot(@Param("recipientId") UUID recipientId, - @Param("status") NotificationStatus status, - @Param("searchText") String searchText, - Pageable pageable); + Page findByDeliveryMethodAndRecipientIdAndStatusNot(@Param("deliveryMethod") NotificationDeliveryMethod deliveryMethod, + @Param("recipientId") UUID recipientId, + @Param("status") NotificationStatus status, + @Param("searchText") String searchText, + Pageable pageable); - @Query("SELECT n FROM NotificationEntity n WHERE n.recipientId = :recipientId " + + @Query("SELECT n FROM NotificationEntity n WHERE n.deliveryMethod = :deliveryMethod AND n.recipientId = :recipientId " + "AND (:searchText is NULL OR ilike(n.subject, concat('%', :searchText, '%')) = true " + "OR ilike(n.text, concat('%', :searchText, '%')) = true)") - Page findByRecipientId(@Param("recipientId") UUID recipientId, - @Param("searchText") String searchText, - Pageable pageable); + Page findByDeliveryMethodAndRecipientId(@Param("deliveryMethod") NotificationDeliveryMethod deliveryMethod, + @Param("recipientId") UUID recipientId, + @Param("searchText") String searchText, + Pageable pageable); @Modifying @Transactional @@ -54,9 +58,7 @@ public interface NotificationRepository extends JpaRepository findByRequestId(UUID requestId, Pageable pageable); + int countByDeliveryMethodAndRecipientIdAndStatusNot(NotificationDeliveryMethod deliveryMethod, UUID recipientId, NotificationStatus status); @Transactional @Modifying @@ -76,8 +78,9 @@ public interface NotificationRepository extends JpaRepository :status") - int updateStatusByRecipientId(@Param("recipientId") UUID recipientId, - @Param("status") NotificationStatus status); + "WHERE n.deliveryMethod = :deliveryMethod AND n.recipientId = :recipientId AND n.status <> :status") + int updateStatusByDeliveryMethodAndRecipientIdAndStatusNot(@Param("deliveryMethod") NotificationDeliveryMethod deliveryMethod, + @Param("recipientId") UUID recipientId, + @Param("status") NotificationStatus status); } diff --git a/dao/src/main/resources/sql/schema-entities-idx.sql b/dao/src/main/resources/sql/schema-entities-idx.sql index a177c1325f..6b4f1a6c09 100644 --- a/dao/src/main/resources/sql/schema-entities-idx.sql +++ b/dao/src/main/resources/sql/schema-entities-idx.sql @@ -118,9 +118,9 @@ CREATE INDEX IF NOT EXISTS idx_notification_id ON notification(id); CREATE INDEX IF NOT EXISTS idx_notification_notification_request_id ON notification(request_id); -CREATE INDEX IF NOT EXISTS idx_notification_recipient_id_created_time ON notification(recipient_id, created_time DESC); +CREATE INDEX IF NOT EXISTS idx_notification_delivery_method_recipient_id_created_time ON notification(delivery_method, recipient_id, created_time DESC); -CREATE INDEX IF NOT EXISTS idx_notification_recipient_id_unread ON notification(recipient_id) WHERE status <> 'READ'; +CREATE INDEX IF NOT EXISTS idx_notification_delivery_method_recipient_id_unread ON notification(delivery_method, recipient_id) WHERE status <> 'READ'; CREATE INDEX IF NOT EXISTS idx_resource_etag ON resource(tenant_id, etag); diff --git a/dao/src/main/resources/sql/schema-entities.sql b/dao/src/main/resources/sql/schema-entities.sql index cdd4edea06..34320ccd9e 100644 --- a/dao/src/main/resources/sql/schema-entities.sql +++ b/dao/src/main/resources/sql/schema-entities.sql @@ -864,6 +864,7 @@ CREATE TABLE IF NOT EXISTS notification ( request_id UUID, recipient_id UUID NOT NULL, type VARCHAR(50) NOT NULL, + delivery_method VARCHAR(50) NOT NULL, subject VARCHAR(255), body VARCHAR(1000) NOT NULL, additional_config VARCHAR(1000), diff --git a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/NotificationCenter.java b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/NotificationCenter.java index fd63ace9c4..d108a6981d 100644 --- a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/NotificationCenter.java +++ b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/NotificationCenter.java @@ -38,7 +38,7 @@ public interface NotificationCenter { void markNotificationAsRead(TenantId tenantId, UserId recipientId, NotificationId notificationId); - void markAllNotificationsAsRead(TenantId tenantId, UserId recipientId); + void markAllNotificationsAsRead(TenantId tenantId, NotificationDeliveryMethod deliveryMethod, UserId recipientId); void deleteNotification(TenantId tenantId, UserId recipientId, NotificationId notificationId); diff --git a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/notification/FirebaseService.java b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/notification/FirebaseService.java index 28aa4c96af..ef10d8cecc 100644 --- a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/notification/FirebaseService.java +++ b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/notification/FirebaseService.java @@ -21,6 +21,6 @@ import java.util.Map; public interface FirebaseService { - void sendMessage(TenantId tenantId, String credentials, String fcmToken, String title, String body, Map data) throws Exception; + void sendMessage(TenantId tenantId, String credentials, String fcmToken, String title, String body, Map data, Integer badge) throws Exception; } From e29a470856e3b5affe3cafefd530f5c3ac03c59d Mon Sep 17 00:00:00 2001 From: IrynaMatveieva Date: Fri, 22 Mar 2024 14:33:03 +0200 Subject: [PATCH 14/80] used assertj instead of junit --- .../dao/rule/BaseRuleChainServiceTest.java | 64 +++++++------------ 1 file changed, 22 insertions(+), 42 deletions(-) diff --git a/dao/src/test/java/org/thingsboard/server/dao/rule/BaseRuleChainServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/rule/BaseRuleChainServiceTest.java index c9a91853ac..ba0fb55a6d 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/rule/BaseRuleChainServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/rule/BaseRuleChainServiceTest.java @@ -15,9 +15,8 @@ */ package org.thingsboard.server.dao.rule; -import org.junit.Assert; +import org.assertj.core.api.Assertions; import org.junit.Test; -import org.junit.jupiter.api.Assertions; import org.springframework.beans.factory.annotation.Autowired; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.id.RuleChainId; @@ -28,9 +27,6 @@ import org.thingsboard.server.dao.service.DaoSqlTest; import java.util.UUID; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertThrows; - @DaoSqlTest public class BaseRuleChainServiceTest extends AbstractServiceTest { @@ -39,18 +35,17 @@ public class BaseRuleChainServiceTest extends AbstractServiceTest { @Test public void givenRuleChain_whenSave_thenReturnsSavedRuleChain() { - RuleChain ruleChain = getRuleChain(ruleChainWithoutId); - ruleChain.setTenantId(tenantId); - RuleChain savedRuleChain = ruleChainService.saveRuleChain(ruleChain); - - Assert.assertNotNull(savedRuleChain); - Assert.assertNotNull(savedRuleChain.getId()); - Assert.assertTrue(savedRuleChain.getCreatedTime() > 0); - Assert.assertEquals(ruleChain.getTenantId(), savedRuleChain.getTenantId()); + RuleChain newRuleChain = getRuleChain(this.ruleChain); + newRuleChain.setTenantId(tenantId); + RuleChain savedRuleChain = ruleChainService.saveRuleChain(newRuleChain); + Assertions.assertThat(savedRuleChain).isNotNull(); + Assertions.assertThat(savedRuleChain.getId()).isNotNull(); + Assertions.assertThat(savedRuleChain.getCreatedTime() > 0).isTrue(); + Assertions.assertThat(newRuleChain.getTenantId()).isEqualTo(savedRuleChain.getTenantId()); RuleChain foundRuleChain = ruleChainService.findRuleChainById(tenantId, savedRuleChain.getId()); - Assertions.assertEquals(foundRuleChain.getName(), savedRuleChain.getName()); + Assertions.assertThat(savedRuleChain.getName()).isEqualTo(foundRuleChain.getName()); ruleChainService.deleteRuleChainsByTenantId(tenantId); } @@ -59,19 +54,20 @@ public class BaseRuleChainServiceTest extends AbstractServiceTest { public void givenRuleChainWithExistingExternalId_whenSave_thenThrowsException() { RuleChainId externalRuleChainId = new RuleChainId(UUID.fromString("2675d180-e1e5-11ee-9f06-71b6c7dc2cbf")); - RuleChain ruleChain = getRuleChain(ruleChainWithoutId); - ruleChain.setTenantId(tenantId); - ruleChain.setExternalId(externalRuleChainId); - RuleChain savedRuleChain = ruleChainService.saveRuleChain(ruleChain); + RuleChain newRuleChain = getRuleChain(ruleChain); + newRuleChain.setTenantId(tenantId); + newRuleChain.setExternalId(externalRuleChainId); + RuleChain savedRuleChain = ruleChainService.saveRuleChain(newRuleChain); - RuleChain ruleChainForSave = getRuleChain(ruleChainWithExternalId); + RuleChain ruleChainForSave = getRuleChain(ruleChain); ruleChainForSave.setTenantId(tenantId); + ruleChainForSave.setExternalId(externalRuleChainId); - String expectedMsg = "Rule Chain with such external id already exists!"; - - assertEquals(savedRuleChain.getExternalId(), ruleChainForSave.getExternalId()); - Exception exception = assertThrows(DataValidationException.class, () -> ruleChainService.saveRuleChain(ruleChainForSave)); - assertEquals(expectedMsg, exception.getMessage()); + Assertions.assertThat(savedRuleChain.getExternalId()).isEqualTo(ruleChainForSave.getExternalId()); + Assertions.assertThatExceptionOfType(DataValidationException.class).isThrownBy(() -> ruleChainService.saveRuleChain(ruleChainForSave)); + Assertions.assertThatThrownBy(() -> ruleChainService.saveRuleChain(ruleChainForSave)) + .isInstanceOf(DataValidationException.class) + .hasMessage("Rule Chain with such external id already exists!"); ruleChainService.deleteRuleChainsByTenantId(tenantId); } @@ -80,7 +76,7 @@ public class BaseRuleChainServiceTest extends AbstractServiceTest { return JacksonUtil.fromString(ruleChainString, RuleChain.class); } - private final String ruleChainWithoutId = "{\n" + + private final String ruleChain = "{\n" + " \"name\": \"Root Rule Chain\",\n" + " \"type\": \"CORE\",\n" + " \"firstRuleNodeId\": {\n" + @@ -91,20 +87,4 @@ public class BaseRuleChainServiceTest extends AbstractServiceTest { " \"configuration\": null,\n" + " \"additionalInfo\": null\n" + "}"; - - private final String ruleChainWithExternalId = "{\n" + - " \"name\": \"Root Rule Chain\",\n" + - " \"type\": \"CORE\",\n" + - " \"firstRuleNodeId\": {\n" + - " \"entityType\": \"RULE_NODE\",\n" + - " \"id\": \"91ad0b00-e779-11ee-9cf0-15d8b6079fdb\"\n" + - " },\n" + - " \"debugMode\": false,\n" + - " \"externalId\": {\n" + - " \"entityType\": \"RULE_CHAIN\",\n" + - " \"id\": \"2675d180-e1e5-11ee-9f06-71b6c7dc2cbf\"\n" + - " },\n" + - " \"configuration\": null,\n" + - " \"additionalInfo\": null\n" + - "}"; -} \ No newline at end of file +} From 024b39b30669bd784b423ab6501edce02f72e240 Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Tue, 26 Mar 2024 16:05:25 +0200 Subject: [PATCH 15/80] Firebase: remove setContentAvailable --- .../service/notification/provider/DefaultFirebaseService.java | 1 - 1 file changed, 1 deletion(-) diff --git a/application/src/main/java/org/thingsboard/server/service/notification/provider/DefaultFirebaseService.java b/application/src/main/java/org/thingsboard/server/service/notification/provider/DefaultFirebaseService.java index a19dc833eb..11651f91c4 100644 --- a/application/src/main/java/org/thingsboard/server/service/notification/provider/DefaultFirebaseService.java +++ b/application/src/main/java/org/thingsboard/server/service/notification/provider/DefaultFirebaseService.java @@ -66,7 +66,6 @@ public class DefaultFirebaseService implements FirebaseService { }); Aps.Builder apsConfig = Aps.builder() - .setContentAvailable(true) .setSound("default"); if (badge != null) { apsConfig.setBadge(badge); From 6754d8eddbdce3448b4a1a225675e1e5adc53fc9 Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Wed, 27 Mar 2024 13:04:15 +0200 Subject: [PATCH 16/80] Add endpoint for getting unread notifications count --- .../server/controller/NotificationController.java | 11 +++++++++++ .../service/notification/NotificationApiTest.java | 3 +++ 2 files changed, 14 insertions(+) diff --git a/application/src/main/java/org/thingsboard/server/controller/NotificationController.java b/application/src/main/java/org/thingsboard/server/controller/NotificationController.java index 8de3997a60..459d3292f8 100644 --- a/application/src/main/java/org/thingsboard/server/controller/NotificationController.java +++ b/application/src/main/java/org/thingsboard/server/controller/NotificationController.java @@ -182,6 +182,17 @@ public class NotificationController extends BaseController { return notificationService.findNotificationsByRecipientIdAndReadStatus(user.getTenantId(), deliveryMethod, user.getId(), unreadOnly, pageLink); } + @ApiOperation(value = "Get unread notifications count (getUnreadNotificationsCount)", + notes = "Returns unread notifications count for chosen delivery method." + + AVAILABLE_FOR_ANY_AUTHORIZED_USER) + @GetMapping("/notifications/unread/count") + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") + public Integer getUnreadNotificationsCount(@ApiParam(value = "Delivery method", allowableValues = DELIVERY_METHOD_ALLOWABLE_VALUES) + @RequestParam(defaultValue = "MOBILE_APP") NotificationDeliveryMethod deliveryMethod, + @AuthenticationPrincipal SecurityUser user) { + return notificationService.countUnreadNotificationsByRecipientId(user.getTenantId(), deliveryMethod, user.getId()); + } + @ApiOperation(value = "Mark notification as read (markNotificationAsRead)", notes = "Marks notification as read by its id." + AVAILABLE_FOR_ANY_AUTHORIZED_USER) diff --git a/application/src/test/java/org/thingsboard/server/service/notification/NotificationApiTest.java b/application/src/test/java/org/thingsboard/server/service/notification/NotificationApiTest.java index 3a6090ef21..060af9eac1 100644 --- a/application/src/test/java/org/thingsboard/server/service/notification/NotificationApiTest.java +++ b/application/src/test/java/org/thingsboard/server/service/notification/NotificationApiTest.java @@ -785,6 +785,9 @@ public class NotificationApiTest extends AbstractNotificationApiTest { verify(firebaseService).sendMessage(eq(tenantId), eq("testCredentials"), eq("customerFcmToken"), eq("Title"), eq("Message"), anyMap(), eq(2)); verifyNoMoreInteractions(firebaseService); + + Integer unreadCount = doGet("/api/notifications/unread/count", Integer.class); + assertThat(unreadCount).isEqualTo(2); } @Test From 4ad4b7fc86c608a494ddb55db50e45458a9bbc5a Mon Sep 17 00:00:00 2001 From: nick Date: Thu, 28 Mar 2024 11:54:03 +0200 Subject: [PATCH 17/80] lwm2m/coap: add californium 3.11.0 --- .../CoapAttributesUpdatesIntegrationTest.java | 5 - ...pAttributesUpdatesJsonIntegrationTest.java | 4 +- ...AttributesUpdatesProtoIntegrationTest.java | 3 - .../client/CoapClientIntegrationTest.java | 4 - ...apServerSideRpcDefaultIntegrationTest.java | 3 - .../CoapServerSideRpcJsonIntegrationTest.java | 3 - ...CoapServerSideRpcProtoIntegrationTest.java | 3 - .../lwm2m/server/client/LwM2mClient.java | 22 +- .../server/store/util/LwM2MClientSerDes.java | 287 +++++++++-------- .../LwM2MTransportBootstrapServiceTest.java | 100 ------ .../lwm2m/server/client/LwM2mClientTest.java | 11 +- .../store/util/LwM2MClientSerDesTest.java | 41 ++- pom.xml | 3 +- .../observation/ObservationServiceImpl.java | 296 ------------------ 14 files changed, 205 insertions(+), 580 deletions(-) delete mode 100644 common/transport/lwm2m/src/test/java/org/thingsboard/server/transport/lwm2m/bootstrap/LwM2MTransportBootstrapServiceTest.java delete mode 100644 transport/lwm2m/src/main/java/org/eclipse/leshan/server/observation/ObservationServiceImpl.java diff --git a/application/src/test/java/org/thingsboard/server/transport/coap/attributes/updates/CoapAttributesUpdatesIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/coap/attributes/updates/CoapAttributesUpdatesIntegrationTest.java index a7de7eb571..9ac2d5fc11 100644 --- a/application/src/test/java/org/thingsboard/server/transport/coap/attributes/updates/CoapAttributesUpdatesIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/coap/attributes/updates/CoapAttributesUpdatesIntegrationTest.java @@ -19,7 +19,6 @@ import lombok.extern.slf4j.Slf4j; import org.eclipse.californium.core.server.resources.Resource; import org.junit.After; import org.junit.Before; -import org.junit.Ignore; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; import org.thingsboard.server.coapserver.DefaultCoapServerService; @@ -60,15 +59,11 @@ public class CoapAttributesUpdatesIntegrationTest extends AbstractCoapAttributes processAfterTest(); } - - - @Ignore // Uncomment when Californium 3.11 is released with https://github.com/eclipse-californium/californium/pull/2215 @Test public void testSubscribeToAttributesUpdatesFromTheServer() throws Exception { processJsonTestSubscribeToAttributesUpdates(false); } - @Ignore // Uncomment when Californium 3.11 is released with https://github.com/eclipse-californium/californium/pull/2215 @Test public void testSubscribeToAttributesUpdatesFromTheServerWithEmptyCurrentStateNotification() throws Exception { processJsonTestSubscribeToAttributesUpdates(true); diff --git a/application/src/test/java/org/thingsboard/server/transport/coap/attributes/updates/CoapAttributesUpdatesJsonIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/coap/attributes/updates/CoapAttributesUpdatesJsonIntegrationTest.java index 4755ba134d..3fa625796c 100644 --- a/application/src/test/java/org/thingsboard/server/transport/coap/attributes/updates/CoapAttributesUpdatesJsonIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/coap/attributes/updates/CoapAttributesUpdatesJsonIntegrationTest.java @@ -18,7 +18,6 @@ package org.thingsboard.server.transport.coap.attributes.updates; import lombok.extern.slf4j.Slf4j; import org.junit.After; import org.junit.Before; -import org.junit.Ignore; import org.junit.Test; import org.thingsboard.server.common.data.CoapDeviceType; import org.thingsboard.server.common.data.TransportPayloadType; @@ -45,12 +44,11 @@ public class CoapAttributesUpdatesJsonIntegrationTest extends AbstractCoapAttrib processAfterTest(); } - @Ignore // Uncomment when Californium 3.11 is released with https://github.com/eclipse-californium/californium/pull/2215 @Test public void testSubscribeToAttributesUpdatesFromTheServer() throws Exception { processJsonTestSubscribeToAttributesUpdates(false); } - @Ignore // Uncomment when Californium 3.11 is released with https://github.com/eclipse-californium/californium/pull/2215 + @Test public void testSubscribeToAttributesUpdatesFromTheServerWithEmptyCurrentStateNotification() throws Exception { processJsonTestSubscribeToAttributesUpdates(true); diff --git a/application/src/test/java/org/thingsboard/server/transport/coap/attributes/updates/CoapAttributesUpdatesProtoIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/coap/attributes/updates/CoapAttributesUpdatesProtoIntegrationTest.java index 1c9589bba9..4b07c8dcc2 100644 --- a/application/src/test/java/org/thingsboard/server/transport/coap/attributes/updates/CoapAttributesUpdatesProtoIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/coap/attributes/updates/CoapAttributesUpdatesProtoIntegrationTest.java @@ -18,7 +18,6 @@ package org.thingsboard.server.transport.coap.attributes.updates; import lombok.extern.slf4j.Slf4j; import org.junit.After; import org.junit.Before; -import org.junit.Ignore; import org.junit.Test; import org.thingsboard.server.common.data.CoapDeviceType; import org.thingsboard.server.common.data.TransportPayloadType; @@ -44,12 +43,10 @@ public class CoapAttributesUpdatesProtoIntegrationTest extends AbstractCoapAttri public void afterTest() throws Exception { processAfterTest(); } - @Ignore // Uncomment when Californium 3.11 is released with https://github.com/eclipse-californium/californium/pull/2215 @Test public void testSubscribeToAttributesUpdatesFromTheServer() throws Exception { processProtoTestSubscribeToAttributesUpdates(false); } - @Ignore // Uncomment when Californium 3.11 is released with https://github.com/eclipse-californium/californium/pull/2215 @Test public void testSubscribeToAttributesUpdatesFromTheServerWithEmptyCurrentStateNotification() throws Exception { processProtoTestSubscribeToAttributesUpdates(true); diff --git a/application/src/test/java/org/thingsboard/server/transport/coap/client/CoapClientIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/coap/client/CoapClientIntegrationTest.java index e7a7485ef5..112d3e6aa5 100644 --- a/application/src/test/java/org/thingsboard/server/transport/coap/client/CoapClientIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/coap/client/CoapClientIntegrationTest.java @@ -26,7 +26,6 @@ import org.eclipse.californium.core.coap.CoAP; import org.eclipse.californium.core.coap.MediaTypeRegistry; import org.junit.After; import org.junit.Before; -import org.junit.Ignore; import org.junit.Test; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.id.DeviceId; @@ -69,7 +68,6 @@ public class CoapClientIntegrationTest extends AbstractCoapIntegrationTest { private static final List EXPECTED_KEYS = Arrays.asList("key1", "key2", "key3", "key4", "key5"); private static final String DEVICE_RESPONSE = "{\"value1\":\"A\",\"value2\":\"B\"}"; - @Before public void beforeTest() throws Exception { CoapTestConfigProperties configProperties = CoapTestConfigProperties.builder() @@ -83,7 +81,6 @@ public class CoapClientIntegrationTest extends AbstractCoapIntegrationTest { processAfterTest(); } - @Ignore // Uncomment when Californium 3.11 is released with https://github.com/eclipse-californium/californium/pull/2215 @Test public void testConfirmableRequests() throws Exception { boolean confirmable = true; @@ -92,7 +89,6 @@ public class CoapClientIntegrationTest extends AbstractCoapIntegrationTest { processTestRequestAttributesValuesFromTheServer(confirmable); } - @Ignore // Uncomment when Californium 3.11 is released with https://github.com/eclipse-californium/californium/pull/2215 @Test public void testNonConfirmableRequests() throws Exception { boolean confirmable = false; diff --git a/application/src/test/java/org/thingsboard/server/transport/coap/rpc/CoapServerSideRpcDefaultIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/coap/rpc/CoapServerSideRpcDefaultIntegrationTest.java index 8f9d3379ef..df1159b128 100644 --- a/application/src/test/java/org/thingsboard/server/transport/coap/rpc/CoapServerSideRpcDefaultIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/coap/rpc/CoapServerSideRpcDefaultIntegrationTest.java @@ -20,7 +20,6 @@ import lombok.extern.slf4j.Slf4j; import org.junit.After; import org.junit.Assert; import org.junit.Before; -import org.junit.Ignore; import org.junit.Test; import org.thingsboard.server.dao.service.DaoSqlTest; import org.thingsboard.server.service.security.AccessValidator; @@ -83,13 +82,11 @@ public class CoapServerSideRpcDefaultIntegrationTest extends AbstractCoapServerS Assert.assertEquals(AccessValidator.DEVICE_WITH_REQUESTED_ID_NOT_FOUND, result); } - @Ignore // Uncomment when Californium 3.11 is released with https://github.com/eclipse-californium/californium/pull/2215 @Test public void testServerCoapOneWayRpc() throws Exception { processOneWayRpcTest(false); } - @Ignore // Uncomment when Californium 3.11 is released with https://github.com/eclipse-californium/californium/pull/2215 @Test public void testServerCoapTwoWayRpc() throws Exception { processTwoWayRpcTest("{\"value1\":\"A\",\"value2\":\"B\"}", false); diff --git a/application/src/test/java/org/thingsboard/server/transport/coap/rpc/CoapServerSideRpcJsonIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/coap/rpc/CoapServerSideRpcJsonIntegrationTest.java index 34d678b5b5..5c94e91b72 100644 --- a/application/src/test/java/org/thingsboard/server/transport/coap/rpc/CoapServerSideRpcJsonIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/coap/rpc/CoapServerSideRpcJsonIntegrationTest.java @@ -18,7 +18,6 @@ package org.thingsboard.server.transport.coap.rpc; import lombok.extern.slf4j.Slf4j; import org.junit.After; import org.junit.Before; -import org.junit.Ignore; import org.junit.Test; import org.thingsboard.server.common.data.CoapDeviceType; import org.thingsboard.server.common.data.TransportPayloadType; @@ -44,13 +43,11 @@ public class CoapServerSideRpcJsonIntegrationTest extends AbstractCoapServerSide processAfterTest(); } - @Ignore // Uncomment when Californium 3.11 is released with https://github.com/eclipse-californium/californium/pull/2215 @Test public void testServerCoapOneWayRpc() throws Exception { processOneWayRpcTest(false); } - @Ignore // Uncomment when Californium 3.11 is released with https://github.com/eclipse-californium/californium/pull/2215 @Test public void testServerCoapTwoWayRpc() throws Exception { processTwoWayRpcTest("{\"value1\":\"A\",\"value2\":\"B\"}", false); diff --git a/application/src/test/java/org/thingsboard/server/transport/coap/rpc/CoapServerSideRpcProtoIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/coap/rpc/CoapServerSideRpcProtoIntegrationTest.java index 138f87e964..2f7d46a390 100644 --- a/application/src/test/java/org/thingsboard/server/transport/coap/rpc/CoapServerSideRpcProtoIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/coap/rpc/CoapServerSideRpcProtoIntegrationTest.java @@ -18,7 +18,6 @@ package org.thingsboard.server.transport.coap.rpc; import lombok.extern.slf4j.Slf4j; import org.junit.After; import org.junit.Before; -import org.junit.Ignore; import org.junit.Test; import org.thingsboard.server.common.data.CoapDeviceType; import org.thingsboard.server.common.data.TransportPayloadType; @@ -45,13 +44,11 @@ public class CoapServerSideRpcProtoIntegrationTest extends AbstractCoapServerSid processAfterTest(); } - @Ignore // Uncomment when Californium 3.11 is released with https://github.com/eclipse-californium/californium/pull/2215 @Test public void testServerCoapOneWayRpc() throws Exception { processOneWayRpcTest(true); } - @Ignore // Uncomment when Californium 3.11 is released with https://github.com/eclipse-californium/californium/pull/2215 @Test public void testServerCoapTwoWayRpc() throws Exception { processTwoWayRpcTest("{\"payload\":\"{\\\"value1\\\":\\\"A\\\",\\\"value2\\\":\\\"B\\\"}\"}", true); diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClient.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClient.java index 29df8f7f65..cb391b27e9 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClient.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClient.java @@ -229,7 +229,7 @@ public class LwM2mClient { this.resources.get(pathRezIdVer).updateLwM2mResource(resource, mode); return true; } else { - LwM2mPath pathIds = new LwM2mPath(fromVersionedIdToObjectId(pathRezIdVer)); + LwM2mPath pathIds = getLwM2mPathFromString(pathRezIdVer); ResourceModel resourceModel = modelProvider.getObjectModel(registration).getResourceModel(pathIds.getObjectId(), pathIds.getResourceId()); if (resourceModel != null) { this.resources.put(pathRezIdVer, new ResourceValue(resource, resourceModel)); @@ -257,7 +257,7 @@ public class LwM2mClient { } public String getRezIdByResourceNameAndObjectInstanceId(String resourceName, String pathObjectInstanceIdVer, LwM2mModelProvider modelProvider) { - LwM2mPath pathIds = new LwM2mPath(fromVersionedIdToObjectId(pathObjectInstanceIdVer)); + LwM2mPath pathIds = getLwM2mPathFromString(pathObjectInstanceIdVer); if (pathIds.isObjectInstance()) { Set rezIds = modelProvider.getObjectModel(registration) .getObjectModel(pathIds.getObjectId()).resources.entrySet() @@ -271,7 +271,7 @@ public class LwM2mClient { } public ResourceModel getResourceModel(String pathIdVer, LwM2mModelProvider modelProvider) { - LwM2mPath pathIds = new LwM2mPath(fromVersionedIdToObjectId(pathIdVer)); + LwM2mPath pathIds = getLwM2mPathFromString(pathIdVer); String verSupportedObject = String.valueOf(registration.getSupportedObject().get(pathIds.getObjectId())); String verRez = getVerFromPathIdVerOrId(pathIdVer); return verRez != null && verRez.equals(verSupportedObject) ? modelProvider.getObjectModel(registration) @@ -289,7 +289,7 @@ public class LwM2mClient { public ObjectModel getObjectModel(String pathIdVer, LwM2mModelProvider modelProvider) { try { - LwM2mPath pathIds = new LwM2mPath(fromVersionedIdToObjectId(pathIdVer)); + LwM2mPath pathIds = getLwM2mPathFromString(pathIdVer); String verSupportedObject = String.valueOf(registration.getSupportedObject().get(pathIds.getObjectId())); String verRez = getVerFromPathIdVerOrId(pathIdVer); return verRez != null && verRez.equals(verSupportedObject) ? modelProvider.getObjectModel(registration) @@ -309,7 +309,7 @@ public class LwM2mClient { public Collection getNewResourceForInstance(String pathRezIdVer, Object params, LwM2mModelProvider modelProvider, LwM2mValueConverter converter) { - LwM2mPath pathIds = new LwM2mPath(fromVersionedIdToObjectId(pathRezIdVer)); + LwM2mPath pathIds = getLwM2mPathFromString(pathRezIdVer); Collection resources = ConcurrentHashMap.newKeySet(); Map resourceModels = modelProvider.getObjectModel(registration) .getObjectModel(pathIds.getObjectId()).resources; @@ -329,7 +329,7 @@ public class LwM2mClient { */ public Collection getNewResourcesForInstance(String pathRezIdVer, Object params, LwM2mModelProvider modelProvider, LwM2mValueConverter converter) { - LwM2mPath pathIds = new LwM2mPath(fromVersionedIdToObjectId(pathRezIdVer)); + LwM2mPath pathIds = getLwM2mPathFromString(pathRezIdVer); Collection resources = ConcurrentHashMap.newKeySet(); Map resourceModels = modelProvider.getObjectModel(registration) .getObjectModel(pathIds.getObjectId()).resources; @@ -370,7 +370,7 @@ public class LwM2mClient { } public String isValidObjectVersion(String path) { - LwM2mPath pathIds = new LwM2mPath(fromVersionedIdToObjectId(path)); + LwM2mPath pathIds = getLwM2mPathFromString(path); LwM2m.Version verSupportedObject = registration.getSupportedObject().get(pathIds.getObjectId()); if (verSupportedObject == null) { return String.format("Specified object id %s absent in the list supported objects of the client or is security object!", pathIds.getObjectId()); @@ -390,7 +390,7 @@ public class LwM2mClient { public void deleteResources(String pathIdVer, LwM2mModelProvider modelProvider) { Set key = getKeysEqualsIdVer(pathIdVer); key.forEach(pathRez -> { - LwM2mPath pathIds = new LwM2mPath(fromVersionedIdToObjectId(pathRez)); + LwM2mPath pathIds = getLwM2mPathFromString(pathRez); ResourceModel resourceModel = modelProvider.getObjectModel(registration).getResourceModel(pathIds.getObjectId(), pathIds.getResourceId()); if (resourceModel != null) { this.resources.get(pathRez).setResourceModel(resourceModel); @@ -410,7 +410,7 @@ public class LwM2mClient { } private void saveResourceModel(String pathRez, LwM2mModelProvider modelProvider) { - LwM2mPath pathIds = new LwM2mPath(fromVersionedIdToObjectId(pathRez)); + LwM2mPath pathIds = getLwM2mPathFromString(pathRez); ResourceModel resourceModel = modelProvider.getObjectModel(registration).getResourceModel(pathIds.getObjectId(), pathIds.getResourceId()); this.resources.get(pathRez).setResourceModel(resourceModel); } @@ -456,5 +456,9 @@ public class LwM2mClient { return result; } + public LwM2mPath getLwM2mPathFromString(String path) { + return new LwM2mPath(fromVersionedIdToObjectId(path)); + } + } diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/util/LwM2MClientSerDes.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/util/LwM2MClientSerDes.java index 30800eb9ee..17fef8acb0 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/util/LwM2MClientSerDes.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/util/LwM2MClientSerDes.java @@ -15,8 +15,12 @@ */ package org.thingsboard.server.transport.lwm2m.server.store.util; -import com.fasterxml.jackson.annotation.JsonValue; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.google.gson.JsonElement; import com.google.gson.JsonObject; +import com.google.gson.JsonParser; import com.google.protobuf.util.JsonFormat; import lombok.SneakyThrows; import org.eclipse.leshan.core.model.ResourceModel; @@ -25,23 +29,35 @@ import org.eclipse.leshan.core.node.LwM2mNodeException; import org.eclipse.leshan.core.node.LwM2mResource; import org.eclipse.leshan.core.node.LwM2mSingleResource; import org.eclipse.leshan.core.node.ObjectLink; +import org.eclipse.leshan.core.util.datatype.ULong; import org.eclipse.leshan.server.redis.serialization.RegistrationSerDes; +import org.thingsboard.server.common.data.device.data.PowerMode; +import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.gen.transport.TransportProtos; +import org.thingsboard.server.transport.lwm2m.server.client.LwM2MClientState; import org.thingsboard.server.transport.lwm2m.server.client.LwM2mClient; +import org.thingsboard.server.transport.lwm2m.server.client.ResourceValue; import java.lang.reflect.Field; +import java.time.Instant; import java.util.Base64; import java.util.Date; import java.util.HashMap; import java.util.Map; +import java.util.Map.Entry; +import java.util.UUID; +import java.util.concurrent.atomic.AtomicLong; + +import static org.thingsboard.common.util.JacksonUtil.toJsonNode; public class LwM2MClientSerDes { public static final String VALUE = "value"; + private static final RegistrationSerDes registrationSerDes = new RegistrationSerDes(); @SneakyThrows public static byte[] serialize(LwM2mClient client) { JsonObject o = new JsonObject(); - o.addProperty("nodeId", "client.getNodeId()"); + o.addProperty("nodeId", client.getNodeId()); o.addProperty("endpoint", client.getEndpoint()); JsonObject resources = new JsonObject(); @@ -92,8 +108,15 @@ public class LwM2MClientSerDes { o.addProperty("pagingTransmissionWindow", client.getPagingTransmissionWindow()); } if (client.getRegistration() != null) { - RegistrationSerDes regDez = new RegistrationSerDes(); - o.addProperty("registration", regDez.jSerialize(client.getRegistration()).toString()); + String registrationAddress = client.getRegistration().getAddress().toString(); + JsonNode registrationNode = registrationSerDes.jSerialize(client.getRegistration()); + if (!registrationAddress.equals(registrationNode.get("transportdata").get("address").asText())){ + ObjectNode actualRegAddress = (ObjectNode)registrationNode.get("transportdata"); + actualRegAddress.put("address", registrationAddress); + ObjectNode actualIdentity = (ObjectNode) actualRegAddress.get("identity"); + actualIdentity.put("address", registrationAddress); + } + o.addProperty("registration", registrationNode.toString()); } o.addProperty("asleep", client.isAsleep()); o.addProperty("lastUplinkTime", client.getLastUplinkTime()); @@ -138,33 +161,35 @@ public class LwM2MClientSerDes { ResourceModel.Type type = ResourceModel.Type.valueOf(o.get("type").getAsString()); if (multiInstances) { Map instances = new HashMap<>(); - o.get("instances").getAsJsonArray().forEach(entry -> { -// instances.put(Integer.valueOf(entry.getAsJsonObject().), parseValue(type, entry.getValue())); - }); + for (Entry entry : o.get("instances").getAsJsonObject().entrySet()) { + JsonObject instance = entry.getValue().getAsJsonObject(); + instances.put(Integer.valueOf(instance.get("id").getAsString()), parseValue(type, instance.get(VALUE))); + } return LwM2mMultipleResource.newResource(id, instances, type); } else { - return LwM2mSingleResource.newResource(id, parseValue(type, (JsonValue) o.get(VALUE))); + return LwM2mSingleResource.newResource(id, parseValue(type, o.get(VALUE))); } } - private static Object parseValue(ResourceModel.Type type, JsonValue value) { + + private static Object parseValue(ResourceModel.Type type, JsonElement value) { switch (type) { case INTEGER: - return value.value(); -// case FLOAT: -// return value.value(); -// case BOOLEAN: -// return value.asBoolean(); -// case OPAQUE: -// return Base64.getDecoder().decode(value.asString()); -// case STRING: -// return value.asString(); -// case TIME: -// return new Date(value.asLong()); -// case OBJLNK: -// return ObjectLink.decodeFromString(value.asString()); -// case UNSIGNED_INTEGER: -// return ULong.valueOf(value.asString()); + return value.getAsInt(); + case FLOAT: + return value.getAsDouble(); + case BOOLEAN: + return value.getAsBoolean(); + case OPAQUE: + return Base64.getDecoder().decode(value.getAsString()); + case STRING: + return value.getAsString(); + case TIME: + return Instant.ofEpochMilli(value.getAsLong()); + case OBJLNK: + return ObjectLink.decodeFromString(value.getAsString()); + case UNSIGNED_INTEGER: + return ULong.valueOf(value.getAsString()); default: throw new LwM2mNodeException(String.format("Type %s is not supported", type.name())); } @@ -212,7 +237,7 @@ public class LwM2MClientSerDes { o.addProperty(VALUE, Base64.getEncoder().encodeToString((byte[]) value)); break; case STRING: - o.addProperty(VALUE, (String) value); + o.addProperty(VALUE, String.valueOf(value)); break; case TIME: o.addProperty(VALUE, ((Date) value).getTime()); @@ -221,7 +246,7 @@ public class LwM2MClientSerDes { o.addProperty(VALUE, ((ObjectLink) value).encodeToString()); break; case UNSIGNED_INTEGER: - o.addProperty(VALUE, value.toString()); + o.addProperty(VALUE, Integer.toUnsignedString((int)value)); break; default: throw new LwM2mNodeException(String.format("Type %s is not supported", type.name())); @@ -230,111 +255,111 @@ public class LwM2MClientSerDes { @SneakyThrows public static LwM2mClient deserialize(byte[] data) { -// JsonObject o = new JsonObject(new String(data))); -// LwM2mClient lwM2mClient = new LwM2mClient(o.get("nodeId").getAsString(), o.get("endpoint").getAsString()); - LwM2mClient lwM2mClient = new LwM2mClient("nodeId", "endpoint"); -// o.get("resources").getAsJsonObject().forEach(entry -> { -// JsonObject resource = entry.getValue().asObject(); -// LwM2mResource lwM2mResource = parseLwM2mResource(resource.get("lwM2mResource").getAsJsonObject()); -// ResourceModel resourceModel = parseResourceModel(resource.get("resourceModel").asObject()); -// ResourceValue resourceValue = new ResourceValue(lwM2mResource, resourceModel); -// lwM2mClient.getResources().put(entry.getName(), resourceValue); -// }); -// -// for (JsonObject.Member entry : o.get("sharedAttributes").asObject()) { -// TransportProtos.TsKvProto.Builder builder = TransportProtos.TsKvProto.newBuilder(); -// JsonFormat.parser().merge(entry.getValue().getAsString(), builder); -// lwM2mClient.getSharedAttributes().put(entry.getName(), builder.build()); -// } -// -// o.get("keyTsLatestMap").asObject().forEach(entry -> { -// lwM2mClient.getKeyTsLatestMap().put(entry.getName(), new AtomicLong(entry.getValue().asLong())); -// }); -// -// lwM2mClient.setState(LwM2MClientState.valueOf(o.get("state").getAsString())); -// -// Class lwM2mClientClass = LwM2mClient.class; -// -// JsonValue session = o.get("session"); -// if (session != null) { -// TransportProtos.SessionInfoProto.Builder builder = TransportProtos.SessionInfoProto.newBuilder(); -// JsonFormat.parser().merge(session.asString(), builder); -// -// Field sessionField = lwM2mClientClass.getDeclaredField("session"); -// sessionField.setAccessible(true); -// sessionField.set(lwM2mClient, builder.build()); -// } -// -// JsonValue tenantId = o.get("tenantId"); -// if (tenantId != null) { -// Field tenantIdField = lwM2mClientClass.getDeclaredField("tenantId"); -// tenantIdField.setAccessible(true); -// tenantIdField.set(lwM2mClient, new TenantId(UUID.fromString(tenantId.asString()))); -// } -// -// JsonValue deviceId = o.get("deviceId"); -// if (tenantId != null) { -// Field deviceIdField = lwM2mClientClass.getDeclaredField("deviceId"); -// deviceIdField.setAccessible(true); -// deviceIdField.set(lwM2mClient, UUID.fromString(deviceId.asString())); -// } -// -// JsonValue profileId = o.get("profileId"); -// if (tenantId != null) { -// Field profileIdField = lwM2mClientClass.getDeclaredField("profileId"); -// profileIdField.setAccessible(true); -// profileIdField.set(lwM2mClient, UUID.fromString(profileId.asString())); -// } -// -// JsonValue powerMode = o.get("powerMode"); -// if (powerMode != null) { -// Field powerModeField = lwM2mClientClass.getDeclaredField("powerMode"); -// powerModeField.setAccessible(true); -// powerModeField.set(lwM2mClient, PowerMode.valueOf(powerMode.asString())); -// } -// -// JsonValue edrxCycle = o.get("edrxCycle"); -// if (edrxCycle != null) { -// Field edrxCycleField = lwM2mClientClass.getDeclaredField("edrxCycle"); -// edrxCycleField.setAccessible(true); -// edrxCycleField.set(lwM2mClient, edrxCycle.asLong()); -// } -// -// JsonValue psmActivityTimer = o.get("psmActivityTimer"); -// if (psmActivityTimer != null) { -// Field psmActivityTimerField = lwM2mClientClass.getDeclaredField("psmActivityTimer"); -// psmActivityTimerField.setAccessible(true); -// psmActivityTimerField.set(lwM2mClient, psmActivityTimer.asLong()); -// } -// -// JsonValue pagingTransmissionWindow = o.get("pagingTransmissionWindow"); -// if (pagingTransmissionWindow != null) { -// Field pagingTransmissionWindowField = lwM2mClientClass.getDeclaredField("pagingTransmissionWindow"); -// pagingTransmissionWindowField.setAccessible(true); -// pagingTransmissionWindowField.set(lwM2mClient, pagingTransmissionWindow.asLong()); -// } -// -// JsonValue registration = o.get("registration"); -// if (registration != null) { -// lwM2mClient.setRegistration(RegistrationSerDes.deserialize(registration.asObject())); -// } -// -// lwM2mClient.setAsleep(o.get("asleep").getAsBoolean()); -// -// Field lastUplinkTimeField = lwM2mClientClass.getDeclaredField("lastUplinkTime"); -// lastUplinkTimeField.setAccessible(true); -// lastUplinkTimeField.setLong(lwM2mClient, o.get("lastUplinkTime").asLong()); -// -// Field firstEdrxDownlinkField = lwM2mClientClass.getDeclaredField("firstEdrxDownlink"); -// firstEdrxDownlinkField.setAccessible(true); -// firstEdrxDownlinkField.setBoolean(lwM2mClient, o.get("firstEdrxDownlink").getAsBoolean()); -// -// lwM2mClient.getRetryAttempts().set(o.get("retryAttempts").asInt()); -// -// JsonValue lastSentRpcId = o.get("lastSentRpcId"); -// if (lastSentRpcId != null) { -// lwM2mClient.setLastSentRpcId(UUID.fromString(lastSentRpcId.asString())); -// } + JsonObject o = JsonParser.parseString(new String(data)).getAsJsonObject(); + LwM2mClient lwM2mClient = new LwM2mClient(o.get("nodeId").getAsString(), o.get("endpoint").getAsString()); + + o.get("resources").getAsJsonObject().entrySet().forEach(entry -> { + JsonObject resource = entry.getValue().getAsJsonObject(); + LwM2mResource lwM2mResource = parseLwM2mResource(resource.get("lwM2mResource").getAsJsonObject()); + ResourceModel resourceModel = parseResourceModel(resource.get("resourceModel").getAsJsonObject()); + ResourceValue resourceValue = new ResourceValue(lwM2mResource, resourceModel); + lwM2mClient.getResources().put(String.valueOf(lwM2mResource.getId()), resourceValue); + }); + + for (Entry entry : o.get("sharedAttributes").getAsJsonObject().entrySet()) { + TransportProtos.TsKvProto.Builder builder = TransportProtos.TsKvProto.newBuilder(); + JsonFormat.parser().merge(entry.getValue().getAsString(), builder); + lwM2mClient.getSharedAttributes().put(entry.getKey(), builder.build()); + } + + o.get("keyTsLatestMap").getAsJsonObject().entrySet().forEach(entry -> { + lwM2mClient.getKeyTsLatestMap().put(entry.getKey(), new AtomicLong(entry.getValue().getAsLong())); + }); + + lwM2mClient.setState(LwM2MClientState.valueOf(o.get("state").getAsString())); + + Class lwM2mClientClass = LwM2mClient.class; + + JsonElement session = o.get("session"); + if (session != null) { + TransportProtos.SessionInfoProto.Builder builder = TransportProtos.SessionInfoProto.newBuilder(); + JsonFormat.parser().merge(session.getAsString(), builder); + + Field sessionField = lwM2mClientClass.getDeclaredField("session"); + sessionField.setAccessible(true); + sessionField.set(lwM2mClient, builder.build()); + } + + JsonElement tenantId = o.get("tenantId"); + if (tenantId != null) { + Field tenantIdField = lwM2mClientClass.getDeclaredField("tenantId"); + tenantIdField.setAccessible(true); + tenantIdField.set(lwM2mClient, new TenantId(UUID.fromString(tenantId.getAsString()))); + } + + JsonElement deviceId = o.get("deviceId"); + if (tenantId != null) { + Field deviceIdField = lwM2mClientClass.getDeclaredField("deviceId"); + deviceIdField.setAccessible(true); + deviceIdField.set(lwM2mClient, UUID.fromString(deviceId.getAsString())); + } + + JsonElement profileId = o.get("profileId"); + if (tenantId != null) { + Field profileIdField = lwM2mClientClass.getDeclaredField("profileId"); + profileIdField.setAccessible(true); + profileIdField.set(lwM2mClient, UUID.fromString(profileId.getAsString())); + } + + JsonElement powerMode = o.get("powerMode"); + if (powerMode != null) { + Field powerModeField = lwM2mClientClass.getDeclaredField("powerMode"); + powerModeField.setAccessible(true); + powerModeField.set(lwM2mClient, PowerMode.valueOf(powerMode.getAsString())); + } + + JsonElement edrxCycle = o.get("edrxCycle"); + if (edrxCycle != null) { + Field edrxCycleField = lwM2mClientClass.getDeclaredField("edrxCycle"); + edrxCycleField.setAccessible(true); + edrxCycleField.set(lwM2mClient, edrxCycle.getAsLong()); + } + + JsonElement psmActivityTimer = o.get("psmActivityTimer"); + if (psmActivityTimer != null) { + Field psmActivityTimerField = lwM2mClientClass.getDeclaredField("psmActivityTimer"); + psmActivityTimerField.setAccessible(true); + psmActivityTimerField.set(lwM2mClient, psmActivityTimer.getAsLong()); + } + + JsonElement pagingTransmissionWindow = o.get("pagingTransmissionWindow"); + if (pagingTransmissionWindow != null) { + Field pagingTransmissionWindowField = lwM2mClientClass.getDeclaredField("pagingTransmissionWindow"); + pagingTransmissionWindowField.setAccessible(true); + pagingTransmissionWindowField.set(lwM2mClient, pagingTransmissionWindow.getAsLong()); + } + + JsonElement registration = o.get("registration"); + if (registration != null) { + lwM2mClient.setRegistration(registrationSerDes.deserialize(toJsonNode(registration.getAsString()))); + } + + lwM2mClient.setAsleep(o.get("asleep").getAsBoolean()); + + Field lastUplinkTimeField = lwM2mClientClass.getDeclaredField("lastUplinkTime"); + lastUplinkTimeField.setAccessible(true); + lastUplinkTimeField.setLong(lwM2mClient, o.get("lastUplinkTime").getAsLong()); + + Field firstEdrxDownlinkField = lwM2mClientClass.getDeclaredField("firstEdrxDownlink"); + firstEdrxDownlinkField.setAccessible(true); + firstEdrxDownlinkField.setBoolean(lwM2mClient, o.get("firstEdrxDownlink").getAsBoolean()); + + lwM2mClient.getRetryAttempts().set(o.get("retryAttempts").getAsInt()); + + JsonElement lastSentRpcId = o.get("lastSentRpcId"); + if (lastSentRpcId != null) { + lwM2mClient.setLastSentRpcId(UUID.fromString(lastSentRpcId.getAsString())); + } return lwM2mClient; } diff --git a/common/transport/lwm2m/src/test/java/org/thingsboard/server/transport/lwm2m/bootstrap/LwM2MTransportBootstrapServiceTest.java b/common/transport/lwm2m/src/test/java/org/thingsboard/server/transport/lwm2m/bootstrap/LwM2MTransportBootstrapServiceTest.java deleted file mode 100644 index 9f49f3b138..0000000000 --- a/common/transport/lwm2m/src/test/java/org/thingsboard/server/transport/lwm2m/bootstrap/LwM2MTransportBootstrapServiceTest.java +++ /dev/null @@ -1,100 +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.transport.lwm2m.bootstrap; - -import org.eclipse.californium.core.network.CoapEndpoint; -import org.eclipse.californium.scandium.config.DtlsConnectorConfig; -import org.junit.Ignore; -import org.junit.jupiter.api.Disabled; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; -import org.springframework.test.util.ReflectionTestUtils; -import org.thingsboard.server.common.transport.TransportService; -import org.thingsboard.server.transport.lwm2m.bootstrap.secure.TbLwM2MDtlsBootstrapCertificateVerifier; -import org.thingsboard.server.transport.lwm2m.bootstrap.store.LwM2MBootstrapSecurityStore; -import org.thingsboard.server.transport.lwm2m.bootstrap.store.LwM2MInMemoryBootstrapConfigStore; -import org.thingsboard.server.transport.lwm2m.config.LwM2MTransportBootstrapConfig; -import org.thingsboard.server.transport.lwm2m.config.LwM2MTransportServerConfig; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.BDDMockito.when; - -@ExtendWith(MockitoExtension.class) -public class LwM2MTransportBootstrapServiceTest { - - @Mock - private LwM2MTransportServerConfig serverConfig; - @Mock - private LwM2MTransportBootstrapConfig bootstrapConfig; - @Mock - private LwM2MBootstrapSecurityStore lwM2MBootstrapSecurityStore; - @Mock - private LwM2MInMemoryBootstrapConfigStore lwM2MInMemoryBootstrapConfigStore; - @Mock - private TransportService transportService; - @Mock - private TbLwM2MDtlsBootstrapCertificateVerifier certificateVerifier; - - @Disabled // fixme: nick - @Test - public void getLHServer_creates_ConnectionIdGenerator_when_connection_id_length_not_null(){ - final Integer CONNECTION_ID_LENGTH = 6; - when(serverConfig.getDtlsCidLength()).thenReturn(CONNECTION_ID_LENGTH); - var lwM2MBootstrapService = createLwM2MBootstrapService(); - - var server = lwM2MBootstrapService.getLhBootstrapServer(); - var securedEndpoint = (CoapEndpoint) ReflectionTestUtils.getField(server, "securedEndpoint"); - assertThat(securedEndpoint).isNotNull(); - - var config = (DtlsConnectorConfig) ReflectionTestUtils.getField(securedEndpoint.getConnector(), "config"); - assertThat(config).isNotNull(); - assertThat(config.getConnectionIdGenerator()).isNotNull(); - assertThat((Integer) ReflectionTestUtils.getField(config.getConnectionIdGenerator(), "connectionIdLength")) - .isEqualTo(CONNECTION_ID_LENGTH); - } - - @Disabled // fixme: nick - @Test - public void getLHServer_creates_no_ConnectionIdGenerator_when_connection_id_length_is_null(){ - when(serverConfig.getDtlsCidLength()).thenReturn(null); - var lwM2MBootstrapService = createLwM2MBootstrapService(); - - var server = lwM2MBootstrapService.getLhBootstrapServer(); - var securedEndpoint = (CoapEndpoint) ReflectionTestUtils.getField(server, "securedEndpoint"); - assertThat(securedEndpoint).isNotNull(); - - var config = (DtlsConnectorConfig) ReflectionTestUtils.getField(securedEndpoint.getConnector(), "config"); - assertThat(config).isNotNull(); - assertThat(config.getConnectionIdGenerator()).isNull(); - } - - private LwM2MTransportBootstrapService createLwM2MBootstrapService() { - setDefaultConfigVariables(); - return new LwM2MTransportBootstrapService(serverConfig, bootstrapConfig, lwM2MBootstrapSecurityStore, - lwM2MInMemoryBootstrapConfigStore, transportService, certificateVerifier); - } - - private void setDefaultConfigVariables(){ - when(bootstrapConfig.getPort()).thenReturn(5683); - when(bootstrapConfig.getSecurePort()).thenReturn(5684); - when(serverConfig.isRecommendedCiphers()).thenReturn(false); - when(serverConfig.getDtlsRetransmissionTimeout()).thenReturn(9000); - } - - -} \ No newline at end of file diff --git a/common/transport/lwm2m/src/test/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClientTest.java b/common/transport/lwm2m/src/test/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClientTest.java index 5bc334756d..8fe58a5276 100644 --- a/common/transport/lwm2m/src/test/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClientTest.java +++ b/common/transport/lwm2m/src/test/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClientTest.java @@ -15,9 +15,10 @@ */ package org.thingsboard.server.transport.lwm2m.server.client; +import org.eclipse.leshan.core.endpoint.EndpointUriUtil; import org.eclipse.leshan.core.link.Link; +import org.eclipse.leshan.core.peer.IpPeer; import org.eclipse.leshan.server.registration.Registration; -import org.junit.Ignore; import org.junit.Test; import org.junit.jupiter.api.Assertions; @@ -25,14 +26,14 @@ import java.net.InetSocketAddress; public class LwM2mClientTest { - @Ignore @Test public void setRegistration() { LwM2mClient client = new LwM2mClient("nodeId", "testEndpoint"); - Registration registration = null; /*new Registration - .Builder("test", "testEndpoint", Identity.unsecure(new InetSocketAddress(1000))) // FIXME: nick + Registration registration = new Registration + .Builder("testId", "testEndpoint", new IpPeer(new InetSocketAddress(1000)), + EndpointUriUtil.createUri("coap://localhost:5685")) .objectLinks(new Link[0]) - .build();*/ + .build(); Assertions.assertDoesNotThrow(() -> client.setRegistration(registration)); } diff --git a/common/transport/lwm2m/src/test/java/org/thingsboard/server/transport/lwm2m/server/store/util/LwM2MClientSerDesTest.java b/common/transport/lwm2m/src/test/java/org/thingsboard/server/transport/lwm2m/server/store/util/LwM2MClientSerDesTest.java index 43187f2a36..b3473a67b2 100644 --- a/common/transport/lwm2m/src/test/java/org/thingsboard/server/transport/lwm2m/server/store/util/LwM2MClientSerDesTest.java +++ b/common/transport/lwm2m/src/test/java/org/thingsboard/server/transport/lwm2m/server/store/util/LwM2MClientSerDesTest.java @@ -15,13 +15,17 @@ */ package org.thingsboard.server.transport.lwm2m.server.store.util; +import org.eclipse.leshan.core.LwM2m.LwM2mVersion; +import org.eclipse.leshan.core.endpoint.EndpointUriUtil; +import org.eclipse.leshan.core.link.Link; import org.eclipse.leshan.core.node.LwM2mMultipleResource; +import org.eclipse.leshan.core.node.LwM2mPath; import org.eclipse.leshan.core.node.LwM2mResource; import org.eclipse.leshan.core.node.LwM2mSingleResource; +import org.eclipse.leshan.core.peer.IpPeer; import org.eclipse.leshan.core.request.WriteRequest; import org.eclipse.leshan.server.registration.Registration; import org.junit.Assert; -import org.junit.Ignore; import org.junit.Test; import org.thingsboard.server.common.data.TbResource; import org.thingsboard.server.common.data.device.data.PowerMode; @@ -41,9 +45,12 @@ import org.thingsboard.server.transport.lwm2m.server.client.LwM2mClient; import org.thingsboard.server.transport.lwm2m.server.client.LwM2mClientContext; import org.thingsboard.server.transport.lwm2m.server.client.ResourceValue; +import java.net.Inet4Address; +import java.net.InetSocketAddress; import java.nio.file.Files; import java.nio.file.Path; import java.util.Map; +import java.util.Map.Entry; import java.util.Optional; import java.util.UUID; @@ -56,10 +63,9 @@ import static org.mockito.Mockito.when; public class LwM2MClientSerDesTest { - @Ignore @Test public void serializeDeserialize() throws Exception { - LwM2mClient client = new LwM2mClient("nodeId", "testEndpoint"); + LwM2mClient client = new LwM2mClient("nodeId", "endpoint"); TransportDeviceInfo tdi = new TransportDeviceInfo(); tdi.setPowerMode(PowerMode.PSM); @@ -78,13 +84,13 @@ public class LwM2MClientSerDesTest { client.init(credentialsResponse, UUID.randomUUID()); - Registration registration = null; // FIXME: nick -// new Registration.Builder("test", "testEndpoint", Identity -// .unsecure(new InetSocketAddress(1000))) -// .supportedContentFormats() -// .supportedObjects(Map.of(15, "1.0", 17, "1.0")) -// .objectLinks(new Link[]{new Link("/")}) -// .build(); + Registration registration = new Registration + .Builder("test", "endpoint", new IpPeer(new InetSocketAddress(Inet4Address.getLoopbackAddress(), 1000)), + EndpointUriUtil.createUri("coap://localhost:5685")) + .supportedContentFormats() + .supportedObjects(Map.of(15, LwM2mVersion.V1_0, 17, LwM2mVersion.V1_0)) + .objectLinks(new Link[] { new Link("/15"), new Link("/17") }) + .build(); client.setRegistration(registration); client.setState(LwM2MClientState.REGISTERED); @@ -130,7 +136,12 @@ public class LwM2MClientSerDesTest { assertEquals(client.getPsmActivityTimer(), desClient.getPsmActivityTimer()); assertEquals(client.getPagingTransmissionWindow(), desClient.getPagingTransmissionWindow()); assertEquals(client.getEdrxCycle(), desClient.getEdrxCycle()); - assertEquals(client.getRegistration(), desClient.getRegistration()); + if (((IpPeer)desClient.getRegistration().getClientTransportData()).getSocketAddress().isUnresolved()) { + String actualReg = desClient.getRegistration().toString().replaceAll("/", ""); + assertEquals(client.getRegistration().toString(), actualReg); + } else { + assertEquals(client.getRegistration(), desClient.getRegistration()); + } assertEquals(client.isAsleep(), desClient.isAsleep()); assertEquals(client.getLastUplinkTime(), desClient.getLastUplinkTime()); assertEquals(client.getSleepTask(), desClient.getSleepTask()); @@ -143,7 +154,11 @@ public class LwM2MClientSerDesTest { Map actualResources = desClient.getResources(); assertNotNull(actualResources); assertEquals(expectedResources.size(), actualResources.size()); - expectedResources.forEach((key, value) -> assertEquals(value.toString(), actualResources.get(key).toString())); + for (Entry entry : expectedResources.entrySet()) { + LwM2mPath expectedPathId = client.getLwM2mPathFromString(entry.getKey().toString()); + String actualOld = actualResources.get(String.valueOf(expectedPathId.getObjectId())).toString(); + String actual = actualOld.replaceAll("\"", ""); + assertEquals(entry.getValue().toString(), actual); + } } - } \ No newline at end of file diff --git a/pom.xml b/pom.xml index d2450e0a65..5f392f7267 100755 --- a/pom.xml +++ b/pom.xml @@ -72,7 +72,7 @@ 1.3.4 4.2.1 2.2.6 - 3.10.0 + 3.11.0 2.0.0-M14 2.9.0 2.3.30 @@ -830,7 +830,6 @@ src/main/scripts/control/** src/main/scripts/windows/** src/main/resources/public/static/rulenode/** - src/main/java/org/eclipse/leshan/server/observation//** **/*.proto.js docker/haproxy/** docker/tb-node/** diff --git a/transport/lwm2m/src/main/java/org/eclipse/leshan/server/observation/ObservationServiceImpl.java b/transport/lwm2m/src/main/java/org/eclipse/leshan/server/observation/ObservationServiceImpl.java deleted file mode 100644 index 89a2f07179..0000000000 --- a/transport/lwm2m/src/main/java/org/eclipse/leshan/server/observation/ObservationServiceImpl.java +++ /dev/null @@ -1,296 +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. - * - * Copyright (c) 2016 Sierra Wireless and others. - * - * All rights reserved. This program and the accompanying materials - * are made available under the terms of the Eclipse Public License v2.0 - * and Eclipse Distribution License v1.0 which accompany this distribution. - * - * The Eclipse Public License is available at - * http://www.eclipse.org/legal/epl-v20.html - * and the Eclipse Distribution License is available at - * http://www.eclipse.org/org/documents/edl-v10.html. - * - * Contributors: - * Sierra Wireless - initial API and implementation - * Michał Wadowski (Orange) - Add Observe-Composite feature. - */ -package org.eclipse.leshan.server.observation; - -import lombok.extern.slf4j.Slf4j; -import org.eclipse.leshan.core.node.LwM2mPath; -import org.eclipse.leshan.core.observation.CompositeObservation; -import org.eclipse.leshan.core.observation.Observation; -import org.eclipse.leshan.core.observation.SingleObservation; -import org.eclipse.leshan.core.peer.LwM2mPeer; -import org.eclipse.leshan.core.response.ObserveCompositeResponse; -import org.eclipse.leshan.core.response.ObserveResponse; -import org.eclipse.leshan.server.endpoint.LwM2mServerEndpoint; -import org.eclipse.leshan.server.endpoint.LwM2mServerEndpointsProvider; -import org.eclipse.leshan.server.profile.ClientProfile; -import org.eclipse.leshan.server.registration.Registration; -import org.eclipse.leshan.server.registration.RegistrationStore; -import org.eclipse.leshan.server.registration.RegistrationUpdate; -import org.eclipse.leshan.server.registration.UpdatedRegistration; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.HashSet; -import java.util.List; -import java.util.Set; -import java.util.concurrent.CopyOnWriteArrayList; - -/** - * Implementation of the {@link ObservationService} accessing the persisted observation via the provided - * {@link RegistrationStore}. - * - * When a new observation is added or changed or canceled, the registered listeners are notified. - */ -@Slf4j -public class ObservationServiceImpl implements ObservationService, LwM2mNotificationReceiver { - - private final Logger LOG = LoggerFactory.getLogger(ObservationServiceImpl.class); - - private final RegistrationStore registrationStore; - private final LwM2mServerEndpointsProvider endpointProvider; - private final boolean updateRegistrationOnNotification; - - private final List listeners = new CopyOnWriteArrayList<>();; - - /** - * Creates an instance of {@link ObservationServiceImpl} - */ - public ObservationServiceImpl(RegistrationStore store, LwM2mServerEndpointsProvider endpointProvider) { - this(store, endpointProvider, false); - } - - /** - * Creates an instance of {@link ObservationServiceImpl} - * - * @param updateRegistrationOnNotification will activate registration update on observe notification. - * - * @since 1.1 - */ - public ObservationServiceImpl(RegistrationStore store, LwM2mServerEndpointsProvider endpointProvider, - boolean updateRegistrationOnNotification) { - this.registrationStore = store; - this.updateRegistrationOnNotification = updateRegistrationOnNotification; - this.endpointProvider = endpointProvider; - } - - @Override - public int cancelObservations(Registration registration) { - // check registration id - String registrationId = registration.getId(); - if (registrationId == null) - return 0; - - Collection observations = registrationStore.removeObservations(registrationId); - if (observations == null) - return 0; - - for (Observation observation : observations) { - cancel(observation); - } - - return observations.size(); - } - - @Override - public int cancelObservations(Registration registration, String nodePath) { - if (registration == null || registration.getId() == null || nodePath == null || nodePath.isEmpty()) - return 0; - - Set observations = getObservationsForCancel(registration.getId(), nodePath); - for (Observation observation : observations) { - cancelObservation(observation); - } - return observations.size(); - } - - @Override - public int cancelCompositeObservations(Registration registration, String[] nodePaths) { - if (registration == null || registration.getId() == null || nodePaths == null || nodePaths.length == 0) - return 0; - - Set observations = getCompositeObservationsForCancel(registration.getId(), nodePaths); - for (Observation observation : observations) { - cancelObservation(observation); - } - return observations.size(); - } - - @Override - public void cancelObservation(Observation observation) { - if (observation == null) - return; - - registrationStore.removeObservation(observation.getRegistrationId(), observation.getId()); - cancel(observation); - } - - private void cancel(Observation observation) { - List endpoints = endpointProvider.getEndpoints(); - for (LwM2mServerEndpoint lwM2mEndpoint : endpoints) { - lwM2mEndpoint.cancelObservation(observation); - } - - for (ObservationListener listener : listeners) { - listener.cancelled(observation); - } - } - - @Override - public Set getObservations(Registration registration) { - return getObservations(registration.getId()); - } - - private Set getObservations(String registrationId) { - if (registrationId == null) - return Collections.emptySet(); - - return new HashSet<>(registrationStore.getObservations(registrationId)); - } - - private Set getCompositeObservationsForCancel(String registrationId, String[] nodePaths) { - if (registrationId == null || nodePaths == null) - return Collections.emptySet(); - - // array of String to array of LWM2M path - List lwPaths = new ArrayList<>(nodePaths.length); - for (int i = 0; i < nodePaths.length; i++) { - lwPaths.add(new LwM2mPath(nodePaths[i])); - } - - // search composite-observation - Set result = new HashSet<>(); - for (Observation obs : getObservations(registrationId)) { - if (obs instanceof CompositeObservation) { - if (lwPaths.equals(((CompositeObservation) obs).getPaths())) { - result.add(obs); - } - } - } - return result; - } - - private Set getObservationsForCancel(String registrationId, String nodePath) { - if (registrationId == null || nodePath == null) - return Collections.emptySet(); - - Set result = new HashSet<>(); - LwM2mPath lwPath = new LwM2mPath(nodePath); - for (Observation obs : getObservations(registrationId)) { - if (obs instanceof SingleObservation) { - LwM2mPath lwPathObs = ((SingleObservation) obs).getPath(); - if (lwPath.equals(lwPathObs) || lwPathObs.startWith(lwPath)) { // nodePath = "3", lwPathObs = "3/0/9": cancel for tne all lwPathObs - result.add(obs); - } else if (!lwPath.equals(lwPathObs) && lwPath.startWith(lwPathObs)) { // nodePath = "3/0/9", lwPathObs = "3": error... - String errorMsg = String.format( - "Unexpected error : There is registration with id [%s] existing observation [%s] includes input observation [%s]!", - registrationId, lwPathObs, lwPath); - throw new IllegalStateException(errorMsg); - } - } - } - - return result; - } - - @Override - public void addListener(ObservationListener listener) { - listeners.add(listener); - } - - @Override - public void removeListener(ObservationListener listener) { - listeners.remove(listener); - } - - private Registration updateRegistrationOnRegistration(Observation observation, LwM2mPeer sender, - ClientProfile profile) { - if (updateRegistrationOnNotification) { - RegistrationUpdate regUpdate = new RegistrationUpdate(observation.getRegistrationId(), sender, null, null, - null, null, null, null, null, null, null, null); - UpdatedRegistration updatedRegistration = registrationStore.updateRegistration(regUpdate); - if (updatedRegistration == null || updatedRegistration.getUpdatedRegistration() == null) { - String errorMsg = String.format( - "Unexpected error: There is no registration with id %s for this observation %s", - observation.getRegistrationId(), observation); - LOG.error(errorMsg); - throw new IllegalStateException(errorMsg); - } - return updatedRegistration.getUpdatedRegistration(); - } - return profile.getRegistration(); - } - - // ********** NotificationListener interface **********// - @Override - public void onNotification(SingleObservation observation, LwM2mPeer sender, ClientProfile profile, - ObserveResponse response) { - try { - Registration updatedRegistration = updateRegistrationOnRegistration(observation, sender, profile); - for (ObservationListener listener : listeners) { - listener.onResponse(observation, updatedRegistration, response); - } - } catch (Exception e) { - for (ObservationListener listener : listeners) { - listener.onError(observation, profile.getRegistration(), e); - } - } - } - - @Override - public void onNotification(CompositeObservation observation, LwM2mPeer sender, ClientProfile profile, - ObserveCompositeResponse response) { - try { - Registration updatedRegistration = updateRegistrationOnRegistration(observation, sender, profile); - for (ObservationListener listener : listeners) { - listener.onResponse(observation, updatedRegistration, response); - } - } catch (Exception e) { - for (ObservationListener listener : listeners) { - listener.onError(observation, profile.getRegistration(), e); - } - } - } - - @Override - public void onError(Observation observation, LwM2mPeer sender, ClientProfile profile, Exception error) { - for (ObservationListener listener : listeners) { - listener.onError(observation, profile.getRegistration(), error); - } - } - - @Override - public void newObservation(Observation observation, Registration registration) { - for (ObservationListener listener : listeners) { - listener.newObservation(observation, registration); - } - } - - @Override - public void cancelled(Observation observation) { - for (ObservationListener listener : listeners) { - listener.cancelled(observation); - } - - } -} From 841729bca3b1e1a12acba60ed99f093f26c7d2d0 Mon Sep 17 00:00:00 2001 From: IrynaMatveieva Date: Fri, 29 Mar 2024 10:51:20 +0200 Subject: [PATCH 18/80] removed unnecessary object creation --- .../dao/rule/BaseRuleChainServiceTest.java | 50 ++++++++----------- 1 file changed, 22 insertions(+), 28 deletions(-) diff --git a/dao/src/test/java/org/thingsboard/server/dao/rule/BaseRuleChainServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/rule/BaseRuleChainServiceTest.java index ba0fb55a6d..b1dad8e4ed 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/rule/BaseRuleChainServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/rule/BaseRuleChainServiceTest.java @@ -35,14 +35,14 @@ public class BaseRuleChainServiceTest extends AbstractServiceTest { @Test public void givenRuleChain_whenSave_thenReturnsSavedRuleChain() { - RuleChain newRuleChain = getRuleChain(this.ruleChain); - newRuleChain.setTenantId(tenantId); - RuleChain savedRuleChain = ruleChainService.saveRuleChain(newRuleChain); + RuleChain ruleChain = getRuleChain(); + ruleChain.setTenantId(tenantId); + RuleChain savedRuleChain = ruleChainService.saveRuleChain(ruleChain); Assertions.assertThat(savedRuleChain).isNotNull(); Assertions.assertThat(savedRuleChain.getId()).isNotNull(); Assertions.assertThat(savedRuleChain.getCreatedTime() > 0).isTrue(); - Assertions.assertThat(newRuleChain.getTenantId()).isEqualTo(savedRuleChain.getTenantId()); + Assertions.assertThat(ruleChain.getTenantId()).isEqualTo(savedRuleChain.getTenantId()); RuleChain foundRuleChain = ruleChainService.findRuleChainById(tenantId, savedRuleChain.getId()); Assertions.assertThat(savedRuleChain.getName()).isEqualTo(foundRuleChain.getName()); @@ -54,37 +54,31 @@ public class BaseRuleChainServiceTest extends AbstractServiceTest { public void givenRuleChainWithExistingExternalId_whenSave_thenThrowsException() { RuleChainId externalRuleChainId = new RuleChainId(UUID.fromString("2675d180-e1e5-11ee-9f06-71b6c7dc2cbf")); - RuleChain newRuleChain = getRuleChain(ruleChain); - newRuleChain.setTenantId(tenantId); - newRuleChain.setExternalId(externalRuleChainId); - RuleChain savedRuleChain = ruleChainService.saveRuleChain(newRuleChain); + RuleChain ruleChain = getRuleChain(); + ruleChain.setTenantId(tenantId); + ruleChain.setExternalId(externalRuleChainId); + ruleChainService.saveRuleChain(ruleChain); - RuleChain ruleChainForSave = getRuleChain(ruleChain); - ruleChainForSave.setTenantId(tenantId); - ruleChainForSave.setExternalId(externalRuleChainId); - - Assertions.assertThat(savedRuleChain.getExternalId()).isEqualTo(ruleChainForSave.getExternalId()); - Assertions.assertThatExceptionOfType(DataValidationException.class).isThrownBy(() -> ruleChainService.saveRuleChain(ruleChainForSave)); - Assertions.assertThatThrownBy(() -> ruleChainService.saveRuleChain(ruleChainForSave)) + Assertions.assertThatThrownBy(() -> ruleChainService.saveRuleChain(ruleChain)) .isInstanceOf(DataValidationException.class) .hasMessage("Rule Chain with such external id already exists!"); ruleChainService.deleteRuleChainsByTenantId(tenantId); } - private RuleChain getRuleChain(String ruleChainString) { - return JacksonUtil.fromString(ruleChainString, RuleChain.class); + private RuleChain getRuleChain() { + String ruleChainStr = "{\n" + + " \"name\": \"Root Rule Chain\",\n" + + " \"type\": \"CORE\",\n" + + " \"firstRuleNodeId\": {\n" + + " \"entityType\": \"RULE_NODE\",\n" + + " \"id\": \"91ad0b00-e779-11ee-9cf0-15d8b6079fdb\"\n" + + " },\n" + + " \"debugMode\": false,\n" + + " \"configuration\": null,\n" + + " \"additionalInfo\": null\n" + + "}"; + return JacksonUtil.fromString(ruleChainStr, RuleChain.class); } - private final String ruleChain = "{\n" + - " \"name\": \"Root Rule Chain\",\n" + - " \"type\": \"CORE\",\n" + - " \"firstRuleNodeId\": {\n" + - " \"entityType\": \"RULE_NODE\",\n" + - " \"id\": \"91ad0b00-e779-11ee-9cf0-15d8b6079fdb\"\n" + - " },\n" + - " \"debugMode\": false,\n" + - " \"configuration\": null,\n" + - " \"additionalInfo\": null\n" + - "}"; } From 85d229be2d15e6bff0c4249f8f6ccd32a88c0356 Mon Sep 17 00:00:00 2001 From: ShvaykaD Date: Fri, 29 Mar 2024 12:05:44 +0200 Subject: [PATCH 19/80] convert ValueWithTs to a record & replace usages of Collections.singletonList to List.of & use single key search for async method --- .../dao/timeseries/TimeseriesService.java | 2 +- .../engine/metadata/CalculateDeltaNode.java | 34 ++++++------------- .../metadata/CalculateDeltaNodeTest.java | 11 +++--- 3 files changed, 17 insertions(+), 30 deletions(-) diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/timeseries/TimeseriesService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/timeseries/TimeseriesService.java index 096a39bff6..c62898ab09 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/timeseries/TimeseriesService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/timeseries/TimeseriesService.java @@ -38,7 +38,7 @@ public interface TimeseriesService { ListenableFuture> findAll(TenantId tenantId, EntityId entityId, List queries); - ListenableFuture> findLatest(TenantId tenantId, EntityId entityId, String keys); + ListenableFuture> findLatest(TenantId tenantId, EntityId entityId, String key); ListenableFuture> findLatest(TenantId tenantId, EntityId entityId, Collection keys); diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/CalculateDeltaNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/CalculateDeltaNode.java index 609888db84..88540ffe4a 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/CalculateDeltaNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/CalculateDeltaNode.java @@ -37,7 +37,6 @@ import org.thingsboard.server.dao.timeseries.TimeseriesService; import java.math.BigDecimal; import java.math.RoundingMode; -import java.util.Collections; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; @@ -129,8 +128,8 @@ public class CalculateDeltaNode implements TbNode { } private ListenableFuture fetchLatestValueAsync(EntityId entityId) { - return Futures.transform(timeseriesService.findLatest(ctx.getTenantId(), entityId, Collections.singletonList(config.getInputValueKey())), - list -> extractValue(list.get(0)) + return Futures.transform(timeseriesService.findLatest(ctx.getTenantId(), entityId, config.getInputValueKey()), + tsKvEntryOpt -> tsKvEntryOpt.map(this::extractValue).orElse(null) , ctx.getDbCallbackExecutor()); } @@ -138,7 +137,7 @@ public class CalculateDeltaNode implements TbNode { List tsKvEntries = timeseriesService.findLatestSync( ctx.getTenantId(), entityId, - Collections.singletonList(config.getInputValueKey())); + List.of(config.getInputValueKey())); return extractValue(tsKvEntries.get(0)); } @@ -161,36 +160,23 @@ public class CalculateDeltaNode implements TbNode { double result = 0.0; long ts = kvEntry.getTs(); switch (kvEntry.getDataType()) { - case LONG: - result = kvEntry.getLongValue().get(); - break; - case DOUBLE: - result = kvEntry.getDoubleValue().get(); - break; - case STRING: + case LONG -> result = kvEntry.getLongValue().get(); + case DOUBLE -> result = kvEntry.getDoubleValue().get(); + case STRING -> { try { result = Double.parseDouble(kvEntry.getStrValue().get()); } catch (NumberFormatException e) { throw new IllegalArgumentException("Calculation failed. Unable to parse value [" + kvEntry.getStrValue().get() + "]" + " of telemetry [" + kvEntry.getKey() + "] to Double"); } - break; - case BOOLEAN: - throw new IllegalArgumentException("Calculation failed. Boolean values are not supported!"); - case JSON: - throw new IllegalArgumentException("Calculation failed. JSON values are not supported!"); + } + case BOOLEAN -> throw new IllegalArgumentException("Calculation failed. Boolean values are not supported!"); + case JSON -> throw new IllegalArgumentException("Calculation failed. JSON values are not supported!"); } return new ValueWithTs(ts, result); } - private static class ValueWithTs { - private final long ts; - private final double value; - - private ValueWithTs(long ts, double value) { - this.ts = ts; - this.value = value; - } + private record ValueWithTs(long ts, double value) { } } diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/CalculateDeltaNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/CalculateDeltaNodeTest.java index 707de1741f..188dd2e4ec 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/CalculateDeltaNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/CalculateDeltaNodeTest.java @@ -47,6 +47,7 @@ import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.dao.timeseries.TimeseriesService; import java.util.List; +import java.util.Optional; import java.util.UUID; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -68,9 +69,9 @@ import static org.mockito.Mockito.when; @ExtendWith(MockitoExtension.class) public class CalculateDeltaNodeTest { - private static final DeviceId DUMMY_DEVICE_ORIGINATOR = new DeviceId(UUID.randomUUID()); - private static final TenantId TENANT_ID = new TenantId(UUID.randomUUID()); - private static final ListeningExecutor DB_EXECUTOR = new TestDbCallbackExecutor(); + private final DeviceId DUMMY_DEVICE_ORIGINATOR = new DeviceId(UUID.fromString("2ba3ded4-882b-40cf-999a-89da9ccd58f9")); + private final TenantId TENANT_ID = new TenantId(UUID.fromString("3842e740-0d89-43a9-8d52-ae44023847ba")); + private final ListeningExecutor DB_EXECUTOR = new TestDbCallbackExecutor(); @Mock private TbContext ctxMock; @Mock @@ -435,8 +436,8 @@ public class CalculateDeltaNodeTest { when(ctxMock.getDbCallbackExecutor()).thenReturn(DB_EXECUTOR); when(ctxMock.getTenantId()).thenReturn(TENANT_ID); when(timeseriesServiceMock.findLatest( - eq(TENANT_ID), eq(DUMMY_DEVICE_ORIGINATOR), argThat(new ListMatcher<>(List.of(tsKvEntry.getKey()))) - )).thenReturn(Futures.immediateFuture(List.of(tsKvEntry))); + eq(TENANT_ID), eq(DUMMY_DEVICE_ORIGINATOR), eq(tsKvEntry.getKey()) + )).thenReturn(Futures.immediateFuture(Optional.of(tsKvEntry))); } @RequiredArgsConstructor From 9d1f751f3a8ec2fb331d2e27a5839ee0a4e4a42e Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Fri, 29 Mar 2024 15:20:56 +0200 Subject: [PATCH 20/80] Fix race condition on tenant creation; refactoring --- .../entitiy/EntityStateSourcingListener.java | 89 ++++++++----------- .../tenant/DefaultTbTenantService.java | 18 ++-- .../service/install/InstallScripts.java | 16 ++-- .../controller/TenantControllerTest.java | 16 ++-- .../server/dao/tenant/TenantService.java | 3 +- .../DefaultNotificationSettingsService.java | 3 +- .../server/dao/tenant/TenantServiceImpl.java | 31 +++---- 7 files changed, 80 insertions(+), 96 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/EntityStateSourcingListener.java b/application/src/main/java/org/thingsboard/server/service/entitiy/EntityStateSourcingListener.java index c699cd6c02..bc7dc62e78 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/EntityStateSourcingListener.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/EntityStateSourcingListener.java @@ -69,80 +69,71 @@ public class EntityStateSourcingListener { @TransactionalEventListener(fallbackExecution = true) public void handleEvent(SaveEntityEvent event) { - log.trace("[{}] SaveEntityEvent called: {}", event.getTenantId(), event); TenantId tenantId = event.getTenantId(); EntityId entityId = event.getEntityId(); EntityType entityType = entityId.getEntityType(); + log.debug("[{}][{}][{}] Handling entity save event: {}", tenantId, entityType, entityId, event); boolean isCreated = event.getCreated() != null && event.getCreated(); ComponentLifecycleEvent lifecycleEvent = isCreated ? ComponentLifecycleEvent.CREATED : ComponentLifecycleEvent.UPDATED; switch (entityType) { - case ASSET: - case ASSET_PROFILE: - case ENTITY_VIEW: - case NOTIFICATION_RULE: + case ASSET, ASSET_PROFILE, ENTITY_VIEW, NOTIFICATION_RULE -> { tbClusterService.broadcastEntityStateChangeEvent(tenantId, entityId, lifecycleEvent); - break; - case RULE_CHAIN: + } + case RULE_CHAIN -> { RuleChain ruleChain = (RuleChain) event.getEntity(); if (RuleChainType.CORE.equals(ruleChain.getType())) { tbClusterService.broadcastEntityStateChangeEvent(ruleChain.getTenantId(), ruleChain.getId(), lifecycleEvent); } - break; - case TENANT: + } + case TENANT -> { Tenant tenant = (Tenant) event.getEntity(); onTenantUpdate(tenant, lifecycleEvent); - break; - case TENANT_PROFILE: + } + case TENANT_PROFILE -> { TenantProfile tenantProfile = (TenantProfile) event.getEntity(); onTenantProfileUpdate(tenantProfile, lifecycleEvent); - break; - case DEVICE: + } + case DEVICE -> { onDeviceUpdate(event.getEntity(), event.getOldEntity()); - break; - case DEVICE_PROFILE: + } + case DEVICE_PROFILE -> { DeviceProfile deviceProfile = (DeviceProfile) event.getEntity(); onDeviceProfileUpdate(deviceProfile, event.getOldEntity(), isCreated); - break; - case EDGE: + } + case EDGE -> { handleEdgeEvent(tenantId, entityId, event.getEntity(), lifecycleEvent); - break; - case TB_RESOURCE: + } + case TB_RESOURCE -> { TbResource tbResource = (TbResource) event.getEntity(); tbClusterService.onResourceChange(tbResource, null); - break; - case API_USAGE_STATE: + } + case API_USAGE_STATE -> { ApiUsageState apiUsageState = (ApiUsageState) event.getEntity(); tbClusterService.onApiStateChange(apiUsageState, null); - break; - default: - break; + } + default -> {} } } @TransactionalEventListener(fallbackExecution = true) public void handleEvent(DeleteEntityEvent event) { - log.trace("[{}] DeleteEntityEvent called: {}", event.getTenantId(), event); TenantId tenantId = event.getTenantId(); EntityId entityId = event.getEntityId(); EntityType entityType = entityId.getEntityType(); + log.debug("[{}][{}][{}] Handling entity deletion event: {}", tenantId, entityType, entityId, event); switch (entityType) { - case ASSET: - case ASSET_PROFILE: - case ENTITY_VIEW: - case CUSTOMER: - case EDGE: - case NOTIFICATION_RULE: + case ASSET, ASSET_PROFILE, ENTITY_VIEW, CUSTOMER, EDGE, NOTIFICATION_RULE -> { tbClusterService.broadcastEntityStateChangeEvent(tenantId, entityId, ComponentLifecycleEvent.DELETED); - break; - case NOTIFICATION_REQUEST: + } + case NOTIFICATION_REQUEST -> { NotificationRequest request = (NotificationRequest) event.getEntity(); if (request.isScheduled()) { tbClusterService.broadcastEntityStateChangeEvent(tenantId, entityId, ComponentLifecycleEvent.DELETED); } - break; - case RULE_CHAIN: + } + case RULE_CHAIN -> { RuleChain ruleChain = (RuleChain) event.getEntity(); if (RuleChainType.CORE.equals(ruleChain.getType())) { Set referencingRuleChainIds = JacksonUtil.fromString(event.getBody(), new TypeReference<>() {}); @@ -152,29 +143,28 @@ public class EntityStateSourcingListener { } tbClusterService.broadcastEntityStateChangeEvent(tenantId, ruleChain.getId(), ComponentLifecycleEvent.DELETED); } - break; - case TENANT: + } + case TENANT -> { Tenant tenant = (Tenant) event.getEntity(); onTenantDeleted(tenant); - break; - case TENANT_PROFILE: + } + case TENANT_PROFILE -> { TenantProfile tenantProfile = (TenantProfile) event.getEntity(); tbClusterService.onTenantProfileDelete(tenantProfile, null); - break; - case DEVICE: + } + case DEVICE -> { Device device = (Device) event.getEntity(); tbClusterService.onDeviceDeleted(tenantId, device, null); - break; - case DEVICE_PROFILE: + } + case DEVICE_PROFILE -> { DeviceProfile deviceProfile = (DeviceProfile) event.getEntity(); onDeviceProfileDelete(event.getTenantId(), event.getEntityId(), deviceProfile); - break; - case TB_RESOURCE: + } + case TB_RESOURCE -> { TbResourceInfo tbResource = (TbResourceInfo) event.getEntity(); tbClusterService.onResourceDeleted(tbResource, null); - break; - default: - break; + } + default -> {} } } @@ -186,8 +176,7 @@ public class EntityStateSourcingListener { && event.getEntity() instanceof DeviceCredentials) { tbClusterService.pushMsgToCore(new DeviceCredentialsUpdateNotificationMsg(event.getTenantId(), (DeviceId) event.getEntityId(), (DeviceCredentials) event.getEntity()), null); - } else if (ActionType.ASSIGNED_TO_TENANT.equals(event.getActionType()) && event.getEntity() instanceof Device) { - Device device = (Device) event.getEntity(); + } else if (ActionType.ASSIGNED_TO_TENANT.equals(event.getActionType()) && event.getEntity() instanceof Device device) { Tenant tenant = JacksonUtil.fromString(event.getBody(), Tenant.class); if (tenant != null) { tbClusterService.onDeviceAssignedToTenant(tenant.getId(), device); diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/tenant/DefaultTbTenantService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/tenant/DefaultTbTenantService.java index 17d52329b2..8096722940 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/tenant/DefaultTbTenantService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/tenant/DefaultTbTenantService.java @@ -16,12 +16,10 @@ package org.thingsboard.server.service.entitiy.tenant; import lombok.RequiredArgsConstructor; -import org.springframework.context.ApplicationEventPublisher; import org.springframework.stereotype.Service; import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.TenantProfile; import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.dao.eventsourcing.SaveEntityEvent; import org.thingsboard.server.dao.tenant.TbTenantProfileCache; import org.thingsboard.server.dao.tenant.TenantProfileService; import org.thingsboard.server.dao.tenant.TenantService; @@ -45,25 +43,19 @@ public class DefaultTbTenantService extends AbstractTbEntityService implements T private final TbQueueService tbQueueService; private final TenantProfileService tenantProfileService; private final EntitiesVersionControlService versionControlService; - private final ApplicationEventPublisher eventPublisher; @Override public Tenant save(Tenant tenant) throws Exception { boolean created = tenant.getId() == null; Tenant oldTenant = !created ? tenantService.findTenantById(tenant.getId()) : null; - Tenant savedTenant = checkNotNull(tenantService.saveTenant(tenant, !created)); - if (created) { - installScripts.createDefaultRuleChains(savedTenant.getId()); - installScripts.createDefaultEdgeRuleChains(savedTenant.getId()); - installScripts.createDefaultTenantDashboards(savedTenant.getId(), null); - } + Tenant savedTenant = tenantService.saveTenant(tenant, tenantId -> { + installScripts.createDefaultRuleChains(tenantId); + installScripts.createDefaultEdgeRuleChains(tenantId); + installScripts.createDefaultTenantDashboards(tenantId, null); + }); tenantProfileCache.evict(savedTenant.getId()); - if (created) { - eventPublisher.publishEvent(SaveEntityEvent.builder().tenantId(TenantId.SYS_TENANT_ID).entityId(savedTenant.getId()).entity(savedTenant).created(true).build()); - } - TenantProfile oldTenantProfile = oldTenant != null ? tenantProfileService.findTenantProfileById(TenantId.SYS_TENANT_ID, oldTenant.getTenantProfileId()) : null; TenantProfile newTenantProfile = tenantProfileService.findTenantProfileById(TenantId.SYS_TENANT_ID, savedTenant.getTenantProfileId()); tbQueueService.updateQueuesByTenants(Collections.singletonList(savedTenant.getTenantId()), newTenantProfile, oldTenantProfile); diff --git a/application/src/main/java/org/thingsboard/server/service/install/InstallScripts.java b/application/src/main/java/org/thingsboard/server/service/install/InstallScripts.java index f5bb67bdce..999079e913 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/InstallScripts.java +++ b/application/src/main/java/org/thingsboard/server/service/install/InstallScripts.java @@ -151,17 +151,18 @@ public class InstallScripts { } } - public void createDefaultRuleChains(TenantId tenantId) throws IOException { + public void createDefaultRuleChains(TenantId tenantId) { Path tenantChainsDir = getTenantRuleChainsDir(); loadRuleChainsFromPath(tenantId, tenantChainsDir); } - public void createDefaultEdgeRuleChains(TenantId tenantId) throws IOException { + public void createDefaultEdgeRuleChains(TenantId tenantId) { Path edgeChainsDir = getEdgeRuleChainsDir(); loadRuleChainsFromPath(tenantId, edgeChainsDir); } - private void loadRuleChainsFromPath(TenantId tenantId, Path ruleChainsPath) throws IOException { + @SneakyThrows + private void loadRuleChainsFromPath(TenantId tenantId, Path ruleChainsPath) { findRuleChainsFromPath(ruleChainsPath).forEach(path -> { try { createRuleChainFromFile(tenantId, path, null); @@ -329,17 +330,18 @@ public class InstallScripts { } } - public void loadDashboards(TenantId tenantId, CustomerId customerId) throws Exception { + public void loadDashboards(TenantId tenantId, CustomerId customerId) { Path dashboardsDir = Paths.get(getDataDir(), JSON_DIR, DEMO_DIR, DASHBOARDS_DIR); loadDashboardsFromDir(tenantId, customerId, dashboardsDir); } - public void createDefaultTenantDashboards(TenantId tenantId, CustomerId customerId) throws Exception { + public void createDefaultTenantDashboards(TenantId tenantId, CustomerId customerId) { Path dashboardsDir = Paths.get(getDataDir(), JSON_DIR, TENANT_DIR, DASHBOARDS_DIR); loadDashboardsFromDir(tenantId, customerId, dashboardsDir); } - private void loadDashboardsFromDir(TenantId tenantId, CustomerId customerId, Path dashboardsDir) throws IOException { + @SneakyThrows + private void loadDashboardsFromDir(TenantId tenantId, CustomerId customerId, Path dashboardsDir) { try (DirectoryStream dirStream = Files.newDirectoryStream(dashboardsDir, path -> path.toString().endsWith(JSON_EXT))) { dirStream.forEach( path -> { @@ -414,7 +416,7 @@ public class InstallScripts { } ); } catch (Exception e) { - log.error("Unable to load resources lwm2m object model from file: [{}]", resourceLwm2mPath.toString()); + log.error("Unable to load resources lwm2m object model from file: [{}]", resourceLwm2mPath); throw new RuntimeException("resource lwm2m object model from file", e); } } diff --git a/application/src/test/java/org/thingsboard/server/controller/TenantControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/TenantControllerTest.java index cd47988658..63747d2c19 100644 --- a/application/src/test/java/org/thingsboard/server/controller/TenantControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/TenantControllerTest.java @@ -663,7 +663,7 @@ public class TenantControllerTest extends AbstractControllerTest { savedDifferentTenant.setTenantProfileId(tenantProfile.getId()); savedDifferentTenant = doPost("/api/tenant", savedDifferentTenant, Tenant.class); TenantId tenantId = differentTenantId; - await().atMost(10, TimeUnit.SECONDS) + await().atMost(30, TimeUnit.SECONDS) .until(() -> { TopicPartitionInfo tpi = partitionService.resolve(ServiceType.TB_RULE_ENGINE, MAIN_QUEUE_NAME, tenantId, tenantId); return !tpi.getTenantId().get().isSysTenantId(); @@ -677,7 +677,7 @@ public class TenantControllerTest extends AbstractControllerTest { tenantProfile.setIsolatedTbRuleEngine(false); tenantProfile.getProfileData().setQueueConfiguration(Collections.emptyList()); tenantProfile = doPost("/api/tenantProfile", tenantProfile, TenantProfile.class); - await().atMost(10, TimeUnit.SECONDS) + await().atMost(30, TimeUnit.SECONDS) .until(() -> partitionService.resolve(ServiceType.TB_RULE_ENGINE, MAIN_QUEUE_NAME, tenantId, tenantId) .getTenantId().get().isSysTenantId()); @@ -689,11 +689,11 @@ public class TenantControllerTest extends AbstractControllerTest { submittedMsgs.add(tbMsg.getId()); Thread.sleep(timeLeft / msgs); } - await().atMost(15, TimeUnit.SECONDS).untilAsserted(() -> { + await().atMost(30, TimeUnit.SECONDS).untilAsserted(() -> { verify(queueAdmin, times(1)).deleteTopic(eq(isolatedTopic)); }); - await().atMost(5, TimeUnit.SECONDS).untilAsserted(() -> { + await().atMost(10, TimeUnit.SECONDS).untilAsserted(() -> { for (UUID msgId : submittedMsgs) { verify(actorContext).tell(argThat(msg -> { return msg instanceof QueueToRuleEngineMsg && ((QueueToRuleEngineMsg) msg).getMsg().getId().equals(msgId); @@ -718,13 +718,13 @@ public class TenantControllerTest extends AbstractControllerTest { savedDifferentTenant.setTenantProfileId(tenantProfile.getId()); savedDifferentTenant = doPost("/api/tenant", savedDifferentTenant, Tenant.class); TenantId tenantId = differentTenantId; - await().atMost(10, TimeUnit.SECONDS).untilAsserted(() -> { + await().atMost(30, TimeUnit.SECONDS).untilAsserted(() -> { assertThat(partitionService.getMyPartitions(new QueueKey(ServiceType.TB_RULE_ENGINE, tenantId))).isNotNull(); }); TopicPartitionInfo tpi = partitionService.resolve(ServiceType.TB_RULE_ENGINE, tenantId, tenantId); assertThat(tpi.getTenantId()).hasValue(tenantId); TbMsg tbMsg = publishTbMsg(tenantId, tpi); - await().atMost(10, TimeUnit.SECONDS).untilAsserted(() -> { + await().atMost(30, TimeUnit.SECONDS).untilAsserted(() -> { verify(actorContext).tell(argThat(msg -> { return msg instanceof QueueToRuleEngineMsg && ((QueueToRuleEngineMsg) msg).getMsg().getId().equals(tbMsg.getId()); })); @@ -732,7 +732,7 @@ public class TenantControllerTest extends AbstractControllerTest { deleteDifferentTenant(); - await().atMost(10, TimeUnit.SECONDS).untilAsserted(() -> { + await().atMost(30, TimeUnit.SECONDS).untilAsserted(() -> { assertThat(partitionService.getMyPartitions(new QueueKey(ServiceType.TB_RULE_ENGINE, tenantId))).isNull(); assertThatThrownBy(() -> partitionService.resolve(ServiceType.TB_RULE_ENGINE, tenantId, tenantId)) .isInstanceOf(TenantNotFoundException.class); @@ -752,7 +752,7 @@ public class TenantControllerTest extends AbstractControllerTest { } private void verifyUsedQueueAndMessage(String queue, TenantId tenantId, EntityId entityId, String msgType, Runnable action, Consumer tpiAssert) { - await().atMost(15, TimeUnit.SECONDS) + await().atMost(30, TimeUnit.SECONDS) .untilAsserted(() -> { TopicPartitionInfo tpi = partitionService.resolve(ServiceType.TB_RULE_ENGINE, queue, tenantId, entityId); tpiAssert.accept(tpi); diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/tenant/TenantService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/tenant/TenantService.java index 11a71eaff5..bef82f921f 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/tenant/TenantService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/tenant/TenantService.java @@ -25,6 +25,7 @@ import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.dao.entity.EntityDaoService; import java.util.List; +import java.util.function.Consumer; public interface TenantService extends EntityDaoService { @@ -36,7 +37,7 @@ public interface TenantService extends EntityDaoService { Tenant saveTenant(Tenant tenant); - Tenant saveTenant(Tenant tenant, boolean publishSaveEvent); + Tenant saveTenant(Tenant tenant, Consumer defaultEntitiesCreator); boolean tenantExists(TenantId tenantId); diff --git a/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationSettingsService.java b/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationSettingsService.java index b719de0192..5b6f464ea6 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationSettingsService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationSettingsService.java @@ -20,7 +20,6 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.cache.annotation.CacheEvict; import org.springframework.cache.annotation.Cacheable; import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.AdminSettings; @@ -157,7 +156,7 @@ public class DefaultNotificationSettingsService implements NotificationSettingsS return new UserNotificationSettings(prefs); } - @Transactional(propagation = Propagation.NOT_SUPPORTED) // so that parent transaction is not aborted on method failure + @Transactional @Override public void createDefaultNotificationConfigs(TenantId tenantId) { NotificationTarget allUsers = createTarget(tenantId, "All users", new AllUsersFilter(), diff --git a/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java index 72db458d66..b2e5125187 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java @@ -63,6 +63,7 @@ import org.thingsboard.server.dao.widget.WidgetsBundleService; import java.util.List; import java.util.Optional; +import java.util.function.Consumer; import static org.thingsboard.server.dao.service.Validator.validateId; @@ -187,12 +188,12 @@ public class TenantServiceImpl extends AbstractCachedEntityService defaultEntitiesCreator) { log.trace("Executing saveTenant [{}]", tenant); tenant.setRegion(DEFAULT_TENANT_REGION); if (tenant.getTenantProfileId() == null) { @@ -201,20 +202,20 @@ public class TenantServiceImpl extends AbstractCachedEntityService Date: Mon, 1 Apr 2024 12:36:49 +0300 Subject: [PATCH 21/80] moved SemaphoreWithTbMsgQueue to separate file & updated logic in CalculateDeltaNode --- .../rule/engine/math/TbMathNode.java | 91 +----------- .../engine/metadata/CalculateDeltaNode.java | 119 +++++++-------- .../engine/util/SemaphoreWithTbMsgQueue.java | 140 ++++++++++++++++++ .../rule/engine/math/TbMathNodeTest.java | 14 +- .../metadata/CalculateDeltaNodeTest.java | 110 +++++++------- 5 files changed, 266 insertions(+), 208 deletions(-) create mode 100644 rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/SemaphoreWithTbMsgQueue.java diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/math/TbMathNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/math/TbMathNode.java index 3813195e23..823c224833 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/math/TbMathNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/math/TbMathNode.java @@ -19,13 +19,10 @@ import com.fasterxml.jackson.databind.node.ObjectNode; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.MoreExecutors; -import lombok.Data; -import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import net.objecthunter.exp4j.Expression; import net.objecthunter.exp4j.ExpressionBuilder; import org.springframework.util.ConcurrentReferenceHashMap; -import org.thingsboard.common.util.DonAsynchron; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.rule.engine.api.RuleNode; import org.thingsboard.rule.engine.api.TbContext; @@ -33,6 +30,7 @@ import org.thingsboard.rule.engine.api.TbNode; import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeException; import org.thingsboard.rule.engine.api.util.TbNodeUtils; +import org.thingsboard.rule.engine.util.SemaphoreWithTbMsgQueue; import org.thingsboard.server.common.data.AttributeScope; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.id.EntityId; @@ -46,10 +44,7 @@ import java.math.BigDecimal; import java.math.RoundingMode; import java.util.List; import java.util.Optional; -import java.util.Queue; -import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.ConcurrentMap; -import java.util.concurrent.Semaphore; import java.util.function.BiFunction; import java.util.function.Function; import java.util.stream.Collectors; @@ -84,7 +79,7 @@ import static org.thingsboard.rule.engine.math.TbMathArgumentType.CONSTANT; ) public class TbMathNode implements TbNode { - private static final ConcurrentMap> locks = new ConcurrentReferenceHashMap<>(16, ConcurrentReferenceHashMap.ReferenceType.WEAK); + private static final ConcurrentMap locks = new ConcurrentReferenceHashMap<>(16, ConcurrentReferenceHashMap.ReferenceType.WEAK); private final ThreadLocal customExpression = new ThreadLocal<>(); private TbMathNodeConfiguration config; private boolean msgBodyToJsonConversionRequired; @@ -110,66 +105,8 @@ public class TbMathNode implements TbNode { @Override public void onMsg(TbContext ctx, TbMsg msg) { - var semaphoreWithQueue = locks.computeIfAbsent(msg.getOriginator(), SemaphoreWithQueue::new); - semaphoreWithQueue.getQueue().add(new TbMsgTbContextBiFunction(msg, ctx, this::processMsgAsync)); - - tryProcessQueue(semaphoreWithQueue); - } - - void tryProcessQueue(SemaphoreWithQueue lockAndQueue) { - final Semaphore semaphore = lockAndQueue.getSemaphore(); - final Queue queue = lockAndQueue.getQueue(); - while (!queue.isEmpty()) { - // The semaphore have to be acquired before EACH poll and released before NEXT poll. - // Otherwise, some message will remain unprocessed in queue - if (!semaphore.tryAcquire()) { - return; - } - TbMsgTbContextBiFunction tbMsgTbContext = null; - try { - tbMsgTbContext = queue.poll(); - if (tbMsgTbContext == null) { - semaphore.release(); - continue; - } - final TbMsg msg = tbMsgTbContext.getMsg(); - if (!msg.getCallback().isMsgValid()) { - log.trace("[{}] Skipping non-valid message [{}]", lockAndQueue.getEntityId(), msg); - semaphore.release(); - continue; - } - //DO PROCESSING - final TbContext ctx = tbMsgTbContext.getCtx(); - final ListenableFuture resultMsgFuture = tbMsgTbContext.getBiFunction().apply(ctx, msg); - DonAsynchron.withCallback(resultMsgFuture, resultMsg -> { - try { - ctx.tellSuccess(resultMsg); - } finally { - lockAndQueue.getSemaphore().release(); - tryProcessQueue(lockAndQueue); - } - }, t -> { - try { - ctx.tellFailure(msg, t); - } finally { - lockAndQueue.getSemaphore().release(); - tryProcessQueue(lockAndQueue); - } - }, ctx.getDbCallbackExecutor()); - } catch (Throwable t) { - semaphore.release(); - if (tbMsgTbContext == null) { // if no message polled, the loop become infinite, will throw exception - log.error("[{}] Failed to process TbMsgTbContext queue", lockAndQueue.getEntityId(), t); - throw t; - } - TbMsg msg = tbMsgTbContext.getMsg(); - TbContext ctx = tbMsgTbContext.getCtx(); - log.warn("[{}] Failed to process message: {}", lockAndQueue.getEntityId(), msg, t); - ctx.tellFailure(msg, t); // you are not allowed to throw here, because queue will remain unprocessed - continue; // We are probably the last who process the queue. We have to continue poll until get successful callback or queue is empty - } - break; //submitted async exact one task. next poll will try on callback - } + var semaphoreWithQueue = locks.computeIfAbsent(msg.getOriginator(), SemaphoreWithTbMsgQueue::new); + semaphoreWithQueue.addToQueueAndTryProcess(msg, ctx, this::processMsgAsync); } ListenableFuture processMsgAsync(TbContext ctx, TbMsg msg) { @@ -426,24 +363,4 @@ public class TbMathNode implements TbNode { } } - @Override - public void destroy() { - } - - @Data - @RequiredArgsConstructor - static public class SemaphoreWithQueue { - final EntityId entityId; - final Semaphore semaphore = new Semaphore(1); - final Queue queue = new ConcurrentLinkedQueue<>(); - } - - @Data - @RequiredArgsConstructor - static public class TbMsgTbContextBiFunction { - final TbMsg msg; - final TbContext ctx; - final BiFunction> biFunction; - } - } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/CalculateDeltaNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/CalculateDeltaNode.java index 88540ffe4a..f459a532ed 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/CalculateDeltaNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/CalculateDeltaNode.java @@ -19,7 +19,9 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ObjectNode; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.MoreExecutors; import lombok.extern.slf4j.Slf4j; +import org.springframework.util.ConcurrentReferenceHashMap; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.rule.engine.api.RuleNode; import org.thingsboard.rule.engine.api.TbContext; @@ -27,6 +29,7 @@ import org.thingsboard.rule.engine.api.TbNode; import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeException; import org.thingsboard.rule.engine.api.util.TbNodeUtils; +import org.thingsboard.rule.engine.util.SemaphoreWithTbMsgQueue; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.kv.TsKvEntry; import org.thingsboard.server.common.data.msg.TbMsgType; @@ -37,15 +40,14 @@ import org.thingsboard.server.dao.timeseries.TimeseriesService; import java.math.BigDecimal; import java.math.RoundingMode; -import java.util.List; import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; import static org.thingsboard.common.util.DonAsynchron.withCallback; @Slf4j @RuleNode(type = ComponentType.ENRICHMENT, - name = "calculate delta", relationTypes = {TbNodeConnectionType.SUCCESS, TbNodeConnectionType.FAILURE, TbNodeConnectionType.OTHER}, + name = "calculate delta", + relationTypes = {TbNodeConnectionType.SUCCESS, TbNodeConnectionType.FAILURE, TbNodeConnectionType.OTHER}, configClazz = CalculateDeltaNodeConfiguration.class, nodeDescription = "Calculates delta and amount of time passed between previous timeseries key reading " + "and current value for this key from the incoming message", @@ -56,19 +58,24 @@ import static org.thingsboard.common.util.DonAsynchron.withCallback; public class CalculateDeltaNode implements TbNode { private Map cache; + private Map locks; + private CalculateDeltaNodeConfiguration config; private TbContext ctx; private TimeseriesService timeseriesService; private boolean useCache; + private String inputKey; @Override public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException { this.config = TbNodeUtils.convert(configuration, CalculateDeltaNodeConfiguration.class); this.ctx = ctx; this.timeseriesService = ctx.getTimeseriesService(); + this.inputKey = config.getInputValueKey(); this.useCache = config.isUseCache(); if (useCache) { - cache = new ConcurrentHashMap<>(); + locks = new ConcurrentReferenceHashMap<>(16, ConcurrentReferenceHashMap.ReferenceType.WEAK); + cache = new ConcurrentReferenceHashMap<>(16, ConcurrentReferenceHashMap.ReferenceType.SOFT); } } @@ -79,78 +86,34 @@ public class CalculateDeltaNode implements TbNode { return; } JsonNode json = JacksonUtil.toJsonNode(msg.getData()); - String inputKey = config.getInputValueKey(); if (!json.has(inputKey)) { ctx.tellNext(msg, TbNodeConnectionType.OTHER); return; } - withCallback(getLastValue(msg.getOriginator()), + if (useCache) { + var semaphoreWithQueue = locks.computeIfAbsent(msg.getOriginator(), SemaphoreWithTbMsgQueue::new); + semaphoreWithQueue.addToQueueAndTryProcess(msg, ctx, this::processMsgAsync); + return; + } + withCallback(fetchLatestValueAsync(msg.getOriginator()), previousData -> { - double currentValue = json.get(inputKey).asDouble(); - long currentTs = msg.getMetaDataTs(); - - if (useCache) { - cache.put(msg.getOriginator(), new ValueWithTs(currentTs, currentValue)); - } - - BigDecimal delta = BigDecimal.valueOf(previousData != null ? currentValue - previousData.value : 0.0); - - if (config.isTellFailureIfDeltaIsNegative() && delta.doubleValue() < 0) { - ctx.tellFailure(msg, new IllegalArgumentException("Delta value is negative!")); - return; - } - - if (config.getRound() != null) { - delta = delta.setScale(config.getRound(), RoundingMode.HALF_UP); - } - - ObjectNode result = (ObjectNode) json; - if (delta.stripTrailingZeros().scale() > 0) { - result.put(config.getOutputValueKey(), delta.doubleValue()); - } else { - result.put(config.getOutputValueKey(), delta.longValueExact()); - } - - if (config.isAddPeriodBetweenMsgs()) { - long period = previousData != null ? currentTs - previousData.ts : 0; - result.put(config.getPeriodValueKey(), period); - } - ctx.tellSuccess(TbMsg.transformMsgData(msg, JacksonUtil.toString(result))); + processCalculateDelta(msg.getOriginator(), msg.getMetaDataTs(), (ObjectNode) json, previousData); + ctx.tellSuccess(TbMsg.transformMsgData(msg, JacksonUtil.toString(json))); }, - t -> ctx.tellFailure(msg, t), ctx.getDbCallbackExecutor()); + t -> ctx.tellFailure(msg, t), MoreExecutors.directExecutor()); } @Override public void destroy() { if (useCache) { cache.clear(); + locks.clear(); } } private ListenableFuture fetchLatestValueAsync(EntityId entityId) { return Futures.transform(timeseriesService.findLatest(ctx.getTenantId(), entityId, config.getInputValueKey()), - tsKvEntryOpt -> tsKvEntryOpt.map(this::extractValue).orElse(null) - , ctx.getDbCallbackExecutor()); - } - - private ValueWithTs fetchLatestValue(EntityId entityId) { - List tsKvEntries = timeseriesService.findLatestSync( - ctx.getTenantId(), - entityId, - List.of(config.getInputValueKey())); - return extractValue(tsKvEntries.get(0)); - } - - private ListenableFuture getLastValue(EntityId entityId) { - if (useCache) { - ValueWithTs latestValue; - if ((latestValue = cache.get(entityId)) == null) { - latestValue = fetchLatestValue(entityId); - } - return Futures.immediateFuture(latestValue); - } else { - return fetchLatestValueAsync(entityId); - } + tsKvEntryOpt -> tsKvEntryOpt.map(this::extractValue).orElse(null), ctx.getDbCallbackExecutor()); } private ValueWithTs extractValue(TsKvEntry kvEntry) { @@ -176,6 +139,44 @@ public class CalculateDeltaNode implements TbNode { return new ValueWithTs(ts, result); } + private void processCalculateDelta(EntityId originator, long msgTs, ObjectNode json, ValueWithTs previousData) { + double currentValue = json.get(inputKey).asDouble(); + if (useCache) { + cache.put(originator, new ValueWithTs(msgTs, currentValue)); + } + BigDecimal delta = BigDecimal.valueOf(previousData != null ? currentValue - previousData.value : 0.0); + if (config.isTellFailureIfDeltaIsNegative() && delta.doubleValue() < 0) { + throw new IllegalArgumentException("Delta value is negative!"); + } + if (config.getRound() != null) { + delta = delta.setScale(config.getRound(), RoundingMode.HALF_UP); + } + if (delta.stripTrailingZeros().scale() > 0) { + json.put(config.getOutputValueKey(), delta.doubleValue()); + } else { + json.put(config.getOutputValueKey(), delta.longValueExact()); + } + if (config.isAddPeriodBetweenMsgs()) { + long period = previousData != null ? msgTs - previousData.ts : 0; + json.put(config.getPeriodValueKey(), period); + } + } + + protected ListenableFuture processMsgAsync(TbContext ctx, TbMsg msg) { + ListenableFuture latestValueFuture = getLatestFromCacheOrFetchFromDb(msg); + return Futures.transform(latestValueFuture, previousData -> { + ObjectNode json = (ObjectNode) JacksonUtil.toJsonNode(msg.getData()); + processCalculateDelta(msg.getOriginator(), msg.getMetaDataTs(), json, previousData); + return TbMsg.transformMsgData(msg, JacksonUtil.toString(json)); + }, MoreExecutors.directExecutor()); + } + + private ListenableFuture getLatestFromCacheOrFetchFromDb(TbMsg msg) { + EntityId originator = msg.getOriginator(); + ValueWithTs valueWithTs = cache.get(msg.getOriginator()); + return valueWithTs != null ? Futures.immediateFuture(valueWithTs) : fetchLatestValueAsync(originator); + } + private record ValueWithTs(long ts, double value) { } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/SemaphoreWithTbMsgQueue.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/SemaphoreWithTbMsgQueue.java new file mode 100644 index 0000000000..39b502a59d --- /dev/null +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/SemaphoreWithTbMsgQueue.java @@ -0,0 +1,140 @@ +/** + * 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.rule.engine.util; + +import com.google.common.util.concurrent.ListenableFuture; +import lombok.Data; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.thingsboard.common.util.DonAsynchron; +import org.thingsboard.rule.engine.api.TbContext; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.msg.TbMsg; + +import java.util.Queue; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.Semaphore; +import java.util.function.BiFunction; + +/** + * A utility class designed to manage a queue of messages for a specific entity, ensuring that + * message processing is synchronized on a per-entity basis. This is achieved through the use of a semaphore, + * allowing only one message at a time to be processed for each entity ID, thus preventing race conditions + * and ensuring thread-safe operations. + *

+ * This class is especially useful in scenarios where the order of message processing and + * resource access synchronization are crucial, such as updating caches or databases in a concurrent environment. + */ +@Data +@Slf4j +@RequiredArgsConstructor +public class SemaphoreWithTbMsgQueue { + + private final EntityId entityId; + private final Semaphore semaphore = new Semaphore(1); + private final Queue queue = new ConcurrentLinkedQueue<>(); + + /** + * Adds a message to the queue for asynchronous processing and attempts to process the queue if possible. + * This method is thread-safe and ensures that messages are processed in the order they were added, + * with each message for a specific entity being processed one at a time due to the semaphore control. + * + * @param msg The message to be processed. + * @param ctx The context in which the message should be processed. + * @param msgProcessingFunction The function that defines how the message will be processed. + */ + public void addToQueueAndTryProcess(TbMsg msg, TbContext ctx, BiFunction> msgProcessingFunction) { + queue.add(new TbMsgTbContextBiFunction(msg, ctx, msgProcessingFunction)); + tryProcessQueue(); + } + + /** + * Attempts to process the next message in the queue. If the semaphore is available (indicating + * that no other message for the same entity is currently being processed), this method will + * acquire the semaphore and start processing the message. If the semaphore is not available, + * this method will return immediately, ensuring that messages are processed sequentially + * for each entity. + *

+ * This method is automatically called after adding a message to the queue to ensure + * that the queue is processed promptly. + */ + private void tryProcessQueue() { + while (!queue.isEmpty()) { + // The semaphore have to be acquired before EACH poll and released before NEXT poll. + // Otherwise, some message will remain unprocessed in queue + if (!semaphore.tryAcquire()) { + return; + } + TbMsgTbContextBiFunction tbMsgTbContext = null; + try { + tbMsgTbContext = queue.poll(); + if (tbMsgTbContext == null) { + semaphore.release(); + continue; + } + final TbMsg msg = tbMsgTbContext.getMsg(); + if (!msg.getCallback().isMsgValid()) { + log.trace("[{}] Skipping non-valid message [{}]", entityId, msg); + semaphore.release(); + continue; + } + //DO PROCESSING + final TbContext ctx = tbMsgTbContext.getCtx(); + final ListenableFuture resultMsgFuture = tbMsgTbContext.getBiFunction().apply(ctx, msg); + DonAsynchron.withCallback(resultMsgFuture, resultMsg -> { + try { + ctx.tellSuccess(resultMsg); + } finally { + semaphore.release(); + tryProcessQueue(); + } + }, t -> { + try { + ctx.tellFailure(msg, t); + } finally { + semaphore.release(); + tryProcessQueue(); + } + }, ctx.getDbCallbackExecutor()); + } catch (Throwable t) { + semaphore.release(); + if (tbMsgTbContext == null) { // if no message polled, the loop become infinite, will throw exception + log.error("[{}] Failed to process TbMsgTbContext queue", entityId, t); + throw t; + } + TbMsg msg = tbMsgTbContext.getMsg(); + TbContext ctx = tbMsgTbContext.getCtx(); + log.warn("[{}] Failed to process message: {}", entityId, msg, t); + ctx.tellFailure(msg, t); // you are not allowed to throw here, because queue will remain unprocessed + continue; // We are probably the last who process the queue. We have to continue poll until get successful callback or queue is empty + } + break; //submitted async exact one task. next poll will try on callback + } + } + + /** + * A utility class to hold the tuple of a {@link TbMsg}, {@link TbContext}, and the message processing function. + * This facilitates passing these three elements as a single object within the queue. + */ + @Data + @RequiredArgsConstructor + private static class TbMsgTbContextBiFunction { + private final TbMsg msg; + private final TbContext ctx; + private final BiFunction> biFunction; + } + +} diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/math/TbMathNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/math/TbMathNodeTest.java index e036e6333e..80e669847c 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/math/TbMathNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/math/TbMathNodeTest.java @@ -20,7 +20,6 @@ import com.google.common.util.concurrent.Futures; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.tuple.Triple; import org.assertj.core.api.SoftAssertions; -import org.junit.Assert; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -69,7 +68,6 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyDouble; @@ -78,9 +76,7 @@ import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.BDDMockito.willAnswer; import static org.mockito.BDDMockito.willReturn; -import static org.mockito.BDDMockito.willReturn; import static org.mockito.BDDMockito.willThrow; -import static org.mockito.Mockito.lenient; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; @@ -543,7 +539,7 @@ public class TbMathNodeTest { ArgumentCaptor tCaptor = ArgumentCaptor.forClass(Throwable.class); Mockito.verify(ctx, Mockito.timeout(5000)).tellFailure(eq(msg), tCaptor.capture()); - Assert.assertNotNull(tCaptor.getValue().getMessage()); + assertNotNull(tCaptor.getValue().getMessage()); } @Test @@ -558,7 +554,7 @@ public class TbMathNodeTest { ArgumentCaptor tCaptor = ArgumentCaptor.forClass(Throwable.class); Mockito.verify(ctx, Mockito.timeout(5000)).tellFailure(eq(msg), tCaptor.capture()); - Assert.assertNotNull(tCaptor.getValue().getMessage()); + assertNotNull(tCaptor.getValue().getMessage()); } @Test @@ -574,10 +570,10 @@ public class TbMathNodeTest { List slowMsgList = IntStream.range(0, 5) .mapToObj(x -> TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, originatorSlow, TbMsgMetaData.EMPTY, JacksonUtil.newObjectNode().put("a", 2).put("b", 2).toString())) - .collect(Collectors.toList()); + .toList(); List fastMsgList = IntStream.range(0, 2) .mapToObj(x -> TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, originatorFast, TbMsgMetaData.EMPTY, JacksonUtil.newObjectNode().put("a", 2).put("b", 2).toString())) - .collect(Collectors.toList()); + .toList(); assertThat(slowMsgList.size()).as("slow msgs >= rule-dispatcher pool size").isGreaterThanOrEqualTo(RULE_DISPATCHER_POOL_SIZE); @@ -714,7 +710,7 @@ public class TbMathNodeTest { }).given(node).onMsg(any(), any()); return Triple.of(ctx, resultKey, node); }) - .collect(Collectors.toList()); + .toList(); ctxNodes.forEach(ctxNode -> ruleEngineDispatcherExecutor.executeAsync(() -> ctxNode.getRight() .onMsg(ctxNode.getLeft(), TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, originator, TbMsgMetaData.EMPTY, "{\"a\":2,\"b\":2}")))); ctxNodes.forEach(ctxNode -> verify(ctxNode.getRight(), timeout(5000)).onMsg(eq(ctxNode.getLeft()), any())); diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/CalculateDeltaNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/CalculateDeltaNodeTest.java index 188dd2e4ec..0dc228235d 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/CalculateDeltaNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/CalculateDeltaNodeTest.java @@ -16,13 +16,12 @@ package org.thingsboard.rule.engine.metadata; import com.google.common.util.concurrent.Futures; -import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.assertj.core.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.ArgumentCaptor; -import org.mockito.ArgumentMatcher; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.thingsboard.common.util.JacksonUtil; @@ -46,7 +45,6 @@ import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.dao.timeseries.TimeseriesService; -import java.util.List; import java.util.Optional; import java.util.UUID; @@ -58,14 +56,13 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyList; import static org.mockito.ArgumentMatchers.anySet; import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.never; import static org.mockito.Mockito.reset; -import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +@Slf4j @ExtendWith(MockitoExtension.class) public class CalculateDeltaNodeTest { @@ -110,7 +107,7 @@ public class CalculateDeltaNodeTest { node.onMsg(ctxMock, msg); // THEN - verify(ctxMock, times(1)).tellNext(eq(msg), eq(TbNodeConnectionType.OTHER)); + verify(ctxMock).tellNext(eq(msg), eq(TbNodeConnectionType.OTHER)); verify(ctxMock, never()).tellSuccess(any()); verify(ctxMock, never()).tellFailure(any(), any()); } @@ -124,7 +121,7 @@ public class CalculateDeltaNodeTest { node.onMsg(ctxMock, msg); // THEN - verify(ctxMock, times(1)).tellNext(eq(msg), eq(TbNodeConnectionType.OTHER)); + verify(ctxMock).tellNext(eq(msg), eq(TbNodeConnectionType.OTHER)); verify(ctxMock, never()).tellSuccess(any()); verify(ctxMock, never()).tellFailure(any(), any()); } @@ -139,7 +136,7 @@ public class CalculateDeltaNodeTest { node.onMsg(ctxMock, msg); // THEN - verify(ctxMock, times(1)).tellNext(eq(msg), eq(TbNodeConnectionType.OTHER)); + verify(ctxMock).tellNext(eq(msg), eq(TbNodeConnectionType.OTHER)); verify(ctxMock, never()).tellSuccess(any()); verify(ctxMock, never()).tellFailure(any(), any()); } @@ -165,7 +162,7 @@ public class CalculateDeltaNodeTest { // THEN var actualMsgCaptor = ArgumentCaptor.forClass(TbMsg.class); - verify(ctxMock, times(1)).tellSuccess(actualMsgCaptor.capture()); + verify(ctxMock).tellSuccess(actualMsgCaptor.capture()); verify(ctxMock, never()).tellNext(any(), anyString()); verify(ctxMock, never()).tellNext(any(), anySet()); verify(ctxMock, never()).tellFailure(any(), any()); @@ -195,7 +192,7 @@ public class CalculateDeltaNodeTest { // THEN var actualMsgCaptor = ArgumentCaptor.forClass(TbMsg.class); - verify(ctxMock, times(1)).tellSuccess(actualMsgCaptor.capture()); + verify(ctxMock).tellSuccess(actualMsgCaptor.capture()); verify(ctxMock, never()).tellNext(any(), anyString()); verify(ctxMock, never()).tellNext(any(), anySet()); verify(ctxMock, never()).tellFailure(any(), any()); @@ -225,7 +222,7 @@ public class CalculateDeltaNodeTest { // THEN var actualMsgCaptor = ArgumentCaptor.forClass(TbMsg.class); - verify(ctxMock, times(1)).tellSuccess(actualMsgCaptor.capture()); + verify(ctxMock).tellSuccess(actualMsgCaptor.capture()); verify(ctxMock, never()).tellNext(any(), anyString()); verify(ctxMock, never()).tellNext(any(), anySet()); verify(ctxMock, never()).tellFailure(any(), any()); @@ -246,7 +243,7 @@ public class CalculateDeltaNodeTest { nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config)); node.init(ctxMock, nodeConfiguration); - mockFindLatest(new BasicTsKvEntry(1L, new DoubleDataEntry("temperature", 40.0))); + mockFindLatestAsync(new BasicTsKvEntry(1L, new DoubleDataEntry("temperature", 40.0))); var msgData = "{\"temperature\": 42,\"airPressure\":123}"; var firstMsgMetaData = new TbMsgMetaData(); @@ -259,7 +256,7 @@ public class CalculateDeltaNodeTest { // THEN var actualMsgCaptor = ArgumentCaptor.forClass(TbMsg.class); - verify(ctxMock, times(1)).tellSuccess(actualMsgCaptor.capture()); + verify(ctxMock).tellSuccess(actualMsgCaptor.capture()); verify(ctxMock, never()).tellNext(any(), anyString()); verify(ctxMock, never()).tellNext(any(), anySet()); verify(ctxMock, never()).tellFailure(any(), any()); @@ -273,6 +270,8 @@ public class CalculateDeltaNodeTest { reset(ctxMock); reset(timeseriesServiceMock); + when(ctxMock.getDbCallbackExecutor()).thenReturn(DB_EXECUTOR); + var secondMsgMetaData = new TbMsgMetaData(); secondMsgMetaData.putValue("ts", String.valueOf(6L)); var secondMsg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, DUMMY_DEVICE_ORIGINATOR, secondMsgMetaData, msgData); @@ -284,7 +283,7 @@ public class CalculateDeltaNodeTest { actualMsgCaptor = ArgumentCaptor.forClass(TbMsg.class); verify(timeseriesServiceMock, never()).findLatest(any(), any(), anyList()); - verify(ctxMock, times(1)).tellSuccess(actualMsgCaptor.capture()); + verify(ctxMock).tellSuccess(actualMsgCaptor.capture()); verify(ctxMock, never()).tellNext(any(), anyString()); verify(ctxMock, never()).tellNext(any(), anySet()); verify(ctxMock, never()).tellFailure(any(), any()); @@ -314,7 +313,7 @@ public class CalculateDeltaNodeTest { // THEN var actualMsgCaptor = ArgumentCaptor.forClass(TbMsg.class); - verify(ctxMock, times(1)).tellSuccess(actualMsgCaptor.capture()); + verify(ctxMock).tellSuccess(actualMsgCaptor.capture()); verify(ctxMock, never()).tellNext(any(), anyString()); verify(ctxMock, never()).tellNext(any(), anySet()); verify(ctxMock, never()).tellFailure(any(), any()); @@ -331,7 +330,7 @@ public class CalculateDeltaNodeTest { nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config)); node.init(ctxMock, nodeConfiguration); - mockFindLatest(new BasicTsKvEntry(System.currentTimeMillis(), new LongDataEntry("pulseCounter", 200L))); + mockFindLatestAsync(new BasicTsKvEntry(System.currentTimeMillis(), new LongDataEntry("pulseCounter", 200L))); var msgData = "{\"pulseCounter\":\"123\"}"; var msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, DUMMY_DEVICE_ORIGINATOR, TbMsgMetaData.EMPTY, msgData); @@ -343,7 +342,7 @@ public class CalculateDeltaNodeTest { var actualMsgCaptor = ArgumentCaptor.forClass(TbMsg.class); var actualExceptionCaptor = ArgumentCaptor.forClass(Exception.class); - verify(ctxMock, times(1)).tellFailure(actualMsgCaptor.capture(), actualExceptionCaptor.capture()); + verify(ctxMock).tellFailure(actualMsgCaptor.capture(), actualExceptionCaptor.capture()); verify(ctxMock, never()).tellSuccess(any()); verify(ctxMock, never()).tellNext(any(), anyString()); verify(ctxMock, never()).tellNext(any(), anySet()); @@ -363,7 +362,7 @@ public class CalculateDeltaNodeTest { nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config)); node.init(ctxMock, nodeConfiguration); - mockFindLatest(new BasicTsKvEntry(System.currentTimeMillis(), new LongDataEntry("pulseCounter", 200L))); + mockFindLatestAsync(new BasicTsKvEntry(System.currentTimeMillis(), new LongDataEntry("pulseCounter", 200L))); var msgData = "{\"pulseCounter\":\"123\"}"; var msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, DUMMY_DEVICE_ORIGINATOR, TbMsgMetaData.EMPTY, msgData); @@ -374,7 +373,7 @@ public class CalculateDeltaNodeTest { // THEN var actualMsgCaptor = ArgumentCaptor.forClass(TbMsg.class); - verify(ctxMock, times(1)).tellSuccess(actualMsgCaptor.capture()); + verify(ctxMock).tellSuccess(actualMsgCaptor.capture()); verify(ctxMock, never()).tellFailure(any(), any()); verify(ctxMock, never()).tellNext(any(), anyString()); verify(ctxMock, never()).tellNext(any(), anySet()); @@ -386,13 +385,23 @@ public class CalculateDeltaNodeTest { @Test public void givenInvalidStringValue_whenOnMsg_thenException() { // GIVEN - mockFindLatest(new BasicTsKvEntry(System.currentTimeMillis(), new StringDataEntry("pulseCounter", "high"))); + mockFindLatestAsync(new BasicTsKvEntry(System.currentTimeMillis(), new StringDataEntry("pulseCounter", "high"))); var msgData = "{\"pulseCounter\":\"123\"}"; var msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, DUMMY_DEVICE_ORIGINATOR, TbMsgMetaData.EMPTY, msgData); - // WHEN-THEN - Assertions.assertThatThrownBy(() -> node.onMsg(ctxMock, msg)) + // WHEN + node.onMsg(ctxMock, msg); + + // THEN + ArgumentCaptor throwableCaptor = ArgumentCaptor.forClass(Throwable.class); + + verify(ctxMock).tellFailure(eq(msg), throwableCaptor.capture()); + verify(ctxMock, never()).tellSuccess(any()); + verify(ctxMock, never()).tellNext(any(), anyString()); + verify(ctxMock, never()).tellNext(any(), anySet()); + + Assertions.assertThat(throwableCaptor.getValue()) .isInstanceOf(IllegalArgumentException.class) .hasMessage("Calculation failed. Unable to parse value [high] of telemetry [pulseCounter] to Double"); } @@ -400,13 +409,23 @@ public class CalculateDeltaNodeTest { @Test public void givenBooleanValue_whenOnMsg_thenException() { // GIVEN - mockFindLatest(new BasicTsKvEntry(System.currentTimeMillis(), new BooleanDataEntry("pulseCounter", false))); + mockFindLatestAsync(new BasicTsKvEntry(System.currentTimeMillis(), new BooleanDataEntry("pulseCounter", false))); var msgData = "{\"pulseCounter\":true}"; var msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, DUMMY_DEVICE_ORIGINATOR, TbMsgMetaData.EMPTY, msgData); - // WHEN-THEN - Assertions.assertThatThrownBy(() -> node.onMsg(ctxMock, msg)) + // WHEN + node.onMsg(ctxMock, msg); + + // THEN + ArgumentCaptor throwableCaptor = ArgumentCaptor.forClass(Throwable.class); + + verify(ctxMock).tellFailure(eq(msg), throwableCaptor.capture()); + verify(ctxMock, never()).tellSuccess(any()); + verify(ctxMock, never()).tellNext(any(), anyString()); + verify(ctxMock, never()).tellNext(any(), anySet()); + + Assertions.assertThat(throwableCaptor.getValue()) .isInstanceOf(IllegalArgumentException.class) .hasMessage("Calculation failed. Boolean values are not supported!"); } @@ -414,24 +433,27 @@ public class CalculateDeltaNodeTest { @Test public void givenJsonValue_whenOnMsg_thenException() { // GIVEN - mockFindLatest(new BasicTsKvEntry(System.currentTimeMillis(), new JsonDataEntry("pulseCounter", "{\"isActive\":false}"))); + mockFindLatestAsync(new BasicTsKvEntry(System.currentTimeMillis(), new JsonDataEntry("pulseCounter", "{\"isActive\":false}"))); var msgData = "{\"pulseCounter\":{\"isActive\":true}}"; var msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, DUMMY_DEVICE_ORIGINATOR, TbMsgMetaData.EMPTY, msgData); - // WHEN-THEN - Assertions.assertThatThrownBy(() -> node.onMsg(ctxMock, msg)) + // WHEN + node.onMsg(ctxMock, msg); + + // THEN + ArgumentCaptor throwableCaptor = ArgumentCaptor.forClass(Throwable.class); + + verify(ctxMock).tellFailure(eq(msg), throwableCaptor.capture()); + verify(ctxMock, never()).tellSuccess(any()); + verify(ctxMock, never()).tellNext(any(), anyString()); + verify(ctxMock, never()).tellNext(any(), anySet()); + + Assertions.assertThat(throwableCaptor.getValue()) .isInstanceOf(IllegalArgumentException.class) .hasMessage("Calculation failed. JSON values are not supported!"); } - private void mockFindLatest(TsKvEntry tsKvEntry) { - when(ctxMock.getTenantId()).thenReturn(TENANT_ID); - when(timeseriesServiceMock.findLatestSync( - eq(TENANT_ID), eq(DUMMY_DEVICE_ORIGINATOR), argThat(new ListMatcher<>(List.of(tsKvEntry.getKey()))) - )).thenReturn(List.of(tsKvEntry)); - } - private void mockFindLatestAsync(TsKvEntry tsKvEntry) { when(ctxMock.getDbCallbackExecutor()).thenReturn(DB_EXECUTOR); when(ctxMock.getTenantId()).thenReturn(TENANT_ID); @@ -440,22 +462,4 @@ public class CalculateDeltaNodeTest { )).thenReturn(Futures.immediateFuture(Optional.of(tsKvEntry))); } - @RequiredArgsConstructor - private static class ListMatcher implements ArgumentMatcher> { - - private final List expectedList; - - @Override - public boolean matches(List actualList) { - if (actualList == expectedList) { - return true; - } - if (actualList.size() != expectedList.size()) { - return false; - } - return actualList.containsAll(expectedList); - } - - } - } From 827c898179e39f24bd4b6c283fb0a1ac1a2f083c Mon Sep 17 00:00:00 2001 From: ShvaykaD Date: Tue, 2 Apr 2024 12:38:37 +0300 Subject: [PATCH 22/80] added test givenConcurrentAccess_whenOnMsg_thenGetFromDBInvokedOnce --- .../metadata/CalculateDeltaNodeTest.java | 79 ++++++++++++++++++- 1 file changed, 75 insertions(+), 4 deletions(-) diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/CalculateDeltaNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/CalculateDeltaNodeTest.java index 0dc228235d..36b9719d86 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/CalculateDeltaNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/CalculateDeltaNodeTest.java @@ -17,13 +17,13 @@ package org.thingsboard.rule.engine.metadata; import com.google.common.util.concurrent.Futures; import lombok.extern.slf4j.Slf4j; -import org.assertj.core.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import org.thingsboard.common.util.AbstractListeningExecutor; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.ListeningExecutor; import org.thingsboard.rule.engine.TestDbCallbackExecutor; @@ -45,9 +45,15 @@ import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.dao.timeseries.TimeseriesService; +import java.util.List; import java.util.Optional; import java.util.UUID; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.stream.IntStream; +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertInstanceOf; @@ -57,8 +63,12 @@ import static org.mockito.ArgumentMatchers.anyList; import static org.mockito.ArgumentMatchers.anySet; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.BDDMockito.willAnswer; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -69,6 +79,10 @@ public class CalculateDeltaNodeTest { private final DeviceId DUMMY_DEVICE_ORIGINATOR = new DeviceId(UUID.fromString("2ba3ded4-882b-40cf-999a-89da9ccd58f9")); private final TenantId TENANT_ID = new TenantId(UUID.fromString("3842e740-0d89-43a9-8d52-ae44023847ba")); private final ListeningExecutor DB_EXECUTOR = new TestDbCallbackExecutor(); + + private static final int RULE_DISPATCHER_POOL_SIZE = 2; + private static final int DB_CALLBACK_POOL_SIZE = 3; + @Mock private TbContext ctxMock; @Mock @@ -401,7 +415,7 @@ public class CalculateDeltaNodeTest { verify(ctxMock, never()).tellNext(any(), anyString()); verify(ctxMock, never()).tellNext(any(), anySet()); - Assertions.assertThat(throwableCaptor.getValue()) + assertThat(throwableCaptor.getValue()) .isInstanceOf(IllegalArgumentException.class) .hasMessage("Calculation failed. Unable to parse value [high] of telemetry [pulseCounter] to Double"); } @@ -425,7 +439,7 @@ public class CalculateDeltaNodeTest { verify(ctxMock, never()).tellNext(any(), anyString()); verify(ctxMock, never()).tellNext(any(), anySet()); - Assertions.assertThat(throwableCaptor.getValue()) + assertThat(throwableCaptor.getValue()) .isInstanceOf(IllegalArgumentException.class) .hasMessage("Calculation failed. Boolean values are not supported!"); } @@ -449,11 +463,68 @@ public class CalculateDeltaNodeTest { verify(ctxMock, never()).tellNext(any(), anyString()); verify(ctxMock, never()).tellNext(any(), anySet()); - Assertions.assertThat(throwableCaptor.getValue()) + assertThat(throwableCaptor.getValue()) .isInstanceOf(IllegalArgumentException.class) .hasMessage("Calculation failed. JSON values are not supported!"); } + @Test + public void givenConcurrentAccess_whenOnMsg_thenGetFromDBInvokedOnce() throws TbNodeException, InterruptedException { + DBCallbackExecutor dbCallbackExecutor = new DBCallbackExecutor(); + dbCallbackExecutor.init(); + + RuleDispatcherExecutor ruleEngineDispatcherExecutor = new RuleDispatcherExecutor(); + ruleEngineDispatcherExecutor.init(); + + assertThat(RULE_DISPATCHER_POOL_SIZE).as("dispatcher pool size have to be > 1").isGreaterThan(1); + + final TbContext ctx = mock(TbContext.class); + final TimeseriesService timeseriesService = mock(TimeseriesService.class); + + when(ctx.getTimeseriesService()).thenReturn(timeseriesService); + when(ctx.getDbCallbackExecutor()).thenReturn(dbCallbackExecutor); + when(timeseriesService.findLatest(any(), any(), anyString())).thenReturn(Futures.immediateFuture(Optional.empty())); + + final CalculateDeltaNodeConfiguration config = new CalculateDeltaNodeConfiguration().defaultConfiguration(); + final TbNodeConfiguration nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config)); + final CalculateDeltaNode node = spy(CalculateDeltaNode.class); + + node.init(ctx, nodeConfiguration); + + List tbMsgList = IntStream.range(0, RULE_DISPATCHER_POOL_SIZE * 2).mapToObj(x -> { + var msgData = "{\"pulseCounter\":" + 2 + "}"; + return TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, DUMMY_DEVICE_ORIGINATOR, TbMsgMetaData.EMPTY, msgData); + }).toList(); + + CountDownLatch processingLatch = new CountDownLatch(tbMsgList.size()); + + willAnswer(invocation -> { + processingLatch.countDown(); + return invocation.callRealMethod(); + }).given(node).processMsgAsync(any(), any()); + + tbMsgList.forEach(msg -> ruleEngineDispatcherExecutor.executeAsync(() -> node.onMsg(ctx, msg))); + + assertThat(processingLatch.await(5, TimeUnit.SECONDS)).as("await on processingLatch").isTrue(); + + verify(timeseriesService).findLatest(any(), any(), anyString()); + await().atMost(5, TimeUnit.SECONDS).untilAsserted(() -> verify(ctx, times(tbMsgList.size())).tellSuccess(any())); + } + + private static class RuleDispatcherExecutor extends AbstractListeningExecutor { + @Override + protected int getThreadPollSize() { + return RULE_DISPATCHER_POOL_SIZE; + } + } + + private static class DBCallbackExecutor extends AbstractListeningExecutor { + @Override + protected int getThreadPollSize() { + return DB_CALLBACK_POOL_SIZE; + } + } + private void mockFindLatestAsync(TsKvEntry tsKvEntry) { when(ctxMock.getDbCallbackExecutor()).thenReturn(DB_EXECUTOR); when(ctxMock.getTenantId()).thenReturn(TENANT_ID); From 2fced834fdde91773dee94366b9fdb7ad6a789ae Mon Sep 17 00:00:00 2001 From: Andrii Landiak Date: Thu, 4 Apr 2024 14:25:08 +0300 Subject: [PATCH 23/80] Improvement to entity data query: add optimization, add fetching by 1024 max --- .../server/dao/entity/BaseEntityService.java | 121 ++++++++++++------ .../server/dao/service/EntityServiceTest.java | 116 ++++++++++++++++- 2 files changed, 199 insertions(+), 38 deletions(-) diff --git a/dao/src/main/java/org/thingsboard/server/dao/entity/BaseEntityService.java b/dao/src/main/java/org/thingsboard/server/dao/entity/BaseEntityService.java index 5edb59f5c8..59475213b3 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/entity/BaseEntityService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/entity/BaseEntityService.java @@ -40,11 +40,16 @@ import org.thingsboard.server.common.data.query.EntityDataQuery; import org.thingsboard.server.common.data.query.EntityFilterType; import org.thingsboard.server.common.data.query.EntityKey; import org.thingsboard.server.common.data.query.EntityListFilter; +import org.thingsboard.server.common.data.query.KeyFilter; import org.thingsboard.server.common.data.query.RelationsQueryFilter; import org.thingsboard.server.dao.exception.IncorrectParameterException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; import java.util.List; import java.util.Optional; +import java.util.Set; import java.util.function.Function; import java.util.stream.Collectors; @@ -63,6 +68,10 @@ public class BaseEntityService extends AbstractEntityService implements EntitySe public static final String INCORRECT_CUSTOMER_ID = "Incorrect customerId "; public static final CustomerId NULL_CUSTOMER_ID = new CustomerId(NULL_UUID); + private static final int MAX_ENTITY_IDS_SIZE = 1024; + private static final Set EXCLUDED_TYPES_FROM_OPTIMIZATION = Set.of( + EntityFilterType.ENTITY_LIST, EntityFilterType.SINGLE_ENTITY, EntityFilterType.RELATIONS_QUERY); + @Autowired private EntityQueryDao entityQueryDao; @@ -86,9 +95,7 @@ public class BaseEntityService extends AbstractEntityService implements EntitySe validateId(customerId, id -> INCORRECT_CUSTOMER_ID + id); validateEntityDataQuery(query); - if (EntityFilterType.RELATIONS_QUERY.equals(query.getEntityFilter().getType()) - || EntityFilterType.SINGLE_ENTITY.equals(query.getEntityFilter().getType()) - || StringUtils.isNotEmpty(query.getPageLink().getTextSearch())) { + if (isOptimizationExcluded(query)) { return this.entityQueryDao.findEntityDataByQuery(tenantId, customerId, query); } @@ -97,41 +104,10 @@ public class BaseEntityService extends AbstractEntityService implements EntitySe if (entityDataByQuery == null || entityDataByQuery.getData().isEmpty()) { return entityDataByQuery; } - // 2 step - find entity data by entity ids from the 1st step - PageData result = findEntityDataByEntityIds(tenantId, customerId, query, entityDataByQuery.getData()); - return new PageData<>(result.getData(), entityDataByQuery.getTotalPages(), entityDataByQuery.getTotalElements(), entityDataByQuery.hasNext()); - } - - private PageData findEntityIdsByFilterAndSorterColumns(TenantId tenantId, CustomerId customerId, EntityDataQuery query) { - List entityFields = null; - List latestValues = null; - if (query.getPageLink().getSortOrder() != null) { - if (query.getEntityFields() != null) { - entityFields = query.getEntityFields().stream() - .filter(entityKey -> entityKey.getKey().equals(query.getPageLink().getSortOrder().getKey().getKey())) - .collect(Collectors.toList()); - } - if (query.getLatestValues() != null) { - latestValues = query.getLatestValues().stream() - .filter(entityKey -> entityKey.getKey().equals(query.getPageLink().getSortOrder().getKey().getKey())) - .collect(Collectors.toList()); - } - } - EntityDataQuery entityQuery = new EntityDataQuery(query.getEntityFilter(), query.getPageLink(), entityFields, latestValues, query.getKeyFilters()); - return this.entityQueryDao.findEntityDataByQuery(tenantId, customerId, entityQuery); - } - - private PageData findEntityDataByEntityIds(TenantId tenantId, CustomerId customerId, EntityDataQuery query, List data) { - List entityIds = data.stream().map(d -> d.getEntityId().getId().toString()).toList(); - EntityType entityType = data.isEmpty() ? null : data.get(0).getEntityId().getEntityType(); - - EntityListFilter filter = new EntityListFilter(); - filter.setEntityType(entityType); - filter.setEntityList(entityIds); - EntityDataPageLink pageLink = new EntityDataPageLink(query.getPageLink().getPageSize(), 0, null, query.getPageLink().getSortOrder()); - EntityDataQuery entityQuery = new EntityDataQuery(filter, pageLink, query.getEntityFields(), query.getLatestValues(), null); - return this.entityQueryDao.findEntityDataByQuery(tenantId, customerId, entityQuery); + // 2 step - find entity data by entity ids from the 1st step + List result = fetchEntityDataByIdsFromInitialQuery(tenantId, customerId, query, entityDataByQuery.getData()); + return new PageData<>(result, entityDataByQuery.getTotalPages(), entityDataByQuery.getTotalElements(), entityDataByQuery.hasNext()); } @Override @@ -228,4 +204,75 @@ public class BaseEntityService extends AbstractEntityService implements EntitySe } } + private boolean isOptimizationExcluded(EntityDataQuery query) { + if (StringUtils.isNotEmpty(query.getPageLink().getTextSearch())) { + return true; + } + + if (EXCLUDED_TYPES_FROM_OPTIMIZATION.contains(query.getEntityFilter().getType())) { + return true; + } + + if ((query.getEntityFields() == null || query.getEntityFields().isEmpty()) && + (query.getLatestValues() == null || query.getLatestValues().isEmpty())) { + return true; + } + + Set entityKeys = new HashSet<>(Optional.ofNullable(query.getKeyFilters()).orElse(Collections.emptyList()).stream().map(KeyFilter::getKey).toList()); + Set entityFields = new HashSet<>(Optional.ofNullable(query.getEntityFields()).orElse(Collections.emptyList())); + Set latestValues = new HashSet<>(Optional.ofNullable(query.getLatestValues()).orElse(Collections.emptyList())); + + return entityKeys.equals(entityFields) && entityKeys.equals(latestValues); + } + + private PageData findEntityIdsByFilterAndSorterColumns(TenantId tenantId, CustomerId customerId, EntityDataQuery query) { + List entityFields = null; + List latestValues = null; + if (query.getPageLink().getSortOrder() != null) { + if (query.getEntityFields() != null) { + entityFields = query.getEntityFields().stream() + .filter(entityKey -> entityKey.getKey().equals(query.getPageLink().getSortOrder().getKey().getKey())) + .collect(Collectors.toList()); + } + if (query.getLatestValues() != null) { + latestValues = query.getLatestValues().stream() + .filter(entityKey -> entityKey.getKey().equals(query.getPageLink().getSortOrder().getKey().getKey())) + .collect(Collectors.toList()); + } + } + EntityDataQuery entityQuery = new EntityDataQuery(query.getEntityFilter(), query.getPageLink(), entityFields, latestValues, query.getKeyFilters()); + return this.entityQueryDao.findEntityDataByQuery(tenantId, customerId, entityQuery); + } + + private List fetchEntityDataByIdsFromInitialQuery(TenantId tenantId, CustomerId customerId, EntityDataQuery query, List initialQueryResult) { + List result = new ArrayList<>(); + + List entityIds = initialQueryResult.stream().map(d -> d.getEntityId().getId().toString()).collect(Collectors.toList()); + EntityType entityType = initialQueryResult.get(0).getEntityId().getEntityType(); + + if (entityIds.size() > MAX_ENTITY_IDS_SIZE) { + List> chunks = new ArrayList<>(); + for (int i = 0; i < entityIds.size(); i += MAX_ENTITY_IDS_SIZE) { + chunks.add(entityIds.subList(i, Math.min(entityIds.size(), i + MAX_ENTITY_IDS_SIZE))); + } + for (List chunk : chunks) { + result.addAll(findEntityDataByEntityIds(tenantId, customerId, query, chunk, entityType, chunk.size())); + } + } else { + result.addAll(findEntityDataByEntityIds(tenantId, customerId, query, entityIds, entityType, query.getPageLink().getPageSize())); + } + return result; + } + + private List findEntityDataByEntityIds(TenantId tenantId, CustomerId customerId, EntityDataQuery query, + List entityIds, EntityType entityType, int pageSize) { + EntityListFilter filter = new EntityListFilter(); + filter.setEntityType(entityType); + filter.setEntityList(entityIds); + + EntityDataPageLink pageLink = new EntityDataPageLink(pageSize, 0, null, query.getPageLink().getSortOrder()); + EntityDataQuery entityQuery = new EntityDataQuery(filter, pageLink, query.getEntityFields(), query.getLatestValues(), null); + return this.entityQueryDao.findEntityDataByQuery(tenantId, customerId, entityQuery).getData(); + } + } diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/EntityServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/EntityServiceTest.java index 216d2abb07..58ee94f8b7 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/EntityServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/EntityServiceTest.java @@ -26,7 +26,6 @@ import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.jdbc.core.ResultSetExtractor; import org.thingsboard.server.common.data.AttributeScope; -import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.StringUtils; @@ -99,6 +98,7 @@ import java.util.stream.Stream; import static org.hamcrest.MatcherAssert.assertThat; import static org.junit.Assert.assertEquals; +import static org.thingsboard.server.common.data.query.EntityKeyType.ENTITY_FIELD; @Slf4j @DaoSqlTest @@ -2113,6 +2113,120 @@ public class EntityServiceTest extends AbstractServiceTest { deviceService.deleteDevicesByTenantId(tenantId); } + @Test + public void testFindEntityQueryWith_3000_pageSize() throws InterruptedException { + int pageSize = 3000; + + List devices = new ArrayList<>(); + + for (int i = 0; i < pageSize; i++) { + Device device = new Device(); + device.setTenantId(tenantId); + device.setName("Device_" + i); + device.setType("default"); + device.setLabel("testLabel" + (int) (Math.random() * 1000)); + devices.add(deviceService.saveDevice(device)); + //TO make sure devices have different created time + Thread.sleep(1); + } + + DeviceTypeFilter filter = new DeviceTypeFilter(); + filter.setDeviceTypes(List.of("default")); + filter.setDeviceNameFilter("D%"); + + EntityDataSortOrder sortOrder = new EntityDataSortOrder(new EntityKey(ENTITY_FIELD, "name"), EntityDataSortOrder.Direction.DESC); + + List deviceTypeFilters = createStringKeyFilters("type", ENTITY_FIELD, StringFilterPredicate.StringOperation.EQUAL, "default"); + + KeyFilter createdTimeFilter = createNumericKeyFilter("createdTime", ENTITY_FIELD, NumericFilterPredicate.NumericOperation.GREATER, 1L); + List createdTimeFilters = Collections.singletonList(createdTimeFilter); + + List nameFilters = createStringKeyFilters("name", ENTITY_FIELD, StringFilterPredicate.StringOperation.CONTAINS, "Device"); + + List entityFields = Arrays.asList(new EntityKey(ENTITY_FIELD, "name"), + new EntityKey(ENTITY_FIELD, "type")); + + // 1. Device type filters: + + // query with textSearch - optimization is not performing + EntityDataPageLink originalPageLink = new EntityDataPageLink(pageSize, 0, "Device", sortOrder); + EntityDataQuery originalQuery = new EntityDataQuery(filter, originalPageLink, entityFields, null, deviceTypeFilters); + PageData originalData = entityService.findEntityDataByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), originalQuery); + + // query without textSearch - optimization is performing + EntityDataPageLink optimizedPageLink = new EntityDataPageLink(pageSize, 0, null, sortOrder); + EntityDataQuery optimizedQuery = new EntityDataQuery(filter, optimizedPageLink, entityFields, null, deviceTypeFilters); + PageData optimizedData = entityService.findEntityDataByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), optimizedQuery); + List loadedEntities = getLoadedEntities(optimizedData, optimizedQuery); + Assert.assertEquals(devices.size(), loadedEntities.size()); + + for (int i = 0; i < devices.size(); i++) { + var originalElement = originalData.getData().get(i); + var optimizedElement = optimizedData.getData().get(i); + Assert.assertEquals(originalElement.getEntityId(), optimizedElement.getEntityId()); + originalElement.getLatest().get(ENTITY_FIELD).forEach((key, value) -> { + Assert.assertEquals(value.getValue(), optimizedElement.getLatest().get(EntityKeyType.ENTITY_FIELD).get(key).getValue()); + Assert.assertEquals(value.getCount(), optimizedElement.getLatest().get(EntityKeyType.ENTITY_FIELD).get(key).getCount()); + }); + } + Assert.assertEquals(originalData.getTotalPages(), optimizedData.getTotalPages()); + Assert.assertEquals(originalData.getTotalElements(), optimizedData.getTotalElements()); + + // 2. Device create time filters + + // query with textSearch - optimization is not performing + originalPageLink = new EntityDataPageLink(pageSize, 0, "Device", sortOrder); + originalQuery = new EntityDataQuery(filter, originalPageLink, entityFields, null, createdTimeFilters); + originalData = entityService.findEntityDataByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), originalQuery); + + // query without textSearch - optimization is performing + optimizedPageLink = new EntityDataPageLink(pageSize, 0, null, sortOrder); + optimizedQuery = new EntityDataQuery(filter, optimizedPageLink, entityFields, null, createdTimeFilters); + optimizedData = entityService.findEntityDataByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), optimizedQuery); + loadedEntities = getLoadedEntities(optimizedData, optimizedQuery); + Assert.assertEquals(devices.size(), loadedEntities.size()); + + for (int i = 0; i < devices.size(); i++) { + var originalElement = originalData.getData().get(i); + var optimizedElement = optimizedData.getData().get(i); + Assert.assertEquals(originalElement.getEntityId(), optimizedElement.getEntityId()); + originalElement.getLatest().get(ENTITY_FIELD).forEach((key, value) -> { + Assert.assertEquals(value.getValue(), optimizedElement.getLatest().get(EntityKeyType.ENTITY_FIELD).get(key).getValue()); + Assert.assertEquals(value.getCount(), optimizedElement.getLatest().get(EntityKeyType.ENTITY_FIELD).get(key).getCount()); + }); + } + Assert.assertEquals(originalData.getTotalPages(), optimizedData.getTotalPages()); + Assert.assertEquals(originalData.getTotalElements(), optimizedData.getTotalElements()); + + // 3. Device name filters + + // query with textSearch - optimization is not performing + originalPageLink = new EntityDataPageLink(pageSize, 0, "Device", sortOrder); + originalQuery = new EntityDataQuery(filter, originalPageLink, entityFields, null, nameFilters); + originalData = entityService.findEntityDataByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), originalQuery); + + // query without textSearch - optimization is performing + optimizedPageLink = new EntityDataPageLink(pageSize, 0, null, sortOrder); + optimizedQuery = new EntityDataQuery(filter, optimizedPageLink, entityFields, null, nameFilters); + optimizedData = entityService.findEntityDataByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), optimizedQuery); + loadedEntities = getLoadedEntities(optimizedData, optimizedQuery); + Assert.assertEquals(devices.size(), loadedEntities.size()); + + for (int i = 0; i < devices.size(); i++) { + var originalElement = originalData.getData().get(i); + var optimizedElement = optimizedData.getData().get(i); + Assert.assertEquals(originalElement.getEntityId(), optimizedElement.getEntityId()); + originalElement.getLatest().get(ENTITY_FIELD).forEach((key, value) -> { + Assert.assertEquals(value.getValue(), optimizedElement.getLatest().get(EntityKeyType.ENTITY_FIELD).get(key).getValue()); + Assert.assertEquals(value.getCount(), optimizedElement.getLatest().get(EntityKeyType.ENTITY_FIELD).get(key).getCount()); + }); + } + Assert.assertEquals(originalData.getTotalPages(), optimizedData.getTotalPages()); + Assert.assertEquals(originalData.getTotalElements(), optimizedData.getTotalElements()); + + deviceService.deleteDevicesByTenantId(tenantId); + } + private Boolean listEqualWithoutOrder(List A, List B) { return A.containsAll(B) && B.containsAll(A); } From 74fed02b8ba30e6eefed5a82864da141034fc17e Mon Sep 17 00:00:00 2001 From: Andrii Landiak Date: Thu, 4 Apr 2024 14:27:46 +0300 Subject: [PATCH 24/80] Set edgeVersion to 3.7.0 --- .../src/main/java/org/thingsboard/edge/rpc/EdgeGrpcClient.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/edge-api/src/main/java/org/thingsboard/edge/rpc/EdgeGrpcClient.java b/common/edge-api/src/main/java/org/thingsboard/edge/rpc/EdgeGrpcClient.java index 3c4a1e6be9..7219613e9b 100644 --- a/common/edge-api/src/main/java/org/thingsboard/edge/rpc/EdgeGrpcClient.java +++ b/common/edge-api/src/main/java/org/thingsboard/edge/rpc/EdgeGrpcClient.java @@ -111,7 +111,7 @@ public class EdgeGrpcClient implements EdgeRpcClient { .setConnectRequestMsg(ConnectRequestMsg.newBuilder() .setEdgeRoutingKey(edgeKey) .setEdgeSecret(edgeSecret) - .setEdgeVersion(EdgeVersion.V_3_6_2) + .setEdgeVersion(EdgeVersion.V_3_7_0) .setMaxInboundMessageSize(maxInboundMessageSize) .build()) .build()); From 0252ab851ae46e15a6dd907ce727aa57bc3f6462 Mon Sep 17 00:00:00 2001 From: Andrii Landiak Date: Thu, 4 Apr 2024 16:00:19 +0300 Subject: [PATCH 25/80] Micro improvement: fix logic for filteringKeys containsAll latestValues && entityFields. Improve test --- .../server/dao/entity/BaseEntityService.java | 14 ++-- .../server/dao/service/EntityServiceTest.java | 67 +++++++++++-------- 2 files changed, 47 insertions(+), 34 deletions(-) diff --git a/dao/src/main/java/org/thingsboard/server/dao/entity/BaseEntityService.java b/dao/src/main/java/org/thingsboard/server/dao/entity/BaseEntityService.java index 59475213b3..3cdcf5e874 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/entity/BaseEntityService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/entity/BaseEntityService.java @@ -95,7 +95,7 @@ public class BaseEntityService extends AbstractEntityService implements EntitySe validateId(customerId, id -> INCORRECT_CUSTOMER_ID + id); validateEntityDataQuery(query); - if (isOptimizationExcluded(query)) { + if (!isValidForOptimization(query)) { return this.entityQueryDao.findEntityDataByQuery(tenantId, customerId, query); } @@ -204,25 +204,25 @@ public class BaseEntityService extends AbstractEntityService implements EntitySe } } - private boolean isOptimizationExcluded(EntityDataQuery query) { + private boolean isValidForOptimization(EntityDataQuery query) { if (StringUtils.isNotEmpty(query.getPageLink().getTextSearch())) { - return true; + return false; } if (EXCLUDED_TYPES_FROM_OPTIMIZATION.contains(query.getEntityFilter().getType())) { - return true; + return false; } if ((query.getEntityFields() == null || query.getEntityFields().isEmpty()) && (query.getLatestValues() == null || query.getLatestValues().isEmpty())) { - return true; + return false; } - Set entityKeys = new HashSet<>(Optional.ofNullable(query.getKeyFilters()).orElse(Collections.emptyList()).stream().map(KeyFilter::getKey).toList()); + Set filteringKeys = new HashSet<>(Optional.ofNullable(query.getKeyFilters()).orElse(Collections.emptyList()).stream().map(KeyFilter::getKey).toList()); Set entityFields = new HashSet<>(Optional.ofNullable(query.getEntityFields()).orElse(Collections.emptyList())); Set latestValues = new HashSet<>(Optional.ofNullable(query.getLatestValues()).orElse(Collections.emptyList())); - return entityKeys.equals(entityFields) && entityKeys.equals(latestValues); + return !(filteringKeys.containsAll(entityFields) && filteringKeys.containsAll(latestValues)); } private PageData findEntityIdsByFilterAndSorterColumns(TenantId tenantId, CustomerId customerId, EntityDataQuery query) { diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/EntityServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/EntityServiceTest.java index 58ee94f8b7..f1efe9f730 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/EntityServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/EntityServiceTest.java @@ -98,6 +98,7 @@ import java.util.stream.Stream; import static org.hamcrest.MatcherAssert.assertThat; import static org.junit.Assert.assertEquals; +import static org.thingsboard.server.common.data.query.EntityKeyType.ATTRIBUTE; import static org.thingsboard.server.common.data.query.EntityKeyType.ENTITY_FIELD; @Slf4j @@ -2114,37 +2115,40 @@ public class EntityServiceTest extends AbstractServiceTest { } @Test - public void testFindEntityQueryWith_3000_pageSize() throws InterruptedException { + public void testFindEntityQuery_for_5000_devices_with_3000_pageSize() { int pageSize = 3000; + int expectedDevicesSize = 4000; + int unexpectedDevicesSize = 1000; - List devices = new ArrayList<>(); - - for (int i = 0; i < pageSize; i++) { + for (int i = 0; i < expectedDevicesSize + unexpectedDevicesSize; i++) { Device device = new Device(); device.setTenantId(tenantId); - device.setName("Device_" + i); + if (i < expectedDevicesSize) { + device.setName("Device_" + i); // match deviceNameFilter 'D%' + } else { + device.setName("Test_" + i); // does not match deviceNameFilter 'D%' + } device.setType("default"); device.setLabel("testLabel" + (int) (Math.random() * 1000)); - devices.add(deviceService.saveDevice(device)); - //TO make sure devices have different created time - Thread.sleep(1); + Device savedDevice = deviceService.saveDevice(device); + + attributesService.save(tenantId, savedDevice.getId(), AttributeScope.CLIENT_SCOPE, + new BaseAttributeKvEntry(System.currentTimeMillis(), new LongDataEntry("telemetry", (long) i))); } DeviceTypeFilter filter = new DeviceTypeFilter(); filter.setDeviceTypes(List.of("default")); filter.setDeviceNameFilter("D%"); - EntityDataSortOrder sortOrder = new EntityDataSortOrder(new EntityKey(ENTITY_FIELD, "name"), EntityDataSortOrder.Direction.DESC); + EntityDataSortOrder sortOrder = new EntityDataSortOrder(new EntityKey(ATTRIBUTE, "telemetry"), EntityDataSortOrder.Direction.DESC); List deviceTypeFilters = createStringKeyFilters("type", ENTITY_FIELD, StringFilterPredicate.StringOperation.EQUAL, "default"); - KeyFilter createdTimeFilter = createNumericKeyFilter("createdTime", ENTITY_FIELD, NumericFilterPredicate.NumericOperation.GREATER, 1L); - List createdTimeFilters = Collections.singletonList(createdTimeFilter); + List attributeFilters = Collections.singletonList(createNumericKeyFilter("telemetry", ATTRIBUTE, NumericFilterPredicate.NumericOperation.LESS, expectedDevicesSize)); List nameFilters = createStringKeyFilters("name", ENTITY_FIELD, StringFilterPredicate.StringOperation.CONTAINS, "Device"); - List entityFields = Arrays.asList(new EntityKey(ENTITY_FIELD, "name"), - new EntityKey(ENTITY_FIELD, "type")); + List entityFields = Arrays.asList(new EntityKey(ENTITY_FIELD, "name"), new EntityKey(ENTITY_FIELD, "type")); // 1. Device type filters: @@ -2158,11 +2162,14 @@ public class EntityServiceTest extends AbstractServiceTest { EntityDataQuery optimizedQuery = new EntityDataQuery(filter, optimizedPageLink, entityFields, null, deviceTypeFilters); PageData optimizedData = entityService.findEntityDataByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), optimizedQuery); List loadedEntities = getLoadedEntities(optimizedData, optimizedQuery); - Assert.assertEquals(devices.size(), loadedEntities.size()); + Assert.assertEquals(expectedDevicesSize, loadedEntities.size()); + loadedEntities = getLoadedEntities(originalData, originalQuery); + Assert.assertEquals(expectedDevicesSize, loadedEntities.size()); + Assert.assertEquals(pageSize, optimizedData.getData().size()); - for (int i = 0; i < devices.size(); i++) { - var originalElement = originalData.getData().get(i); - var optimizedElement = optimizedData.getData().get(i); + for (int i = 0; i < pageSize; i++) { + EntityData originalElement = originalData.getData().get(i); + EntityData optimizedElement = optimizedData.getData().get(i); Assert.assertEquals(originalElement.getEntityId(), optimizedElement.getEntityId()); originalElement.getLatest().get(ENTITY_FIELD).forEach((key, value) -> { Assert.assertEquals(value.getValue(), optimizedElement.getLatest().get(EntityKeyType.ENTITY_FIELD).get(key).getValue()); @@ -2176,19 +2183,22 @@ public class EntityServiceTest extends AbstractServiceTest { // query with textSearch - optimization is not performing originalPageLink = new EntityDataPageLink(pageSize, 0, "Device", sortOrder); - originalQuery = new EntityDataQuery(filter, originalPageLink, entityFields, null, createdTimeFilters); + originalQuery = new EntityDataQuery(filter, originalPageLink, entityFields, null, attributeFilters); originalData = entityService.findEntityDataByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), originalQuery); // query without textSearch - optimization is performing optimizedPageLink = new EntityDataPageLink(pageSize, 0, null, sortOrder); - optimizedQuery = new EntityDataQuery(filter, optimizedPageLink, entityFields, null, createdTimeFilters); + optimizedQuery = new EntityDataQuery(filter, optimizedPageLink, entityFields, null, attributeFilters); optimizedData = entityService.findEntityDataByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), optimizedQuery); loadedEntities = getLoadedEntities(optimizedData, optimizedQuery); - Assert.assertEquals(devices.size(), loadedEntities.size()); + Assert.assertEquals(expectedDevicesSize, loadedEntities.size()); + loadedEntities = getLoadedEntities(originalData, originalQuery); + Assert.assertEquals(expectedDevicesSize, loadedEntities.size()); + Assert.assertEquals(pageSize, optimizedData.getData().size()); - for (int i = 0; i < devices.size(); i++) { - var originalElement = originalData.getData().get(i); - var optimizedElement = optimizedData.getData().get(i); + for (int i = 0; i < pageSize; i++) { + EntityData originalElement = originalData.getData().get(i); + EntityData optimizedElement = optimizedData.getData().get(i); Assert.assertEquals(originalElement.getEntityId(), optimizedElement.getEntityId()); originalElement.getLatest().get(ENTITY_FIELD).forEach((key, value) -> { Assert.assertEquals(value.getValue(), optimizedElement.getLatest().get(EntityKeyType.ENTITY_FIELD).get(key).getValue()); @@ -2210,11 +2220,14 @@ public class EntityServiceTest extends AbstractServiceTest { optimizedQuery = new EntityDataQuery(filter, optimizedPageLink, entityFields, null, nameFilters); optimizedData = entityService.findEntityDataByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), optimizedQuery); loadedEntities = getLoadedEntities(optimizedData, optimizedQuery); - Assert.assertEquals(devices.size(), loadedEntities.size()); + Assert.assertEquals(expectedDevicesSize, loadedEntities.size()); + loadedEntities = getLoadedEntities(originalData, originalQuery); + Assert.assertEquals(expectedDevicesSize, loadedEntities.size()); + Assert.assertEquals(pageSize, optimizedData.getData().size()); - for (int i = 0; i < devices.size(); i++) { - var originalElement = originalData.getData().get(i); - var optimizedElement = optimizedData.getData().get(i); + for (int i = 0; i < pageSize; i++) { + EntityData originalElement = originalData.getData().get(i); + EntityData optimizedElement = optimizedData.getData().get(i); Assert.assertEquals(originalElement.getEntityId(), optimizedElement.getEntityId()); originalElement.getLatest().get(ENTITY_FIELD).forEach((key, value) -> { Assert.assertEquals(value.getValue(), optimizedElement.getLatest().get(EntityKeyType.ENTITY_FIELD).get(key).getValue()); From 9e1c195e599087e733c79c2773e97af76ad22364 Mon Sep 17 00:00:00 2001 From: Andrii Landiak Date: Thu, 4 Apr 2024 16:06:21 +0300 Subject: [PATCH 26/80] Fix comment --- .../org/thingsboard/server/dao/service/EntityServiceTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/EntityServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/EntityServiceTest.java index f1efe9f730..d9b94fb26b 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/EntityServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/EntityServiceTest.java @@ -2179,7 +2179,7 @@ public class EntityServiceTest extends AbstractServiceTest { Assert.assertEquals(originalData.getTotalPages(), optimizedData.getTotalPages()); Assert.assertEquals(originalData.getTotalElements(), optimizedData.getTotalElements()); - // 2. Device create time filters + // 2. Device attribute filters // query with textSearch - optimization is not performing originalPageLink = new EntityDataPageLink(pageSize, 0, "Device", sortOrder); From 641008c262a10a69b08facf509965e9a8204fe55 Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Mon, 8 Apr 2024 14:00:00 +0300 Subject: [PATCH 27/80] onClick action from mobile notification center --- .../MobileAppNotificationChannel.java | 22 +++++++++++++------ .../thingsboard/common/util/JacksonUtil.java | 4 ++++ 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/notification/channels/MobileAppNotificationChannel.java b/application/src/main/java/org/thingsboard/server/service/notification/channels/MobileAppNotificationChannel.java index 116e0237a9..f796e2b440 100644 --- a/application/src/main/java/org/thingsboard/server/service/notification/channels/MobileAppNotificationChannel.java +++ b/application/src/main/java/org/thingsboard/server/service/notification/channels/MobileAppNotificationChannel.java @@ -16,6 +16,7 @@ package org.thingsboard.server.service.notification.channels; import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; import com.google.common.base.Strings; import com.google.firebase.messaging.FirebaseMessagingException; import com.google.firebase.messaging.MessagingErrorCode; @@ -60,6 +61,19 @@ public class MobileAppNotificationChannel implements NotificationChannel { data.put("stateEntityId", stateEntityId.getId().toString()); data.put("stateEntityType", stateEntityId.getEntityType().name()); - if (!"true".equals(data.get("onClick.enabled")) && info.getDashboardId() != null) { - data.put("onClick.enabled", "true"); - data.put("onClick.linkType", "DASHBOARD"); - data.put("onClick.setEntityIdInState", "true"); - data.put("onClick.dashboardId", info.getDashboardId().toString()); - } }); data.put("notificationType", ctx.getNotificationType().name()); switch (ctx.getNotificationType()) { diff --git a/common/util/src/main/java/org/thingsboard/common/util/JacksonUtil.java b/common/util/src/main/java/org/thingsboard/common/util/JacksonUtil.java index 53c3860dde..d71d97b1e4 100644 --- a/common/util/src/main/java/org/thingsboard/common/util/JacksonUtil.java +++ b/common/util/src/main/java/org/thingsboard/common/util/JacksonUtil.java @@ -231,6 +231,10 @@ public class JacksonUtil { return node; } + public static ObjectNode asObject(JsonNode node) { + return node != null && node.isObject() ? ((ObjectNode) node) : newObjectNode(); + } + public static void replaceUuidsRecursively(JsonNode node, Set skippedRootFields, Pattern includedFieldsPattern, UnaryOperator replacer, boolean root) { if (node == null) { return; From 140f8dc4891ba08ae069dc25de940ce63723c6ab Mon Sep 17 00:00:00 2001 From: dashevchenko Date: Mon, 8 Apr 2024 16:21:35 +0300 Subject: [PATCH 28/80] attribute scope is prepared correctly for rule engine message metadata --- .../server/service/action/EntityActionService.java | 9 +++++---- .../sync/ie/importing/csv/AbstractBulkImportService.java | 4 ++-- .../server/dao/audit/AuditLogServiceImpl.java | 9 +++++---- 3 files changed, 12 insertions(+), 10 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/action/EntityActionService.java b/application/src/main/java/org/thingsboard/server/service/action/EntityActionService.java index db051ca0da..447c993e55 100644 --- a/application/src/main/java/org/thingsboard/server/service/action/EntityActionService.java +++ b/application/src/main/java/org/thingsboard/server/service/action/EntityActionService.java @@ -22,6 +22,7 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.thingsboard.common.util.JacksonUtil; 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.HasName; @@ -127,20 +128,20 @@ public class EntityActionService { } else { entityNode = JacksonUtil.newObjectNode(); if (actionType == ActionType.ATTRIBUTES_UPDATED) { - String scope = extractParameter(String.class, 0, additionalInfo); + AttributeScope scope = extractParameter(AttributeScope.class, 0, additionalInfo); @SuppressWarnings("unchecked") List attributes = extractParameter(List.class, 1, additionalInfo); - metaData.putValue(DataConstants.SCOPE, scope); + metaData.putValue(DataConstants.SCOPE, scope.name()); if (attributes != null) { for (AttributeKvEntry attr : attributes) { JacksonUtil.addKvEntry(entityNode, attr); } } } else if (actionType == ActionType.ATTRIBUTES_DELETED) { - String scope = extractParameter(String.class, 0, additionalInfo); + AttributeScope scope = extractParameter(AttributeScope.class, 0, additionalInfo); @SuppressWarnings("unchecked") List keys = extractParameter(List.class, 1, additionalInfo); - metaData.putValue(DataConstants.SCOPE, scope); + metaData.putValue(DataConstants.SCOPE, scope.name()); ArrayNode attrsArrayNode = entityNode.putArray("attributes"); if (keys != null) { keys.forEach(attrsArrayNode::add); diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/csv/AbstractBulkImportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/csv/AbstractBulkImportService.java index d4c80f730a..909300a4b5 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/csv/AbstractBulkImportService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/csv/AbstractBulkImportService.java @@ -236,14 +236,14 @@ public abstract class AbstractBulkImportService attributes = extractParameter(List.class, 1, additionalInfo); - actionData.put("scope", scope); + actionData.put("scope", scope.name()); ObjectNode attrsNode = JacksonUtil.newObjectNode(); if (attributes != null) { for (AttributeKvEntry attr : attributes) { @@ -215,8 +216,8 @@ public class AuditLogServiceImpl implements AuditLogService { case ATTRIBUTES_DELETED: case ATTRIBUTES_READ: actionData.put("entityId", entityId.toString()); - scope = extractParameter(String.class, 0, additionalInfo); - actionData.put("scope", scope); + scope = extractParameter(AttributeScope.class, 0, additionalInfo); + actionData.put("scope", scope.name()); @SuppressWarnings("unchecked") List keys = extractParameter(List.class, 1, additionalInfo); ArrayNode attrsArrayNode = actionData.putArray("attributes"); From 4c984dc82c8b68cb53d954d9a973341ffd16bebb Mon Sep 17 00:00:00 2001 From: IrynaMatveieva Date: Mon, 8 Apr 2024 17:51:16 +0300 Subject: [PATCH 29/80] removed unnecessary method creation --- .../server/dao/rule/BaseRuleChainService.java | 22 ++++++++----------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java b/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java index a636ccf879..f1c279c4bd 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java @@ -118,20 +118,16 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC @Transactional public RuleChain saveRuleChain(RuleChain ruleChain, boolean publishSaveEvent) { ruleChainValidator.validate(ruleChain, RuleChain::getTenantId); - RuleChain savedRuleChain = saveRuleChainInternal(ruleChain); - if (ruleChain.getId() == null) { - entityCountService.publishCountEntityEvictEvent(ruleChain.getTenantId(), EntityType.RULE_CHAIN); - } - if (publishSaveEvent) { - eventPublisher.publishEvent(SaveEntityEvent.builder().tenantId(savedRuleChain.getTenantId()) - .entity(savedRuleChain).entityId(savedRuleChain.getId()).created(ruleChain.getId() == null).build()); - } - return savedRuleChain; - } - - private RuleChain saveRuleChainInternal(RuleChain ruleChain) { try { - return ruleChainDao.saveAndFlush(ruleChain.getTenantId(), ruleChain); + RuleChain savedRuleChain = ruleChainDao.saveAndFlush(ruleChain.getTenantId(), ruleChain); + if (ruleChain.getId() == null) { + entityCountService.publishCountEntityEvictEvent(ruleChain.getTenantId(), EntityType.RULE_CHAIN); + } + if (publishSaveEvent) { + eventPublisher.publishEvent(SaveEntityEvent.builder().tenantId(savedRuleChain.getTenantId()) + .entity(savedRuleChain).entityId(savedRuleChain.getId()).created(ruleChain.getId() == null).build()); + } + return savedRuleChain; } catch (Exception e) { checkConstraintViolation(e, "rule_chain_external_id_unq_key", "Rule Chain with such external id already exists!"); throw e; From c4ad105f73a6a1418e0e97641e005842a0e8ba3c Mon Sep 17 00:00:00 2001 From: IrynaMatveieva Date: Tue, 9 Apr 2024 14:56:34 +0300 Subject: [PATCH 30/80] used Optional.ofNullable instead of explicitly checking for null --- .../thingsboard/server/controller/RuleChainController.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/controller/RuleChainController.java b/application/src/main/java/org/thingsboard/server/controller/RuleChainController.java index 29e1bc9172..a378467d3f 100644 --- a/application/src/main/java/org/thingsboard/server/controller/RuleChainController.java +++ b/application/src/main/java/org/thingsboard/server/controller/RuleChainController.java @@ -46,6 +46,7 @@ import org.thingsboard.server.common.data.EventInfo; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.exception.ThingsboardException; +import org.thingsboard.server.common.data.id.AssetProfileId; import org.thingsboard.server.common.data.id.EdgeId; import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.RuleNodeId; @@ -76,6 +77,7 @@ import org.thingsboard.server.service.security.permission.Resource; import java.util.ArrayList; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.Set; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.TimeUnit; @@ -339,8 +341,8 @@ public class RuleChainController extends BaseController { RuleNodeId ruleNodeId = new RuleNodeId(toUUID(strRuleNodeId)); checkRuleNode(ruleNodeId, Operation.READ); TenantId tenantId = getCurrentUser().getTenantId(); - EventInfo eventInfo = eventService.findLatestDebugRuleNodeInEvent(tenantId, ruleNodeId); - return eventInfo == null ? null : eventInfo.getBody(); + return Optional.ofNullable(eventService.findLatestDebugRuleNodeInEvent(tenantId, ruleNodeId)) + .map(EventInfo::getBody).orElse(null); } @ApiOperation(value = "Is TBEL script executor enabled", From d152e0b24bd2b05f6360f8455174137df0b4bf2f Mon Sep 17 00:00:00 2001 From: IrynaMatveieva Date: Tue, 9 Apr 2024 15:18:47 +0300 Subject: [PATCH 31/80] moved tests to RuleChainServiceTest class --- .../dao/rule/BaseRuleChainServiceTest.java | 84 ------------------- .../dao/service/RuleChainServiceTest.java | 34 ++++++++ 2 files changed, 34 insertions(+), 84 deletions(-) delete mode 100644 dao/src/test/java/org/thingsboard/server/dao/rule/BaseRuleChainServiceTest.java diff --git a/dao/src/test/java/org/thingsboard/server/dao/rule/BaseRuleChainServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/rule/BaseRuleChainServiceTest.java deleted file mode 100644 index b1dad8e4ed..0000000000 --- a/dao/src/test/java/org/thingsboard/server/dao/rule/BaseRuleChainServiceTest.java +++ /dev/null @@ -1,84 +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.dao.rule; - -import org.assertj.core.api.Assertions; -import org.junit.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.thingsboard.common.util.JacksonUtil; -import org.thingsboard.server.common.data.id.RuleChainId; -import org.thingsboard.server.common.data.rule.RuleChain; -import org.thingsboard.server.dao.exception.DataValidationException; -import org.thingsboard.server.dao.service.AbstractServiceTest; -import org.thingsboard.server.dao.service.DaoSqlTest; - -import java.util.UUID; - -@DaoSqlTest -public class BaseRuleChainServiceTest extends AbstractServiceTest { - - @Autowired - private BaseRuleChainService ruleChainService; - - @Test - public void givenRuleChain_whenSave_thenReturnsSavedRuleChain() { - RuleChain ruleChain = getRuleChain(); - ruleChain.setTenantId(tenantId); - RuleChain savedRuleChain = ruleChainService.saveRuleChain(ruleChain); - - Assertions.assertThat(savedRuleChain).isNotNull(); - Assertions.assertThat(savedRuleChain.getId()).isNotNull(); - Assertions.assertThat(savedRuleChain.getCreatedTime() > 0).isTrue(); - Assertions.assertThat(ruleChain.getTenantId()).isEqualTo(savedRuleChain.getTenantId()); - - RuleChain foundRuleChain = ruleChainService.findRuleChainById(tenantId, savedRuleChain.getId()); - Assertions.assertThat(savedRuleChain.getName()).isEqualTo(foundRuleChain.getName()); - - ruleChainService.deleteRuleChainsByTenantId(tenantId); - } - - @Test - public void givenRuleChainWithExistingExternalId_whenSave_thenThrowsException() { - RuleChainId externalRuleChainId = new RuleChainId(UUID.fromString("2675d180-e1e5-11ee-9f06-71b6c7dc2cbf")); - - RuleChain ruleChain = getRuleChain(); - ruleChain.setTenantId(tenantId); - ruleChain.setExternalId(externalRuleChainId); - ruleChainService.saveRuleChain(ruleChain); - - Assertions.assertThatThrownBy(() -> ruleChainService.saveRuleChain(ruleChain)) - .isInstanceOf(DataValidationException.class) - .hasMessage("Rule Chain with such external id already exists!"); - - ruleChainService.deleteRuleChainsByTenantId(tenantId); - } - - private RuleChain getRuleChain() { - String ruleChainStr = "{\n" + - " \"name\": \"Root Rule Chain\",\n" + - " \"type\": \"CORE\",\n" + - " \"firstRuleNodeId\": {\n" + - " \"entityType\": \"RULE_NODE\",\n" + - " \"id\": \"91ad0b00-e779-11ee-9cf0-15d8b6079fdb\"\n" + - " },\n" + - " \"debugMode\": false,\n" + - " \"configuration\": null,\n" + - " \"additionalInfo\": null\n" + - "}"; - return JacksonUtil.fromString(ruleChainStr, RuleChain.class); - } - -} diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/RuleChainServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/RuleChainServiceTest.java index 50f46020d8..86b76bd5a5 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/RuleChainServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/RuleChainServiceTest.java @@ -40,8 +40,11 @@ import java.io.IOException; import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.UUID; import java.util.function.Function; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + /** * Created by igor on 3/13/18. */ @@ -560,4 +563,35 @@ public class RuleChainServiceTest extends AbstractServiceTest { Assert.assertFalse(pageData.hasNext()); Assert.assertEquals(0, pageData.getData().size()); } + + @Test + public void testSaveRuleChainWithExistingExternalId() { + RuleChainId externalRuleChainId = new RuleChainId(UUID.fromString("2675d180-e1e5-11ee-9f06-71b6c7dc2cbf")); + + RuleChain ruleChain = getRuleChain(); + ruleChain.setTenantId(tenantId); + ruleChain.setExternalId(externalRuleChainId); + ruleChainService.saveRuleChain(ruleChain); + + assertThatThrownBy(() -> ruleChainService.saveRuleChain(ruleChain)) + .isInstanceOf(DataValidationException.class) + .hasMessage("Rule Chain with such external id already exists!"); + + ruleChainService.deleteRuleChainsByTenantId(tenantId); + } + + private RuleChain getRuleChain() { + String ruleChainStr = "{\n" + + " \"name\": \"Root Rule Chain\",\n" + + " \"type\": \"CORE\",\n" + + " \"firstRuleNodeId\": {\n" + + " \"entityType\": \"RULE_NODE\",\n" + + " \"id\": \"91ad0b00-e779-11ee-9cf0-15d8b6079fdb\"\n" + + " },\n" + + " \"debugMode\": false,\n" + + " \"configuration\": null,\n" + + " \"additionalInfo\": null\n" + + "}"; + return JacksonUtil.fromString(ruleChainStr, RuleChain.class); + } } From d041faae89b4787dcff7cea8c69462e072d57306 Mon Sep 17 00:00:00 2001 From: Andrii Landiak Date: Tue, 9 Apr 2024 15:42:54 +0300 Subject: [PATCH 32/80] Micro refactoring: using java 17 --- .../server/actors/ActorSystemContext.java | 5 +- .../device/DeviceActorMessageProcessor.java | 45 ++------------ .../controller/AbstractRpcController.java | 12 +--- .../controller/TelemetryController.java | 10 ++- .../service/action/EntityActionService.java | 2 +- .../service/edge/rpc/EdgeGrpcSession.java | 3 +- .../BaseMsgConstructorFactory.java | 16 ++--- .../telemetry/BaseTelemetryProcessor.java | 42 +++++-------- .../service/security/AccessValidator.java | 62 +++++-------------- .../csv/AbstractBulkImportService.java | 19 +++--- .../thingsboard/edge/rpc/EdgeGrpcClient.java | 1 + .../server/common/util/ProtoUtils.java | 20 +++--- .../server/dao/edge/EdgeServiceImpl.java | 17 +++-- .../rest/client/utils/RestJsonConverter.java | 2 +- .../engine/edge/AbstractTbMsgPushNode.java | 14 ++--- .../edge/BaseTbMsgPushNodeConfiguration.java | 1 + .../TbMsgPushToCloudNodeConfiguration.java | 5 +- .../rule/engine/edge/TbMsgPushToEdgeNode.java | 1 + .../TbMsgPushToEdgeNodeConfiguration.java | 5 +- 19 files changed, 94 insertions(+), 188 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java b/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java index 640d47362a..f411344e3f 100644 --- a/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java +++ b/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.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.Nullable; +import jakarta.annotation.PostConstruct; import lombok.Getter; import lombok.Setter; import lombok.extern.slf4j.Slf4j; @@ -116,9 +118,6 @@ import org.thingsboard.server.service.telemetry.AlarmSubscriptionService; import org.thingsboard.server.service.telemetry.TelemetrySubscriptionService; import org.thingsboard.server.service.transport.TbCoreToTransportService; -import jakarta.annotation.Nullable; -import jakarta.annotation.PostConstruct; - import java.io.PrintWriter; import java.io.StringWriter; import java.util.concurrent.ConcurrentHashMap; diff --git a/application/src/main/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessor.java b/application/src/main/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessor.java index 51205e97c9..341fa8765e 100644 --- a/application/src/main/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessor.java +++ b/application/src/main/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessor.java @@ -43,7 +43,6 @@ import org.thingsboard.server.common.data.id.RpcId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.kv.AttributeKey; import org.thingsboard.server.common.data.kv.AttributeKvEntry; -import org.thingsboard.server.common.data.kv.KvEntry; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.page.SortOrder; @@ -74,8 +73,6 @@ import org.thingsboard.server.gen.transport.TransportProtos.ClaimDeviceMsg; import org.thingsboard.server.gen.transport.TransportProtos.DeviceSessionsCacheEntry; import org.thingsboard.server.gen.transport.TransportProtos.GetAttributeRequestMsg; import org.thingsboard.server.gen.transport.TransportProtos.GetAttributeResponseMsg; -import org.thingsboard.server.gen.transport.TransportProtos.KeyValueProto; -import org.thingsboard.server.gen.transport.TransportProtos.KeyValueType; import org.thingsboard.server.gen.transport.TransportProtos.SessionCloseNotificationProto; import org.thingsboard.server.gen.transport.TransportProtos.SessionEvent; import org.thingsboard.server.gen.transport.TransportProtos.SessionEventMsg; @@ -254,14 +251,12 @@ public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcesso } private boolean isSendNewRpcAvailable() { - switch (rpcSubmitStrategy) { - case SEQUENTIAL_ON_ACK_FROM_DEVICE: - return toDeviceRpcPendingMap.values().stream().filter(md -> !md.isDelivered()).findAny().isEmpty(); - case SEQUENTIAL_ON_RESPONSE_FROM_DEVICE: - return toDeviceRpcPendingMap.values().stream().filter(ToDeviceRpcRequestMetadata::isDelivered).findAny().isEmpty(); - default: - return true; - } + return switch (rpcSubmitStrategy) { + case SEQUENTIAL_ON_ACK_FROM_DEVICE -> toDeviceRpcPendingMap.values().stream().filter(md -> !md.isDelivered()).findAny().isEmpty(); + case SEQUENTIAL_ON_RESPONSE_FROM_DEVICE -> + toDeviceRpcPendingMap.values().stream().filter(ToDeviceRpcRequestMetadata::isDelivered).findAny().isEmpty(); + default -> true; + }; } private void createRpc(ToDeviceRpcRequest request, RpcStatus status) { @@ -927,34 +922,6 @@ public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcesso return systemContext.getEdgeEventService().saveAsync(edgeEvent); } - private KeyValueProto toKeyValueProto(KvEntry kvEntry) { - KeyValueProto.Builder builder = KeyValueProto.newBuilder(); - builder.setKey(kvEntry.getKey()); - switch (kvEntry.getDataType()) { - case BOOLEAN: - builder.setType(KeyValueType.BOOLEAN_V); - builder.setBoolV(kvEntry.getBooleanValue().get()); - break; - case DOUBLE: - builder.setType(KeyValueType.DOUBLE_V); - builder.setDoubleV(kvEntry.getDoubleValue().get()); - break; - case LONG: - builder.setType(KeyValueType.LONG_V); - builder.setLongV(kvEntry.getLongValue().get()); - break; - case STRING: - builder.setType(KeyValueType.STRING_V); - builder.setStringV(kvEntry.getStrValue().get()); - break; - case JSON: - builder.setType(KeyValueType.JSON_V); - builder.setJsonV(kvEntry.getJsonValue().get()); - break; - } - return builder.build(); - } - void restoreSessions() { if (systemContext.isLocalCacheType()) { return; diff --git a/application/src/main/java/org/thingsboard/server/controller/AbstractRpcController.java b/application/src/main/java/org/thingsboard/server/controller/AbstractRpcController.java index 93e8c0be90..3df3d6887a 100644 --- a/application/src/main/java/org/thingsboard/server/controller/AbstractRpcController.java +++ b/application/src/main/java/org/thingsboard/server/controller/AbstractRpcController.java @@ -122,15 +122,9 @@ public abstract class AbstractRpcController extends BaseController { logRpcCall(rpcRequest, rpcError, null); RpcError error = rpcError.get(); switch (error) { - case TIMEOUT: - responseWriter.setResult(new ResponseEntity<>(timeoutStatus)); - break; - case NO_ACTIVE_CONNECTION: - responseWriter.setResult(new ResponseEntity<>(noActiveConnectionStatus)); - break; - default: - responseWriter.setResult(new ResponseEntity<>(timeoutStatus)); - break; + case TIMEOUT -> responseWriter.setResult(new ResponseEntity<>(timeoutStatus)); + case NO_ACTIVE_CONNECTION -> responseWriter.setResult(new ResponseEntity<>(noActiveConnectionStatus)); + default -> responseWriter.setResult(new ResponseEntity<>(timeoutStatus)); } } else { Optional responseData = response.getResponse(); diff --git a/application/src/main/java/org/thingsboard/server/controller/TelemetryController.java b/application/src/main/java/org/thingsboard/server/controller/TelemetryController.java index c73ac09cf1..65bcc2c969 100644 --- a/application/src/main/java/org/thingsboard/server/controller/TelemetryController.java +++ b/application/src/main/java/org/thingsboard/server/controller/TelemetryController.java @@ -25,7 +25,6 @@ import com.google.gson.JsonElement; import com.google.gson.JsonParseException; import com.google.gson.JsonParser; import io.swagger.v3.oas.annotations.Parameter; -import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponses; @@ -36,7 +35,6 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.http.HttpStatus; -import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.PathVariable; @@ -49,9 +47,6 @@ import org.springframework.web.bind.annotation.RestController; import org.springframework.web.context.request.async.DeferredResult; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.ThingsBoardThreadFactory; -import org.thingsboard.server.common.data.kv.AggregationParams; -import org.thingsboard.server.common.data.kv.IntervalType; -import org.thingsboard.server.common.msg.rule.engine.DeviceAttributesEventNotificationMsg; import org.thingsboard.server.common.adaptor.JsonConverter; import org.thingsboard.server.common.data.AttributeScope; import org.thingsboard.server.common.data.EntityType; @@ -65,6 +60,7 @@ import org.thingsboard.server.common.data.id.EntityIdFactory; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.UUIDBased; import org.thingsboard.server.common.data.kv.Aggregation; +import org.thingsboard.server.common.data.kv.AggregationParams; import org.thingsboard.server.common.data.kv.AttributeKvEntry; import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry; import org.thingsboard.server.common.data.kv.BaseDeleteTsKvQuery; @@ -74,6 +70,7 @@ import org.thingsboard.server.common.data.kv.BooleanDataEntry; import org.thingsboard.server.common.data.kv.DataType; import org.thingsboard.server.common.data.kv.DeleteTsKvQuery; import org.thingsboard.server.common.data.kv.DoubleDataEntry; +import org.thingsboard.server.common.data.kv.IntervalType; import org.thingsboard.server.common.data.kv.JsonDataEntry; import org.thingsboard.server.common.data.kv.KvEntry; import org.thingsboard.server.common.data.kv.LongDataEntry; @@ -379,7 +376,7 @@ public class TelemetryController extends BaseController { public DeferredResult saveEntityAttributesV1( @Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true, schema = @Schema(defaultValue = "DEVICE")) @PathVariable("entityType") String entityType, @Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @PathVariable("entityId") String entityIdStr, - @Parameter(description = ATTRIBUTES_SCOPE_DESCRIPTION, schema = @Schema(allowableValues = {"SERVER_SCOPE", "SHARED_SCOPE"})) @PathVariable("scope")AttributeScope scope, + @Parameter(description = ATTRIBUTES_SCOPE_DESCRIPTION, schema = @Schema(allowableValues = {"SERVER_SCOPE", "SHARED_SCOPE"})) @PathVariable("scope") AttributeScope scope, @io.swagger.v3.oas.annotations.parameters.RequestBody(description = ATTRIBUTES_JSON_REQUEST_DESCRIPTION, required = true) @RequestBody JsonNode request) throws ThingsboardException { EntityId entityId = EntityIdFactory.getByTypeAndId(entityType, entityIdStr); return saveAttributes(getTenantId(), entityId, scope, request); @@ -918,4 +915,5 @@ public class TelemetryController extends BaseController { } return entry.getValue(); } + } diff --git a/application/src/main/java/org/thingsboard/server/service/action/EntityActionService.java b/application/src/main/java/org/thingsboard/server/service/action/EntityActionService.java index 447c993e55..ab6d32fb44 100644 --- a/application/src/main/java/org/thingsboard/server/service/action/EntityActionService.java +++ b/application/src/main/java/org/thingsboard/server/service/action/EntityActionService.java @@ -250,7 +250,7 @@ public class EntityActionService { return result; } - private void addTimeseries(ObjectNode entityNode, List timeseries) throws Exception { + private void addTimeseries(ObjectNode entityNode, List timeseries) { if (timeseries != null && !timeseries.isEmpty()) { ArrayNode result = entityNode.putArray("timeseries"); Map> groupedTelemetry = timeseries.stream() 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 75bb8e10e9..7d0a1aaa92 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 @@ -557,8 +557,7 @@ public final class EdgeGrpcSession implements Closeable { } case ATTRIBUTES_UPDATED, POST_ATTRIBUTES, ATTRIBUTES_DELETED, TIMESERIES_UPDATED -> downlinkMsg = ctx.getTelemetryProcessor().convertTelemetryEventToDownlink(edge, edgeEvent); - default -> - log.warn("[{}][{}] Unsupported action type [{}]", this.tenantId, this.sessionId, edgeEvent.getAction()); + default -> log.warn("[{}][{}] Unsupported action type [{}]", this.tenantId, this.sessionId, edgeEvent.getAction()); } } catch (Exception e) { log.error("[{}][{}] Exception during converting edge event to downlink msg", this.tenantId, this.sessionId, e); diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/BaseMsgConstructorFactory.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/BaseMsgConstructorFactory.java index 5253e5ac33..dd76f97d8f 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/BaseMsgConstructorFactory.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/BaseMsgConstructorFactory.java @@ -31,16 +31,10 @@ public abstract class BaseMsgConstructorFactory v1Constructor; + default -> v2Constructor; + }; } + } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/telemetry/BaseTelemetryProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/telemetry/BaseTelemetryProcessor.java index 2a9af53807..ae8c851a85 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/telemetry/BaseTelemetryProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/telemetry/BaseTelemetryProcessor.java @@ -23,10 +23,13 @@ import com.google.common.util.concurrent.SettableFuture; import com.google.gson.Gson; import com.google.gson.JsonObject; import com.google.gson.JsonParser; +import jakarta.annotation.Nullable; +import jakarta.annotation.PostConstruct; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.tuple.ImmutablePair; import org.apache.commons.lang3.tuple.Pair; import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.common.adaptor.JsonConverter; import org.thingsboard.server.common.data.AttributeScope; import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.Device; @@ -53,7 +56,6 @@ import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; -import org.thingsboard.server.common.adaptor.JsonConverter; import org.thingsboard.server.common.msg.rule.engine.DeviceAttributesEventNotificationMsg; import org.thingsboard.server.common.transport.util.JsonUtils; import org.thingsboard.server.dao.model.ModelConstants; @@ -66,8 +68,6 @@ import org.thingsboard.server.queue.TbQueueProducer; import org.thingsboard.server.queue.common.TbProtoQueueMsg; import org.thingsboard.server.service.edge.rpc.processor.BaseEdgeProcessor; -import jakarta.annotation.Nullable; -import jakarta.annotation.PostConstruct; import java.util.ArrayList; import java.util.List; import java.util.UUID; @@ -317,36 +317,22 @@ public abstract class BaseTelemetryProcessor extends BaseEdgeProcessor { JsonNode body) { EntityId entityId; switch (entityType) { - case DEVICE: - entityId = new DeviceId(entityUUID); - break; - case ASSET: - entityId = new AssetId(entityUUID); - break; - case ENTITY_VIEW: - entityId = new EntityViewId(entityUUID); - break; - case DASHBOARD: - entityId = new DashboardId(entityUUID); - break; - case TENANT: - entityId = TenantId.fromUUID(entityUUID); - break; - case CUSTOMER: - entityId = new CustomerId(entityUUID); - break; - case USER: - entityId = new UserId(entityUUID); - break; - case EDGE: - entityId = new EdgeId(entityUUID); - break; - default: + case DEVICE -> entityId = new DeviceId(entityUUID); + case ASSET -> entityId = new AssetId(entityUUID); + case ENTITY_VIEW -> entityId = new EntityViewId(entityUUID); + case DASHBOARD -> entityId = new DashboardId(entityUUID); + case TENANT -> entityId = TenantId.fromUUID(entityUUID); + case CUSTOMER -> entityId = new CustomerId(entityUUID); + case USER -> entityId = new UserId(entityUUID); + case EDGE -> entityId = new EdgeId(entityUUID); + default -> { log.warn("[{}] Unsupported edge event type [{}]", tenantId, entityType); return null; + } } String bodyJackson = JacksonUtil.toString(body); return bodyJackson == null ? null : entityDataMsgConstructor.constructEntityDataMsg(tenantId, entityId, actionType, JsonParser.parseString(bodyJackson)); } + } diff --git a/application/src/main/java/org/thingsboard/server/service/security/AccessValidator.java b/application/src/main/java/org/thingsboard/server/service/security/AccessValidator.java index 1542f4095e..ebbd29e589 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/AccessValidator.java +++ b/application/src/main/java/org/thingsboard/server/service/security/AccessValidator.java @@ -208,52 +208,22 @@ public class AccessValidator { public void validate(SecurityUser currentUser, Operation operation, EntityId entityId, FutureCallback callback) { switch (entityId.getEntityType()) { - case DEVICE: - validateDevice(currentUser, operation, entityId, callback); - return; - case DEVICE_PROFILE: - validateDeviceProfile(currentUser, operation, entityId, callback); - return; - case ASSET: - validateAsset(currentUser, operation, entityId, callback); - return; - case ASSET_PROFILE: - validateAssetProfile(currentUser, operation, entityId, callback); - return; - case RULE_CHAIN: - validateRuleChain(currentUser, operation, entityId, callback); - return; - case CUSTOMER: - validateCustomer(currentUser, operation, entityId, callback); - return; - case TENANT: - validateTenant(currentUser, operation, entityId, callback); - return; - case TENANT_PROFILE: - validateTenantProfile(currentUser, operation, entityId, callback); - return; - case USER: - validateUser(currentUser, operation, entityId, callback); - return; - case ENTITY_VIEW: - validateEntityView(currentUser, operation, entityId, callback); - return; - case EDGE: - validateEdge(currentUser, operation, entityId, callback); - return; - case API_USAGE_STATE: - validateApiUsageState(currentUser, operation, entityId, callback); - return; - case TB_RESOURCE: - validateResource(currentUser, operation, entityId, callback); - return; - case OTA_PACKAGE: - validateOtaPackage(currentUser, operation, entityId, callback); - return; - case RPC: - validateRpc(currentUser, operation, entityId, callback); - return; - default: + case DEVICE -> validateDevice(currentUser, operation, entityId, callback); + case DEVICE_PROFILE -> validateDeviceProfile(currentUser, operation, entityId, callback); + case ASSET -> validateAsset(currentUser, operation, entityId, callback); + case ASSET_PROFILE -> validateAssetProfile(currentUser, operation, entityId, callback); + case RULE_CHAIN -> validateRuleChain(currentUser, operation, entityId, callback); + case CUSTOMER -> validateCustomer(currentUser, operation, entityId, callback); + case TENANT -> validateTenant(currentUser, operation, entityId, callback); + case TENANT_PROFILE -> validateTenantProfile(currentUser, operation, entityId, callback); + case USER -> validateUser(currentUser, operation, entityId, callback); + case ENTITY_VIEW -> validateEntityView(currentUser, operation, entityId, callback); + case EDGE -> validateEdge(currentUser, operation, entityId, callback); + case API_USAGE_STATE -> validateApiUsageState(currentUser, operation, entityId, callback); + case TB_RESOURCE -> validateResource(currentUser, operation, entityId, callback); + case OTA_PACKAGE -> validateOtaPackage(currentUser, operation, entityId, callback); + case RPC -> validateRpc(currentUser, operation, entityId, callback); + default -> //TODO: add support of other entities throw new IllegalStateException("Not Implemented!"); } diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/csv/AbstractBulkImportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/csv/AbstractBulkImportService.java index 909300a4b5..3890e5791d 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/csv/AbstractBulkImportService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/csv/AbstractBulkImportService.java @@ -301,18 +301,13 @@ public abstract class AbstractBulkImportService new JsonPrimitive((String) value); + case LONG -> new JsonPrimitive((Long) value); + case DOUBLE -> new JsonPrimitive((Double) value); + case BOOLEAN -> new JsonPrimitive((Boolean) value); + default -> null; + }; } public String stringValue() { diff --git a/common/edge-api/src/main/java/org/thingsboard/edge/rpc/EdgeGrpcClient.java b/common/edge-api/src/main/java/org/thingsboard/edge/rpc/EdgeGrpcClient.java index 7219613e9b..23bfe55f1b 100644 --- a/common/edge-api/src/main/java/org/thingsboard/edge/rpc/EdgeGrpcClient.java +++ b/common/edge-api/src/main/java/org/thingsboard/edge/rpc/EdgeGrpcClient.java @@ -248,4 +248,5 @@ public class EdgeGrpcClient implements EdgeRpcClient { uplinkMsgLock.unlock(); } } + } 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 38f84bb4a1..e89ce5634c 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 @@ -258,31 +258,31 @@ public class ProtoUtils { .setLastUpdateTs(attributeKvEntry.getLastUpdateTs()) .setKey(attributeKvEntry.getKey()); switch (attributeKvEntry.getDataType()) { - case BOOLEAN: + case BOOLEAN -> { attributeKvEntry.getBooleanValue().ifPresent(attributeValueBuilder::setBoolV); attributeValueBuilder.setHasV(attributeKvEntry.getBooleanValue().isPresent()); attributeValueBuilder.setType(TransportProtos.KeyValueType.BOOLEAN_V); - break; - case STRING: + } + case STRING -> { attributeKvEntry.getStrValue().ifPresent(attributeValueBuilder::setStringV); attributeValueBuilder.setHasV(attributeKvEntry.getStrValue().isPresent()); attributeValueBuilder.setType(TransportProtos.KeyValueType.STRING_V); - break; - case DOUBLE: + } + case DOUBLE -> { attributeKvEntry.getDoubleValue().ifPresent(attributeValueBuilder::setDoubleV); attributeValueBuilder.setHasV(attributeKvEntry.getDoubleValue().isPresent()); attributeValueBuilder.setType(TransportProtos.KeyValueType.DOUBLE_V); - break; - case LONG: + } + case LONG -> { attributeKvEntry.getLongValue().ifPresent(attributeValueBuilder::setLongV); attributeValueBuilder.setHasV(attributeKvEntry.getLongValue().isPresent()); attributeValueBuilder.setType(TransportProtos.KeyValueType.LONG_V); - break; - case JSON: + } + case JSON -> { attributeKvEntry.getJsonValue().ifPresent(attributeValueBuilder::setJsonV); attributeValueBuilder.setHasV(attributeKvEntry.getJsonValue().isPresent()); attributeValueBuilder.setType(TransportProtos.KeyValueType.JSON_V); - break; + } } builder.addValues(attributeValueBuilder.build()); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/edge/EdgeServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/edge/EdgeServiceImpl.java index c1eb51be95..94aa3380ee 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/edge/EdgeServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/edge/EdgeServiceImpl.java @@ -21,6 +21,7 @@ import com.google.common.base.Function; 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 lombok.Getter; import lombok.extern.slf4j.Slf4j; import org.hibernate.exception.ConstraintViolationException; @@ -31,7 +32,7 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.event.TransactionalEventListener; import org.thingsboard.common.util.JacksonUtil; -import org.thingsboard.server.common.data.DataConstants; +import org.thingsboard.server.common.data.AttributeScope; import org.thingsboard.server.common.data.EntitySubtype; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.StringUtils; @@ -71,7 +72,6 @@ import org.thingsboard.server.dao.service.Validator; import org.thingsboard.server.dao.timeseries.TimeseriesService; import org.thingsboard.server.dao.user.UserService; -import jakarta.annotation.Nullable; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; @@ -354,7 +354,7 @@ public class EdgeServiceImpl extends AbstractCachedEntityService, List>() { + edges = Futures.transform(edges, new Function<>() { @Nullable @Override public List apply(@Nullable List edgeList) { @@ -414,7 +414,7 @@ public class EdgeServiceImpl extends AbstractCachedEntityService tenantEdgesRemover = - new PaginatedRemover() { + new PaginatedRemover<>() { @Override protected PageData findEntities(TenantId tenantId, TenantId id, PageLink pageLink) { @@ -427,7 +427,7 @@ public class EdgeServiceImpl extends AbstractCachedEntityService customerEdgeUnassigner = new PaginatedRemover() { + private PaginatedRemover customerEdgeUnassigner = new PaginatedRemover<>() { @Override protected PageData findEntities(TenantId tenantId, CustomerId id, PageLink pageLink) { @@ -513,7 +513,7 @@ public class EdgeServiceImpl extends AbstractCachedEntityService edgeRuleChains = findEdgeRuleChains(tenantId, edgeId); - List edgeRuleChainIds = edgeRuleChains.stream().map(IdBased::getId).collect(Collectors.toList()); + List edgeRuleChainIds = edgeRuleChains.stream().map(IdBased::getId).toList(); ObjectNode result = JacksonUtil.newObjectNode(); for (RuleChain edgeRuleChain : edgeRuleChains) { List ruleNodes = @@ -522,8 +522,7 @@ public class EdgeServiceImpl extends AbstractCachedEntityService connectedRuleChains = ruleNodes.stream() .filter(rn -> rn.getType().equals(tbRuleChainInputNodeClassName)) - .map(rn -> new RuleChainId(UUID.fromString(rn.getConfiguration().get("ruleChainId").asText()))) - .collect(Collectors.toList()); + .map(rn -> new RuleChainId(UUID.fromString(rn.getConfiguration().get("ruleChainId").asText()))).toList(); List missingRuleChains = new ArrayList<>(); for (RuleChainId connectedRuleChain : connectedRuleChains) { if (!edgeRuleChainIds.contains(connectedRuleChain)) { @@ -549,7 +548,7 @@ public class EdgeServiceImpl extends AbstractCachedEntityService Futures.immediateFuture(kvEntryOpt.flatMap(KvEntry::getBooleanValue).orElse(false)), MoreExecutors.directExecutor()); diff --git a/rest-client/src/main/java/org/thingsboard/rest/client/utils/RestJsonConverter.java b/rest-client/src/main/java/org/thingsboard/rest/client/utils/RestJsonConverter.java index 903983d00f..31a0c7b63b 100644 --- a/rest-client/src/main/java/org/thingsboard/rest/client/utils/RestJsonConverter.java +++ b/rest-client/src/main/java/org/thingsboard/rest/client/utils/RestJsonConverter.java @@ -62,7 +62,7 @@ public class RestJsonConverter { KvEntry entry = parseValue(key, ts.get(VALUE)); return new BasicTsKvEntry(ts.get(TS).asLong(), entry); } - ).collect(Collectors.toList())) + ).toList()) ); return result; } else { diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/edge/AbstractTbMsgPushNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/edge/AbstractTbMsgPushNode.java index 4176ead31c..73368d40f5 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/edge/AbstractTbMsgPushNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/edge/AbstractTbMsgPushNode.java @@ -87,24 +87,23 @@ public abstract class AbstractTbMsgPushNode entityBody = new HashMap<>(); JsonNode dataJson = JacksonUtil.toJsonNode(msg.getData()); switch (actionType) { - case ATTRIBUTES_UPDATED: - case POST_ATTRIBUTES: + case ATTRIBUTES_UPDATED, POST_ATTRIBUTES -> { entityBody.put("kv", dataJson); entityBody.put(SCOPE, getScope(metadata)); if (EdgeEventActionType.POST_ATTRIBUTES.equals(actionType)) { entityBody.put("isPostAttributes", true); } - break; - case ATTRIBUTES_DELETED: + } + case ATTRIBUTES_DELETED -> { List keys = JacksonUtil.convertValue(dataJson.get("attributes"), new TypeReference<>() { }); entityBody.put("keys", keys); entityBody.put(SCOPE, getScope(metadata)); - break; - case TIMESERIES_UPDATED: + } + case TIMESERIES_UPDATED -> { entityBody.put("data", dataJson); entityBody.put("ts", msg.getMetaDataTs()); - break; + } } return buildEvent(ctx.getTenantId(), actionType, @@ -179,4 +178,5 @@ public abstract class AbstractTbMsgPushNode Date: Wed, 10 Apr 2024 10:56:34 +0300 Subject: [PATCH 33/80] used Optional.ofNullable --- .../org/thingsboard/server/dao/event/BaseEventService.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/dao/src/main/java/org/thingsboard/server/dao/event/BaseEventService.java b/dao/src/main/java/org/thingsboard/server/dao/event/BaseEventService.java index fd5a55a167..5314dcd405 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/event/BaseEventService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/event/BaseEventService.java @@ -37,6 +37,7 @@ import org.thingsboard.server.common.data.page.TimePageLink; import org.thingsboard.server.dao.service.DataValidator; import java.util.List; +import java.util.Optional; import java.util.function.BiConsumer; import java.util.function.Function; import java.util.stream.Collectors; @@ -146,7 +147,7 @@ public class BaseEventService implements EventService { } private EventInfo convert(EntityType entityType, Event event) { - return event == null ? null : event.toInfo(entityType); + return Optional.ofNullable(event).map(e -> e.toInfo(entityType)).orElse(null); } } From c37b406795f881092992482613eb9eb04c462b5e Mon Sep 17 00:00:00 2001 From: nick Date: Wed, 10 Apr 2024 14:57:12 +0300 Subject: [PATCH 34/80] msa: lwm2m tests add NoSec Psk --- msa/black-box-tests/pom.xml | 4 + .../server/msa/AbstractLwm2mClientTest.java | 321 ++++++++++++++ .../server/msa/TestRestClient.java | 40 ++ .../connectivity/Lwm2mClientNoSecTest.java | 45 ++ .../msa/connectivity/Lwm2mClientPskTest.java | 45 ++ .../msa/connectivity/lwm2m/FwLwM2MDevice.java | 157 +++++++ .../connectivity/lwm2m/LwM2MTestClient.java | 337 +++++++++++++++ .../lwm2m/LwM2mValueConverterImpl.java | 192 +++++++++ .../connectivity/lwm2m/Lwm2mTestHelper.java | 134 ++++++ .../connectivity/lwm2m/SimpleLwM2MDevice.java | 218 ++++++++++ .../src/test/resources/lwm2m-registry/0.xml | 405 ++++++++++++++++++ .../src/test/resources/lwm2m-registry/1.xml | 360 ++++++++++++++++ .../src/test/resources/lwm2m-registry/2.xml | 123 ++++++ .../src/test/resources/lwm2m-registry/3.xml | 331 ++++++++++++++ .../src/test/resources/lwm2m-registry/5.xml | 204 +++++++++ .../tb-transports/lwm2m}/conf/logback.xml | 6 +- msa/tb/README.md | 4 +- msa/tb/docker-cassandra/Dockerfile | 1 + msa/tb/docker-postgres/Dockerfile | 1 + 19 files changed, 2925 insertions(+), 3 deletions(-) create mode 100644 msa/black-box-tests/src/test/java/org/thingsboard/server/msa/AbstractLwm2mClientTest.java create mode 100644 msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/Lwm2mClientNoSecTest.java create mode 100644 msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/Lwm2mClientPskTest.java create mode 100644 msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/lwm2m/FwLwM2MDevice.java create mode 100644 msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/lwm2m/LwM2MTestClient.java create mode 100644 msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/lwm2m/LwM2mValueConverterImpl.java create mode 100644 msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/lwm2m/Lwm2mTestHelper.java create mode 100644 msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/lwm2m/SimpleLwM2MDevice.java create mode 100644 msa/black-box-tests/src/test/resources/lwm2m-registry/0.xml create mode 100644 msa/black-box-tests/src/test/resources/lwm2m-registry/1.xml create mode 100644 msa/black-box-tests/src/test/resources/lwm2m-registry/2.xml create mode 100644 msa/black-box-tests/src/test/resources/lwm2m-registry/3.xml create mode 100644 msa/black-box-tests/src/test/resources/lwm2m-registry/5.xml rename {docker/tb-transports/coap => msa/black-box-tests/src/test/resources/tb-transports/lwm2m}/conf/logback.xml (85%) diff --git a/msa/black-box-tests/pom.xml b/msa/black-box-tests/pom.xml index d13bb54557..cbbd76a1f6 100644 --- a/msa/black-box-tests/pom.xml +++ b/msa/black-box-tests/pom.xml @@ -186,6 +186,10 @@ allure-testng test + + org.eclipse.leshan + leshan-client-cf + diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/AbstractLwm2mClientTest.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/AbstractLwm2mClientTest.java new file mode 100644 index 0000000000..d0a8b53c32 --- /dev/null +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/AbstractLwm2mClientTest.java @@ -0,0 +1,321 @@ +/** + * 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.msa; + +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.io.IOUtils; +import org.eclipse.leshan.client.object.Security; +import org.eclipse.leshan.core.util.Hex; +import org.junit.Assert; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.common.util.ThingsBoardThreadFactory; +import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.DeviceProfile; +import org.thingsboard.server.common.data.DeviceProfileProvisionType; +import org.thingsboard.server.common.data.DeviceProfileType; +import org.thingsboard.server.common.data.DeviceTransportType; +import org.thingsboard.server.common.data.ResourceType; +import org.thingsboard.server.common.data.TbResource; +import org.thingsboard.server.common.data.device.credentials.lwm2m.LwM2MBootstrapClientCredentials; +import org.thingsboard.server.common.data.device.credentials.lwm2m.LwM2MClientCredential; +import org.thingsboard.server.common.data.device.credentials.lwm2m.LwM2MDeviceCredentials; +import org.thingsboard.server.common.data.device.credentials.lwm2m.NoSecBootstrapClientCredential; +import org.thingsboard.server.common.data.device.credentials.lwm2m.NoSecClientCredential; +import org.thingsboard.server.common.data.device.credentials.lwm2m.PSKBootstrapClientCredential; +import org.thingsboard.server.common.data.device.credentials.lwm2m.PSKClientCredential; +import org.thingsboard.server.common.data.device.profile.DefaultDeviceProfileConfiguration; +import org.thingsboard.server.common.data.device.profile.DeviceProfileData; +import org.thingsboard.server.common.data.device.profile.DisabledDeviceProfileProvisionConfiguration; +import org.thingsboard.server.common.data.device.profile.Lwm2mDeviceProfileTransportConfiguration; +import org.thingsboard.server.common.data.device.profile.lwm2m.OtherConfiguration; +import org.thingsboard.server.common.data.device.profile.lwm2m.TelemetryMappingConfiguration; +import org.thingsboard.server.common.data.device.profile.lwm2m.bootstrap.LwM2MBootstrapServerCredential; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.common.data.security.DeviceCredentials; +import org.thingsboard.server.common.data.security.DeviceCredentialsType; +import org.thingsboard.server.msa.connectivity.lwm2m.LwM2MTestClient; +import org.thingsboard.server.msa.connectivity.lwm2m.Lwm2mTestHelper.LwM2MClientState; + +import java.net.ServerSocket; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; +import static org.eclipse.leshan.client.object.Security.psk; +import static org.thingsboard.server.msa.connectivity.lwm2m.Lwm2mTestHelper.CLIENT_ENDPOINT_NO_SEC; +import static org.thingsboard.server.msa.connectivity.lwm2m.Lwm2mTestHelper.CLIENT_ENDPOINT_PSK; +import static org.thingsboard.server.msa.connectivity.lwm2m.Lwm2mTestHelper.CLIENT_LWM2M_SETTINGS; +import static org.thingsboard.server.msa.connectivity.lwm2m.Lwm2mTestHelper.CLIENT_PSK_IDENTITY; +import static org.thingsboard.server.msa.connectivity.lwm2m.Lwm2mTestHelper.CLIENT_PSK_KEY; +import static org.thingsboard.server.msa.connectivity.lwm2m.Lwm2mTestHelper.LwM2MClientState.ON_INIT; +import static org.thingsboard.server.msa.connectivity.lwm2m.Lwm2mTestHelper.LwM2MClientState.ON_REGISTRATION_STARTED; +import static org.thingsboard.server.msa.connectivity.lwm2m.Lwm2mTestHelper.LwM2MClientState.ON_REGISTRATION_SUCCESS; +import static org.thingsboard.server.msa.connectivity.lwm2m.Lwm2mTestHelper.LwM2MClientState.ON_UPDATE_SUCCESS; +import static org.thingsboard.server.msa.connectivity.lwm2m.Lwm2mTestHelper.OBSERVE_ATTRIBUTES_WITHOUT_PARAMS; +import static org.thingsboard.server.msa.connectivity.lwm2m.Lwm2mTestHelper.SECURE_URI; +import static org.thingsboard.server.msa.connectivity.lwm2m.Lwm2mTestHelper.SECURITY_NO_SEC; +import static org.thingsboard.server.msa.connectivity.lwm2m.Lwm2mTestHelper.resources; +import static org.thingsboard.server.msa.connectivity.lwm2m.Lwm2mTestHelper.shortServerId; + +@Slf4j +public class AbstractLwm2mClientTest extends AbstractContainerTest{ + protected ScheduledExecutorService executor; + + protected Security security; + + protected final PageLink pageLink = new PageLink(30); + protected TenantId tenantId; + protected DeviceProfile lwm2mDeviceProfile; + protected Device lwM2MDeviceTest; + protected LwM2MTestClient lwM2MTestClient; + + public final Set expectedStatusesRegistrationLwm2mSuccess = new HashSet<>(Arrays.asList(ON_INIT, ON_REGISTRATION_STARTED, ON_REGISTRATION_SUCCESS)); + + public void connectLwm2mClientNoSec() throws Exception { + LwM2MDeviceCredentials deviceCredentials = getDeviceCredentialsNoSec(createNoSecClientCredentials(CLIENT_ENDPOINT_NO_SEC)); + basicTestConnection(SECURITY_NO_SEC, + deviceCredentials, + CLIENT_ENDPOINT_NO_SEC, + "TestConnection Lwm2m NoSec (msa)"); + } + + public void connectLwm2mClientPsk() throws Exception { + String clientEndpoint = CLIENT_ENDPOINT_PSK; + String identity = CLIENT_PSK_IDENTITY; + String keyPsk = CLIENT_PSK_KEY; + PSKClientCredential clientCredentials = new PSKClientCredential(); + clientCredentials.setEndpoint(clientEndpoint); + clientCredentials.setIdentity(identity); + clientCredentials.setKey(keyPsk); + Security security = psk(SECURE_URI, + shortServerId, + identity.getBytes(StandardCharsets.UTF_8), + Hex.decodeHex(keyPsk.toCharArray())); + LwM2MDeviceCredentials deviceCredentials = getDeviceCredentialsSecurePsk(clientCredentials); + + basicTestConnection(security, + deviceCredentials, + clientEndpoint, + "TestConnection Lwm2m Rpc (msa)"); + } + + public void basicTestConnection(Security security, + LwM2MDeviceCredentials deviceCredentials, + String clientEndpoint, String alias) throws Exception { + // create lwm2mClient and lwM2MDevice + lwM2MDeviceTest = createDeviceWithCredentials(deviceCredentials, clientEndpoint); + lwM2MTestClient = createNewClient(security, clientEndpoint, executor); + + LwM2MClientState finishState = ON_REGISTRATION_SUCCESS; + await(alias + " - " + ON_REGISTRATION_STARTED) + .atMost(40, TimeUnit.SECONDS) + .until(() -> { + log.warn("msa basicTestConnection started -> finishState: [{}] states: {}", finishState, lwM2MTestClient.getClientStates()); + return lwM2MTestClient.getClientStates().contains(finishState) || lwM2MTestClient.getClientStates().contains(ON_REGISTRATION_STARTED); + }); + await(alias + " - " + ON_UPDATE_SUCCESS) + .atMost(40, TimeUnit.SECONDS) + .until(() -> { + log.warn("msa basicTestConnection update -> finishState: [{}] states: {}", finishState, lwM2MTestClient.getClientStates()); + return lwM2MTestClient.getClientStates().contains(finishState) || lwM2MTestClient.getClientStates().contains(ON_UPDATE_SUCCESS); + }); + Assert.assertTrue(lwM2MTestClient.getClientStates().containsAll(expectedStatusesRegistrationLwm2mSuccess)); + } + + public LwM2MTestClient createNewClient(Security security, + String endpoint, ScheduledExecutorService executor) throws Exception { + this.executor = executor; + LwM2MTestClient lwM2MTestClient = new LwM2MTestClient(endpoint); + try (ServerSocket socket = new ServerSocket(0)) { + int clientPort = socket.getLocalPort(); + lwM2MTestClient.init(security, clientPort); + } + return lwM2MTestClient; + } + + protected void destroyAfter(){ + clientDestroy(); + deviceDestroy(); + deviceProfileDestroy(); + if (executor != null) { + executor.shutdown(); + } + } + + protected void clientDestroy() { + try { + if (lwM2MTestClient != null) { + lwM2MTestClient.destroy(); + } + } catch (Exception e) { + log.error("Failed client Destroy", e); + } + } + protected void deviceDestroy() { + try { + if (lwM2MDeviceTest != null) { + testRestClient.deleteDeviceIfExists(lwM2MDeviceTest.getId()); + } + } catch (Exception e) { + log.error("Failed device Delete", e); + } + } + + protected void initTest(String deviceProfileName) throws Exception { + if (executor != null) { + executor.shutdown(); + } + executor = Executors.newScheduledThreadPool(10, ThingsBoardThreadFactory.forName("test-scheduled-" + deviceProfileName)); + + lwm2mDeviceProfile = getDeviceProfile(deviceProfileName); + tenantId = lwm2mDeviceProfile.getTenantId(); + + for (String resourceName : resources) { + TbResource lwModel = new TbResource(); + lwModel.setResourceType(ResourceType.LWM2M_MODEL); + lwModel.setTitle(resourceName); + lwModel.setFileName(resourceName); + lwModel.setTenantId(tenantId); + byte[] bytes = IOUtils.toByteArray(AbstractLwm2mClientTest.class.getClassLoader().getResourceAsStream("lwm2m-registry/" + resourceName)); + lwModel.setData(bytes); + testRestClient.postTbResourceIfNotExists(lwModel); + } + } + + protected DeviceProfile getDeviceProfile(String deviceProfileName) throws Exception { + DeviceProfile deviceProfile = getDeviceProfileIfExists(deviceProfileName); + if (deviceProfile == null) { + deviceProfile = testRestClient.postDeviceProfile(createDeviceProfile(deviceProfileName)); + } + return deviceProfile; + } + + protected DeviceProfile getDeviceProfileIfExists(String deviceProfileName) throws Exception { + return testRestClient.getDeviceProfiles(pageLink).getData().stream() + .filter(x -> x.getName().equals(deviceProfileName)) + .findFirst() + .orElse(null); + } + + + protected DeviceProfile createDeviceProfile(String deviceProfileName) throws Exception { + DeviceProfile deviceProfile = new DeviceProfile(); + deviceProfile.setName(deviceProfileName); + deviceProfile.setType(DeviceProfileType.DEFAULT); + deviceProfile.setTransportType(DeviceTransportType.LWM2M); + deviceProfile.setProvisionType(DeviceProfileProvisionType.DISABLED); + deviceProfile.setDescription(deviceProfile.getName()); + + DeviceProfileData deviceProfileData = new DeviceProfileData(); + deviceProfileData.setConfiguration(new DefaultDeviceProfileConfiguration()); + deviceProfileData.setProvisionConfiguration(new DisabledDeviceProfileProvisionConfiguration(null)); + deviceProfileData.setTransportConfiguration(getTransportConfiguration()); + deviceProfile.setProfileData(deviceProfileData); + return deviceProfile; + } + + protected void deviceProfileDestroy(){ + try { + if (lwm2mDeviceProfile != null) { + testRestClient.deleteDeviceProfileIfExists(lwm2mDeviceProfile); + } + } catch (Exception e) { + log.error("Failed deviceProfile Delete", e); + } + } + + protected Device createDeviceWithCredentials(LwM2MDeviceCredentials deviceCredentials, String clientEndpoint) throws Exception { + Device device = createDevice(deviceCredentials, clientEndpoint); + return device; + } + + protected Device createDevice(LwM2MDeviceCredentials credentials, String clientEndpoint) throws Exception { + Device device = testRestClient.getDeviceByNameIfExists(clientEndpoint); + if (device == null) { + device = new Device(); + device.setName(clientEndpoint); + device.setDeviceProfileId(lwm2mDeviceProfile.getId()); + device.setTenantId(tenantId); + device = testRestClient.postDevice("", device); + } + + DeviceCredentials deviceCredentials = testRestClient.getDeviceCredentialsByDeviceId(device.getId()); + deviceCredentials.setCredentialsType(DeviceCredentialsType.LWM2M_CREDENTIALS); + deviceCredentials.setCredentialsValue(JacksonUtil.toString(credentials)); + deviceCredentials = testRestClient.postDeviceCredentials(deviceCredentials); + assertThat(deviceCredentials).isNotNull(); + return device; + } + + protected LwM2MDeviceCredentials getDeviceCredentialsNoSec(LwM2MClientCredential clientCredentials) { + LwM2MDeviceCredentials credentials = new LwM2MDeviceCredentials(); + credentials.setClient(clientCredentials); + LwM2MBootstrapClientCredentials bootstrapCredentials = new LwM2MBootstrapClientCredentials(); + NoSecBootstrapClientCredential serverCredentials = new NoSecBootstrapClientCredential(); + bootstrapCredentials.setBootstrapServer(serverCredentials); + bootstrapCredentials.setLwm2mServer(serverCredentials); + credentials.setBootstrap(bootstrapCredentials); + return credentials; + } + + public NoSecClientCredential createNoSecClientCredentials(String clientEndpoint) { + NoSecClientCredential clientCredentials = new NoSecClientCredential(); + clientCredentials.setEndpoint(clientEndpoint); + return clientCredentials; + } + + protected Lwm2mDeviceProfileTransportConfiguration getTransportConfiguration() { + List bootstrapServerCredentials = new ArrayList<>(); + Lwm2mDeviceProfileTransportConfiguration transportConfiguration = new Lwm2mDeviceProfileTransportConfiguration(); + TelemetryMappingConfiguration observeAttrConfiguration = JacksonUtil.fromString(OBSERVE_ATTRIBUTES_WITHOUT_PARAMS, TelemetryMappingConfiguration.class); + OtherConfiguration clientLwM2mSettings = JacksonUtil.fromString(CLIENT_LWM2M_SETTINGS, OtherConfiguration.class); + transportConfiguration.setBootstrapServerUpdateEnable(true); + transportConfiguration.setObserveAttr(observeAttrConfiguration); + transportConfiguration.setClientLwM2mSettings(clientLwM2mSettings); + transportConfiguration.setBootstrap(bootstrapServerCredentials); + return transportConfiguration; + } + + protected LwM2MDeviceCredentials getDeviceCredentialsSecurePsk(LwM2MClientCredential clientCredentials) { + LwM2MDeviceCredentials credentials = new LwM2MDeviceCredentials(); + credentials.setClient(clientCredentials); + LwM2MBootstrapClientCredentials bootstrapCredentials; + bootstrapCredentials = getBootstrapClientCredentialsPsk(clientCredentials); + credentials.setBootstrap(bootstrapCredentials); + return credentials; + } + + private LwM2MBootstrapClientCredentials getBootstrapClientCredentialsPsk(LwM2MClientCredential clientCredentials) { + LwM2MBootstrapClientCredentials bootstrapCredentials = new LwM2MBootstrapClientCredentials(); + PSKBootstrapClientCredential serverCredentials = new PSKBootstrapClientCredential(); + if (clientCredentials != null) { + serverCredentials.setClientSecretKey(((PSKClientCredential) clientCredentials).getKey()); + serverCredentials.setClientPublicKeyOrId(((PSKClientCredential) clientCredentials).getIdentity()); + } + bootstrapCredentials.setBootstrapServer(serverCredentials); + bootstrapCredentials.setLwm2mServer(serverCredentials); + return bootstrapCredentials; + } +} diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/TestRestClient.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/TestRestClient.java index 83c9c927aa..c76627d3a1 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/TestRestClient.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/TestRestClient.java @@ -23,6 +23,7 @@ import io.restassured.config.RestAssuredConfig; import io.restassured.filter.log.RequestLoggingFilter; import io.restassured.filter.log.ResponseLoggingFilter; import io.restassured.http.ContentType; +import io.restassured.internal.ValidatableResponseImpl; import io.restassured.path.json.JsonPath; import io.restassured.response.ValidatableResponse; import io.restassured.specification.RequestSpecification; @@ -32,6 +33,7 @@ import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.EntityView; import org.thingsboard.server.common.data.EventInfo; +import org.thingsboard.server.common.data.TbResource; import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.asset.Asset; @@ -63,6 +65,7 @@ import java.util.List; import java.util.Map; import static io.restassured.RestAssured.given; +import static java.net.HttpURLConnection.HTTP_BAD_REQUEST; import static java.net.HttpURLConnection.HTTP_NOT_FOUND; import static java.net.HttpURLConnection.HTTP_OK; import static org.hamcrest.Matchers.is; @@ -548,6 +551,43 @@ public class TestRestClient { .as(new TypeRef<>() {}); } + public ValidatableResponse postTbResourceIfNotExists(TbResource lwModel) { + return given().spec(requestSpec).body(lwModel) + .post("/api/resource") + .then() + .statusCode(anyOf(is(HTTP_OK), is(HTTP_BAD_REQUEST))); + } + public void deleteDeviceProfileIfExists(DeviceProfile deviceProfile) { + given().spec(requestSpec) + .delete("/api/deviceProfile/" + deviceProfile.getId().getId().toString()) + .then() + .statusCode(anyOf(is(HTTP_OK), is(HTTP_NOT_FOUND))); + } + + public Device getDeviceByNameIfExists(String deviceName) { + ValidatableResponse response = given().spec(requestSpec) + .pathParams("deviceName", deviceName) + .get("/api/tenant/devices?deviceName={deviceName}") + .then() + .statusCode(anyOf(is(HTTP_OK), is(HTTP_NOT_FOUND))); + if(((ValidatableResponseImpl) response).extract().response().getStatusCode()==HTTP_OK){ + return response.extract() + .as(Device.class); + } else { + return null; + } + } + + public DeviceCredentials postDeviceCredentials(DeviceCredentials deviceCredentials) { + return given().spec(requestSpec).body(deviceCredentials) + .post("/api/device/credentials") + .then() + .assertThat() + .statusCode(HTTP_OK) + .extract() + .as(DeviceCredentials.class); + } + private void addTimePageLinkToParam(Map params, TimePageLink pageLink) { this.addPageLinkToParam(params, pageLink); if (pageLink.getStartTime() != null) { diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/Lwm2mClientNoSecTest.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/Lwm2mClientNoSecTest.java new file mode 100644 index 0000000000..15658140b0 --- /dev/null +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/Lwm2mClientNoSecTest.java @@ -0,0 +1,45 @@ +/** + * 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.msa.connectivity; + +import org.testng.annotations.AfterMethod; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; +import org.thingsboard.server.msa.AbstractLwm2mClientTest; +import org.thingsboard.server.msa.DisableUIListeners; + +import static org.thingsboard.server.msa.ui.utils.Const.TENANT_EMAIL; +import static org.thingsboard.server.msa.ui.utils.Const.TENANT_PASSWORD; + +@DisableUIListeners +public class Lwm2mClientNoSecTest extends AbstractLwm2mClientTest { + + @BeforeMethod + public void setUp() throws Exception { + testRestClient.login(TENANT_EMAIL, TENANT_PASSWORD); + initTest("lwm2m-NoSec"); + } + + @AfterMethod + public void tearDown() { + destroyAfter(); + } + + @Test + public void connectLwm2mClientNoSecWithLwm2mServer() throws Exception { + connectLwm2mClientNoSec(); + } +} diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/Lwm2mClientPskTest.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/Lwm2mClientPskTest.java new file mode 100644 index 0000000000..fbd8139d8f --- /dev/null +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/Lwm2mClientPskTest.java @@ -0,0 +1,45 @@ +/** + * 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.msa.connectivity; + +import org.testng.annotations.AfterMethod; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; +import org.thingsboard.server.msa.AbstractLwm2mClientTest; +import org.thingsboard.server.msa.DisableUIListeners; + +import static org.thingsboard.server.msa.ui.utils.Const.TENANT_EMAIL; +import static org.thingsboard.server.msa.ui.utils.Const.TENANT_PASSWORD; + +@DisableUIListeners +public class Lwm2mClientPskTest extends AbstractLwm2mClientTest { + + @BeforeMethod + public void setUp() throws Exception { + testRestClient.login(TENANT_EMAIL, TENANT_PASSWORD); + initTest("lwm2m-Psk"); + } + + @AfterMethod + public void tearDown() { + destroyAfter(); + } + + @Test + public void connectLwm2mClientPskWithLwm2mServer() throws Exception { + connectLwm2mClientPsk(); + } +} diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/lwm2m/FwLwM2MDevice.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/lwm2m/FwLwM2MDevice.java new file mode 100644 index 0000000000..2910a2b7da --- /dev/null +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/lwm2m/FwLwM2MDevice.java @@ -0,0 +1,157 @@ +/** + * 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.msa.connectivity.lwm2m; + +import lombok.extern.slf4j.Slf4j; +import org.eclipse.leshan.client.resource.BaseInstanceEnabler; +import org.eclipse.leshan.client.servers.LwM2mServer; +import org.eclipse.leshan.core.model.ObjectModel; +import org.eclipse.leshan.core.node.LwM2mResource; +import org.eclipse.leshan.core.request.argument.Arguments; +import org.eclipse.leshan.core.response.ExecuteResponse; +import org.eclipse.leshan.core.response.ReadResponse; +import org.eclipse.leshan.core.response.WriteResponse; +import org.thingsboard.common.util.ThingsBoardThreadFactory; + +import javax.security.auth.Destroyable; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +@Slf4j +public class FwLwM2MDevice extends BaseInstanceEnabler implements Destroyable { + + private static final List supportedResources = Arrays.asList(0, 1, 2, 3, 5, 6, 7, 9); + + private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(ThingsBoardThreadFactory.forName(getClass().getSimpleName() + "-test-scope")); + + private final AtomicInteger state = new AtomicInteger(0); + + private final AtomicInteger updateResult = new AtomicInteger(0); + + @Override + public ReadResponse read(LwM2mServer identity, int resourceId) { + if (!identity.isSystem()) + log.info("Read on Device resource /{}/{}/{}", getModel().id, getId(), resourceId); + switch (resourceId) { + case 3: + return ReadResponse.success(resourceId, getState()); + case 5: + return ReadResponse.success(resourceId, getUpdateResult()); + case 6: + return ReadResponse.success(resourceId, getPkgName()); + case 7: + return ReadResponse.success(resourceId, getPkgVersion()); + case 9: + return ReadResponse.success(resourceId, getFirmwareUpdateDeliveryMethod()); + default: + return super.read(identity, resourceId); + } + } + + @Override + public ExecuteResponse execute(LwM2mServer identity, int resourceId, Arguments arguments) { + String withArguments = ""; + if (!arguments.isEmpty()) + withArguments = " with arguments " + arguments; + log.info("Execute on Device resource /{}/{}/{} {}", getModel().id, getId(), resourceId, withArguments); + + + switch (resourceId) { + case 2: + startUpdating(); + return ExecuteResponse.success(); + default: + return super.execute(identity, resourceId, arguments); + } + } + + @Override + public WriteResponse write(LwM2mServer identity, boolean replace, int resourceId, LwM2mResource value) { + log.info("Write on Device resource /{}/{}/{}", getModel().id, getId(), resourceId); + + switch (resourceId) { + case 0: + startDownloading(); + return WriteResponse.success(); + case 1: + startDownloading(); + return WriteResponse.success(); + default: + return super.write(identity, replace, resourceId, value); + } + } + + private int getState() { + return state.get(); + } + + private int getUpdateResult() { + return updateResult.get(); + } + + private String getPkgName() { + return "firmware"; + } + + private String getPkgVersion() { + return "1.0.0"; + } + + private int getFirmwareUpdateDeliveryMethod() { + return 1; + } + + @Override + public List getAvailableResourceIds(ObjectModel model) { + return supportedResources; + } + + @Override + public void destroy() { + scheduler.shutdownNow(); + } + + private void startDownloading() { + scheduler.schedule(() -> { + try { + state.set(1); + fireResourceChange(3); + Thread.sleep(100); + state.set(2); + fireResourceChange(3); + } catch (Exception e) { + } + }, 100, TimeUnit.MILLISECONDS); + } + + private void startUpdating() { + scheduler.schedule(() -> { + try { + state.set(3); + fireResourceChange(3); + Thread.sleep(100); + updateResult.set(1); + fireResourceChange(5); + } catch (Exception e) { + } + }, 100, TimeUnit.MILLISECONDS); + } + +} diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/lwm2m/LwM2MTestClient.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/lwm2m/LwM2MTestClient.java new file mode 100644 index 0000000000..4c20199c8e --- /dev/null +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/lwm2m/LwM2MTestClient.java @@ -0,0 +1,337 @@ +/** + * 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.msa.connectivity.lwm2m; + +import lombok.Data; +import lombok.extern.slf4j.Slf4j; +import org.eclipse.californium.elements.config.Configuration; +import org.eclipse.californium.scandium.config.DtlsConnectorConfig; +import org.eclipse.leshan.client.LeshanClient; +import org.eclipse.leshan.client.LeshanClientBuilder; +import org.eclipse.leshan.client.californium.endpoint.CaliforniumClientEndpointFactory; +import org.eclipse.leshan.client.californium.endpoint.CaliforniumClientEndpointsProvider; +import org.eclipse.leshan.client.californium.endpoint.ClientProtocolProvider; +import org.eclipse.leshan.client.californium.endpoint.coap.CoapOscoreProtocolProvider; +import org.eclipse.leshan.client.californium.endpoint.coaps.CoapsClientEndpointFactory; +import org.eclipse.leshan.client.californium.endpoint.coaps.CoapsClientProtocolProvider; +import org.eclipse.leshan.client.endpoint.LwM2mClientEndpointsProvider; +import org.eclipse.leshan.client.engine.DefaultRegistrationEngineFactory; +import org.eclipse.leshan.client.object.Security; +import org.eclipse.leshan.client.object.Server; +import org.eclipse.leshan.client.observer.LwM2mClientObserver; +import org.eclipse.leshan.client.resource.DummyInstanceEnabler; +import org.eclipse.leshan.client.resource.LwM2mObjectEnabler; +import org.eclipse.leshan.client.resource.ObjectsInitializer; +import org.eclipse.leshan.client.resource.listener.ObjectsListenerAdapter; +import org.eclipse.leshan.client.send.ManualDataSender; +import org.eclipse.leshan.client.servers.LwM2mServer; +import org.eclipse.leshan.core.ResponseCode; +import org.eclipse.leshan.core.model.InvalidDDFFileException; +import org.eclipse.leshan.core.model.LwM2mModel; +import org.eclipse.leshan.core.model.ObjectLoader; +import org.eclipse.leshan.core.model.ObjectModel; +import org.eclipse.leshan.core.model.StaticModel; +import org.eclipse.leshan.core.node.codec.DefaultLwM2mDecoder; +import org.eclipse.leshan.core.node.codec.DefaultLwM2mEncoder; +import org.eclipse.leshan.core.request.BootstrapRequest; +import org.eclipse.leshan.core.request.DeregisterRequest; +import org.eclipse.leshan.core.request.RegisterRequest; +import org.eclipse.leshan.core.request.UpdateRequest; +import org.junit.Assert; +import org.thingsboard.server.msa.connectivity.lwm2m.Lwm2mTestHelper.LwM2MClientState; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static org.eclipse.californium.scandium.config.DtlsConfig.DTLS_CONNECTION_ID_LENGTH; +import static org.eclipse.californium.scandium.config.DtlsConfig.DTLS_RECOMMENDED_CIPHER_SUITES_ONLY; +import static org.eclipse.leshan.core.LwM2mId.ACCESS_CONTROL; +import static org.eclipse.leshan.core.LwM2mId.DEVICE; +import static org.eclipse.leshan.core.LwM2mId.FIRMWARE; +import static org.eclipse.leshan.core.LwM2mId.SECURITY; +import static org.eclipse.leshan.core.LwM2mId.SERVER; +import static org.thingsboard.server.msa.connectivity.lwm2m.Lwm2mTestHelper.LwM2MClientState.ON_BOOTSTRAP_FAILURE; +import static org.thingsboard.server.msa.connectivity.lwm2m.Lwm2mTestHelper.LwM2MClientState.ON_BOOTSTRAP_STARTED; +import static org.thingsboard.server.msa.connectivity.lwm2m.Lwm2mTestHelper.LwM2MClientState.ON_BOOTSTRAP_SUCCESS; +import static org.thingsboard.server.msa.connectivity.lwm2m.Lwm2mTestHelper.LwM2MClientState.ON_BOOTSTRAP_TIMEOUT; +import static org.thingsboard.server.msa.connectivity.lwm2m.Lwm2mTestHelper.LwM2MClientState.ON_DEREGISTRATION_FAILURE; +import static org.thingsboard.server.msa.connectivity.lwm2m.Lwm2mTestHelper.LwM2MClientState.ON_DEREGISTRATION_STARTED; +import static org.thingsboard.server.msa.connectivity.lwm2m.Lwm2mTestHelper.LwM2MClientState.ON_DEREGISTRATION_SUCCESS; +import static org.thingsboard.server.msa.connectivity.lwm2m.Lwm2mTestHelper.LwM2MClientState.ON_DEREGISTRATION_TIMEOUT; +import static org.thingsboard.server.msa.connectivity.lwm2m.Lwm2mTestHelper.LwM2MClientState.ON_EXPECTED_ERROR; +import static org.thingsboard.server.msa.connectivity.lwm2m.Lwm2mTestHelper.LwM2MClientState.ON_INIT; +import static org.thingsboard.server.msa.connectivity.lwm2m.Lwm2mTestHelper.LwM2MClientState.ON_REGISTRATION_FAILURE; +import static org.thingsboard.server.msa.connectivity.lwm2m.Lwm2mTestHelper.LwM2MClientState.ON_REGISTRATION_STARTED; +import static org.thingsboard.server.msa.connectivity.lwm2m.Lwm2mTestHelper.LwM2MClientState.ON_REGISTRATION_SUCCESS; +import static org.thingsboard.server.msa.connectivity.lwm2m.Lwm2mTestHelper.LwM2MClientState.ON_REGISTRATION_TIMEOUT; +import static org.thingsboard.server.msa.connectivity.lwm2m.Lwm2mTestHelper.LwM2MClientState.ON_UPDATE_FAILURE; +import static org.thingsboard.server.msa.connectivity.lwm2m.Lwm2mTestHelper.LwM2MClientState.ON_UPDATE_STARTED; +import static org.thingsboard.server.msa.connectivity.lwm2m.Lwm2mTestHelper.LwM2MClientState.ON_UPDATE_SUCCESS; +import static org.thingsboard.server.msa.connectivity.lwm2m.Lwm2mTestHelper.LwM2MClientState.ON_UPDATE_TIMEOUT; +import static org.thingsboard.server.msa.connectivity.lwm2m.Lwm2mTestHelper.resources; +import static org.thingsboard.server.msa.connectivity.lwm2m.Lwm2mTestHelper.serverId; +import static org.thingsboard.server.msa.connectivity.lwm2m.Lwm2mTestHelper.shortServerId; + + +@Slf4j +@Data +public class LwM2MTestClient { + + private final String endpoint; + private LeshanClient leshanClient; + private SimpleLwM2MDevice lwM2MDevice; + + private Set clientStates; + + private FwLwM2MDevice fwLwM2MDevice; + private Map clientDtlsCid; + + public void init(Security security, int clientPort) throws InvalidDDFFileException, IOException { + Assert.assertNull("client already initialized", leshanClient); + + List models = new ArrayList<>(); + for (String resourceName : resources) { + models.addAll(ObjectLoader.loadDdfFile(LwM2MTestClient.class.getClassLoader().getResourceAsStream("lwm2m-registry/" + resourceName), resourceName)); + } + LwM2mModel model = new StaticModel(models); + ObjectsInitializer initializer = new ObjectsInitializer(model); + + // SECURITY + initializer.setInstancesForObject(SECURITY, security); + // SERVER + Server lwm2mServer = new Server(shortServerId, 300); + lwm2mServer.setId(serverId); + initializer.setInstancesForObject(SERVER, lwm2mServer); + + initializer.setInstancesForObject(DEVICE, lwM2MDevice = new SimpleLwM2MDevice()); + initializer.setClassForObject(ACCESS_CONTROL, DummyInstanceEnabler.class); + initializer.setInstancesForObject(FIRMWARE, fwLwM2MDevice = new FwLwM2MDevice()); + + List enablers = initializer.createAll(); + + // Create Californium Endpoints Provider: + // -------------------------------------- + // Define Custom CoAPS protocol provider + CoapsClientProtocolProvider customCoapsProtocolProvider = new CoapsClientProtocolProvider() { + @Override + public CaliforniumClientEndpointFactory createDefaultEndpointFactory() { + return new CoapsClientEndpointFactory() { + + @Override + protected DtlsConnectorConfig.Builder createRootDtlsConnectorConfigBuilder( + Configuration configuration) { + DtlsConnectorConfig.Builder builder = super.createRootDtlsConnectorConfigBuilder(configuration); + return builder; + }; + }; + } + }; + + // Create client endpoints Provider + List protocolProvider = new ArrayList<>(); + + /** + * "Use java-coap for CoAP protocol instead of Californium." + */ + protocolProvider.add(new CoapOscoreProtocolProvider()); + protocolProvider.add(customCoapsProtocolProvider); + CaliforniumClientEndpointsProvider.Builder endpointsBuilder = new CaliforniumClientEndpointsProvider.Builder( + protocolProvider.toArray(new ClientProtocolProvider[protocolProvider.size()])); + + + // Create Californium Configuration + Configuration clientCoapConfig = endpointsBuilder.createDefaultConfiguration(); + + // Set some DTLS stuff + // These configuration values are always overwritten by CLI therefore set them to transient. + clientCoapConfig.setTransient(DTLS_RECOMMENDED_CIPHER_SUITES_ONLY); + clientCoapConfig.setTransient(DTLS_CONNECTION_ID_LENGTH); + boolean supportDeprecatedCiphers = false; + clientCoapConfig.set(DTLS_RECOMMENDED_CIPHER_SUITES_ONLY, !supportDeprecatedCiphers); + + // Set Californium Configuration + + endpointsBuilder.setConfiguration(clientCoapConfig); + endpointsBuilder.setClientAddress(new InetSocketAddress(clientPort).getAddress()); + + + // creates EndpointsProvider + List endpointsProvider = new ArrayList<>(); + endpointsProvider.add(endpointsBuilder.build()); + + // Configure Registration Engine + DefaultRegistrationEngineFactory engineFactory = new DefaultRegistrationEngineFactory(); + /** + * Force reconnection/rehandshake on registration update. + */ + int comPeriodInSec = 5; + if (comPeriodInSec > 0) engineFactory.setCommunicationPeriod(comPeriodInSec * 1000); + + /** + * By default client will try to resume DTLS session by using abbreviated Handshake. This option force to always do a full handshake." + */ + boolean reconnectOnUpdate = false; + engineFactory.setReconnectOnUpdate(reconnectOnUpdate); + engineFactory.setResumeOnConnect(true); + + /** + * Client use queue mode. + */ + engineFactory.setQueueMode(false); + + // Create client + LeshanClientBuilder builder = new LeshanClientBuilder(endpoint); + builder.setObjects(enablers); + builder.setEndpointsProviders(endpointsProvider.toArray(new LwM2mClientEndpointsProvider[endpointsProvider.size()])); + builder.setDataSenders(new ManualDataSender()); + builder.setRegistrationEngineFactory(engineFactory); + boolean supportOldFormat = true; + if (supportOldFormat) { + builder.setDecoder(new DefaultLwM2mDecoder(supportOldFormat)); + builder.setEncoder(new DefaultLwM2mEncoder(new LwM2mValueConverterImpl(), supportOldFormat)); + } + + builder.setRegistrationEngineFactory(engineFactory); +// builder.setSharedExecutor(executor); + + clientStates = new HashSet<>(); + clientDtlsCid = new HashMap<>(); + clientStates.add(ON_INIT); + leshanClient = builder.build(); + + LwM2mClientObserver observer = new LwM2mClientObserver() { + @Override + public void onBootstrapStarted(LwM2mServer bsserver, BootstrapRequest request) { + clientStates.add(ON_BOOTSTRAP_STARTED); + } + + @Override + public void onBootstrapSuccess(LwM2mServer bsserver, BootstrapRequest request) { + clientStates.add(ON_BOOTSTRAP_SUCCESS); + } + + @Override + public void onBootstrapFailure(LwM2mServer bsserver, BootstrapRequest request, ResponseCode responseCode, String errorMessage, Exception cause) { + clientStates.add(ON_BOOTSTRAP_FAILURE); + } + + @Override + public void onBootstrapTimeout(LwM2mServer bsserver, BootstrapRequest request) { + clientStates.add(ON_BOOTSTRAP_TIMEOUT); + } + + @Override + public void onRegistrationStarted(LwM2mServer server, RegisterRequest request) { + clientStates.add(ON_REGISTRATION_STARTED); + } + + @Override + public void onRegistrationSuccess(LwM2mServer server, RegisterRequest request, String registrationID) { + clientStates.add(ON_REGISTRATION_SUCCESS); + } + + @Override + public void onRegistrationFailure(LwM2mServer server, RegisterRequest request, ResponseCode responseCode, String errorMessage, Exception cause) { + clientStates.add(ON_REGISTRATION_FAILURE); + } + + @Override + public void onRegistrationTimeout(LwM2mServer server, RegisterRequest request) { + clientStates.add(ON_REGISTRATION_TIMEOUT); + } + + @Override + public void onUpdateStarted(LwM2mServer server, UpdateRequest request) { + clientStates.add(ON_UPDATE_STARTED); + } + + @Override + public void onUpdateSuccess(LwM2mServer server, UpdateRequest request) { + clientStates.add(ON_UPDATE_SUCCESS); + } + + @Override + public void onUpdateFailure(LwM2mServer server, UpdateRequest request, ResponseCode responseCode, String errorMessage, Exception cause) { + clientStates.add(ON_UPDATE_FAILURE); + } + + @Override + public void onUpdateTimeout(LwM2mServer server, UpdateRequest request) { + clientStates.add(ON_UPDATE_TIMEOUT); + } + + @Override + public void onDeregistrationStarted(LwM2mServer server, DeregisterRequest request) { + clientStates.add(ON_DEREGISTRATION_STARTED); + } + + @Override + public void onDeregistrationSuccess(LwM2mServer server, DeregisterRequest request) { + clientStates.add(ON_DEREGISTRATION_SUCCESS); + } + + @Override + public void onDeregistrationFailure(LwM2mServer server, DeregisterRequest request, ResponseCode responseCode, String errorMessage, Exception cause) { + clientStates.add(ON_DEREGISTRATION_FAILURE); + } + + @Override + public void onDeregistrationTimeout(LwM2mServer server, DeregisterRequest request) { + clientStates.add(ON_DEREGISTRATION_TIMEOUT); + } + + @Override + public void onUnexpectedError(Throwable unexpectedError) { + clientStates.add(ON_EXPECTED_ERROR); + } + }; + this.leshanClient.addObserver(observer); + + // Add some log about object tree life cycle. + this.leshanClient.getObjectTree().addListener(new ObjectsListenerAdapter() { + + @Override + public void objectRemoved(LwM2mObjectEnabler object) { + log.info("Object {} v{} disabled.", object.getId(), object.getObjectModel().version); + } + + @Override + public void objectAdded(LwM2mObjectEnabler object) { + log.info("Object {} v{} enabled.", object.getId(), object.getObjectModel().version); + } + }); + + leshanClient.start(); + } + + public void destroy() { + if (leshanClient != null) { + leshanClient.destroy(true); + } + if (lwM2MDevice != null) { + lwM2MDevice.destroy(); + } + if (fwLwM2MDevice != null) { + fwLwM2MDevice.destroy(); + } + } +} \ No newline at end of file diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/lwm2m/LwM2mValueConverterImpl.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/lwm2m/LwM2mValueConverterImpl.java new file mode 100644 index 0000000000..b49d54f27e --- /dev/null +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/lwm2m/LwM2mValueConverterImpl.java @@ -0,0 +1,192 @@ +/** + * 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.msa.connectivity.lwm2m; + +import lombok.extern.slf4j.Slf4j; +import org.eclipse.leshan.core.model.ResourceModel.Type; +import org.eclipse.leshan.core.node.LwM2mPath; +import org.eclipse.leshan.core.node.ObjectLink; +import org.eclipse.leshan.core.node.codec.CodecException; +import org.eclipse.leshan.core.node.codec.LwM2mValueConverter; +import org.eclipse.leshan.core.util.Hex; +import org.thingsboard.server.common.data.StringUtils; + +import java.math.BigInteger; +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.Base64; +import java.util.Date; + +import static org.eclipse.leshan.core.model.ResourceModel.Type.OPAQUE; + +@Slf4j +public class LwM2mValueConverterImpl implements LwM2mValueConverter { + + private static final LwM2mValueConverterImpl INSTANCE = new LwM2mValueConverterImpl(); + + public static LwM2mValueConverterImpl getInstance() { + return INSTANCE; + } + + @Override + public Object convertValue(Object value, Type currentType, Type expectedType, LwM2mPath resourcePath) + throws CodecException { + if (value == null) { + return null; + } + if (expectedType == null) { + /** unknown resource, trusted value */ + return value; + } + + if (currentType == expectedType) { + /** expected type */ + return value; + } + if (currentType == null) { + currentType = OPAQUE; + } + + switch (expectedType) { + case INTEGER: + switch (currentType) { + case FLOAT: + log.debug("Trying to convert float value [{}] to Integer", value); + Long longValue = ((Double) value).longValue(); + if ((double) value == longValue.doubleValue()) { + return longValue; + } + case STRING: + log.debug("Trying to convert String value [{}] to Integer", value); + return Long.parseLong((String) value); + default: + break; + } + break; + case FLOAT: + switch (currentType) { + case INTEGER: + log.debug("Trying to convert integer value [{}] to float", value); + Double floatValue = ((Long) value).doubleValue(); + if ((long) value == floatValue.longValue()) { + return floatValue; + } + case STRING: + log.debug("Trying to convert String value [{}] to Float", value); + return Float.valueOf((String) value); + default: + break; + } + break; + case BOOLEAN: + switch (currentType) { + case STRING: + log.debug("Trying to convert string value {} to boolean", value); + if (StringUtils.equalsIgnoreCase((String) value, "true")) { + return true; + } else if (StringUtils.equalsIgnoreCase((String) value, "false")) { + return false; + } + break; + case INTEGER: + log.debug("Trying to convert int value {} to boolean", value); + Long val = (Long) value; + if (val == 1) { + return true; + } else if (val == 0) { + return false; + } + break; + default: + break; + } + break; + case TIME: + switch (currentType) { + case INTEGER: + log.debug("Trying to convert long value {} to date", value); + /* let's assume we received the millisecond since 1970/1/1 */ + return new Date(((Number) value).longValue() * 1000L); + case STRING: + log.debug("Trying to convert string value {} to date", value); + /** let's assume we received an ISO 8601 format date */ + try { + return new Date(Long.decode(value.toString())); + /** + DatatypeFactory datatypeFactory = DatatypeFactory.newInstance(); + XMLGregorianCalendar cal = datatypeFactory.newXMLGregorianCalendar((String) value); + return cal.toGregorianCalendar().getTime(); + **/ + } catch (IllegalArgumentException e) { + log.debug("Unable to convert string to date", e); + throw new CodecException("Unable to convert string (%s) to date for resource %s", value, + resourcePath); + } + default: + break; + } + break; + case STRING: + switch (currentType) { + case BOOLEAN: + case INTEGER: + case FLOAT: + return String.valueOf(value); + case TIME: + String DATE_FORMAT = "MMM d, yyyy HH:mm a"; + Long timeValue; + try { + timeValue = ((Date) value).getTime(); + } + catch (Exception e){ + timeValue = new BigInteger((byte [])value).longValue(); + } + DateFormat formatter = new SimpleDateFormat(DATE_FORMAT); + return formatter.format(new Date(timeValue)); + case OPAQUE: + return Hex.encodeHexString((byte[])value); + case OBJLNK: + return ObjectLink.decodeFromString((String) value); + default: + break; + } + break; + case OPAQUE: + if (currentType == Type.STRING) { + /** let's assume we received an hexadecimal string */ + log.debug("Trying to convert hexadecimal/base64 string [{}] to byte array", value); + try { + return Hex.decodeHex(((String)value).toCharArray()); + } catch (IllegalArgumentException e) { + try { + return Base64.getDecoder().decode((String) value); + } catch (IllegalArgumentException ea) { + throw new CodecException("Unable to convert hexastring or base64 [%s] to byte array for resource %s", + value, resourcePath); + } + } + } + break; + case OBJLNK: + if (currentType == Type.STRING) { + return ObjectLink.fromPath(value.toString()); + } + default: + } + throw new CodecException("Invalid value type for resource %s, expected %s, got %s", resourcePath, expectedType, + currentType); + } +} \ No newline at end of file diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/lwm2m/Lwm2mTestHelper.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/lwm2m/Lwm2mTestHelper.java new file mode 100644 index 0000000000..dfb4c71a1e --- /dev/null +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/lwm2m/Lwm2mTestHelper.java @@ -0,0 +1,134 @@ +/** + * 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.msa.connectivity.lwm2m; + +import org.eclipse.californium.elements.config.Configuration; +import org.eclipse.leshan.client.object.Security; + +import static org.eclipse.californium.scandium.config.DtlsConfig.DTLS_CONNECTION_ID_LENGTH; +import static org.eclipse.californium.scandium.config.DtlsConfig.DTLS_CONNECTION_ID_NODE_ID; +import static org.eclipse.leshan.client.object.Security.noSec; + +public class Lwm2mTestHelper { + + // Models + public static final String[] resources = new String[]{ "0.xml", "1.xml", "2.xml", "3.xml", "5.xml"}; + public static final int serverId = 1; + + public static final int port = 5685; + public static final int securityPort = 5686; + public static final Integer shortServerId = 123; + + public static final String host = "localhost"; + public static final String COAP = "coap://"; + public static final String COAPS = "coaps://"; + public static final String URI = COAP + host + ":" + port; + public static final String SECURE_URI = COAPS + host + ":" + securityPort; + public static final String CLIENT_ENDPOINT_NO_SEC = "LwNoSec00000000"; + public static final Security SECURITY_NO_SEC = noSec(URI, shortServerId); + + public static final String CLIENT_ENDPOINT_PSK = "LwPsk00000000"; + public static final String CLIENT_PSK_IDENTITY = "SOME_PSK_ID"; + public static final String CLIENT_PSK_KEY = "73656372657450534b73656372657450"; + + + public static final String OBSERVE_ATTRIBUTES_WITHOUT_PARAMS = + " {\n" + + " \"keyName\": {},\n" + + " \"observe\": [],\n" + + " \"attribute\": [],\n" + + " \"telemetry\": [],\n" + + " \"attributeLwm2m\": {}\n" + + " }"; + + public static final String CLIENT_LWM2M_SETTINGS = + " {\n" + + " \"edrxCycle\": null,\n" + + " \"powerMode\": \"DRX\",\n" + + " \"fwUpdateResource\": null,\n" + + " \"fwUpdateStrategy\": 1,\n" + + " \"psmActivityTimer\": null,\n" + + " \"swUpdateResource\": null,\n" + + " \"swUpdateStrategy\": 1,\n" + + " \"pagingTransmissionWindow\": null,\n" + + " \"clientOnlyObserveAfterConnect\": 1\n" + + " }"; + + public static final int BINARY_APP_DATA_CONTAINER = 19; + public static final int OBJECT_INSTANCE_ID_0 = 0; + public static final int OBJECT_INSTANCE_ID_1 = 1; + + public enum LwM2MClientState { + + ON_INIT(0, "onInit"), + ON_BOOTSTRAP_STARTED(1, "onBootstrapStarted"), + ON_BOOTSTRAP_SUCCESS(2, "onBootstrapSuccess"), + ON_BOOTSTRAP_FAILURE(3, "onBootstrapFailure"), + ON_BOOTSTRAP_TIMEOUT(4, "onBootstrapTimeout"), + ON_REGISTRATION_STARTED(5, "onRegistrationStarted"), + ON_REGISTRATION_SUCCESS(6, "onRegistrationSuccess"), + ON_REGISTRATION_FAILURE(7, "onRegistrationFailure"), + ON_REGISTRATION_TIMEOUT(7, "onRegistrationTimeout"), + ON_UPDATE_STARTED(8, "onUpdateStarted"), + ON_UPDATE_SUCCESS(9, "onUpdateSuccess"), + ON_UPDATE_FAILURE(10, "onUpdateFailure"), + ON_UPDATE_TIMEOUT(11, "onUpdateTimeout"), + ON_DEREGISTRATION_STARTED(12, "onDeregistrationStarted"), + ON_DEREGISTRATION_SUCCESS(13, "onDeregistrationSuccess"), + ON_DEREGISTRATION_FAILURE(14, "onDeregistrationFailure"), + ON_DEREGISTRATION_TIMEOUT(15, "onDeregistrationTimeout"), + ON_EXPECTED_ERROR(16, "onUnexpectedError"), + ON_READ_CONNECTION_ID (17, "onReadConnection"), + ON_WRITE_CONNECTION_ID (18, "onWriteConnection"); + + public int code; + public String type; + + LwM2MClientState(int code, String type) { + this.code = code; + this.type = type; + } + + public static LwM2MClientState fromLwM2MClientStateByType(String type) { + for (LwM2MClientState to : LwM2MClientState.values()) { + if (to.type.equals(type)) { + return to; + } + } + throw new IllegalArgumentException(String.format("Unsupported Client State type : %s", type)); + } + + public static LwM2MClientState fromLwM2MClientStateByCode(int code) { + for (LwM2MClientState to : LwM2MClientState.values()) { + if (to.code == code) { + return to; + } + } + throw new IllegalArgumentException(String.format("Unsupported Client State code : %s", code)); + } + } + + public static void setDtlsConnectorConfigCidLength(Configuration serverCoapConfig, Integer cIdLength) { + serverCoapConfig.setTransient(DTLS_CONNECTION_ID_LENGTH); + serverCoapConfig.setTransient(DTLS_CONNECTION_ID_NODE_ID); + serverCoapConfig.set(DTLS_CONNECTION_ID_LENGTH, cIdLength); + if ( cIdLength > 4) { + serverCoapConfig.set(DTLS_CONNECTION_ID_NODE_ID, 0); + } else { + serverCoapConfig.set(DTLS_CONNECTION_ID_NODE_ID, null); + } + } +} \ No newline at end of file diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/lwm2m/SimpleLwM2MDevice.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/lwm2m/SimpleLwM2MDevice.java new file mode 100644 index 0000000000..d2aa1af4d2 --- /dev/null +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/lwm2m/SimpleLwM2MDevice.java @@ -0,0 +1,218 @@ +/** + * 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.msa.connectivity.lwm2m; + +import lombok.extern.slf4j.Slf4j; +import org.eclipse.leshan.client.resource.BaseInstanceEnabler; +import org.eclipse.leshan.client.servers.LwM2mServer; +import org.eclipse.leshan.core.Destroyable; +import org.eclipse.leshan.core.model.ObjectModel; +import org.eclipse.leshan.core.model.ResourceModel; +import org.eclipse.leshan.core.node.LwM2mResource; +import org.eclipse.leshan.core.request.argument.Arguments; +import org.eclipse.leshan.core.response.ExecuteResponse; +import org.eclipse.leshan.core.response.ReadResponse; +import org.eclipse.leshan.core.response.WriteResponse; + +import java.text.SimpleDateFormat; +import java.util.Arrays; +import java.util.Calendar; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.PrimitiveIterator; +import java.util.Random; +import java.util.TimeZone; + +@Slf4j +public class SimpleLwM2MDevice extends BaseInstanceEnabler implements Destroyable { + + + private static final Random RANDOM = new Random(); + private static final int min = 5; + private static final int max = 50; + private static final PrimitiveIterator.OfInt randomIterator = new Random().ints(min,max + 1).iterator(); + private static final List supportedResources = Arrays.asList(0, 1, 2, 3, 6, 7, 8, 9, 10, 11, 13, 14, 15, 16, 17, 18, 19, 20, 21); + + + public SimpleLwM2MDevice() { + } + + + + @Override + public ReadResponse read(LwM2mServer identity, int resourceId) { + if (!identity.isSystem()) + log.info("Read on Device resource /{}/{}/{}", getModel().id, getId(), resourceId); + switch (resourceId) { + case 0: + return ReadResponse.success(resourceId, getManufacturer()); + case 1: + return ReadResponse.success(resourceId, getModelNumber()); + case 2: + return ReadResponse.success(resourceId, getSerialNumber()); + case 3: + return ReadResponse.success(resourceId, getFirmwareVersion()); + case 6: + return ReadResponse.success(resourceId, getAvailablePowerSources(), ResourceModel.Type.INTEGER); + case 9: + return ReadResponse.success(resourceId, getBatteryLevel()); + case 10: + return ReadResponse.success(resourceId, getMemoryFree()); + case 11: + Map errorCodes = new HashMap<>(); + errorCodes.put(0, getErrorCode()); + return ReadResponse.success(resourceId, errorCodes, ResourceModel.Type.INTEGER); + case 14: + return ReadResponse.success(resourceId, getUtcOffset()); + case 15: + return ReadResponse.success(resourceId, getTimezone()); + case 16: + return ReadResponse.success(resourceId, getSupportedBinding()); + case 17: + return ReadResponse.success(resourceId, getDeviceType()); + case 18: + return ReadResponse.success(resourceId, getHardwareVersion()); + case 19: + return ReadResponse.success(resourceId, getSoftwareVersion()); + case 20: + return ReadResponse.success(resourceId, getBatteryStatus()); + case 21: + return ReadResponse.success(resourceId, getMemoryTotal()); + default: + return super.read(identity, resourceId); + } + } + + @Override + public ExecuteResponse execute(LwM2mServer identity, int resourceId, Arguments arguments) { + String withArguments = ""; + if (!arguments.isEmpty()) + withArguments = " with arguments " + arguments; + log.info("Execute on Device resource /{}/{}/{} {}", getModel().id, getId(), resourceId, withArguments); + return ExecuteResponse.success(); + } + + @Override + public WriteResponse write(LwM2mServer identity, boolean replace, int resourceId, LwM2mResource value) { + log.info("Write on Device resource /{}/{}/{}", getModel().id, getId(), resourceId); + + switch (resourceId) { + case 13: + return WriteResponse.notFound(); + case 14: + setUtcOffset((String) value.getValue()); + fireResourceChange(resourceId); + return WriteResponse.success(); + case 15: + setTimezone((String) value.getValue()); + fireResourceChange(resourceId); + return WriteResponse.success(); + default: + return super.write(identity, replace, resourceId, value); + } + } + + private String getManufacturer() { + return "Thingsboard Demo Lwm2mDevice"; + } + + private String getModelNumber() { + return "Model 500"; + } + + private String getSerialNumber() { + return "Thingsboard-500-000-0001"; + } + + private String getFirmwareVersion() { + return "1.0.2"; + } + + private long getErrorCode() { + return 0; + } + + private Map getAvailablePowerSources() { + Map availablePowerSources = new HashMap<>(); + availablePowerSources.put(0, 1L); + availablePowerSources.put(1, 2L); + availablePowerSources.put(2, 5L); + return availablePowerSources; + } + + private int getBatteryLevel() { + return randomIterator.nextInt(); +// return 42; + } + + private long getMemoryFree() { + return Runtime.getRuntime().freeMemory() / 1024; + } + + private String utcOffset = new SimpleDateFormat("X").format(Calendar.getInstance().getTime()); + + private String getUtcOffset() { + return utcOffset; + } + + private void setUtcOffset(String t) { + utcOffset = t; + } + + private String timeZone = TimeZone.getDefault().getID(); + + private String getTimezone() { + return timeZone; + } + + private void setTimezone(String t) { + timeZone = t; + } + + private String getSupportedBinding() { + return "U"; + } + + private String getDeviceType() { + return "Demo"; + } + + private String getHardwareVersion() { + return "1.0.1"; + } + + private String getSoftwareVersion() { + return "1.0.2"; + } + + private int getBatteryStatus() { + return RANDOM.nextInt(7); + } + + private long getMemoryTotal() { + return Runtime.getRuntime().totalMemory() / 1024; + } + + @Override + public List getAvailableResourceIds(ObjectModel model) { + return supportedResources; + } + + @Override + public void destroy() { + } +} diff --git a/msa/black-box-tests/src/test/resources/lwm2m-registry/0.xml b/msa/black-box-tests/src/test/resources/lwm2m-registry/0.xml new file mode 100644 index 0000000000..81e8523880 --- /dev/null +++ b/msa/black-box-tests/src/test/resources/lwm2m-registry/0.xml @@ -0,0 +1,405 @@ + + + + + + + LWM2M Security + + 0 + urn:oma:lwm2m:oma:0:1.2 + 1.1 + 1.2 + Multiple + Mandatory + + + LWM2M Server URI + + Single + Mandatory + String + 0..255 + + + + + Bootstrap-Server + + Single + Mandatory + Boolean + + + + + + Security Mode + + Single + Mandatory + Integer + 0..4 + + + + + Public Key or Identity + + Single + Mandatory + Opaque + + + + + + Server Public Key + + Single + Mandatory + Opaque + + + + + + Secret Key + + Single + Mandatory + Opaque + + + + + + SMS Security Mode + + Single + Optional + Integer + 0..255 + + + + + SMS Binding Key Parameters + + Single + Optional + Opaque + 6 + + + + + SMS Binding Secret Key(s) + + Single + Optional + Opaque + 16,32,48 + + + + + LwM2M Server SMS Number + + Single + Optional + String + + + + + + Short Server ID + + Single + Optional + Integer + 1..65534 + + + + + Client Hold Off Time + + Single + Optional + Integer + + s + + + + Bootstrap-Server Account Timeout + + Single + Optional + Integer + + s + + + + Matching Type + + Single + Optional + Integer + 0..3 + + + + + SNI + + Single + Optional + String + + + + + + Certificate Usage + + Single + Optional + Integer + 0..3 + + + + + DTLS/TLS Ciphersuite + + Multiple + Optional + Integer + + + + + OSCORE Security Mode + + Single + Optional + Objlnk + + + + + + Groups To Use by Client + + Multiple + Optional + Integer + 0..65535 + + + + + Signature Algorithms Supported by Server + + Multiple + Optional + Integer + 0..65535 + + + + Signature Algorithms To Use by Client + + Multiple + Optional + Integer + 0..65535 + + + + + Signature Algorithm Certs Supported by Server + + Multiple + Optional + Integer + 0..65535 + + + + + TLS 1.3 Features To Use by Client + + Single + Optional + Integer + 0..65535 + + + + + TLS Extensions Supported by Server + + Single + Optional + Integer + 0..65535 + + + + + TLS Extensions To Use by Client + + Single + Optional + Integer + 0..65535 + + + + + Secondary LwM2M Server URI + + Multiple + Optional + String + 0..255 + + + + MQTT Server + + Single + Optional + Objlnk + + + + + LwM2M COSE Security + + Multiple + Optional + Objlnk + + + + + RDS Destination Port + + Single + Optional + Integer + 0..15 + + + + RDS Source Port + + Single + Optional + Integer + 0..15 + + + + RDS Application ID + + Single + Optional + String + + + + + + + + diff --git a/msa/black-box-tests/src/test/resources/lwm2m-registry/1.xml b/msa/black-box-tests/src/test/resources/lwm2m-registry/1.xml new file mode 100644 index 0000000000..f31e839c96 --- /dev/null +++ b/msa/black-box-tests/src/test/resources/lwm2m-registry/1.xml @@ -0,0 +1,360 @@ + + + + + + + LwM2M Server + + 1 + urn:oma:lwm2m:oma:1:1.2 + 1.2 + 1.2 + Multiple + Mandatory + + + Short Server ID + R + Single + Mandatory + Integer + 1..65534 + + + + + Lifetime + RW + Single + Mandatory + Integer + + s + + + + Default Minimum Period + RW + Single + Optional + Integer + + s + + + + Default Maximum Period + RW + Single + Optional + Integer + + s + + + + Disable + E + Single + Optional + + + + + + + Disable Timeout + RW + Single + Optional + Integer + + s + + + + Notification Storing When Disabled or Offline + RW + Single + Mandatory + Boolean + + + + + + Binding + RW + Single + Mandatory + String + + + + + + Registration Update Trigger + E + Single + Mandatory + + + + + + + Bootstrap-Request Trigger + E + Single + Optional + + + + + + + APN Link + RW + Single + Optional + Objlnk + + + + + + TLS-DTLS Alert Code + R + Single + Optional + Integer + 0..255 + + + + + Last Bootstrapped + R + Single + Optional + Time + + + + + + Registration Priority Order + R + Single + Optional + Integer + + + + + + Initial Registration Delay Timer + RW + Single + Optional + Integer + + s + + + + Registration Failure Block + R + Single + Optional + Boolean + + + + + + Bootstrap on Registration Failure + R + Single + Optional + Boolean + + + + + + Communication Retry Count + RW + Single + Optional + Integer + + + + + + Communication Retry Timer + RW + Single + Optional + Integer + + s + + + + Communication Sequence Delay Timer + RW + Single + Optional + Integer + + s + + + + Communication Sequence Retry Count + RW + Single + Optional + Integer + + + + + + Trigger + RW + Single + Optional + Boolean + + + + + + Preferred Transport + RW + Single + Optional + String + The possible values are those listed in the LwM2M Core Specification + + + + Mute Send + RW + Single + Optional + Boolean + + + + + + Alternate APN Links + RW + Multiple + Optional + Objlnk + + + + + + Supported Server Versions + RW + Multiple + Optional + String + + + + + + Default Notification Mode + RW + Single + Optional + Integer + 0..1 + + + + + Profile ID Hash Algorithm + RW + Single + Optional + Integer + 0..255 + + + + + + + diff --git a/msa/black-box-tests/src/test/resources/lwm2m-registry/2.xml b/msa/black-box-tests/src/test/resources/lwm2m-registry/2.xml new file mode 100644 index 0000000000..4ea5805b36 --- /dev/null +++ b/msa/black-box-tests/src/test/resources/lwm2m-registry/2.xml @@ -0,0 +1,123 @@ + + + + + + + LwM2M Access Control + + 2 + urn:oma:lwm2m:oma:2:1.1 + 1.0 + 1.1 + Multiple + Optional + + + Object ID + R + Single + Mandatory + Integer + 1..65534 + + + + + Object Instance ID + R + Single + Mandatory + Integer + 0..65535 + + + + + ACL + RW + Multiple + Optional + Integer + 0..31 + + + + + Access Control Owner + RW + Single + Mandatory + Integer + 0..65535 + + + + + + + diff --git a/msa/black-box-tests/src/test/resources/lwm2m-registry/3.xml b/msa/black-box-tests/src/test/resources/lwm2m-registry/3.xml new file mode 100644 index 0000000000..e71c2045c2 --- /dev/null +++ b/msa/black-box-tests/src/test/resources/lwm2m-registry/3.xml @@ -0,0 +1,331 @@ + + + + + + + Device + + 3 + urn:oma:lwm2m:oma:3:1.0 + 1.1 + 1.2 + Single + Mandatory + + + Manufacturer + R + Single + Optional + String + + + + + + Model Number + R + Single + Optional + String + + + + + + Serial Number + R + Single + Optional + String + + + + + + Firmware Version + R + Single + Optional + String + + + + + + Reboot + E + Single + Mandatory + + + + + + + Factory Reset + E + Single + Optional + + + + + + + Available Power Sources + R + Multiple + Optional + Integer + 0..7 + + + + + Power Source Voltage + R + Multiple + Optional + Integer + + + + + + Power Source Current + R + Multiple + Optional + Integer + + + + + + Battery Level + R + Single + Optional + Integer + 0..100 + /100 + + + + Memory Free + R + Single + Optional + Integer + + + + + + Error Code + R + Multiple + Mandatory + Integer + 0..32 + + + + + Reset Error Code + E + Single + Optional + + + + + + + Current Time + RW + Single + Optional + Time + + + + + + UTC Offset + RW + Single + Optional + String + + + + + + Timezone + RW + Single + Optional + String + + + + + + Supported Binding and Modes + R + Single + Mandatory + String + + + + + Device Type + R + Single + Optional + String + + + + + Hardware Version + R + Single + Optional + String + + + + + Software Version + R + Single + Optional + String + + + + + Battery Status + R + Single + Optional + Integer + 0..6 + + + + Memory Total + R + Single + Optional + Integer + + + + + ExtDevInfo + R + Multiple + Optional + Objlnk + + + + + + + diff --git a/msa/black-box-tests/src/test/resources/lwm2m-registry/5.xml b/msa/black-box-tests/src/test/resources/lwm2m-registry/5.xml new file mode 100644 index 0000000000..ddcea323b0 --- /dev/null +++ b/msa/black-box-tests/src/test/resources/lwm2m-registry/5.xml @@ -0,0 +1,204 @@ + + + + + + + Firmware Update V_1_0 + + 5 + urn:oma:lwm2m:oma:5 + 1.0 + 1.2 + Single + Optional + + + Package + W + Single + Mandatory + Opaque + + + + + + Package URI + RW + Single + Mandatory + String + 0..255 + + + + + Update + E + Single + Mandatory + + + + + + + State + R + Single + Mandatory + Integer + 0..3 + + + + + Update Result + R + Single + Mandatory + Integer + 0..9 + + + + + PkgName + R + Single + Optional + String + 0..255 + + + + + PkgVersion + R + Single + Optional + String + 0..255 + + + + + Firmware Update Protocol Support + R + Multiple + Optional + Integer + 0..5 + + + + + Firmware Update Delivery Method + R + Single + Mandatory + Integer + 0..2 + + + + + + + diff --git a/docker/tb-transports/coap/conf/logback.xml b/msa/black-box-tests/src/test/resources/tb-transports/lwm2m/conf/logback.xml similarity index 85% rename from docker/tb-transports/coap/conf/logback.xml rename to msa/black-box-tests/src/test/resources/tb-transports/lwm2m/conf/logback.xml index 22621a93de..f8ecd5d96f 100644 --- a/docker/tb-transports/coap/conf/logback.xml +++ b/msa/black-box-tests/src/test/resources/tb-transports/lwm2m/conf/logback.xml @@ -21,10 +21,10 @@ - /var/log/tb-coap-transport/${TB_SERVICE_ID}/tb-coap-transport.log + /var/log/tb-lwm2m-transport/${TB_SERVICE_ID}/tb-lwm2m-transport.log - /var/log/tb-coap-transport/${TB_SERVICE_ID}/tb-coap-transport.%d{yyyy-MM-dd}.%i.log + /var/log/tb-lwm2m-transport/${TB_SERVICE_ID}/tb-lwm2m-transport.%d{yyyy-MM-dd}.%i.log 100MB 30 3GB @@ -47,6 +47,8 @@ + + diff --git a/msa/tb/README.md b/msa/tb/README.md index 6987c42bfa..2ece5a3e2b 100644 --- a/msa/tb/README.md +++ b/msa/tb/README.md @@ -21,7 +21,7 @@ In this example `thingsboard/tb` image will be used. You can choose any other im Execute the following command to run this docker directly: ` -$ docker run -it -p 9090:9090 -p 1883:1883 -p 5683:5683/udp -p 5685:5685/udp -v ~/.mytb-data:/data --name mytb thingsboard/tb +$ docker run -it -p 9090:9090 -p 1883:1883 -p 5683:5683/udp -p 5685:5685/udp -p 5686:5686/udp -v ~/.mytb-data:/data --name mytb thingsboard/tb ` Where: @@ -32,6 +32,7 @@ Where: - `-p 1883:1883` - connect local port 1883 to exposed internal MQTT port 1883 - `-p 5683:5683` - connect local port 5683 to exposed internal COAP port 5683 - `-p 5685:5685` - connect local port 5685 to exposed internal COAP port 5685 (lwm2m) +- `-p 5686:5686` - connect local port 5686 to exposed internal COAPS port 5686 (lwm2m) - `-v ~/.mytb-data:/data` - mounts the host's dir `~/.mytb-data` to ThingsBoard DataBase data directory - `--name mytb` - friendly local name of this machine - `thingsboard/tb` - docker image, can be also `thingsboard/tb-postgres` or `thingsboard/tb-cassandra` @@ -47,6 +48,7 @@ Where: > $ VBoxManage controlvm "default" natpf1 "tcp-port1883,tcp,,1883,,1883" > $ VBoxManage controlvm "default" natpf1 "tcp-port5683,tcp,,5683,,5683" > $ VBoxManage controlvm "default" natpf1 "tcp-port5683,tcp,,5685,,5685" +> $ VBoxManage controlvm "default" natpf1 "tcp-port5683,tcp,,5686,,5686" > ``` After executing `docker run` command you can open `http://{your-host-ip}:9090` in you browser (for ex. `http://localhost:9090`). You should see ThingsBoard login page. diff --git a/msa/tb/docker-cassandra/Dockerfile b/msa/tb/docker-cassandra/Dockerfile index 3d4997dd88..18071b249b 100644 --- a/msa/tb/docker-cassandra/Dockerfile +++ b/msa/tb/docker-cassandra/Dockerfile @@ -87,6 +87,7 @@ EXPOSE 9090 EXPOSE 1883 EXPOSE 5683/udp EXPOSE 5685/udp +EXPOSE 5686/udp VOLUME ["/data"] diff --git a/msa/tb/docker-postgres/Dockerfile b/msa/tb/docker-postgres/Dockerfile index dcf766f00e..984e390496 100644 --- a/msa/tb/docker-postgres/Dockerfile +++ b/msa/tb/docker-postgres/Dockerfile @@ -69,6 +69,7 @@ EXPOSE 9090 EXPOSE 1883 EXPOSE 5683/udp EXPOSE 5685/udp +EXPOSE 5686/udp VOLUME ["/data"] From 6b0aead10b631d4fc0c380ea031dfa5248970030 Mon Sep 17 00:00:00 2001 From: nick Date: Wed, 10 Apr 2024 15:12:02 +0300 Subject: [PATCH 35/80] msa: lwm2m tests add NoSec Psk (2) --- docker/tb-transports/coap/conf/logback.xml | 54 ++++++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 docker/tb-transports/coap/conf/logback.xml diff --git a/docker/tb-transports/coap/conf/logback.xml b/docker/tb-transports/coap/conf/logback.xml new file mode 100644 index 0000000000..d09529daca --- /dev/null +++ b/docker/tb-transports/coap/conf/logback.xml @@ -0,0 +1,54 @@ + + + + + + + /var/log/tb-coap-transport/${TB_SERVICE_ID}/tb-coap-transport.log + + /var/log/tb-coap-transport/${TB_SERVICE_ID}/tb-coap-transport.%d{yyyy-MM-dd}.%i.log + 100MB + 30 + 3GB + + + %d{ISO8601} [%thread] %-5level %logger{36} - %msg%n + + + + + + %d{ISO8601} [%thread] %-5level %logger{36} - %msg%n + + + + + + + + + + + + + + + From 2b334ecbff30729477c1c3ce34022820ee5e2f7e Mon Sep 17 00:00:00 2001 From: nick Date: Wed, 10 Apr 2024 15:15:44 +0300 Subject: [PATCH 36/80] msa: lwm2m tests add NoSec Psk (3) --- docker/tb-transports/coap/conf/logback.xml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docker/tb-transports/coap/conf/logback.xml b/docker/tb-transports/coap/conf/logback.xml index d09529daca..22621a93de 100644 --- a/docker/tb-transports/coap/conf/logback.xml +++ b/docker/tb-transports/coap/conf/logback.xml @@ -43,6 +43,7 @@ + @@ -51,4 +52,4 @@ - + \ No newline at end of file From b43a90d1dfab6f3bacfc8b7a2e0e3e21d637e740 Mon Sep 17 00:00:00 2001 From: Dmytro Skarzhynets Date: Wed, 10 Apr 2024 15:24:58 +0300 Subject: [PATCH 37/80] Separate create alarm node tests from clear alarm node tests, refactor existing and add more create alarm node tests --- .../rule/engine/action/TbCreateAlarmNode.java | 6 +- .../rule/engine/action/TbAlarmNodeTest.java | 642 --------- .../engine/action/TbClearAlarmNodeTest.java | 218 +++ .../engine/action/TbCreateAlarmNodeTest.java | 1223 +++++++++++++++++ 4 files changed, 1446 insertions(+), 643 deletions(-) delete mode 100644 rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbAlarmNodeTest.java create mode 100644 rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbClearAlarmNodeTest.java create mode 100644 rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbCreateAlarmNodeTest.java diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbCreateAlarmNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbCreateAlarmNode.java index 9377fca636..7e535596cd 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbCreateAlarmNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbCreateAlarmNode.java @@ -176,7 +176,7 @@ public class TbCreateAlarmNode extends TbAbstractAlarmNode successCaptor; - @Captor - private ArgumentCaptor> failureCaptor; - - private final RuleChainId ruleChainId = new RuleChainId(Uuids.timeBased()); - private final RuleNodeId ruleNodeId = new RuleNodeId(Uuids.timeBased()); - - private ListeningExecutor dbExecutor; - - private final EntityId originator = new DeviceId(Uuids.timeBased()); - private final EntityId alarmOriginator = new AlarmId(Uuids.timeBased()); - private final TenantId tenantId = TenantId.fromUUID(Uuids.timeBased()); - private final TbMsgMetaData metaData = new TbMsgMetaData(); - private final String rawJson = "{\"name\": \"Vit\", \"passed\": 5}"; - - @Before - public void before() { - dbExecutor = new TestDbCallbackExecutor(); - } - - @Test - public void newAlarmCanBeCreated() { - initWithCreateAlarmScript(); - metaData.putValue("key", "value"); - TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, originator, metaData, TbMsgDataType.JSON, rawJson, ruleChainId, ruleNodeId); - long ts = msg.getTs(); - - when(detailsJs.executeJsonAsync(msg)).thenReturn(Futures.immediateFuture(null)); - when(alarmService.findLatestActiveByOriginatorAndType(tenantId, originator, "SomeType")).thenReturn(null); - Alarm expectedAlarm = Alarm.builder() - .startTs(ts) - .endTs(ts) - .tenantId(tenantId) - .originator(originator) - .severity(AlarmSeverity.CRITICAL) - .propagate(true) - .type("SomeType") - .details(null) - .build(); - when(alarmService.createAlarm(any(AlarmCreateOrUpdateActiveRequest.class))).thenReturn( - AlarmApiCallResult.builder() - .created(true) - .alarm(new AlarmInfo(expectedAlarm)) - .build()); - - node.onMsg(ctx, msg); - - verify(ctx).enqueue(any(), successCaptor.capture(), failureCaptor.capture()); - successCaptor.getValue().run(); - verify(ctx).tellNext(any(), eq("Created")); - - ArgumentCaptor msgCaptor = ArgumentCaptor.forClass(TbMsg.class); - ArgumentCaptor typeCaptor = ArgumentCaptor.forClass(TbMsgType.class); - ArgumentCaptor originatorCaptor = ArgumentCaptor.forClass(EntityId.class); - ArgumentCaptor metadataCaptor = ArgumentCaptor.forClass(TbMsgMetaData.class); - ArgumentCaptor dataCaptor = ArgumentCaptor.forClass(String.class); - verify(ctx).transformMsg(msgCaptor.capture(), typeCaptor.capture(), originatorCaptor.capture(), metadataCaptor.capture(), dataCaptor.capture()); - - assertEquals(TbMsgType.ALARM, typeCaptor.getValue()); - assertEquals(originator, originatorCaptor.getValue()); - assertEquals("value", metadataCaptor.getValue().getValue("key")); - assertEquals(Boolean.TRUE.toString(), metadataCaptor.getValue().getValue(DataConstants.IS_NEW_ALARM)); - assertNotSame(metaData, metadataCaptor.getValue()); - - Alarm actualAlarm = JacksonUtil.fromBytes(dataCaptor.getValue().getBytes(), Alarm.class); - assertEquals(expectedAlarm, actualAlarm); - } - - @Test - public void buildDetailsThrowsException() { - initWithCreateAlarmScript(); - metaData.putValue("key", "value"); - TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, originator, metaData, TbMsgDataType.JSON, rawJson, ruleChainId, ruleNodeId); - - when(detailsJs.executeJsonAsync(msg)).thenReturn(Futures.immediateFailedFuture(new NotImplementedException("message"))); - when(alarmService.findLatestActiveByOriginatorAndType(tenantId, originator, "SomeType")).thenReturn(null); - - node.onMsg(ctx, msg); - - verifyError(msg, "message", NotImplementedException.class); - - verify(ctx).createScriptEngine(ScriptLanguage.JS, "DETAILS"); - verify(ctx).getAlarmService(); - verify(ctx, times(2)).getDbCallbackExecutor(); - verify(ctx).logJsEvalRequest(); - verify(ctx).getTenantId(); - verify(alarmService).findLatestActiveByOriginatorAndType(tenantId, originator, "SomeType"); - - verifyNoMoreInteractions(ctx, alarmService); - } - - @Test - public void ifAlarmClearedCreateNew() { - initWithCreateAlarmScript(); - metaData.putValue("key", "value"); - TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, originator, metaData, TbMsgDataType.JSON, rawJson, ruleChainId, ruleNodeId); - long ts = msg.getTs(); - Alarm clearedAlarm = Alarm.builder().cleared(true).acknowledged(true).build(); - - when(detailsJs.executeJsonAsync(msg)).thenReturn(Futures.immediateFuture(null)); - when(alarmService.findLatestActiveByOriginatorAndType(tenantId, originator, "SomeType")).thenReturn(clearedAlarm); - - Alarm expectedAlarm = Alarm.builder() - .startTs(ts) - .endTs(ts) - .tenantId(tenantId) - .originator(originator) - .severity(AlarmSeverity.CRITICAL) - .propagate(true) - .type("SomeType") - .details(null) - .build(); - when(alarmService.createAlarm(any(AlarmCreateOrUpdateActiveRequest.class))).thenReturn( - AlarmApiCallResult.builder() - .successful(true) - .created(true) - .alarm(new AlarmInfo(expectedAlarm)) - .build()); - - node.onMsg(ctx, msg); - - verify(ctx).enqueue(any(), successCaptor.capture(), failureCaptor.capture()); - successCaptor.getValue().run(); - verify(ctx).tellNext(any(), eq("Created")); - - ArgumentCaptor msgCaptor = ArgumentCaptor.forClass(TbMsg.class); - ArgumentCaptor typeCaptor = ArgumentCaptor.forClass(TbMsgType.class); - ArgumentCaptor originatorCaptor = ArgumentCaptor.forClass(EntityId.class); - ArgumentCaptor metadataCaptor = ArgumentCaptor.forClass(TbMsgMetaData.class); - ArgumentCaptor dataCaptor = ArgumentCaptor.forClass(String.class); - verify(ctx).transformMsg(msgCaptor.capture(), typeCaptor.capture(), originatorCaptor.capture(), metadataCaptor.capture(), dataCaptor.capture()); - - assertEquals(TbMsgType.ALARM, typeCaptor.getValue()); - assertEquals(originator, originatorCaptor.getValue()); - assertEquals("value", metadataCaptor.getValue().getValue("key")); - assertEquals(Boolean.TRUE.toString(), metadataCaptor.getValue().getValue(DataConstants.IS_NEW_ALARM)); - assertNotSame(metaData, metadataCaptor.getValue()); - - - Alarm actualAlarm = JacksonUtil.fromBytes(dataCaptor.getValue().getBytes(), Alarm.class); - assertEquals(expectedAlarm, actualAlarm); - } - - @Test - public void alarmCanBeUpdated() { - initWithCreateAlarmScript(); - metaData.putValue("key", "value"); - TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, originator, metaData, TbMsgDataType.JSON, rawJson, ruleChainId, ruleNodeId); - - long oldEndDate = System.currentTimeMillis(); - Alarm activeAlarm = Alarm.builder().type("SomeType").tenantId(tenantId).originator(originator).severity(AlarmSeverity.WARNING).endTs(oldEndDate).build(); - - when(detailsJs.executeJsonAsync(msg)).thenReturn(Futures.immediateFuture(null)); - when(alarmService.findLatestActiveByOriginatorAndType(tenantId, originator, "SomeType")).thenReturn(activeAlarm); - - Alarm expectedAlarm = Alarm.builder() - .tenantId(tenantId) - .originator(originator) - .severity(AlarmSeverity.CRITICAL) - .propagate(true) - .type("SomeType") - .details(null) - .endTs(activeAlarm.getEndTs()) - .build(); - when(alarmService.updateAlarm(any(AlarmUpdateRequest.class))).thenReturn( - AlarmApiCallResult.builder() - .successful(true) - .modified(true) - .old(new Alarm(activeAlarm)) - .alarm(new AlarmInfo(expectedAlarm)) - .build()); - node.onMsg(ctx, msg); - - verify(ctx).enqueue(any(), successCaptor.capture(), failureCaptor.capture()); - successCaptor.getValue().run(); - verify(ctx).tellNext(any(), eq("Updated")); - - ArgumentCaptor msgCaptor = ArgumentCaptor.forClass(TbMsg.class); - ArgumentCaptor typeCaptor = ArgumentCaptor.forClass(TbMsgType.class); - ArgumentCaptor originatorCaptor = ArgumentCaptor.forClass(EntityId.class); - ArgumentCaptor metadataCaptor = ArgumentCaptor.forClass(TbMsgMetaData.class); - ArgumentCaptor dataCaptor = ArgumentCaptor.forClass(String.class); - verify(ctx).transformMsg(msgCaptor.capture(), typeCaptor.capture(), originatorCaptor.capture(), metadataCaptor.capture(), dataCaptor.capture()); - - assertEquals(TbMsgType.ALARM, typeCaptor.getValue()); - assertEquals(originator, originatorCaptor.getValue()); - assertEquals("value", metadataCaptor.getValue().getValue("key")); - assertEquals(Boolean.TRUE.toString(), metadataCaptor.getValue().getValue(DataConstants.IS_EXISTING_ALARM)); - assertNotSame(metaData, metadataCaptor.getValue()); - - Alarm actualAlarm = JacksonUtil.fromBytes(dataCaptor.getValue().getBytes(), Alarm.class); - assertTrue(activeAlarm.getEndTs() >= oldEndDate); - assertEquals(expectedAlarm, actualAlarm); - } - - @Test - public void alarmCanBeCleared() { - initWithClearAlarmScript(); - metaData.putValue("key", "value"); - TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, originator, metaData, TbMsgDataType.JSON, rawJson, ruleChainId, ruleNodeId); - - long oldEndDate = System.currentTimeMillis(); - Alarm activeAlarm = Alarm.builder().type("SomeType").tenantId(tenantId).originator(originator).severity(AlarmSeverity.WARNING).endTs(oldEndDate).build(); - - Alarm expectedAlarm = Alarm.builder() - .tenantId(tenantId) - .originator(originator) - .cleared(true) - .severity(AlarmSeverity.WARNING) - .propagate(false) - .type("SomeType") - .details(null) - .endTs(oldEndDate) - .build(); - - when(detailsJs.executeJsonAsync(msg)).thenReturn(Futures.immediateFuture(null)); - when(alarmService.findLatestActiveByOriginatorAndType(tenantId, originator, "SomeType")).thenReturn(activeAlarm); - when(alarmService.clearAlarm(eq(activeAlarm.getTenantId()), eq(activeAlarm.getId()), anyLong(), nullable(JsonNode.class))) - .thenReturn(AlarmApiCallResult.builder() - .successful(true) - .cleared(true) - .alarm(new AlarmInfo(expectedAlarm)) - .build()); - - node.onMsg(ctx, msg); - - verify(ctx).enqueue(any(), successCaptor.capture(), failureCaptor.capture()); - successCaptor.getValue().run(); - verify(ctx).tellNext(any(), eq("Cleared")); - - ArgumentCaptor msgCaptor = ArgumentCaptor.forClass(TbMsg.class); - ArgumentCaptor typeCaptor = ArgumentCaptor.forClass(TbMsgType.class); - ArgumentCaptor originatorCaptor = ArgumentCaptor.forClass(EntityId.class); - ArgumentCaptor metadataCaptor = ArgumentCaptor.forClass(TbMsgMetaData.class); - ArgumentCaptor dataCaptor = ArgumentCaptor.forClass(String.class); - verify(ctx).transformMsg(msgCaptor.capture(), typeCaptor.capture(), originatorCaptor.capture(), metadataCaptor.capture(), dataCaptor.capture()); - - assertEquals(TbMsgType.ALARM, typeCaptor.getValue()); - assertEquals(originator, originatorCaptor.getValue()); - assertEquals("value", metadataCaptor.getValue().getValue("key")); - assertEquals(Boolean.TRUE.toString(), metadataCaptor.getValue().getValue(DataConstants.IS_CLEARED_ALARM)); - assertNotSame(metaData, metadataCaptor.getValue()); - - Alarm actualAlarm = JacksonUtil.fromBytes(dataCaptor.getValue().getBytes(), Alarm.class); - assertEquals(expectedAlarm, actualAlarm); - } - - @Test - public void alarmCanBeClearedWithAlarmOriginator() throws ScriptException, IOException { - initWithClearAlarmScript(); - metaData.putValue("key", "value"); - TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, alarmOriginator, metaData, TbMsgDataType.JSON, rawJson, ruleChainId, ruleNodeId); - - long oldEndDate = System.currentTimeMillis(); - AlarmId id = new AlarmId(alarmOriginator.getId()); - Alarm activeAlarm = Alarm.builder().type("SomeType").tenantId(tenantId).originator(originator).severity(AlarmSeverity.WARNING).endTs(oldEndDate).build(); - activeAlarm.setId(id); - - Alarm expectedAlarm = Alarm.builder() - .tenantId(tenantId) - .originator(originator) - .cleared(true) - .severity(AlarmSeverity.WARNING) - .propagate(false) - .type("SomeType") - .details(null) - .endTs(oldEndDate) - .build(); - expectedAlarm.setId(id); - - when(detailsJs.executeJsonAsync(msg)).thenReturn(Futures.immediateFuture(null)); - when(alarmService.findAlarmById(tenantId, id)).thenReturn(activeAlarm); - when(alarmService.clearAlarm(eq(activeAlarm.getTenantId()), eq(activeAlarm.getId()), anyLong(), nullable(JsonNode.class))) - .thenReturn(AlarmApiCallResult.builder() - .successful(true) - .cleared(true) - .alarm(new AlarmInfo(expectedAlarm)) - .build()); - - node.onMsg(ctx, msg); - - verify(ctx).enqueue(any(), successCaptor.capture(), failureCaptor.capture()); - successCaptor.getValue().run(); - verify(ctx).tellNext(any(), eq("Cleared")); - - ArgumentCaptor msgCaptor = ArgumentCaptor.forClass(TbMsg.class); - ArgumentCaptor typeCaptor = ArgumentCaptor.forClass(TbMsgType.class); - ArgumentCaptor originatorCaptor = ArgumentCaptor.forClass(EntityId.class); - ArgumentCaptor metadataCaptor = ArgumentCaptor.forClass(TbMsgMetaData.class); - ArgumentCaptor dataCaptor = ArgumentCaptor.forClass(String.class); - verify(ctx).transformMsg(msgCaptor.capture(), typeCaptor.capture(), originatorCaptor.capture(), metadataCaptor.capture(), dataCaptor.capture()); - - assertEquals(TbMsgType.ALARM, typeCaptor.getValue()); - assertEquals(alarmOriginator, originatorCaptor.getValue()); - assertEquals("value", metadataCaptor.getValue().getValue("key")); - assertEquals(Boolean.TRUE.toString(), metadataCaptor.getValue().getValue(DataConstants.IS_CLEARED_ALARM)); - assertNotSame(metaData, metadataCaptor.getValue()); - - Alarm actualAlarm = JacksonUtil.fromBytes(dataCaptor.getValue().getBytes(), Alarm.class); - assertEquals(expectedAlarm, actualAlarm); - } - - @Test - public void testCreateAlarmWithDynamicSeverityFromMessageBody() throws Exception { - TbCreateAlarmNodeConfiguration config = new TbCreateAlarmNodeConfiguration(); - config.setPropagate(true); - config.setSeverity("$[alarmSeverity]"); - config.setAlarmType("SomeType"); - config.setScriptLang(ScriptLanguage.JS); - config.setAlarmDetailsBuildJs("DETAILS"); - config.setDynamicSeverity(true); - TbNodeConfiguration nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config)); - - when(ctx.createScriptEngine(ScriptLanguage.JS, "DETAILS")).thenReturn(detailsJs); - - when(ctx.getTenantId()).thenReturn(tenantId); - when(ctx.getAlarmService()).thenReturn(alarmService); - when(ctx.getDbCallbackExecutor()).thenReturn(dbExecutor); - - node = new TbCreateAlarmNode(); - node.init(ctx, nodeConfiguration); - - String rawJson = "{\"alarmSeverity\": \"WARNING\", \"passed\": 5}"; - metaData.putValue("key", "value"); - TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, originator, metaData, TbMsgDataType.JSON, rawJson, ruleChainId, ruleNodeId); - long ts = msg.getTs(); - Alarm expectedAlarm = Alarm.builder() - .startTs(ts) - .endTs(ts) - .tenantId(tenantId) - .originator(originator) - .severity(AlarmSeverity.WARNING) - .propagate(true) - .type("SomeType") - .details(null) - .build(); - - when(detailsJs.executeJsonAsync(msg)).thenReturn(Futures.immediateFuture(null)); - when(alarmService.findLatestActiveByOriginatorAndType(tenantId, originator, "SomeType")).thenReturn(null); - when(alarmService.createAlarm(any(AlarmCreateOrUpdateActiveRequest.class))).thenReturn( - AlarmApiCallResult.builder() - .successful(true) - .created(true) - .alarm(new AlarmInfo(expectedAlarm)) - .build()); - - node.onMsg(ctx, msg); - - verify(ctx).enqueue(any(), successCaptor.capture(), failureCaptor.capture()); - successCaptor.getValue().run(); - verify(ctx).tellNext(any(), eq("Created")); - - ArgumentCaptor msgCaptor = ArgumentCaptor.forClass(TbMsg.class); - ArgumentCaptor typeCaptor = ArgumentCaptor.forClass(TbMsgType.class); - ArgumentCaptor originatorCaptor = ArgumentCaptor.forClass(EntityId.class); - ArgumentCaptor metadataCaptor = ArgumentCaptor.forClass(TbMsgMetaData.class); - ArgumentCaptor dataCaptor = ArgumentCaptor.forClass(String.class); - verify(ctx).transformMsg(msgCaptor.capture(), typeCaptor.capture(), originatorCaptor.capture(), metadataCaptor.capture(), dataCaptor.capture()); - - assertEquals(TbMsgType.ALARM, typeCaptor.getValue()); - assertEquals(originator, originatorCaptor.getValue()); - assertEquals("value", metadataCaptor.getValue().getValue("key")); - assertEquals(Boolean.TRUE.toString(), metadataCaptor.getValue().getValue(DataConstants.IS_NEW_ALARM)); - assertNotSame(metaData, metadataCaptor.getValue()); - - Alarm actualAlarm = JacksonUtil.fromBytes(dataCaptor.getValue().getBytes(), Alarm.class); - assertEquals(expectedAlarm, actualAlarm); - } - - @Test - public void testCreateAlarmWithDynamicSeverityFromMetadata() throws Exception { - TbCreateAlarmNodeConfiguration config = new TbCreateAlarmNodeConfiguration(); - config.setPropagate(true); - config.setScriptLang(ScriptLanguage.JS); - config.setSeverity("${alarmSeverity}"); - config.setAlarmType("SomeType"); - config.setAlarmDetailsBuildJs("DETAILS"); - config.setDynamicSeverity(true); - TbNodeConfiguration nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config)); - - when(ctx.createScriptEngine(ScriptLanguage.JS, "DETAILS")).thenReturn(detailsJs); - - when(ctx.getTenantId()).thenReturn(tenantId); - when(ctx.getAlarmService()).thenReturn(alarmService); - when(ctx.getDbCallbackExecutor()).thenReturn(dbExecutor); - - node = new TbCreateAlarmNode(); - node.init(ctx, nodeConfiguration); - - metaData.putValue("alarmSeverity", "WARNING"); - TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, originator, metaData, TbMsgDataType.JSON, rawJson, ruleChainId, ruleNodeId); - long ts = msg.getTs(); - Alarm expectedAlarm = Alarm.builder() - .startTs(ts) - .endTs(ts) - .tenantId(tenantId) - .originator(originator) - .severity(AlarmSeverity.WARNING) - .propagate(true) - .type("SomeType") - .details(null) - .build(); - - when(detailsJs.executeJsonAsync(msg)).thenReturn(Futures.immediateFuture(null)); - when(alarmService.findLatestActiveByOriginatorAndType(tenantId, originator, "SomeType")).thenReturn(null); - when(alarmService.createAlarm(any(AlarmCreateOrUpdateActiveRequest.class))).thenReturn( - AlarmApiCallResult.builder() - .successful(true) - .created(true) - .alarm(new AlarmInfo(expectedAlarm)) - .build()); - - node.onMsg(ctx, msg); - - verify(ctx).enqueue(any(), successCaptor.capture(), failureCaptor.capture()); - successCaptor.getValue().run(); - verify(ctx).tellNext(any(), eq("Created")); - - ArgumentCaptor msgCaptor = ArgumentCaptor.forClass(TbMsg.class); - ArgumentCaptor typeCaptor = ArgumentCaptor.forClass(TbMsgType.class); - ArgumentCaptor originatorCaptor = ArgumentCaptor.forClass(EntityId.class); - ArgumentCaptor metadataCaptor = ArgumentCaptor.forClass(TbMsgMetaData.class); - ArgumentCaptor dataCaptor = ArgumentCaptor.forClass(String.class); - verify(ctx).transformMsg(msgCaptor.capture(), typeCaptor.capture(), originatorCaptor.capture(), metadataCaptor.capture(), dataCaptor.capture()); - - assertEquals(TbMsgType.ALARM, typeCaptor.getValue()); - assertEquals(originator, originatorCaptor.getValue()); - assertEquals(Boolean.TRUE.toString(), metadataCaptor.getValue().getValue(DataConstants.IS_NEW_ALARM)); - assertNotSame(metaData, metadataCaptor.getValue()); - - Alarm actualAlarm = JacksonUtil.fromBytes(dataCaptor.getValue().getBytes(), Alarm.class); - assertEquals(expectedAlarm, actualAlarm); - } - - @Test - public void testCreateAlarmsWithPropagationToTenantWithDynamicTypes() throws Exception { - for (int i = 0; i < 10; i++) { - var config = new TbCreateAlarmNodeConfiguration(); - config.setPropagateToTenant(true); - config.setSeverity(AlarmSeverity.CRITICAL.name()); - config.setAlarmType("SomeType" + i); - config.setScriptLang(ScriptLanguage.JS); - config.setAlarmDetailsBuildJs("DETAILS"); - config.setDynamicSeverity(true); - TbNodeConfiguration nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config)); - - when(ctx.createScriptEngine(ScriptLanguage.JS, "DETAILS")).thenReturn(detailsJs); - - when(ctx.getTenantId()).thenReturn(tenantId); - when(ctx.getAlarmService()).thenReturn(alarmService); - when(ctx.getDbCallbackExecutor()).thenReturn(dbExecutor); - - node = new TbCreateAlarmNode(); - node.init(ctx, nodeConfiguration); - - metaData.putValue("key", "value"); - TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, originator, metaData, TbMsgDataType.JSON, rawJson, ruleChainId, ruleNodeId); - long ts = msg.getTs(); - Alarm expectedAlarm = Alarm.builder() - .startTs(ts) - .endTs(ts) - .tenantId(tenantId) - .originator(originator) - .severity(AlarmSeverity.CRITICAL) - .propagateToTenant(true) - .type("SomeType" + i) - .details(null) - .build(); - - when(detailsJs.executeJsonAsync(msg)).thenReturn(Futures.immediateFuture(null)); - when(alarmService.findLatestActiveByOriginatorAndType(tenantId, originator, "SomeType" + i)).thenReturn(null); - when(alarmService.createAlarm(any(AlarmCreateOrUpdateActiveRequest.class))).thenReturn( - AlarmApiCallResult.builder() - .successful(true) - .created(true) - .alarm(new AlarmInfo(expectedAlarm)) - .build()); - node.onMsg(ctx, msg); - - verify(ctx, atMost(10)).enqueue(any(), successCaptor.capture(), failureCaptor.capture()); - successCaptor.getValue().run(); - verify(ctx, atMost(10)).tellNext(any(), eq("Created")); - - ArgumentCaptor msgCaptor = ArgumentCaptor.forClass(TbMsg.class); - ArgumentCaptor typeCaptor = ArgumentCaptor.forClass(TbMsgType.class); - ArgumentCaptor originatorCaptor = ArgumentCaptor.forClass(EntityId.class); - ArgumentCaptor metadataCaptor = ArgumentCaptor.forClass(TbMsgMetaData.class); - ArgumentCaptor dataCaptor = ArgumentCaptor.forClass(String.class); - verify(ctx, atMost(10)).transformMsg(msgCaptor.capture(), typeCaptor.capture(), originatorCaptor.capture(), metadataCaptor.capture(), dataCaptor.capture()); - - assertEquals(TbMsgType.ALARM, typeCaptor.getValue()); - assertEquals(originator, originatorCaptor.getValue()); - assertEquals("value", metadataCaptor.getValue().getValue("key")); - assertEquals(Boolean.TRUE.toString(), metadataCaptor.getValue().getValue(DataConstants.IS_NEW_ALARM)); - assertNotSame(metaData, metadataCaptor.getValue()); - - Alarm actualAlarm = JacksonUtil.fromBytes(dataCaptor.getValue().getBytes(), Alarm.class); - assertEquals(expectedAlarm, actualAlarm); - } - } - - private void initWithCreateAlarmScript() { - try { - TbCreateAlarmNodeConfiguration config = new TbCreateAlarmNodeConfiguration(); - config.setPropagate(true); - config.setSeverity(AlarmSeverity.CRITICAL.name()); - config.setAlarmType("SomeType"); - config.setScriptLang(ScriptLanguage.JS); - config.setAlarmDetailsBuildJs("DETAILS"); - TbNodeConfiguration nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config)); - - when(ctx.createScriptEngine(ScriptLanguage.JS, "DETAILS")).thenReturn(detailsJs); - - when(ctx.getTenantId()).thenReturn(tenantId); - when(ctx.getAlarmService()).thenReturn(alarmService); - when(ctx.getDbCallbackExecutor()).thenReturn(dbExecutor); - - node = new TbCreateAlarmNode(); - node.init(ctx, nodeConfiguration); - } catch (TbNodeException ex) { - throw new IllegalStateException(ex); - } - } - - private void initWithClearAlarmScript() { - try { - TbClearAlarmNodeConfiguration config = new TbClearAlarmNodeConfiguration(); - config.setAlarmType("SomeType"); - config.setScriptLang(ScriptLanguage.JS); - config.setAlarmDetailsBuildJs("DETAILS"); - TbNodeConfiguration nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config)); - - when(ctx.createScriptEngine(ScriptLanguage.JS, "DETAILS")).thenReturn(detailsJs); - - when(ctx.getTenantId()).thenReturn(tenantId); - when(ctx.getAlarmService()).thenReturn(alarmService); - when(ctx.getDbCallbackExecutor()).thenReturn(dbExecutor); - - node = new TbClearAlarmNode(); - node.init(ctx, nodeConfiguration); - } catch (TbNodeException ex) { - throw new IllegalStateException(ex); - } - } - - private void verifyError(TbMsg msg, String message, Class expectedClass) { - ArgumentCaptor captor = ArgumentCaptor.forClass(Throwable.class); - verify(ctx).tellFailure(same(msg), captor.capture()); - - Throwable value = captor.getValue(); - assertEquals(expectedClass, value.getClass()); - assertEquals(message, value.getMessage()); - } - -} diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbClearAlarmNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbClearAlarmNodeTest.java new file mode 100644 index 0000000000..afab0a0a99 --- /dev/null +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbClearAlarmNodeTest.java @@ -0,0 +1,218 @@ +/** + * 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.rule.engine.action; + +import com.datastax.oss.driver.api.core.uuid.Uuids; +import com.fasterxml.jackson.databind.JsonNode; +import com.google.common.util.concurrent.Futures; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.common.util.ListeningExecutor; +import org.thingsboard.rule.engine.TestDbCallbackExecutor; +import org.thingsboard.rule.engine.api.RuleEngineAlarmService; +import org.thingsboard.rule.engine.api.ScriptEngine; +import org.thingsboard.rule.engine.api.TbContext; +import org.thingsboard.rule.engine.api.TbNodeConfiguration; +import org.thingsboard.rule.engine.api.TbNodeException; +import org.thingsboard.server.common.data.DataConstants; +import org.thingsboard.server.common.data.alarm.Alarm; +import org.thingsboard.server.common.data.alarm.AlarmApiCallResult; +import org.thingsboard.server.common.data.alarm.AlarmInfo; +import org.thingsboard.server.common.data.alarm.AlarmSeverity; +import org.thingsboard.server.common.data.id.AlarmId; +import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.msg.TbMsgType; +import org.thingsboard.server.common.data.script.ScriptLanguage; +import org.thingsboard.server.common.msg.TbMsg; +import org.thingsboard.server.common.msg.TbMsgMetaData; + +import java.util.function.Consumer; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.nullable; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class TbClearAlarmNodeTest { + + @Mock + TbContext ctxMock; + @Mock + RuleEngineAlarmService alarmServiceMock; + @Mock + ScriptEngine alarmDetailsScriptMock; + + @Captor + ArgumentCaptor successCaptor; + @Captor + ArgumentCaptor> failureCaptor; + + TbClearAlarmNode node; + + final TenantId tenantId = TenantId.fromUUID(Uuids.timeBased()); + final EntityId msgOriginator = new DeviceId(Uuids.timeBased()); + final EntityId alarmOriginator = new AlarmId(Uuids.timeBased()); + TbMsgMetaData metadata; + + ListeningExecutor dbExecutor; + + @BeforeEach + void before() { + dbExecutor = new TestDbCallbackExecutor(); + metadata = new TbMsgMetaData(); + } + + @Test + void alarmCanBeCleared() { + initWithClearAlarmScript(); + metadata.putValue("key", "value"); + TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, msgOriginator, metadata, "{\"temperature\": 50}"); + + long oldEndDate = System.currentTimeMillis(); + Alarm activeAlarm = Alarm.builder().type("SomeType").tenantId(tenantId).originator(msgOriginator).severity(AlarmSeverity.WARNING).endTs(oldEndDate).build(); + + Alarm expectedAlarm = Alarm.builder() + .tenantId(tenantId) + .originator(msgOriginator) + .cleared(true) + .severity(AlarmSeverity.WARNING) + .propagate(false) + .type("SomeType") + .details(null) + .endTs(oldEndDate) + .build(); + + when(alarmDetailsScriptMock.executeJsonAsync(msg)).thenReturn(Futures.immediateFuture(null)); + when(alarmServiceMock.findLatestActiveByOriginatorAndType(tenantId, msgOriginator, "SomeType")).thenReturn(activeAlarm); + when(alarmServiceMock.clearAlarm(eq(activeAlarm.getTenantId()), eq(activeAlarm.getId()), anyLong(), nullable(JsonNode.class))) + .thenReturn(AlarmApiCallResult.builder() + .successful(true) + .cleared(true) + .alarm(new AlarmInfo(expectedAlarm)) + .build()); + + node.onMsg(ctxMock, msg); + + verify(ctxMock).enqueue(any(), successCaptor.capture(), failureCaptor.capture()); + successCaptor.getValue().run(); + verify(ctxMock).tellNext(any(), eq("Cleared")); + + ArgumentCaptor msgCaptor = ArgumentCaptor.forClass(TbMsg.class); + ArgumentCaptor typeCaptor = ArgumentCaptor.forClass(TbMsgType.class); + ArgumentCaptor originatorCaptor = ArgumentCaptor.forClass(EntityId.class); + ArgumentCaptor metadataCaptor = ArgumentCaptor.forClass(TbMsgMetaData.class); + ArgumentCaptor dataCaptor = ArgumentCaptor.forClass(String.class); + verify(ctxMock).transformMsg(msgCaptor.capture(), typeCaptor.capture(), originatorCaptor.capture(), metadataCaptor.capture(), dataCaptor.capture()); + + assertThat(TbMsgType.ALARM).isEqualTo(typeCaptor.getValue()); + assertThat(msgOriginator).isEqualTo(originatorCaptor.getValue()); + assertThat("value").isEqualTo(metadataCaptor.getValue().getValue("key")); + assertThat(Boolean.TRUE.toString()).isEqualTo(metadataCaptor.getValue().getValue(DataConstants.IS_CLEARED_ALARM)); + assertThat(metadata).isNotSameAs(metadataCaptor.getValue()); + + Alarm actualAlarm = JacksonUtil.fromBytes(dataCaptor.getValue().getBytes(), Alarm.class); + assertThat(actualAlarm).isEqualTo(expectedAlarm); + } + + @Test + void alarmCanBeClearedWithAlarmOriginator() { + initWithClearAlarmScript(); + metadata.putValue("key", "value"); + TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, alarmOriginator, metadata, "{\"temperature\": 50}"); + + long oldEndDate = System.currentTimeMillis(); + AlarmId id = new AlarmId(alarmOriginator.getId()); + Alarm activeAlarm = Alarm.builder().type("SomeType").tenantId(tenantId).originator(msgOriginator).severity(AlarmSeverity.WARNING).endTs(oldEndDate).build(); + activeAlarm.setId(id); + + Alarm expectedAlarm = Alarm.builder() + .tenantId(tenantId) + .originator(msgOriginator) + .cleared(true) + .severity(AlarmSeverity.WARNING) + .propagate(false) + .type("SomeType") + .details(null) + .endTs(oldEndDate) + .build(); + expectedAlarm.setId(id); + + when(alarmDetailsScriptMock.executeJsonAsync(msg)).thenReturn(Futures.immediateFuture(null)); + when(alarmServiceMock.findAlarmById(tenantId, id)).thenReturn(activeAlarm); + when(alarmServiceMock.clearAlarm(eq(activeAlarm.getTenantId()), eq(activeAlarm.getId()), anyLong(), nullable(JsonNode.class))) + .thenReturn(AlarmApiCallResult.builder() + .successful(true) + .cleared(true) + .alarm(new AlarmInfo(expectedAlarm)) + .build()); + + node.onMsg(ctxMock, msg); + + verify(ctxMock).enqueue(any(), successCaptor.capture(), failureCaptor.capture()); + successCaptor.getValue().run(); + verify(ctxMock).tellNext(any(), eq("Cleared")); + + ArgumentCaptor msgCaptor = ArgumentCaptor.forClass(TbMsg.class); + ArgumentCaptor typeCaptor = ArgumentCaptor.forClass(TbMsgType.class); + ArgumentCaptor originatorCaptor = ArgumentCaptor.forClass(EntityId.class); + ArgumentCaptor metadataCaptor = ArgumentCaptor.forClass(TbMsgMetaData.class); + ArgumentCaptor dataCaptor = ArgumentCaptor.forClass(String.class); + verify(ctxMock).transformMsg(msgCaptor.capture(), typeCaptor.capture(), originatorCaptor.capture(), metadataCaptor.capture(), dataCaptor.capture()); + + assertThat(TbMsgType.ALARM).isEqualTo(typeCaptor.getValue()); + assertThat(alarmOriginator).isEqualTo(originatorCaptor.getValue()); + assertThat("value").isEqualTo(metadataCaptor.getValue().getValue("key")); + assertThat(Boolean.TRUE.toString()).isEqualTo(metadataCaptor.getValue().getValue(DataConstants.IS_CLEARED_ALARM)); + assertThat(metadata).isNotSameAs(metadataCaptor.getValue()); + + Alarm actualAlarm = JacksonUtil.fromBytes(dataCaptor.getValue().getBytes(), Alarm.class); + assertThat(actualAlarm).isEqualTo(expectedAlarm); + } + + private void initWithClearAlarmScript() { + try { + TbClearAlarmNodeConfiguration config = new TbClearAlarmNodeConfiguration(); + config.setAlarmType("SomeType"); + config.setScriptLang(ScriptLanguage.JS); + config.setAlarmDetailsBuildJs("DETAILS"); + TbNodeConfiguration nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config)); + + when(ctxMock.createScriptEngine(ScriptLanguage.JS, "DETAILS")).thenReturn(alarmDetailsScriptMock); + + when(ctxMock.getTenantId()).thenReturn(tenantId); + when(ctxMock.getAlarmService()).thenReturn(alarmServiceMock); + when(ctxMock.getDbCallbackExecutor()).thenReturn(dbExecutor); + + node = new TbClearAlarmNode(); + node.init(ctxMock, nodeConfiguration); + } catch (TbNodeException ex) { + throw new IllegalStateException(ex); + } + } + +} diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbCreateAlarmNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbCreateAlarmNodeTest.java new file mode 100644 index 0000000000..c0e27231c4 --- /dev/null +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbCreateAlarmNodeTest.java @@ -0,0 +1,1223 @@ +/** + * 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.rule.engine.action; + +import com.datastax.oss.driver.api.core.uuid.Uuids; +import com.fasterxml.jackson.databind.JsonNode; +import com.google.common.util.concurrent.Futures; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mock; +import org.mockito.Spy; +import org.mockito.junit.jupiter.MockitoExtension; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.common.util.ListeningExecutor; +import org.thingsboard.rule.engine.TestDbCallbackExecutor; +import org.thingsboard.rule.engine.api.RuleEngineAlarmService; +import org.thingsboard.rule.engine.api.ScriptEngine; +import org.thingsboard.rule.engine.api.TbContext; +import org.thingsboard.rule.engine.api.TbNodeConfiguration; +import org.thingsboard.server.common.data.DataConstants; +import org.thingsboard.server.common.data.alarm.Alarm; +import org.thingsboard.server.common.data.alarm.AlarmApiCallResult; +import org.thingsboard.server.common.data.alarm.AlarmCreateOrUpdateActiveRequest; +import org.thingsboard.server.common.data.alarm.AlarmInfo; +import org.thingsboard.server.common.data.alarm.AlarmPropagationInfo; +import org.thingsboard.server.common.data.alarm.AlarmSeverity; +import org.thingsboard.server.common.data.alarm.AlarmUpdateRequest; +import org.thingsboard.server.common.data.id.AlarmId; +import org.thingsboard.server.common.data.id.AssetId; +import org.thingsboard.server.common.data.id.CustomerId; +import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.RuleNodeId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.msg.TbMsgType; +import org.thingsboard.server.common.data.msg.TbNodeConnectionType; +import org.thingsboard.server.common.data.script.ScriptLanguage; +import org.thingsboard.server.common.msg.TbMsg; +import org.thingsboard.server.common.msg.TbMsgMetaData; + +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ExecutionException; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.then; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.eq; +import static org.mockito.Mockito.never; + +@ExtendWith(MockitoExtension.class) +class TbCreateAlarmNodeTest { + + @Mock + TbContext ctxMock; + @Mock + RuleEngineAlarmService alarmServiceMock; + @Mock + ScriptEngine alarmDetailsScriptMock; + @Mock + TbMsg alarmActionMsgMock; + + @Captor + ArgumentCaptor successCaptor; + + @Spy + TbCreateAlarmNode nodeSpy; + TbCreateAlarmNodeConfiguration config; + + final TenantId tenantId = TenantId.fromUUID(Uuids.timeBased()); + final EntityId msgOriginator = new DeviceId(Uuids.timeBased()); + TbMsgMetaData metadata; + + ListeningExecutor dbExecutor; + + @BeforeEach + void before() { + dbExecutor = new TestDbCallbackExecutor(); + metadata = new TbMsgMetaData(); + config = new TbCreateAlarmNodeConfiguration(); + } + + @Test + @DisplayName("When defaultConfiguration() is called, then correct values are set.") + void whenDefaultConfiguration_thenShouldSetCorrectValues() { + // GIVEN-WHEN + config = config.defaultConfiguration(); + + // THEN + assertThat(config.getAlarmType()).isEqualTo("General Alarm"); + assertThat(config.getScriptLang()).isEqualTo(ScriptLanguage.TBEL); + assertThat(config.getAlarmDetailsBuildJs()).isEqualTo(""" + \ + var details = {}; + if (metadata.prevAlarmDetails) { + details = JSON.parse(metadata.prevAlarmDetails); + //remove prevAlarmDetails from metadata + delete metadata.prevAlarmDetails; + //now metadata is the same as it comes IN this rule node + } + + + return details;"""); + assertThat(config.getAlarmDetailsBuildTbel()).isEqualTo(""" + \ + var details = {}; + if (metadata.prevAlarmDetails != null) { + details = JSON.parse(metadata.prevAlarmDetails); + //remove prevAlarmDetails from metadata + metadata.remove('prevAlarmDetails'); + //now metadata is the same as it comes IN this rule node + } + + + return details;"""); + assertThat(config.getSeverity()).isEqualTo(AlarmSeverity.CRITICAL.name()); + assertThat(config.isPropagate()).isFalse(); + assertThat(config.isPropagateToOwner()).isFalse(); + assertThat(config.isPropagateToTenant()).isFalse(); + assertThat(config.isUseMessageAlarmData()).isFalse(); + assertThat(config.isOverwriteAlarmDetails()).isFalse(); + assertThat(config.isDynamicSeverity()).isFalse(); + assertThat(config.getRelationTypes()).isEmpty(); + } + + @Test + @DisplayName("When node is taking alarm info from default node config and alarm does not exist, then should create new alarm using info from default config.") + void whenAlarmDataIsTakenFromDefaultNodeConfigAndAlarmDoesNotExist_thenNewAlarmIsCreated() throws Exception { + // GIVEN + + // node configuration + config = config.defaultConfiguration(); + + // other values + String alarmType = config.getAlarmType(); + AlarmSeverity alarmSeverity = AlarmSeverity.valueOf(config.getSeverity()); + JsonNode alarmDetails = JacksonUtil.newObjectNode(); + + long metadataTs = 1711631716127L; + metadata.putValue("ts", Long.toString(metadataTs)); + metadata.putValue("location", "Company office"); + + var ruleNodeSelfId = new RuleNodeId(Uuids.timeBased()); + + var incomingMsg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, msgOriginator, metadata, "{\"temperature\": 50}"); + + Alarm existingAlarm = null; + + // expected values + var expectedAlarm = Alarm.builder() + .tenantId(tenantId) + .originator(msgOriginator) + .cleared(false) + .acknowledged(false) + .severity(alarmSeverity) + .propagate(false) + .propagateToOwner(false) + .propagateToTenant(false) + .propagateRelationTypes(Collections.emptyList()) + .type(alarmType) + .startTs(metadataTs) + .endTs(metadataTs) + .details(alarmDetails) + .build(); + var expectedCreatedAlarmInfo = new AlarmInfo(expectedAlarm); + expectedCreatedAlarmInfo.setId(new AlarmId(Uuids.timeBased())); + + var expectedCreateAlarmRequest = AlarmCreateOrUpdateActiveRequest.builder() + .tenantId(tenantId) + .customerId(null) + .type(alarmType) + .originator(msgOriginator) + .severity(alarmSeverity) + .startTs(metadataTs) + .endTs(metadataTs) + .details(alarmDetails) + .propagation(AlarmPropagationInfo.builder() + .propagate(false) + .propagateToOwner(false) + .propagateToTenant(false) + .propagateRelationTypes(Collections.emptyList()).build()) + .userId(null) + .edgeAlarmId(null) + .build(); + + // mocks + given(ctxMock.getTenantId()).willReturn(tenantId); + given(ctxMock.getAlarmService()).willReturn(alarmServiceMock); + given(ctxMock.getDbCallbackExecutor()).willReturn(dbExecutor); + given(ctxMock.getSelfId()).willReturn(ruleNodeSelfId); + given(alarmServiceMock.findLatestActiveByOriginatorAndType(tenantId, msgOriginator, alarmType)).willReturn(existingAlarm); + given(alarmDetailsScriptMock.executeJsonAsync(incomingMsg)).willReturn(Futures.immediateFuture(alarmDetails)); + var apiCallResult = AlarmApiCallResult.builder() + .successful(true) + .created(true) + .modified(false) + .cleared(false) + .deleted(false) + .alarm(expectedCreatedAlarmInfo) + .old(null) + .propagatedEntitiesList(Collections.emptyList()) + .build(); + given(alarmServiceMock.createAlarm(expectedCreateAlarmRequest)).willReturn(apiCallResult); + given(ctxMock.alarmActionMsg(expectedCreatedAlarmInfo, ruleNodeSelfId, TbMsgType.ENTITY_CREATED)).willReturn(alarmActionMsgMock); + given(ctxMock.transformMsg(any(TbMsg.class), any(TbMsgType.class), any(EntityId.class), any(TbMsgMetaData.class), anyString())) + .willAnswer(answer -> TbMsg.transformMsg( + answer.getArgument(0, TbMsg.class), + answer.getArgument(1, TbMsgType.class), + answer.getArgument(2, EntityId.class), + answer.getArgument(3, TbMsgMetaData.class), + answer.getArgument(4, String.class)) + ); + given(ctxMock.createScriptEngine(ScriptLanguage.TBEL, TbAbstractAlarmNodeConfiguration.ALARM_DETAILS_BUILD_TBEL_TEMPLATE)).willReturn(alarmDetailsScriptMock); + + // node initialization + nodeSpy.init(ctxMock, new TbNodeConfiguration(JacksonUtil.valueToTree(config))); + + // WHEN + nodeSpy.onMsg(ctxMock, incomingMsg); + + // THEN + + // verify alarm details script evaluation + then(ctxMock).should().logJsEvalRequest(); + then(alarmDetailsScriptMock).should().executeJsonAsync(incomingMsg); + then(ctxMock).should().logJsEvalResponse(); + + // verify we called createAlarm() with correct AlarmCreateOrUpdateActiveRequest + then(alarmServiceMock).should().createAlarm(expectedCreateAlarmRequest); + then(alarmServiceMock).should(never()).updateAlarm(any()); + + // verify that we created a correct alarm action message and enqueued it + then(ctxMock).should().alarmActionMsg(expectedCreatedAlarmInfo, ruleNodeSelfId, TbMsgType.ENTITY_CREATED); + then(ctxMock).should().enqueue(eq(alarmActionMsgMock), successCaptor.capture(), any()); + + // run success captor to emulate successful sending and to trigger further processing on the success path + successCaptor.getValue().run(); + + // capture and verify an outgoing message + var outgoingMsgCaptor = ArgumentCaptor.forClass(TbMsg.class); + then(ctxMock).should().tellNext(outgoingMsgCaptor.capture(), eq("Created")); + var actualOutgoingMsg = outgoingMsgCaptor.getValue(); + assertThat(actualOutgoingMsg.getType()).isEqualTo(TbMsgType.ALARM.name()); + assertThat(actualOutgoingMsg.getOriginator()).isEqualTo(msgOriginator); + assertThat(actualOutgoingMsg.getData()).isEqualTo(JacksonUtil.valueToTree(expectedCreatedAlarmInfo).toString()); + + Map actualOutgoingMsgMetadataContent = actualOutgoingMsg.getMetaData().getData(); + assertThat(actualOutgoingMsgMetadataContent).containsAllEntriesOf(metadata.getData()); + assertThat(actualOutgoingMsgMetadataContent).containsEntry(DataConstants.IS_NEW_ALARM, Boolean.TRUE.toString()); + assertThat(actualOutgoingMsgMetadataContent).size().isEqualTo(metadata.getData().size() + 1); + + // verify wrong processing paths were not taken + then(ctxMock).should(never()).tellNext(any(), eq(TbNodeConnectionType.FALSE)); + then(ctxMock).should(never()).tellNext(any(), eq("Updated")); + then(ctxMock).should(never()).tellNext(any(), eq("Cleared")); + then(ctxMock).should(never()).tellSuccess(any()); + then(ctxMock).should(never()).tellFailure(any(), any()); + } + + @Test + @DisplayName("When node is taking alarm info from node config and cleared alarm exists, then should create new alarm using info from config.") + void whenAlarmDataIsTakenFromNodeConfigAndClearedAlarmExists_thenNewAlarmIsCreated() throws Exception { + // GIVEN + + // node configuration + config.setAlarmType("$[alarmType]"); + config.setScriptLang(ScriptLanguage.JS); + config.setAlarmDetailsBuildJs(""" + return { + alarmDetails: "Some alarm details" + }; + """); + config.setAlarmDetailsBuildTbel(""" + return { + alarmDetails: "Some alarm details" + }; + """); + config.setDynamicSeverity(true); + config.setSeverity("${alarmSeverity}"); + config.setPropagate(true); + config.setPropagateToOwner(true); + config.setPropagateToTenant(true); + config.setRelationTypes(List.of("RELATION_TYPE_1", "RELATION_TYPE_2", "RELATION_TYPE_3")); + config.setUseMessageAlarmData(false); + config.setOverwriteAlarmDetails(false); + + // other values + String alarmType = "High Temperature"; + AlarmSeverity alarmSeverity = AlarmSeverity.MAJOR; + JsonNode alarmDetails = JacksonUtil.newObjectNode().put("alarmDetails", "Some alarm details"); + + long metadataTs = 1711631716127L; + metadata.putValue("ts", Long.toString(metadataTs)); + metadata.putValue("alarmSeverity", alarmSeverity.name()); + metadata.putValue("location", "Company office"); + + var ruleNodeSelfId = new RuleNodeId(Uuids.timeBased()); + + var incomingMsg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, msgOriginator, metadata, "{\"temperature\": 50, \"alarmType\": \"" + alarmType + "\"}"); + + var existingClearedAlarm = Alarm.builder() + .tenantId(tenantId) + .originator(msgOriginator) + .cleared(true) + .acknowledged(false) + .severity(AlarmSeverity.WARNING) + .propagate(false) + .propagateToOwner(false) + .propagateToTenant(false) + .propagateRelationTypes(Collections.emptyList()) + .type(alarmType) + .startTs(100L) + .endTs(200L) + .details(JacksonUtil.newObjectNode().put("oldAlarmDetailsProperty", "oldAlarmDetailsPropertyValue")) + .build(); + existingClearedAlarm.setId(new AlarmId(Uuids.timeBased())); + + // expected values + var expectedCreatedAlarm = Alarm.builder() + .tenantId(tenantId) + .originator(msgOriginator) + .cleared(false) + .acknowledged(false) + .severity(alarmSeverity) + .propagate(true) + .propagateToOwner(true) + .propagateToTenant(true) + .propagateRelationTypes(config.getRelationTypes()) + .type(alarmType) + .startTs(metadataTs) + .endTs(metadataTs) + .details(alarmDetails) + .build(); + var expectedCreatedAlarmInfo = new AlarmInfo(expectedCreatedAlarm); + expectedCreatedAlarmInfo.setId(new AlarmId(Uuids.timeBased())); + + var expectedCreateAlarmRequest = AlarmCreateOrUpdateActiveRequest.builder() + .tenantId(tenantId) + .customerId(null) + .type(alarmType) + .originator(msgOriginator) + .severity(alarmSeverity) + .startTs(metadataTs) + .endTs(metadataTs) + .details(alarmDetails) + .propagation(AlarmPropagationInfo.builder() + .propagate(true) + .propagateToOwner(true) + .propagateToTenant(true) + .propagateRelationTypes(config.getRelationTypes()).build()) + .userId(null) + .edgeAlarmId(null) + .build(); + + // mocks + given(ctxMock.getTenantId()).willReturn(tenantId); + given(ctxMock.getAlarmService()).willReturn(alarmServiceMock); + given(ctxMock.getDbCallbackExecutor()).willReturn(dbExecutor); + given(ctxMock.getSelfId()).willReturn(ruleNodeSelfId); + given(alarmServiceMock.findLatestActiveByOriginatorAndType(tenantId, msgOriginator, alarmType)).willReturn(existingClearedAlarm); + given(alarmDetailsScriptMock.executeJsonAsync(incomingMsg)).willReturn(Futures.immediateFuture(alarmDetails)); + var apiCallResult = AlarmApiCallResult.builder() + .successful(true) + .created(true) + .modified(false) + .cleared(false) + .deleted(false) + .alarm(expectedCreatedAlarmInfo) + .old(null) + .propagatedEntitiesList(List.of(TenantId.fromUUID(Uuids.timeBased()), new CustomerId(Uuids.timeBased()), new AssetId(Uuids.timeBased()))) + .build(); + given(alarmServiceMock.createAlarm(expectedCreateAlarmRequest)).willReturn(apiCallResult); + given(ctxMock.alarmActionMsg(expectedCreatedAlarmInfo, ruleNodeSelfId, TbMsgType.ENTITY_CREATED)).willReturn(alarmActionMsgMock); + given(ctxMock.transformMsg(any(TbMsg.class), any(TbMsgType.class), any(EntityId.class), any(TbMsgMetaData.class), anyString())) + .willAnswer(answer -> TbMsg.transformMsg( + answer.getArgument(0, TbMsg.class), + answer.getArgument(1, TbMsgType.class), + answer.getArgument(2, EntityId.class), + answer.getArgument(3, TbMsgMetaData.class), + answer.getArgument(4, String.class)) + ); + given(ctxMock.createScriptEngine(ScriptLanguage.JS, config.getAlarmDetailsBuildJs())).willReturn(alarmDetailsScriptMock); + + // node initialization + nodeSpy.init(ctxMock, new TbNodeConfiguration(JacksonUtil.valueToTree(config))); + + // WHEN + nodeSpy.onMsg(ctxMock, incomingMsg); + + // THEN + + // verify alarm details script evaluation + then(ctxMock).should().logJsEvalRequest(); + then(alarmDetailsScriptMock).should().executeJsonAsync(incomingMsg); + then(ctxMock).should().logJsEvalResponse(); + + // verify we called createAlarm() with correct AlarmCreateOrUpdateActiveRequest + then(alarmServiceMock).should().createAlarm(expectedCreateAlarmRequest); + then(alarmServiceMock).should(never()).updateAlarm(any()); + + // verify that we created a correct alarm action message and enqueued it + then(ctxMock).should().alarmActionMsg(expectedCreatedAlarmInfo, ruleNodeSelfId, TbMsgType.ENTITY_CREATED); + then(ctxMock).should().enqueue(eq(alarmActionMsgMock), successCaptor.capture(), any()); + + // run success captor to emulate successful sending and to trigger further processing on the success path + successCaptor.getValue().run(); + + // capture and verify an outgoing message + var outgoingMsgCaptor = ArgumentCaptor.forClass(TbMsg.class); + then(ctxMock).should().tellNext(outgoingMsgCaptor.capture(), eq("Created")); + var actualOutgoingMsg = outgoingMsgCaptor.getValue(); + assertThat(actualOutgoingMsg.getType()).isEqualTo(TbMsgType.ALARM.name()); + assertThat(actualOutgoingMsg.getOriginator()).isEqualTo(msgOriginator); + assertThat(actualOutgoingMsg.getData()).isEqualTo(JacksonUtil.valueToTree(expectedCreatedAlarmInfo).toString()); + + Map actualOutgoingMsgMetadataContent = actualOutgoingMsg.getMetaData().getData(); + assertThat(actualOutgoingMsgMetadataContent).containsAllEntriesOf(metadata.getData()); + assertThat(actualOutgoingMsgMetadataContent).containsEntry(DataConstants.IS_NEW_ALARM, Boolean.TRUE.toString()); + assertThat(actualOutgoingMsgMetadataContent).size().isEqualTo(metadata.getData().size() + 1); + + // verify wrong processing paths were not taken + then(ctxMock).should(never()).tellNext(any(), eq(TbNodeConnectionType.FALSE)); + then(ctxMock).should(never()).tellNext(any(), eq("Updated")); + then(ctxMock).should(never()).tellNext(any(), eq("Cleared")); + then(ctxMock).should(never()).tellSuccess(any()); + then(ctxMock).should(never()).tellFailure(any(), any()); + } + + @Test + @DisplayName("When node is taking alarm info from node config and active alarm exists, then should update existing alarm using info from config.") + void whenAlarmDataIsTakenFromNodeConfigAndActiveAlarmExists_thenExistingAlarmIsUpdated() throws Exception { + // GIVEN + + // values that changed between existing alarm and updated alarm + AlarmSeverity oldAlarmSeverity = AlarmSeverity.WARNING; + AlarmSeverity newAlarmSeverity = AlarmSeverity.MAJOR; + + boolean oldPropagate = true; + boolean newPropagate = false; + + boolean oldPropagateToOwner = false; + boolean newPropagateToOwner = true; + + boolean oldPropagateToTenant = false; + boolean newPropagateToTenant = true; + + List oldPropagateRelationTypes = List.of("RELATION_TYPE_1", "RELATION_TYPE_2", "RELATION_TYPE_3"); + List newPropagateRelationTypes = Collections.emptyList(); + + JsonNode oldAlarmDetails = JacksonUtil.newObjectNode().put("oldAlarmDetailsKey", "oldAlarmDetailsValue"); + JsonNode newAlarmDetails = JacksonUtil.newObjectNode().put("newAlarmDetails", "Some alarm details TBEL").set("oldAlarmDetails", oldAlarmDetails); + + long oldEndTs = 200L; + long newEndTs = 300L; + + // node configuration + config.setAlarmType("${alarmType}"); + config.setScriptLang(ScriptLanguage.TBEL); + config.setAlarmDetailsBuildJs(""" + return { + oldAlarmDetails: metadata.prevAlarmDetails, + newAlarmDetails: "Some alarm details JS" + }; + """); + config.setAlarmDetailsBuildTbel(""" + return { + oldAlarmDetails: metadata.prevAlarmDetails, + newAlarmDetails: "Some alarm details TBEL" + }; + """); + config.setDynamicSeverity(true); + config.setSeverity("$[alarmSeverity]"); + config.setPropagate(newPropagate); + config.setPropagateToOwner(newPropagateToOwner); + config.setPropagateToTenant(newPropagateToTenant); + config.setRelationTypes(newPropagateRelationTypes); + config.setUseMessageAlarmData(false); + config.setOverwriteAlarmDetails(false); + + // other values + String alarmType = "High Temperature"; + + long metadataTs = 1711631716127L; + metadata.putValue("ts", Long.toString(metadataTs)); + metadata.putValue("alarmType", alarmType); + metadata.putValue("location", "Company office"); + + var ruleNodeSelfId = new RuleNodeId(Uuids.timeBased()); + + var incomingMsg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, msgOriginator, metadata, "{\"temperature\": 50, \"alarmSeverity\": \"" + newAlarmSeverity.name() + "\"}"); + + var existingAlarmId = new AlarmId(Uuids.timeBased()); + var existingActiveAlarm = Alarm.builder() + .tenantId(tenantId) + .originator(msgOriginator) + .cleared(false) + .acknowledged(false) + .severity(oldAlarmSeverity) + .propagate(oldPropagate) + .propagateToOwner(oldPropagateToOwner) + .propagateToTenant(oldPropagateToTenant) + .propagateRelationTypes(oldPropagateRelationTypes) + .type(alarmType) + .startTs(100L) + .endTs(oldEndTs) + .details(oldAlarmDetails) + .build(); + existingActiveAlarm.setId(existingAlarmId); + + // expected values + var expectedUpdatedAlarm = Alarm.builder() + .tenantId(tenantId) + .originator(msgOriginator) + .cleared(false) + .acknowledged(false) + .severity(newAlarmSeverity) + .propagate(newPropagate) + .propagateToOwner(newPropagateToOwner) + .propagateToTenant(newPropagateToTenant) + .propagateRelationTypes(newPropagateRelationTypes) + .type(alarmType) + .startTs(100L) + .endTs(newEndTs) + .details(newAlarmDetails) + .build(); + expectedUpdatedAlarm.setId(existingAlarmId); + var expectedUpdatedAlarmInfo = new AlarmInfo(expectedUpdatedAlarm); + + var expectedUpdateAlarmRequest = AlarmUpdateRequest.builder() + .tenantId(tenantId) + .alarmId(existingAlarmId) + .severity(newAlarmSeverity) + .startTs(100L) + .endTs(newEndTs) + .details(newAlarmDetails) + .propagation(AlarmPropagationInfo.builder() + .propagate(newPropagate) + .propagateToOwner(newPropagateToOwner) + .propagateToTenant(newPropagateToTenant) + .propagateRelationTypes(newPropagateRelationTypes).build()) + .userId(null) + .build(); + + // mocks + given(ctxMock.getTenantId()).willReturn(tenantId); + given(ctxMock.getAlarmService()).willReturn(alarmServiceMock); + given(ctxMock.getDbCallbackExecutor()).willReturn(dbExecutor); + given(ctxMock.getSelfId()).willReturn(ruleNodeSelfId); + given(alarmServiceMock.findLatestActiveByOriginatorAndType(tenantId, msgOriginator, alarmType)).willReturn(existingActiveAlarm); + given(alarmDetailsScriptMock.executeJsonAsync(any())).willReturn(Futures.immediateFuture(newAlarmDetails)); + doReturn(newEndTs).when(nodeSpy).currentTimeMillis(); + var apiCallResult = AlarmApiCallResult.builder() + .successful(true) + .created(false) + .modified(true) + .cleared(false) + .deleted(false) + .alarm(expectedUpdatedAlarmInfo) + .old(new Alarm(existingActiveAlarm)) + .propagatedEntitiesList(List.of(tenantId)) + .build(); + given(alarmServiceMock.updateAlarm(expectedUpdateAlarmRequest)).willReturn(apiCallResult); + given(ctxMock.alarmActionMsg(expectedUpdatedAlarmInfo, ruleNodeSelfId, TbMsgType.ENTITY_UPDATED)).willReturn(alarmActionMsgMock); + given(ctxMock.transformMsg(any(TbMsg.class), any(TbMsgType.class), any(EntityId.class), any(TbMsgMetaData.class), anyString())) + .willAnswer(answer -> TbMsg.transformMsg( + answer.getArgument(0, TbMsg.class), + answer.getArgument(1, TbMsgType.class), + answer.getArgument(2, EntityId.class), + answer.getArgument(3, TbMsgMetaData.class), + answer.getArgument(4, String.class)) + ); + given(ctxMock.createScriptEngine(ScriptLanguage.TBEL, config.getAlarmDetailsBuildTbel())).willReturn(alarmDetailsScriptMock); + + // node initialization + nodeSpy.init(ctxMock, new TbNodeConfiguration(JacksonUtil.valueToTree(config))); + + // WHEN + nodeSpy.onMsg(ctxMock, incomingMsg); + + // THEN + + // verify alarm details script evaluation + then(ctxMock).should().logJsEvalRequest(); + var dummyMsgCaptor = ArgumentCaptor.forClass(TbMsg.class); + then(alarmDetailsScriptMock).should().executeJsonAsync(dummyMsgCaptor.capture()); + TbMsg actualDummyMsg = dummyMsgCaptor.getValue(); + assertThat(actualDummyMsg.getType()).isEqualTo(incomingMsg.getType()); + assertThat(actualDummyMsg.getData()).isEqualTo(incomingMsg.getData()); + assertThat(actualDummyMsg.getMetaData().getData()).containsEntry("prevAlarmDetails", JacksonUtil.toString(oldAlarmDetails)); + then(ctxMock).should().logJsEvalResponse(); + + // verify we called updateAlarm() with correct AlarmUpdateRequest + then(alarmServiceMock).should().updateAlarm(expectedUpdateAlarmRequest); + then(alarmServiceMock).should(never()).createAlarm(any()); + + // verify that we created a correct alarm action message and enqueued it + then(ctxMock).should().alarmActionMsg(expectedUpdatedAlarmInfo, ruleNodeSelfId, TbMsgType.ENTITY_UPDATED); + then(ctxMock).should().enqueue(eq(alarmActionMsgMock), successCaptor.capture(), any()); + + // run success captor to emulate successful queueing of an alarm action message and to trigger further processing on the success path + successCaptor.getValue().run(); + + // capture and verify an outgoing message + var outgoingMsgCaptor = ArgumentCaptor.forClass(TbMsg.class); + then(ctxMock).should().tellNext(outgoingMsgCaptor.capture(), eq("Updated")); + var actualOutgoingMsg = outgoingMsgCaptor.getValue(); + assertThat(actualOutgoingMsg.getType()).isEqualTo(TbMsgType.ALARM.name()); + assertThat(actualOutgoingMsg.getOriginator()).isEqualTo(msgOriginator); + assertThat(actualOutgoingMsg.getData()).isEqualTo(JacksonUtil.valueToTree(expectedUpdatedAlarmInfo).toString()); + + Map actualOutgoingMsgMetadataContent = actualOutgoingMsg.getMetaData().getData(); + assertThat(actualOutgoingMsgMetadataContent).containsAllEntriesOf(metadata.getData()); + assertThat(actualOutgoingMsgMetadataContent).containsEntry(DataConstants.IS_EXISTING_ALARM, Boolean.TRUE.toString()); + assertThat(actualOutgoingMsgMetadataContent).size().isEqualTo(metadata.getData().size() + 1); + + // verify wrong processing paths were not taken + then(ctxMock).should(never()).tellNext(any(), eq(TbNodeConnectionType.FALSE)); + then(ctxMock).should(never()).tellNext(any(), eq("Created")); + then(ctxMock).should(never()).tellNext(any(), eq("Cleared")); + then(ctxMock).should(never()).tellSuccess(any()); + then(ctxMock).should(never()).tellFailure(any(), any()); + } + + @Test + @DisplayName("When node is taking alarm data from incoming message and cleared alarm exists, then should create new alarm using info from incoming message.") + void whenAlarmDataIsTakenFromMsgAndClearedAlarmExists_thenNewAlarmIsCreated() throws Exception { + // GIVEN + + // node configuration + config = config.defaultConfiguration(); + config.setUseMessageAlarmData(true); + config.setOverwriteAlarmDetails(false); + + // other values + String alarmType = "High Temperature"; + AlarmSeverity alarmSeverity = AlarmSeverity.MAJOR; + JsonNode alarmDetails = JacksonUtil.newObjectNode().put("alarmDetails", "Some alarm details"); + + // alarm that is inside an incoming message + var alarmFromIncomingMessage = Alarm.builder() + .tenantId(tenantId) + .originator(msgOriginator) + .cleared(false) + .acknowledged(false) + .severity(alarmSeverity) + .propagate(true) + .propagateToOwner(true) + .propagateToTenant(true) + .propagateRelationTypes(Collections.emptyList()) + .type(alarmType) + .startTs(100L) + .endTs(300L) + .details(alarmDetails) + .build(); + + long metadataTs = 1711631716127L; + metadata.putValue("ts", Long.toString(metadataTs)); + metadata.putValue("location", "Company office"); + + var ruleNodeSelfId = new RuleNodeId(Uuids.timeBased()); + + var incomingMsg = TbMsg.newMsg(TbMsgType.ALARM, msgOriginator, metadata, JacksonUtil.toString(alarmFromIncomingMessage)); + + var existingClearedAlarm = Alarm.builder() + .tenantId(tenantId) + .originator(msgOriginator) + .cleared(true) + .acknowledged(false) + .severity(AlarmSeverity.WARNING) + .propagate(false) + .propagateToOwner(true) + .propagateToTenant(true) + .propagateRelationTypes(Collections.emptyList()) + .type(alarmType) + .startTs(100L) + .endTs(200L) + .details(JacksonUtil.newObjectNode()) + .build(); + existingClearedAlarm.setId(new AlarmId(Uuids.timeBased())); + + // expected values + var expectedCreatedAlarm = Alarm.builder() + .tenantId(tenantId) + .originator(msgOriginator) + .cleared(false) + .acknowledged(false) + .severity(alarmSeverity) + .propagate(true) + .propagateToOwner(true) + .propagateToTenant(true) + .propagateRelationTypes(Collections.emptyList()) + .type(alarmType) + .startTs(100L) + .endTs(300L) + .details(alarmDetails) + .build(); + var expectedCreatedAlarmInfo = new AlarmInfo(expectedCreatedAlarm); + expectedCreatedAlarmInfo.setId(new AlarmId(Uuids.timeBased())); + + var expectedCreateAlarmRequest = AlarmCreateOrUpdateActiveRequest.builder() + .tenantId(tenantId) + .customerId(null) + .type(alarmType) + .originator(msgOriginator) + .severity(alarmSeverity) + .startTs(100L) + .endTs(300L) + .details(alarmDetails) + .propagation(AlarmPropagationInfo.builder() + .propagate(true) + .propagateToOwner(true) + .propagateToTenant(true) + .propagateRelationTypes(Collections.emptyList()).build()) + .userId(null) + .edgeAlarmId(null) + .build(); + + // mocks + given(ctxMock.getTenantId()).willReturn(tenantId); + given(ctxMock.getAlarmService()).willReturn(alarmServiceMock); + given(ctxMock.getDbCallbackExecutor()).willReturn(dbExecutor); + given(ctxMock.getSelfId()).willReturn(ruleNodeSelfId); + given(alarmServiceMock.findLatestActiveByOriginatorAndType(tenantId, msgOriginator, alarmType)).willReturn(existingClearedAlarm); + var apiCallResult = AlarmApiCallResult.builder() + .successful(true) + .created(true) + .modified(false) + .cleared(false) + .deleted(false) + .alarm(expectedCreatedAlarmInfo) + .old(null) + .propagatedEntitiesList(List.of(TenantId.fromUUID(Uuids.timeBased()), new CustomerId(Uuids.timeBased()), new AssetId(Uuids.timeBased()))) + .build(); + given(alarmServiceMock.createAlarm(expectedCreateAlarmRequest)).willReturn(apiCallResult); + given(ctxMock.alarmActionMsg(expectedCreatedAlarmInfo, ruleNodeSelfId, TbMsgType.ENTITY_CREATED)).willReturn(alarmActionMsgMock); + given(ctxMock.transformMsg(any(TbMsg.class), any(TbMsgType.class), any(EntityId.class), any(TbMsgMetaData.class), anyString())) + .willAnswer(answer -> TbMsg.transformMsg( + answer.getArgument(0, TbMsg.class), + answer.getArgument(1, TbMsgType.class), + answer.getArgument(2, EntityId.class), + answer.getArgument(3, TbMsgMetaData.class), + answer.getArgument(4, String.class)) + ); + given(ctxMock.createScriptEngine(ScriptLanguage.TBEL, config.getAlarmDetailsBuildTbel())).willReturn(alarmDetailsScriptMock); + + // node initialization + nodeSpy.init(ctxMock, new TbNodeConfiguration(JacksonUtil.valueToTree(config))); + + // WHEN + nodeSpy.onMsg(ctxMock, incomingMsg); + + // THEN + + // verify alarm details script was not evaluated + then(ctxMock).should(never()).logJsEvalRequest(); + then(alarmDetailsScriptMock).should(never()).executeJsonAsync(any()); + then(ctxMock).should(never()).logJsEvalResponse(); + + // verify we called createAlarm() with correct AlarmCreateOrUpdateActiveRequest + then(alarmServiceMock).should().createAlarm(expectedCreateAlarmRequest); + then(alarmServiceMock).should(never()).updateAlarm(any()); + + // verify that we created a correct alarm action message and enqueued it + then(ctxMock).should().alarmActionMsg(expectedCreatedAlarmInfo, ruleNodeSelfId, TbMsgType.ENTITY_CREATED); + then(ctxMock).should().enqueue(eq(alarmActionMsgMock), successCaptor.capture(), any()); + + // run success captor to emulate successful sending and to trigger further processing on the success path + successCaptor.getValue().run(); + + // capture and verify an outgoing message + var outgoingMsgCaptor = ArgumentCaptor.forClass(TbMsg.class); + then(ctxMock).should().tellNext(outgoingMsgCaptor.capture(), eq("Created")); + var actualOutgoingMsg = outgoingMsgCaptor.getValue(); + assertThat(actualOutgoingMsg.getType()).isEqualTo(TbMsgType.ALARM.name()); + assertThat(actualOutgoingMsg.getOriginator()).isEqualTo(msgOriginator); + assertThat(actualOutgoingMsg.getData()).isEqualTo(JacksonUtil.valueToTree(expectedCreatedAlarmInfo).toString()); + + Map actualOutgoingMsgMetadataContent = actualOutgoingMsg.getMetaData().getData(); + assertThat(actualOutgoingMsgMetadataContent).containsAllEntriesOf(metadata.getData()); + assertThat(actualOutgoingMsgMetadataContent).containsEntry(DataConstants.IS_NEW_ALARM, Boolean.TRUE.toString()); + assertThat(actualOutgoingMsgMetadataContent).size().isEqualTo(metadata.getData().size() + 1); + + // verify wrong processing paths were not taken + then(ctxMock).should(never()).tellNext(any(), eq(TbNodeConnectionType.FALSE)); + then(ctxMock).should(never()).tellNext(any(), eq("Updated")); + then(ctxMock).should(never()).tellNext(any(), eq("Cleared")); + then(ctxMock).should(never()).tellSuccess(any()); + then(ctxMock).should(never()).tellFailure(any(), any()); + } + + @Test + @DisplayName("When node is taking alarm data from incoming message and active alarm exists, then should update existing alarm using info from incoming message.") + void whenAlarmDataIsTakenFromMsgAndActiveAlarmExists_thenExistingAlarmIsUpdated() throws Exception { + // GIVEN + + // values that changed between existing alarm and updated alarm + AlarmSeverity oldAlarmSeverity = AlarmSeverity.WARNING; + AlarmSeverity newAlarmSeverity = AlarmSeverity.MAJOR; + + boolean oldPropagate = true; + boolean newPropagate = false; + + boolean oldPropagateToOwner = false; + boolean newPropagateToOwner = true; + + boolean oldPropagateToTenant = false; + boolean newPropagateToTenant = true; + + List oldPropagateRelationTypes = List.of("RELATION_TYPE_1", "RELATION_TYPE_2", "RELATION_TYPE_3"); + List newPropagateRelationTypes = Collections.emptyList(); + + JsonNode oldAlarmDetails = JacksonUtil.newObjectNode().put("oldAlarmDetailsKey", "oldAlarmDetailsValue"); + JsonNode newAlarmDetails = JacksonUtil.newObjectNode().put("newAlarmDetails", "Some alarm details TBEL").set("oldAlarmDetails", oldAlarmDetails); + + long oldEndTs = 200L; + long newEndTs = 300L; + + // node configuration + config = config.defaultConfiguration(); + config.setUseMessageAlarmData(true); + config.setOverwriteAlarmDetails(true); + + // other values + String alarmType = "High Temperature"; + + long metadataTs = 1711631716127L; + metadata.putValue("ts", Long.toString(metadataTs)); + metadata.putValue("location", "Company office"); + + var ruleNodeSelfId = new RuleNodeId(Uuids.timeBased()); + + // alarm that is inside an incoming message + var alarmFromIncomingMessage = Alarm.builder() + .tenantId(tenantId) + .originator(msgOriginator) + .cleared(false) + .acknowledged(false) + .severity(newAlarmSeverity) + .propagate(newPropagate) + .propagateToOwner(newPropagateToOwner) + .propagateToTenant(newPropagateToTenant) + .propagateRelationTypes(newPropagateRelationTypes) + .type(alarmType) + .startTs(100L) + .endTs(newEndTs) + .details(newAlarmDetails) + .build(); + + var incomingMsg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, msgOriginator, metadata, JacksonUtil.toString(alarmFromIncomingMessage)); + + var existingAlarmId = new AlarmId(Uuids.timeBased()); + var existingActiveAlarm = Alarm.builder() + .tenantId(tenantId) + .originator(msgOriginator) + .cleared(false) + .acknowledged(false) + .severity(oldAlarmSeverity) + .propagate(oldPropagate) + .propagateToOwner(oldPropagateToOwner) + .propagateToTenant(oldPropagateToTenant) + .propagateRelationTypes(oldPropagateRelationTypes) + .type(alarmType) + .startTs(100L) + .endTs(oldEndTs) + .details(oldAlarmDetails) + .build(); + existingActiveAlarm.setId(existingAlarmId); + + // expected values + var expectedUpdatedAlarm = Alarm.builder() + .tenantId(tenantId) + .originator(msgOriginator) + .cleared(false) + .acknowledged(false) + .severity(newAlarmSeverity) + .propagate(newPropagate) + .propagateToOwner(newPropagateToOwner) + .propagateToTenant(newPropagateToTenant) + .propagateRelationTypes(newPropagateRelationTypes) + .type(alarmType) + .startTs(100L) + .endTs(newEndTs) + .details(newAlarmDetails) + .build(); + expectedUpdatedAlarm.setId(existingAlarmId); + var expectedUpdatedAlarmInfo = new AlarmInfo(expectedUpdatedAlarm); + + var expectedUpdateAlarmRequest = AlarmUpdateRequest.builder() + .tenantId(tenantId) + .alarmId(existingAlarmId) + .severity(newAlarmSeverity) + .startTs(100L) + .endTs(newEndTs) + .details(newAlarmDetails) + .propagation(AlarmPropagationInfo.builder() + .propagate(newPropagate) + .propagateToOwner(newPropagateToOwner) + .propagateToTenant(newPropagateToTenant) + .propagateRelationTypes(newPropagateRelationTypes).build()) + .userId(null) + .build(); + + // mocks + given(ctxMock.getTenantId()).willReturn(tenantId); + given(ctxMock.getAlarmService()).willReturn(alarmServiceMock); + given(ctxMock.getDbCallbackExecutor()).willReturn(dbExecutor); + given(ctxMock.getSelfId()).willReturn(ruleNodeSelfId); + given(alarmServiceMock.findLatestActiveByOriginatorAndType(tenantId, msgOriginator, alarmType)).willReturn(existingActiveAlarm); + given(alarmDetailsScriptMock.executeJsonAsync(any())).willReturn(Futures.immediateFuture(newAlarmDetails)); + doReturn(newEndTs).when(nodeSpy).currentTimeMillis(); + var apiCallResult = AlarmApiCallResult.builder() + .successful(true) + .created(false) + .modified(true) + .cleared(false) + .deleted(false) + .alarm(expectedUpdatedAlarmInfo) + .old(new Alarm(existingActiveAlarm)) + .propagatedEntitiesList(List.of(tenantId)) + .build(); + given(alarmServiceMock.updateAlarm(expectedUpdateAlarmRequest)).willReturn(apiCallResult); + given(ctxMock.alarmActionMsg(expectedUpdatedAlarmInfo, ruleNodeSelfId, TbMsgType.ENTITY_UPDATED)).willReturn(alarmActionMsgMock); + given(ctxMock.transformMsg(any(TbMsg.class), any(TbMsgType.class), any(EntityId.class), any(TbMsgMetaData.class), anyString())) + .willAnswer(answer -> TbMsg.transformMsg( + answer.getArgument(0, TbMsg.class), + answer.getArgument(1, TbMsgType.class), + answer.getArgument(2, EntityId.class), + answer.getArgument(3, TbMsgMetaData.class), + answer.getArgument(4, String.class)) + ); + given(ctxMock.createScriptEngine(ScriptLanguage.TBEL, config.getAlarmDetailsBuildTbel())).willReturn(alarmDetailsScriptMock); + + // node initialization + nodeSpy.init(ctxMock, new TbNodeConfiguration(JacksonUtil.valueToTree(config))); + + // WHEN + nodeSpy.onMsg(ctxMock, incomingMsg); + + // THEN + + // verify alarm details script evaluation + then(ctxMock).should().logJsEvalRequest(); + var dummyMsgCaptor = ArgumentCaptor.forClass(TbMsg.class); + then(alarmDetailsScriptMock).should().executeJsonAsync(dummyMsgCaptor.capture()); + TbMsg actualDummyMsg = dummyMsgCaptor.getValue(); + assertThat(actualDummyMsg.getType()).isEqualTo(incomingMsg.getType()); + assertThat(actualDummyMsg.getData()).isEqualTo(incomingMsg.getData()); + assertThat(actualDummyMsg.getMetaData().getData()).containsEntry("prevAlarmDetails", JacksonUtil.toString(oldAlarmDetails)); + then(ctxMock).should().logJsEvalResponse(); + + // verify we called updateAlarm() with correct AlarmUpdateRequest + then(alarmServiceMock).should().updateAlarm(expectedUpdateAlarmRequest); + then(alarmServiceMock).should(never()).createAlarm(any()); + + // verify that we created a correct alarm action message and enqueued it + then(ctxMock).should().alarmActionMsg(expectedUpdatedAlarmInfo, ruleNodeSelfId, TbMsgType.ENTITY_UPDATED); + then(ctxMock).should().enqueue(eq(alarmActionMsgMock), successCaptor.capture(), any()); + + // run success captor to emulate successful queueing of an alarm action message and to trigger further processing on the success path + successCaptor.getValue().run(); + + // capture and verify an outgoing message + var outgoingMsgCaptor = ArgumentCaptor.forClass(TbMsg.class); + then(ctxMock).should().tellNext(outgoingMsgCaptor.capture(), eq("Updated")); + var actualOutgoingMsg = outgoingMsgCaptor.getValue(); + assertThat(actualOutgoingMsg.getType()).isEqualTo(TbMsgType.ALARM.name()); + assertThat(actualOutgoingMsg.getOriginator()).isEqualTo(msgOriginator); + assertThat(actualOutgoingMsg.getData()).isEqualTo(JacksonUtil.valueToTree(expectedUpdatedAlarmInfo).toString()); + + Map actualOutgoingMsgMetadataContent = actualOutgoingMsg.getMetaData().getData(); + assertThat(actualOutgoingMsgMetadataContent).containsAllEntriesOf(metadata.getData()); + assertThat(actualOutgoingMsgMetadataContent).containsEntry(DataConstants.IS_EXISTING_ALARM, Boolean.TRUE.toString()); + assertThat(actualOutgoingMsgMetadataContent).size().isEqualTo(metadata.getData().size() + 1); + + // verify wrong processing paths were not taken + then(ctxMock).should(never()).tellNext(any(), eq(TbNodeConnectionType.FALSE)); + then(ctxMock).should(never()).tellNext(any(), eq("Created")); + then(ctxMock).should(never()).tellNext(any(), eq("Cleared")); + then(ctxMock).should(never()).tellSuccess(any()); + then(ctxMock).should(never()).tellFailure(any(), any()); + } + + @Test + @DisplayName("When only severity was updated (other fields the same), then should consider this as an alarm update and take update processing path.") + void whenOnlySeverityWasUpdated_thenShouldTakeAlarmUpdatedPath() throws Exception { + // GIVEN + + // values that changed between existing alarm and updated alarm + AlarmSeverity oldAlarmSeverity = AlarmSeverity.WARNING; + AlarmSeverity newAlarmSeverity = AlarmSeverity.MAJOR; + + boolean propagate = true; + boolean propagateToOwner = false; + boolean propagateToTenant = false; + List propagateRelationTypes = List.of("RELATION_TYPE_1", "RELATION_TYPE_2", "RELATION_TYPE_3"); + JsonNode alarmDetails = JacksonUtil.newObjectNode().put("oldAlarmDetailsKey", "oldAlarmDetailsValue"); + long endTs = 200L; + + // node configuration + config = config.defaultConfiguration(); + config.setUseMessageAlarmData(true); + config.setOverwriteAlarmDetails(true); + + // other values + String alarmType = "High Temperature"; + + long metadataTs = 1711631716127L; + metadata.putValue("ts", Long.toString(metadataTs)); + metadata.putValue("location", "Company office"); + + var ruleNodeSelfId = new RuleNodeId(Uuids.timeBased()); + + // alarm that is inside an incoming message + var alarmFromIncomingMessage = Alarm.builder() + .tenantId(tenantId) + .originator(msgOriginator) + .cleared(false) + .acknowledged(false) + .severity(newAlarmSeverity) + .propagate(propagate) + .propagateToOwner(propagateToOwner) + .propagateToTenant(propagateToTenant) + .propagateRelationTypes(propagateRelationTypes) + .type(alarmType) + .startTs(100L) + .endTs(endTs) + .details(alarmDetails) + .build(); + + var incomingMsg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, msgOriginator, metadata, JacksonUtil.toString(alarmFromIncomingMessage)); + + var existingAlarmId = new AlarmId(Uuids.timeBased()); + var existingActiveAlarm = Alarm.builder() + .tenantId(tenantId) + .originator(msgOriginator) + .cleared(false) + .acknowledged(false) + .severity(oldAlarmSeverity) + .propagate(propagate) + .propagateToOwner(propagateToOwner) + .propagateToTenant(propagateToTenant) + .propagateRelationTypes(propagateRelationTypes) + .type(alarmType) + .startTs(100L) + .endTs(endTs) + .details(alarmDetails) + .build(); + existingActiveAlarm.setId(existingAlarmId); + + // expected values + var expectedUpdatedAlarm = Alarm.builder() + .tenantId(tenantId) + .originator(msgOriginator) + .cleared(false) + .acknowledged(false) + .severity(newAlarmSeverity) + .propagate(propagate) + .propagateToOwner(propagateToOwner) + .propagateToTenant(propagateToTenant) + .propagateRelationTypes(propagateRelationTypes) + .type(alarmType) + .startTs(100L) + .endTs(endTs) + .details(alarmDetails) + .build(); + expectedUpdatedAlarm.setId(existingAlarmId); + var expectedUpdatedAlarmInfo = new AlarmInfo(expectedUpdatedAlarm); + + var expectedUpdateAlarmRequest = AlarmUpdateRequest.builder() + .tenantId(tenantId) + .alarmId(existingAlarmId) + .severity(newAlarmSeverity) + .startTs(100L) + .endTs(endTs) + .details(alarmDetails) + .propagation(AlarmPropagationInfo.builder() + .propagate(propagate) + .propagateToOwner(propagateToOwner) + .propagateToTenant(propagateToTenant) + .propagateRelationTypes(propagateRelationTypes).build()) + .userId(null) + .build(); + + // mocks + given(ctxMock.getTenantId()).willReturn(tenantId); + given(ctxMock.getAlarmService()).willReturn(alarmServiceMock); + given(ctxMock.getDbCallbackExecutor()).willReturn(dbExecutor); + given(ctxMock.getSelfId()).willReturn(ruleNodeSelfId); + given(alarmServiceMock.findLatestActiveByOriginatorAndType(tenantId, msgOriginator, alarmType)).willReturn(existingActiveAlarm); + given(alarmDetailsScriptMock.executeJsonAsync(any())).willReturn(Futures.immediateFuture(alarmDetails)); + doReturn(endTs).when(nodeSpy).currentTimeMillis(); + var apiCallResult = AlarmApiCallResult.builder() + .successful(true) + .created(false) + .modified(true) + .cleared(false) + .deleted(false) + .alarm(expectedUpdatedAlarmInfo) + .old(new Alarm(existingActiveAlarm)) + .propagatedEntitiesList(List.of(tenantId)) + .build(); + given(alarmServiceMock.updateAlarm(expectedUpdateAlarmRequest)).willReturn(apiCallResult); + given(ctxMock.alarmActionMsg(expectedUpdatedAlarmInfo, ruleNodeSelfId, TbMsgType.ENTITY_UPDATED)).willReturn(alarmActionMsgMock); + given(ctxMock.transformMsg(any(TbMsg.class), any(TbMsgType.class), any(EntityId.class), any(TbMsgMetaData.class), anyString())) + .willAnswer(answer -> TbMsg.transformMsg( + answer.getArgument(0, TbMsg.class), + answer.getArgument(1, TbMsgType.class), + answer.getArgument(2, EntityId.class), + answer.getArgument(3, TbMsgMetaData.class), + answer.getArgument(4, String.class)) + ); + given(ctxMock.createScriptEngine(ScriptLanguage.TBEL, config.getAlarmDetailsBuildTbel())).willReturn(alarmDetailsScriptMock); + + // node initialization + nodeSpy.init(ctxMock, new TbNodeConfiguration(JacksonUtil.valueToTree(config))); + + // WHEN + nodeSpy.onMsg(ctxMock, incomingMsg); + + // THEN + + // verify alarm details script evaluation + then(ctxMock).should().logJsEvalRequest(); + var dummyMsgCaptor = ArgumentCaptor.forClass(TbMsg.class); + then(alarmDetailsScriptMock).should().executeJsonAsync(dummyMsgCaptor.capture()); + TbMsg actualDummyMsg = dummyMsgCaptor.getValue(); + assertThat(actualDummyMsg.getType()).isEqualTo(incomingMsg.getType()); + assertThat(actualDummyMsg.getData()).isEqualTo(incomingMsg.getData()); + assertThat(actualDummyMsg.getMetaData().getData()).containsEntry("prevAlarmDetails", JacksonUtil.toString(alarmDetails)); + then(ctxMock).should().logJsEvalResponse(); + + // verify we called updateAlarm() with correct AlarmUpdateRequest + then(alarmServiceMock).should().updateAlarm(expectedUpdateAlarmRequest); + then(alarmServiceMock).should(never()).createAlarm(any()); + + // verify that we created a correct alarm action message and enqueued it + then(ctxMock).should().alarmActionMsg(expectedUpdatedAlarmInfo, ruleNodeSelfId, TbMsgType.ENTITY_UPDATED); + then(ctxMock).should().enqueue(eq(alarmActionMsgMock), successCaptor.capture(), any()); + + // run success captor to emulate successful queueing of an alarm action message and to trigger further processing on the success path + successCaptor.getValue().run(); + + // capture and verify an outgoing message + var outgoingMsgCaptor = ArgumentCaptor.forClass(TbMsg.class); + then(ctxMock).should().tellNext(outgoingMsgCaptor.capture(), eq("Updated")); + var actualOutgoingMsg = outgoingMsgCaptor.getValue(); + assertThat(actualOutgoingMsg.getType()).isEqualTo(TbMsgType.ALARM.name()); + assertThat(actualOutgoingMsg.getOriginator()).isEqualTo(msgOriginator); + assertThat(actualOutgoingMsg.getData()).isEqualTo(JacksonUtil.valueToTree(expectedUpdatedAlarmInfo).toString()); + + Map actualOutgoingMsgMetadataContent = actualOutgoingMsg.getMetaData().getData(); + assertThat(actualOutgoingMsgMetadataContent).containsAllEntriesOf(metadata.getData()); + assertThat(actualOutgoingMsgMetadataContent).containsEntry(DataConstants.IS_EXISTING_ALARM, Boolean.TRUE.toString()); + assertThat(actualOutgoingMsgMetadataContent).size().isEqualTo(metadata.getData().size() + 1); + + // verify wrong processing paths were not taken + then(ctxMock).should(never()).tellNext(any(), eq(TbNodeConnectionType.FALSE)); + then(ctxMock).should(never()).tellNext(any(), eq("Created")); + then(ctxMock).should(never()).tellNext(any(), eq("Cleared")); + then(ctxMock).should(never()).tellSuccess(any()); + then(ctxMock).should(never()).tellFailure(any(), any()); + } + + @Test + @DisplayName("When the alarm details script throws an exception, " + + "node should tell failure with that exception, and it should neither create nor update any alarms, nor should it send any other messages.") + void whenAlarmDetailsScriptThrowsException_thenShouldTellFailureAndNoOtherActions() throws Exception { + // GIVEN + config = config.defaultConfiguration(); + + var incomingMsg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, msgOriginator, metadata, "{\"temperature\": 50}"); + + given(ctxMock.getTenantId()).willReturn(tenantId); + given(ctxMock.getAlarmService()).willReturn(alarmServiceMock); + given(ctxMock.getDbCallbackExecutor()).willReturn(dbExecutor); + given(ctxMock.createScriptEngine(ScriptLanguage.TBEL, config.getAlarmDetailsBuildTbel())).willReturn(alarmDetailsScriptMock); + + var expectedException = new ExecutionException("Failed to execute script.", new RuntimeException("Something went wrong!")); + given(alarmDetailsScriptMock.executeJsonAsync(incomingMsg)).willReturn(Futures.immediateFailedFuture(expectedException)); + + nodeSpy.init(ctxMock, new TbNodeConfiguration(JacksonUtil.valueToTree(config))); + + // WHEN + nodeSpy.onMsg(ctxMock, incomingMsg); + + // THEN + var exceptionCaptor = ArgumentCaptor.forClass(Throwable.class); + then(ctxMock).should().tellFailure(eq(incomingMsg), exceptionCaptor.capture()); + Throwable actualException = exceptionCaptor.getValue(); + assertThat(actualException).isEqualTo(expectedException); + + then(alarmServiceMock).should(never()).createAlarm(any()); + then(alarmServiceMock).should(never()).updateAlarm(any()); + + then(ctxMock).should(never()).tellNext(any(), eq(TbNodeConnectionType.FALSE)); + then(ctxMock).should(never()).tellNext(any(), eq("Created")); + then(ctxMock).should(never()).tellNext(any(), eq("Updated")); + then(ctxMock).should(never()).tellNext(any(), eq("Cleared")); + then(ctxMock).should(never()).tellSuccess(any()); + } + +} From dde2a55d3dd5a387e618e28644ba09b2e85bf056 Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Wed, 10 Apr 2024 16:23:22 +0300 Subject: [PATCH 38/80] UI: Implement new version of State chart widget based on Echarts. --- .../json/system/widget_bundles/charts.json | 5 +- .../json/system/widget_types/state_chart.json | 34 +++-- .../widget_types/state_chart_deprecated.json | 25 +++ ui-ngx/src/app/core/api/data-aggregator.ts | 2 + ui-ngx/src/app/core/api/widget-api.models.ts | 3 +- ...e-series-chart-basic-config.component.html | 4 + ...ime-series-chart-basic-config.component.ts | 12 +- .../widget/lib/chart/echarts-widget.models.ts | 95 +++++++++--- .../chart/time-series-chart-state.models.ts | 109 +++++++++++++ .../lib/chart/time-series-chart.models.ts | 105 ++++++++++--- .../widget/lib/chart/time-series-chart.ts | 87 +++++++++-- ...e-series-chart-key-settings.component.html | 10 ++ ...ime-series-chart-key-settings.component.ts | 7 +- ...-series-chart-line-settings.component.html | 4 +- ...me-series-chart-line-settings.component.ts | 12 +- ...eries-chart-widget-settings.component.html | 11 ++ ...-series-chart-widget-settings.component.ts | 10 ++ ...-series-chart-axis-settings.component.html | 9 ++ ...me-series-chart-axis-settings.component.ts | 3 +- ...time-series-chart-state-row.component.html | 62 ++++++++ ...time-series-chart-state-row.component.scss | 51 +++++++ .../time-series-chart-state-row.component.ts | 138 +++++++++++++++++ ...e-series-chart-states-panel.component.html | 47 ++++++ ...e-series-chart-states-panel.component.scss | 60 ++++++++ ...ime-series-chart-states-panel.component.ts | 144 ++++++++++++++++++ .../common/widget-settings-common.module.ts | 10 ++ .../widget/lib/chart/ticks_generator_fn.md | 61 ++++++++ .../assets/locale/locale.constant-en_US.json | 15 ++ 28 files changed, 1053 insertions(+), 82 deletions(-) create mode 100644 application/src/main/data/json/system/widget_types/state_chart_deprecated.json create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart-state.models.ts create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-state-row.component.html create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-state-row.component.scss create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-state-row.component.ts create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-states-panel.component.html create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-states-panel.component.scss create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-states-panel.component.ts create mode 100644 ui-ngx/src/assets/help/en_US/widget/lib/chart/ticks_generator_fn.md diff --git a/application/src/main/data/json/system/widget_bundles/charts.json b/application/src/main/data/json/system/widget_bundles/charts.json index 979b241e67..a66975d1e2 100644 --- a/application/src/main/data/json/system/widget_bundles/charts.json +++ b/application/src/main/data/json/system/widget_bundles/charts.json @@ -12,10 +12,11 @@ "line_chart", "bar_chart", "point_chart", + "state_chart", + "bar_chart_with_labels", + "range_chart", "charts.basic_timeseries", "charts.state_chart", - "range_chart", - "bar_chart_with_labels", "charts.timeseries_bars_flot", "cards.aggregated_value_card", "charts.bars", diff --git a/application/src/main/data/json/system/widget_types/state_chart.json b/application/src/main/data/json/system/widget_types/state_chart.json index a78fc06f64..0af2631813 100644 --- a/application/src/main/data/json/system/widget_types/state_chart.json +++ b/application/src/main/data/json/system/widget_types/state_chart.json @@ -1,26 +1,34 @@ { - "fqn": "charts.state_chart", - "name": "State Chart", + "fqn": "state_chart", + "name": "State chart", "deprecated": false, - "image": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAACgCAMAAAB+IdObAAAB9VBMVEUAAAAhlvMilvMymeE+nNVDoetInslNq/VZqOdpuPd3d3d5p5Z5suB6enp8fHyBgYGDg4ODqoqEhISIiIiKioqKuN2MjIyNjY2Ojo6QkJCRkZGSkpKUlJSVlZWWlpaXl5eYmJiZmZmampqcnJydnZ2enp6goKCgr3KhoaGioqKisXSjo6OjvsmkpKSlpaWlwdempqanp6eoqKiosGOo1vqpqampsGOqqqqqsWOrq6usrKysw9Wurq6wsLCysrK0tLS1tbW1wbK2tra3t7e4wau5ubm6urq7u7u8vLy9vb2+vr6/xbTAwMDAxbjCwsLC3ejDw8PExMTFxcXGxsbHx8fIv3jIyMjJycnKysrK5vzMzc7Nzc3Ozs7Pz8/QuDnRzcHS0tLT09PT39HU1NTU6/3VzLLV1dXV3sjW1tbX19fYuTHY2NjZ2dna0Ira2trb29vc3Nzex4De3t7f39/guyvhvCfhvCvh4eHh6Nbi4uLjvCTj4+PkyXbk5OTlvCTm5ubm693o6Ojpxl7q6urr6+vswTTs7Ozt7e3uvhju7u7vvhjv7+/wvxjw8PDx8fHyvxX0yTv09PT19fX29vb39/f4wyL4+Pj5+fn6+vr75J37+/v8whP8/Pz9/f39/v/+/v7/wQf/xRb/3HT/5JH/9tz/++/////APs7XAAAAAWJLR0Smt7AblQAAA1RJREFUeNrt3dlT01AUBvAEd8UNl2oLrdrFolZArUul1hWlVhQXFAUF1xaxIqi4FUQUrDsUClZi4nb+Th96S9NQkjDOOKZ+3wuZw8md++M25OXMlKOccJQnTUYocdRqdw8UAqTfOjG4lvr7uowOqbtAtG6o5OiGFoNDapuJHO8tFK4xOKS9msTVgoUiaQjPclnSl01snZ/y4ly2yBNZbRarbc+3yvdpt/hLawOPJp9ur7O0mRiEmzFkY1M6N/4I8qJpulzR2sBx1sgRjQuyA2pk+STdylw2VqR/FPFfWdcCvjhduiM7kVF2db1Nuspu7JGes+I76SRba7f0Wfnn/6F6It+mfo7m8TvYvh7KTiT3PQIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggPwXkFa7e7AQIP3WiUFzdl7LuBDFvJZxIYp5LeNCFPNaBn7Yc+e1KlheSYcrFCniL7LZqPl8cbp0TjavNcqu9rZJB9gdPdIJVnwjbWG1ndI95UzWM9V5rS9Ti3P4VWy1+5jXAgQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAMK+FeS3Ma2FeC/NamNfCCxEQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBA/hZkTKSkwSGn7VUJOmOLV241NiRSRZ2uYcvt+Io+Y0N83USm+MqGWEnU2JCqXqI1ZE+QhYiIy36tXR5INpOQbB5kftcmK85mtdey2iJekeWqX6/3kc+TLCQTrq6eEuZCgKTsbnNXBsIZOERJMedNQnry73XpW8UAKVhIyBPScVfQE9RqEaP7iMT6g+pdw7vKbxKl3IJqV8K7OUqXPHvG9UMGykk2lz1d4g5yvNXo6QqZiA41aHT5OwQTUWBJSrWrvlMwj1jFU2f1Q8K1dCysCUmVhdenNLvMRKZt3hGNriEnxfxlGqt9aPYRkb9bP6Q1SMFrmltMukKuMT2QpckWjc/WhP2lYB/TgjzdHyCKVM/gGXnsp+qY5hbba+hIVA/ETL0+9SepsoNiTs/igGpXZIhKqVv9QVJARIfXIWqfiM1nS+qBnHdae1V7Qss8ngEijRO5a6sMCAtdqv+HfgPwpNPbU6ipOwAAAABJRU5ErkJggg==", + "image": "tb-image:Y2hhcnRfKDUpLnN2Zw==:IlN0YXRlIGNoYXJ0IiBzeXN0ZW0gd2lkZ2V0IGltYWdl;data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAwIiBoZWlnaHQ9IjE2MCIgdmlld0JveD0iMCAwIDIwMCAxNjAiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMF80NDM3Xzk2ODU4KSI+CjxwYXRoIGQ9Ik0yLjc2NTYyIDEuODE2NDFMMC44ODI4MTIgN0gwLjExMzI4MUwyLjI4MTI1IDEuMzEyNUgyLjc3NzM0TDIuNzY1NjIgMS44MTY0MVpNNC4zNDM3NSA3TDIuNDU3MDMgMS44MTY0MUwyLjQ0NTMxIDEuMzEyNUgyLjk0MTQxTDUuMTE3MTkgN0g0LjM0Mzc1Wk00LjI0NjA5IDQuODk0NTNWNS41MTE3MkgxLjA1MDc4VjQuODk0NTNINC4yNDYwOVoiIGZpbGw9ImJsYWNrIiBmaWxsLW9wYWNpdHk9IjAuNTQiLz4KPHBhdGggZD0iTTMuNjg3NSAzMi41NDY0SDIuMjQ2MDlMMi4yMzgyOCAzMS45NDA5SDMuNTQ2ODhDMy43NjMwMiAzMS45NDA5IDMuOTUxODIgMzEuOTA0NSA0LjExMzI4IDMxLjgzMTVDNC4yNzQ3NCAzMS43NTg2IDQuMzk5NzQgMzEuNjU0NSA0LjQ4ODI4IDMxLjUxOUM0LjU3OTQzIDMxLjM4MSA0LjYyNSAzMS4yMTcgNC42MjUgMzEuMDI2OUM0LjYyNSAzMC44MTg1IDQuNTg0NjQgMzAuNjQ5MyA0LjUwMzkxIDMwLjUxOUM0LjQyNTc4IDMwLjM4NjIgNC4zMDQ2OSAzMC4yODk5IDQuMTQwNjIgMzAuMjNDMy45NzkxNyAzMC4xNjc1IDMuNzczNDQgMzAuMTM2MiAzLjUyMzQ0IDMwLjEzNjJIMi40MTQwNlYzNS4yMDY1SDEuNjYwMTZWMjkuNTE5SDMuNTIzNDRDMy44MTUxIDI5LjUxOSA0LjA3NTUyIDI5LjU0OSA0LjMwNDY5IDI5LjYwODlDNC41MzM4NSAyOS42NjYyIDQuNzI3ODYgMjkuNzU3MyA0Ljg4NjcyIDI5Ljg4MjNDNS4wNDgxOCAzMC4wMDQ3IDUuMTcwNTcgMzAuMTYxIDUuMjUzOTEgMzAuMzUxMUM1LjMzNzI0IDMwLjU0MTIgNS4zNzg5MSAzMC43NjkgNS4zNzg5MSAzMS4wMzQ3QzUuMzc4OTEgMzEuMjY5IDUuMzE5MDEgMzEuNDgxMyA1LjE5OTIyIDMxLjY3MTRDNS4wNzk0MyAzMS44NTg5IDQuOTEyNzYgMzIuMDEyNSA0LjY5OTIyIDMyLjEzMjNDNC40ODgyOCAzMi4yNTIxIDQuMjQwODkgMzIuMzI4OSAzLjk1NzAzIDMyLjM2MjhMMy42ODc1IDMyLjU0NjRaTTMuNjUyMzQgMzUuMjA2NUgxLjk0OTIyTDIuMzc1IDM0LjU5MzNIMy42NTIzNEMzLjg5MTkzIDM0LjU5MzMgNC4wOTUwNSAzNC41NTE2IDQuMjYxNzIgMzQuNDY4M0M0LjQzMDk5IDM0LjM4NDkgNC41NTk5IDM0LjI2NzcgNC42NDg0NCAzNC4xMTY3QzQuNzM2OTggMzMuOTYzMSA0Ljc4MTI1IDMzLjc4MjEgNC43ODEyNSAzMy41NzM3QzQuNzgxMjUgMzMuMzYyOCA0Ljc0MzQ5IDMzLjE4MDUgNC42Njc5NyAzMy4wMjY5QzQuNTkyNDUgMzIuODczMiA0LjQ3Mzk2IDMyLjc1NDcgNC4zMTI1IDMyLjY3MTRDNC4xNTEwNCAzMi41ODgxIDMuOTQyNzEgMzIuNTQ2NCAzLjY4NzUgMzIuNTQ2NEgyLjYxMzI4TDIuNjIxMDkgMzEuOTQwOUg0LjA4OTg0TDQuMjUgMzIuMTU5N0M0LjUyMzQ0IDMyLjE4MzEgNC43NTUyMSAzMi4yNjEyIDQuOTQ1MzEgMzIuMzk0QzUuMTM1NDIgMzIuNTI0MyA1LjI3OTk1IDMyLjY5MDkgNS4zNzg5MSAzMi44OTRDNS40ODA0NyAzMy4wOTcyIDUuNTMxMjUgMzMuMzIxMSA1LjUzMTI1IDMzLjU2NTlDNS41MzEyNSAzMy45MjAxIDUuNDUzMTIgMzQuMjE5NiA1LjI5Njg4IDM0LjQ2NDRDNS4xNDMyMyAzNC43MDY1IDQuOTI1NzggMzQuODkxNCA0LjY0NDUzIDM1LjAxOUM0LjM2MzI4IDM1LjE0NCA0LjAzMjU1IDM1LjIwNjUgMy42NTIzNCAzNS4yMDY1WiIgZmlsbD0iYmxhY2siIGZpbGwtb3BhY2l0eT0iMC41NCIvPgo8cGF0aCBkPSJNNC4wOTM3NSA2MS42MDVINC44NDM3NUM0LjgwNDY5IDYxLjk2NDQgNC43MDE4MiA2Mi4yODYgNC41MzUxNiA2Mi41Njk4QzQuMzY4NDkgNjIuODUzNyA0LjEzMjgxIDYzLjA3ODkgMy44MjgxMiA2My4yNDU2QzMuNTIzNDQgNjMuNDA5NyAzLjE0MzIzIDYzLjQ5MTcgMi42ODc1IDYzLjQ5MTdDMi4zNTQxNyA2My40OTE3IDIuMDUwNzggNjMuNDI5MiAxLjc3NzM0IDYzLjMwNDJDMS41MDY1MSA2My4xNzkyIDEuMjczNDQgNjMuMDAyMSAxLjA3ODEyIDYyLjc3MjlDMC44ODI4MTIgNjIuNTQxMiAwLjczMTc3MSA2Mi4yNjM4IDAuNjI1IDYxLjk0MDlDMC41MjA4MzMgNjEuNjE1NCAwLjQ2ODc1IDYxLjI1MzQgMC40Njg3NSA2MC44NTVWNjAuMjg4NkMwLjQ2ODc1IDU5Ljg5MDEgMC41MjA4MzMgNTkuNTI5NSAwLjYyNSA1OS4yMDY1QzAuNzMxNzcxIDU4Ljg4MSAwLjg4NDExNSA1OC42MDI0IDEuMDgyMDMgNTguMzcwNkMxLjI4MjU1IDU4LjEzODggMS41MjM0NCA1Ny45NjA0IDEuODA0NjkgNTcuODM1NEMyLjA4NTk0IDU3LjcxMDQgMi40MDIzNCA1Ny42NDc5IDIuNzUzOTEgNTcuNjQ3OUMzLjE4MzU5IDU3LjY0NzkgMy41NDY4OCA1Ny43Mjg3IDMuODQzNzUgNTcuODkwMUM0LjE0MDYyIDU4LjA1MTYgNC4zNzEwOSA1OC4yNzU2IDQuNTM1MTYgNTguNTYyQzQuNzAxODIgNTguODQ1OSA0LjgwNDY5IDU5LjE3NTMgNC44NDM3NSA1OS41NTAzSDQuMDkzNzVDNC4wNTcyOSA1OS4yODQ3IDMuOTg5NTggNTkuMDU2OCAzLjg5MDYyIDU4Ljg2NjdDMy43OTE2NyA1OC42NzQgMy42NTEwNCA1OC41MjU2IDMuNDY4NzUgNTguNDIxNEMzLjI4NjQ2IDU4LjMxNzIgMy4wNDgxOCA1OC4yNjUxIDIuNzUzOTEgNTguMjY1MUMyLjUwMTMgNTguMjY1MSAyLjI3ODY1IDU4LjMxMzMgMi4wODU5NCA1OC40MDk3QzEuODk1ODMgNTguNTA2IDEuNzM1NjggNTguNjQyNyAxLjYwNTQ3IDU4LjgxOThDMS40Nzc4NiA1OC45OTY5IDEuMzgxNTEgNTkuMjA5MSAxLjMxNjQxIDU5LjQ1NjVDMS4yNTEzIDU5LjcwMzkgMS4yMTg3NSA1OS45Nzg3IDEuMjE4NzUgNjAuMjgwOFY2MC44NTVDMS4yMTg3NSA2MS4xMzM2IDEuMjQ3NCA2MS4zOTUzIDEuMzA0NjkgNjEuNjQwMUMxLjM2NDU4IDYxLjg4NDkgMS40NTQ0MyA2Mi4wOTk4IDEuNTc0MjIgNjIuMjg0N0MxLjY5NDAxIDYyLjQ2OTYgMS44NDYzNSA2Mi42MTU0IDIuMDMxMjUgNjIuNzIyMkMyLjIxNjE1IDYyLjgyNjMgMi40MzQ5IDYyLjg3ODQgMi42ODc1IDYyLjg3ODRDMy4wMDc4MSA2Mi44Nzg0IDMuMjYzMDIgNjIuODI3NiAzLjQ1MzEyIDYyLjcyNjFDMy42NDMyMyA2Mi42MjQ1IDMuNzg2NDYgNjIuNDc4NyAzLjg4MjgxIDYyLjI4ODZDMy45ODE3NyA2Mi4wOTg1IDQuMDUyMDggNjEuODcwNiA0LjA5Mzc1IDYxLjYwNVoiIGZpbGw9ImJsYWNrIiBmaWxsLW9wYWNpdHk9IjAuNTQiLz4KPHBhdGggZD0iTTIuMTk5MjIgOTEuNjIwMUgxLjAxMTcyTDEuMDE5NTMgOTEuMDA2OEgyLjE5OTIyQzIuNjA1NDcgOTEuMDA2OCAyLjk0NDAxIDkwLjkyMjIgMy4yMTQ4NCA5MC43NTI5QzMuNDg1NjggOTAuNTgxMSAzLjY4ODggOTAuMzQxNSAzLjgyNDIyIDkwLjAzNDJDMy45NjIyNCA4OS43MjQzIDQuMDMxMjUgODkuMzYyMyA0LjAzMTI1IDg4Ljk0ODJWODguNjAwNkM0LjAzMTI1IDg4LjI3NTEgMy45OTIxOSA4Ny45ODYgMy45MTQwNiA4Ny43MzM0QzMuODM1OTQgODcuNDc4MiAzLjcyMTM1IDg3LjI2MzMgMy41NzAzMSA4Ny4wODg5QzMuNDE5MjcgODYuOTExOCAzLjIzNDM4IDg2Ljc3NzcgMy4wMTU2MiA4Ni42ODY1QzIuNzk5NDggODYuNTk1NCAyLjU1MDc4IDg2LjU0OTggMi4yNjk1MyA4Ni41NDk4SDAuOTg4MjgxVjg1LjkzMjZIMi4yNjk1M0MyLjY0MTkzIDg1LjkzMjYgMi45ODE3NyA4NS45OTUxIDMuMjg5MDYgODYuMTIwMUMzLjU5NjM1IDg2LjI0MjUgMy44NjA2OCA4Ni40MjA5IDQuMDgyMDMgODYuNjU1M0M0LjMwNTk5IDg2Ljg4NyA0LjQ3Nzg2IDg3LjE2ODMgNC41OTc2NiA4Ny40OTlDNC43MTc0NSA4Ny44MjcxIDQuNzc3MzQgODguMTk2OSA0Ljc3NzM0IDg4LjYwODRWODguOTQ4MkM0Ljc3NzM0IDg5LjM1OTcgNC43MTc0NSA4OS43MzA4IDQuNTk3NjYgOTAuMDYxNUM0LjQ3Nzg2IDkwLjM4OTYgNC4zMDQ2OSA5MC42Njk2IDQuMDc4MTIgOTAuOTAxNEMzLjg1NDE3IDkxLjEzMzEgMy41ODMzMyA5MS4zMTE1IDMuMjY1NjIgOTEuNDM2NUMyLjk1MDUyIDkxLjU1ODkgMi41OTUwNSA5MS42MjAxIDIuMTk5MjIgOTEuNjIwMVpNMS40MTQwNiA4NS45MzI2VjkxLjYyMDFIMC42NjAxNTZWODUuOTMyNkgxLjQxNDA2WiIgZmlsbD0iYmxhY2siIGZpbGwtb3BhY2l0eT0iMC41NCIvPgo8cGF0aCBkPSJNNS4yNzM0NCAxMTkuMjE0VjExOS44MjdIMi4yNjE3MlYxMTkuMjE0SDUuMjczNDRaTTIuNDE0MDYgMTE0LjE0VjExOS44MjdIMS42NjAxNlYxMTQuMTRIMi40MTQwNlpNNC44NzUgMTE2LjU4NVYxMTcuMTk4SDIuMjYxNzJWMTE2LjU4NUg0Ljg3NVpNNS4yMzQzOCAxMTQuMTRWMTE0Ljc1N0gyLjI2MTcyVjExNC4xNEg1LjIzNDM4WiIgZmlsbD0iYmxhY2siIGZpbGwtb3BhY2l0eT0iMC41NCIvPgo8cGF0aCBkPSJNMTEgNC4xNjExM0wxODYgNC4xNjExNiIgc3Ryb2tlPSJibGFjayIgc3Ryb2tlLW9wYWNpdHk9IjAuMTIiLz4KPHBhdGggZD0iTTExIDMzLjE2MTFMMTg2IDMzLjE2MTIiIHN0cm9rZT0iYmxhY2siIHN0cm9rZS1vcGFjaXR5PSIwLjEyIi8+CjxwYXRoIGQ9Ik0xMSA2MS4xNjExTDE4NiA2MS4xNjEyIiBzdHJva2U9ImJsYWNrIiBzdHJva2Utb3BhY2l0eT0iMC4xMiIvPgo8cGF0aCBkPSJNMTEgODkuMTYxMUwxODYgODkuMTYxMiIgc3Ryb2tlPSJibGFjayIgc3Ryb2tlLW9wYWNpdHk9IjAuMTIiLz4KPHBhdGggZD0iTTExIDExOC4xNjFMMTg2IDExOC4xNjEiIHN0cm9rZT0iYmxhY2siIHN0cm9rZS1vcGFjaXR5PSIwLjEyIi8+CjxsaW5lIHgxPSI5LjIiIHkxPSIxNDUuOTYxIiB4Mj0iMTg4LjgiIHkyPSIxNDUuOTYxIiBzdHJva2U9ImJsYWNrIiBzdHJva2Utb3BhY2l0eT0iMC43IiBzdHJva2Utd2lkdGg9IjAuNCIgc3Ryb2tlLWxpbmVjYXA9InNxdWFyZSIvPgo8bGluZSB4MT0iMjYuNDUwMiIgeTE9IjE0OC4wNzIiIHgyPSIyNi40NTAyIiB5Mj0iMTQ3LjI1IiBzdHJva2U9ImJsYWNrIiBzdHJva2Utb3BhY2l0eT0iMC41IiBzdHJva2Utd2lkdGg9IjAuNSIgc3Ryb2tlLWxpbmVjYXA9InNxdWFyZSIvPgo8cGF0aCBkPSJNMTguNTA5MSAxNTIuMDI1VjE1Mi44OTNDMTguNTA5MSAxNTMuMzU5IDE4LjQ2NzQgMTUzLjc1MiAxOC4zODQxIDE1NC4wNzJDMTguMzAwNyAxNTQuMzkzIDE4LjE4MDkgMTU0LjY1IDE4LjAyNDcgMTU0Ljg0NkMxNy44Njg0IDE1NS4wNDEgMTcuNjc5NiAxNTUuMTgzIDE3LjQ1ODMgMTU1LjI3MUMxNy4yMzk1IDE1NS4zNTcgMTYuOTkyMSAxNTUuNCAxNi43MTYxIDE1NS40QzE2LjQ5NzMgMTU1LjQgMTYuMjk1NSAxNTUuMzczIDE2LjExMDYgMTU1LjMxOEMxNS45MjU3IDE1NS4yNjQgMTUuNzU5MSAxNTUuMTc2IDE1LjYxMDYgMTU1LjA1N0MxNS40NjQ4IDE1NC45MzQgMTUuMzM5OCAxNTQuNzc1IDE1LjIzNTYgMTU0LjU4QzE1LjEzMTUgMTU0LjM4NSAxNS4wNTIgMTU0LjE0OCAxNC45OTczIDE1My44NjlDMTQuOTQyNyAxNTMuNTkgMTQuOTE1MyAxNTMuMjY1IDE0LjkxNTMgMTUyLjg5M1YxNTIuMDI1QzE0LjkxNTMgMTUxLjU1OSAxNC45NTcgMTUxLjE2OSAxNS4wNDAzIDE1MC44NTRDMTUuMTI2MiAxNTAuNTM4IDE1LjI0NzMgMTUwLjI4NiAxNS40MDM2IDE1MC4wOTZDMTUuNTU5OCAxNDkuOTAzIDE1Ljc0NzMgMTQ5Ljc2NSAxNS45NjYxIDE0OS42ODJDMTYuMTg3NCAxNDkuNTk4IDE2LjQzNDggMTQ5LjU1NyAxNi43MDgzIDE0OS41NTdDMTYuOTI5NiAxNDkuNTU3IDE3LjEzMjggMTQ5LjU4NCAxNy4zMTc3IDE0OS42MzlDMTcuNTA1MiAxNDkuNjkxIDE3LjY3MTggMTQ5Ljc3NSAxNy44MTc3IDE0OS44OTNDMTcuOTYzNSAxNTAuMDA3IDE4LjA4NzIgMTUwLjE2MSAxOC4xODg3IDE1MC4zNTRDMTguMjkyOSAxNTAuNTQ0IDE4LjM3MjMgMTUwLjc3NyAxOC40MjcgMTUxLjA1M0MxOC40ODE3IDE1MS4zMjkgMTguNTA5MSAxNTEuNjUzIDE4LjUwOTEgMTUyLjAyNVpNMTcuNzgyNSAxNTMuMDFWMTUxLjkwNEMxNy43ODI1IDE1MS42NDkgMTcuNzY2OSAxNTEuNDI1IDE3LjczNTYgMTUxLjIzMkMxNy43MDcgMTUxLjAzNyAxNy42NjQgMTUwLjg3IDE3LjYwNjcgMTUwLjczMkMxNy41NDk0IDE1MC41OTQgMTcuNDc2NSAxNTAuNDgyIDE3LjM4OCAxNTAuMzk2QzE3LjMwMiAxNTAuMzExIDE3LjIwMTggMTUwLjI0OCAxNy4wODcyIDE1MC4yMDlDMTYuOTc1MiAxNTAuMTY3IDE2Ljg0ODkgMTUwLjE0NiAxNi43MDgzIDE1MC4xNDZDMTYuNTM2NCAxNTAuMTQ2IDE2LjM4NDEgMTUwLjE3OSAxNi4yNTEyIDE1MC4yNDRDMTYuMTE4NCAxNTAuMzA3IDE2LjAwNjUgMTUwLjQwNyAxNS45MTUzIDE1MC41NDVDMTUuODI2OCAxNTAuNjgzIDE1Ljc1OTEgMTUwLjg2NCAxNS43MTIyIDE1MS4wODhDMTUuNjY1MyAxNTEuMzEyIDE1LjY0MTkgMTUxLjU4NCAxNS42NDE5IDE1MS45MDRWMTUzLjAxQzE1LjY0MTkgMTUzLjI2NSAxNS42NTYyIDE1My40OSAxNS42ODQ4IDE1My42ODZDMTUuNzE2MSAxNTMuODgxIDE1Ljc2MTcgMTU0LjA1IDE1LjgyMTYgMTU0LjE5M0MxNS44ODE1IDE1NC4zMzQgMTUuOTU0NCAxNTQuNDUgMTYuMDQwMyAxNTQuNTQxQzE2LjEyNjIgMTU0LjYzMiAxNi4yMjUyIDE1NC43IDE2LjMzNzIgMTU0Ljc0NEMxNi40NTE4IDE1NC43ODYgMTYuNTc4MSAxNTQuODA3IDE2LjcxNjEgMTU0LjgwN0MxNi44OTMyIDE1NC44MDcgMTcuMDQ4MSAxNTQuNzczIDE3LjE4MDkgMTU0LjcwNUMxNy4zMTM3IDE1NC42MzcgMTcuNDI0NCAxNTQuNTMyIDE3LjUxMyAxNTQuMzg5QzE3LjYwNDEgMTU0LjI0MyAxNy42NzE4IDE1NC4wNTcgMTcuNzE2MSAxNTMuODNDMTcuNzYwNCAxNTMuNjAxIDE3Ljc4MjUgMTUzLjMyNyAxNy43ODI1IDE1My4wMVpNMjEuODk2NCAxNDkuNjA0VjE1NS4zMjJIMjEuMTczN1YxNTAuNTA2TDE5LjcxNjcgMTUxLjAzN1YxNTAuMzg1TDIxLjc4MzEgMTQ5LjYwNEgyMS44OTY0Wk0yNy4xMTI0IDE0OS42MzVWMTU1LjMyMkgyNi4zNTg1VjE0OS42MzVIMjcuMTEyNFpNMjkuNDk1MiAxNTIuMTkzVjE1Mi44MTFIMjYuOTQ4M1YxNTIuMTkzSDI5LjQ5NTJaTTI5Ljg4MTkgMTQ5LjYzNVYxNTAuMjUySDI2Ljk0ODNWMTQ5LjYzNUgyOS44ODE5Wk0zMi40MjE2IDE1NS40QzMyLjEyNzMgMTU1LjQgMzEuODYwNCAxNTUuMzUxIDMxLjYyMDggMTU1LjI1MkMzMS4zODM4IDE1NS4xNSAzMS4xNzk0IDE1NS4wMDggMzEuMDA3NSAxNTQuODI2QzMwLjgzODMgMTU0LjY0NCAzMC43MDgxIDE1NC40MjggMzAuNjE2OSAxNTQuMTc4QzMwLjUyNTggMTUzLjkyOCAzMC40ODAyIDE1My42NTQgMzAuNDgwMiAxNTMuMzU3VjE1My4xOTNDMzAuNDgwMiAxNTIuODUgMzAuNTMxIDE1Mi41NDQgMzAuNjMyNSAxNTIuMjc1QzMwLjczNDEgMTUyLjAwNSAzMC44NzIxIDE1MS43NzUgMzEuMDQ2NiAxNTEuNTg4QzMxLjIyMTEgMTUxLjQgMzEuNDE5IDE1MS4yNTggMzEuNjQwMyAxNTEuMTYyQzMxLjg2MTcgMTUxLjA2NiAzMi4wOTA5IDE1MS4wMTggMzIuMzI3OCAxNTEuMDE4QzMyLjYyOTkgMTUxLjAxOCAzMi44OTAzIDE1MS4wNyAzMy4xMDkxIDE1MS4xNzRDMzMuMzMwNSAxNTEuMjc4IDMzLjUxMTQgMTUxLjQyNCAzMy42NTIxIDE1MS42MTFDMzMuNzkyNyAxNTEuNzk2IDMzLjg5NjkgMTUyLjAxNSAzMy45NjQ2IDE1Mi4yNjhDMzQuMDMyMyAxNTIuNTE4IDM0LjA2NjEgMTUyLjc5MSAzNC4wNjYxIDE1My4wODhWMTUzLjQxMkgzMC45MDk5VjE1Mi44MjJIMzMuMzQzNVYxNTIuNzY4QzMzLjMzMzEgMTUyLjU4IDMzLjI5NCAxNTIuMzk4IDMzLjIyNjMgMTUyLjIyMUMzMy4xNjEyIDE1Mi4wNDQgMzMuMDU3IDE1MS44OTggMzIuOTEzOCAxNTEuNzgzQzMyLjc3MDYgMTUxLjY2OSAzMi41NzUyIDE1MS42MTEgMzIuMzI3OCAxNTEuNjExQzMyLjE2MzggMTUxLjYxMSAzMi4wMTI3IDE1MS42NDYgMzEuODc0NyAxNTEuNzE3QzMxLjczNjcgMTUxLjc4NSAzMS42MTgyIDE1MS44ODYgMzEuNTE5MyAxNTIuMDIxQzMxLjQyMDMgMTUyLjE1NyAzMS4zNDM1IDE1Mi4zMjIgMzEuMjg4OCAxNTIuNTE4QzMxLjIzNDEgMTUyLjcxMyAzMS4yMDY4IDE1Mi45MzggMzEuMjA2OCAxNTMuMTkzVjE1My4zNTdDMzEuMjA2OCAxNTMuNTU4IDMxLjIzNDEgMTUzLjc0NyAzMS4yODg4IDE1My45MjRDMzEuMzQ2MSAxNTQuMDk4IDMxLjQyODEgMTU0LjI1MiAzMS41MzQ5IDE1NC4zODVDMzEuNjQ0MyAxNTQuNTE4IDMxLjc3NTggMTU0LjYyMiAzMS45Mjk0IDE1NC42OTdDMzIuMDg1NyAxNTQuNzczIDMyLjI2MjcgMTU0LjgxMSAzMi40NjA3IDE1NC44MTFDMzIuNzE1OSAxNTQuODExIDMyLjkzMiAxNTQuNzU4IDMzLjEwOTEgMTU0LjY1NEMzMy4yODYyIDE1NC41NSAzMy40NDExIDE1NC40MTEgMzMuNTczOSAxNTQuMjM2TDM0LjAxMTQgMTU0LjU4NEMzMy45MjAzIDE1NC43MjIgMzMuODA0NCAxNTQuODU0IDMzLjY2MzggMTU0Ljk3OUMzMy41MjMyIDE1NS4xMDQgMzMuMzUgMTU1LjIwNSAzMy4xNDQzIDE1NS4yODNDMzIuOTQxMSAxNTUuMzYxIDMyLjcwMDIgMTU1LjQgMzIuNDIxNiAxNTUuNFpNMzQuOTg4NiAxNDkuMzIySDM1LjcxNTJWMTU0LjUwMkwzNS42NTI3IDE1NS4zMjJIMzQuOTg4NlYxNDkuMzIyWk0zOC41NzA2IDE1My4xNzRWMTUzLjI1NkMzOC41NzA2IDE1My41NjMgMzguNTM0MiAxNTMuODQ4IDM4LjQ2MTMgMTU0LjExMUMzOC4zODgzIDE1NC4zNzIgMzguMjgxNiAxNTQuNTk4IDM4LjE0MDkgMTU0Ljc5MUMzOC4wMDAzIDE1NC45ODQgMzcuODI4NCAxNTUuMTMzIDM3LjYyNTMgMTU1LjI0QzM3LjQyMjIgMTU1LjM0NyAzNy4xODkxIDE1NS40IDM2LjkyNjEgMTU1LjRDMzYuNjU3OSAxNTUuNCAzNi40MjIyIDE1NS4zNTUgMzYuMjE5MSAxNTUuMjY0QzM2LjAxODUgMTU1LjE3IDM1Ljg0OTMgMTU1LjAzNiAzNS43MTEzIDE1NC44NjFDMzUuNTczMiAxNTQuNjg3IDM1LjQ2MjYgMTU0LjQ3NiAzNS4zNzkyIDE1NC4yMjlDMzUuMjk4NSAxNTMuOTgxIDM1LjI0MjUgMTUzLjcwMiAzNS4yMTEzIDE1My4zOTNWMTUzLjAzM0MzNS4yNDI1IDE1Mi43MjEgMzUuMjk4NSAxNTIuNDQxIDM1LjM3OTIgMTUyLjE5M0MzNS40NjI2IDE1MS45NDYgMzUuNTczMiAxNTEuNzM1IDM1LjcxMTMgMTUxLjU2MUMzNS44NDkzIDE1MS4zODMgMzYuMDE4NSAxNTEuMjQ5IDM2LjIxOTEgMTUxLjE1OEMzNi40MTk2IDE1MS4wNjQgMzYuNjUyNyAxNTEuMDE4IDM2LjkxODMgMTUxLjAxOEMzNy4xODM5IDE1MS4wMTggMzcuNDE5NiAxNTEuMDcgMzcuNjI1MyAxNTEuMTc0QzM3LjgzMSAxNTEuMjc1IDM4LjAwMjkgMTUxLjQyMSAzOC4xNDA5IDE1MS42MTFDMzguMjgxNiAxNTEuODAxIDM4LjM4ODMgMTUyLjAyOSAzOC40NjEzIDE1Mi4yOTVDMzguNTM0MiAxNTIuNTU4IDM4LjU3MDYgMTUyLjg1MSAzOC41NzA2IDE1My4xNzRaTTM3Ljg0NDEgMTUzLjI1NlYxNTMuMTc0QzM3Ljg0NDEgMTUyLjk2MyAzNy44MjQ1IDE1Mi43NjUgMzcuNzg1NSAxNTIuNThDMzcuNzQ2NCAxNTIuMzkzIDM3LjY4MzkgMTUyLjIyOSAzNy41OTggMTUyLjA4OEMzNy41MTIgMTUxLjk0NSAzNy4zOTg4IDE1MS44MzMgMzcuMjU4MSAxNTEuNzUyQzM3LjExNzUgMTUxLjY2OSAzNi45NDQzIDE1MS42MjcgMzYuNzM4NiAxNTEuNjI3QzM2LjU1NjMgMTUxLjYyNyAzNi4zOTc1IDE1MS42NTggMzYuMjYyIDE1MS43MjFDMzYuMTI5MiAxNTEuNzgzIDM2LjAxNTkgMTUxLjg2OCAzNS45MjIyIDE1MS45NzVDMzUuODI4NCAxNTIuMDc5IDM1Ljc1MTYgMTUyLjE5OSAzNS42OTE3IDE1Mi4zMzRDMzUuNjM0NCAxNTIuNDY3IDM1LjU5MTUgMTUyLjYwNSAzNS41NjI4IDE1Mi43NDhWMTUzLjY4OUMzNS42MDQ1IDE1My44NzIgMzUuNjcyMiAxNTQuMDQ4IDM1Ljc2NTkgMTU0LjIxN0MzNS44NjIzIDE1NC4zODMgMzUuOTg5OSAxNTQuNTIgMzYuMTQ4OCAxNTQuNjI3QzM2LjMxMDIgMTU0LjczNCAzNi41MDk0IDE1NC43ODcgMzYuNzQ2NCAxNTQuNzg3QzM2Ljk0MTcgMTU0Ljc4NyAzNy4xMDg0IDE1NC43NDggMzcuMjQ2NCAxNTQuNjdDMzcuMzg3IDE1NC41ODkgMzcuNTAwMyAxNTQuNDc5IDM3LjU4NjMgMTU0LjMzOEMzNy42NzQ4IDE1NC4xOTcgMzcuNzM5OSAxNTQuMDM1IDM3Ljc4MTYgMTUzLjg1QzM3LjgyMzIgMTUzLjY2NSAzNy44NDQxIDE1My40NjcgMzcuODQ0MSAxNTMuMjU2WiIgZmlsbD0iYmxhY2siIGZpbGwtb3BhY2l0eT0iMC41NCIvPgo8ZyBjbGlwLXBhdGg9InVybCgjY2xpcDFfNDQzN185Njg1OCkiPgo8bGluZSB4MT0iNjEuODUwMSIgeTE9IjE0Ny4wNzIiIHgyPSI2MS44NTAxIiB5Mj0iMTQ2LjI1IiBzdHJva2U9ImJsYWNrIiBzdHJva2Utb3BhY2l0eT0iMC41IiBzdHJva2Utd2lkdGg9IjAuNSIgc3Ryb2tlLWxpbmVjYXA9InNxdWFyZSIvPgo8cGF0aCBkPSJNNTMuOTA5IDE1Mi4wMjVWMTUyLjg5M0M1My45MDkgMTUzLjM1OSA1My44NjczIDE1My43NTIgNTMuNzg0IDE1NC4wNzJDNTMuNzAwNiAxNTQuMzkzIDUzLjU4MDggMTU0LjY1IDUzLjQyNDYgMTU0Ljg0NkM1My4yNjgzIDE1NS4wNDEgNTMuMDc5NSAxNTUuMTgzIDUyLjg1ODIgMTU1LjI3MUM1Mi42Mzk0IDE1NS4zNTcgNTIuMzkyIDE1NS40IDUyLjExNiAxNTUuNEM1MS44OTcyIDE1NS40IDUxLjY5NTQgMTU1LjM3MyA1MS41MTA1IDE1NS4zMThDNTEuMzI1NiAxNTUuMjY0IDUxLjE1OSAxNTUuMTc2IDUxLjAxMDUgMTU1LjA1N0M1MC44NjQ3IDE1NC45MzQgNTAuNzM5NyAxNTQuNzc1IDUwLjYzNTUgMTU0LjU4QzUwLjUzMTQgMTU0LjM4NSA1MC40NTE5IDE1NC4xNDggNTAuMzk3MiAxNTMuODY5QzUwLjM0MjYgMTUzLjU5IDUwLjMxNTIgMTUzLjI2NSA1MC4zMTUyIDE1Mi44OTNWMTUyLjAyNUM1MC4zMTUyIDE1MS41NTkgNTAuMzU2OSAxNTEuMTY5IDUwLjQ0MDIgMTUwLjg1NEM1MC41MjYxIDE1MC41MzggNTAuNjQ3MiAxNTAuMjg2IDUwLjgwMzUgMTUwLjA5NkM1MC45NTk3IDE0OS45MDMgNTEuMTQ3MiAxNDkuNzY1IDUxLjM2NiAxNDkuNjgyQzUxLjU4NzMgMTQ5LjU5OCA1MS44MzQ3IDE0OS41NTcgNTIuMTA4MiAxNDkuNTU3QzUyLjMyOTUgMTQ5LjU1NyA1Mi41MzI3IDE0OS41ODQgNTIuNzE3NiAxNDkuNjM5QzUyLjkwNTEgMTQ5LjY5MSA1My4wNzE3IDE0OS43NzUgNTMuMjE3NiAxNDkuODkzQzUzLjM2MzQgMTUwLjAwNyA1My40ODcxIDE1MC4xNjEgNTMuNTg4NiAxNTAuMzU0QzUzLjY5MjggMTUwLjU0NCA1My43NzIyIDE1MC43NzcgNTMuODI2OSAxNTEuMDUzQzUzLjg4MTYgMTUxLjMyOSA1My45MDkgMTUxLjY1MyA1My45MDkgMTUyLjAyNVpNNTMuMTgyNCAxNTMuMDFWMTUxLjkwNEM1My4xODI0IDE1MS42NDkgNTMuMTY2OCAxNTEuNDI1IDUzLjEzNTUgMTUxLjIzMkM1My4xMDY5IDE1MS4wMzcgNTMuMDYzOSAxNTAuODcgNTMuMDA2NiAxNTAuNzMyQzUyLjk0OTMgMTUwLjU5NCA1Mi44NzY0IDE1MC40ODIgNTIuNzg3OSAxNTAuMzk2QzUyLjcwMTkgMTUwLjMxMSA1Mi42MDE3IDE1MC4yNDggNTIuNDg3MSAxNTAuMjA5QzUyLjM3NTEgMTUwLjE2NyA1Mi4yNDg4IDE1MC4xNDYgNTIuMTA4MiAxNTAuMTQ2QzUxLjkzNjMgMTUwLjE0NiA1MS43ODQgMTUwLjE3OSA1MS42NTExIDE1MC4yNDRDNTEuNTE4MyAxNTAuMzA3IDUxLjQwNjQgMTUwLjQwNyA1MS4zMTUyIDE1MC41NDVDNTEuMjI2NyAxNTAuNjgzIDUxLjE1OSAxNTAuODY0IDUxLjExMjEgMTUxLjA4OEM1MS4wNjUyIDE1MS4zMTIgNTEuMDQxOCAxNTEuNTg0IDUxLjA0MTggMTUxLjkwNFYxNTMuMDFDNTEuMDQxOCAxNTMuMjY1IDUxLjA1NjEgMTUzLjQ5IDUxLjA4NDcgMTUzLjY4NkM1MS4xMTYgMTUzLjg4MSA1MS4xNjE2IDE1NC4wNSA1MS4yMjE1IDE1NC4xOTNDNTEuMjgxNCAxNTQuMzM0IDUxLjM1NDMgMTU0LjQ1IDUxLjQ0MDIgMTU0LjU0MUM1MS41MjYxIDE1NC42MzIgNTEuNjI1MSAxNTQuNyA1MS43MzcxIDE1NC43NDRDNTEuODUxNyAxNTQuNzg2IDUxLjk3OCAxNTQuODA3IDUyLjExNiAxNTQuODA3QzUyLjI5MzEgMTU0LjgwNyA1Mi40NDggMTU0Ljc3MyA1Mi41ODA4IDE1NC43MDVDNTIuNzEzNiAxNTQuNjM3IDUyLjgyNDMgMTU0LjUzMiA1Mi45MTI5IDE1NC4zODlDNTMuMDA0IDE1NC4yNDMgNTMuMDcxNyAxNTQuMDU3IDUzLjExNiAxNTMuODNDNTMuMTYwMyAxNTMuNjAxIDUzLjE4MjQgMTUzLjMyNyA1My4xODI0IDE1My4wMVpNNTguNjQ3OCAxNTQuNzI5VjE1NS4zMjJINTQuOTI1MlYxNTQuODAzTDU2Ljc4ODUgMTUyLjcyOUM1Ny4wMTc2IDE1Mi40NzMgNTcuMTk0NyAxNTIuMjU3IDU3LjMxOTcgMTUyLjA4QzU3LjQ0NzMgMTUxLjkgNTcuNTM1OSAxNTEuNzQgNTcuNTg1MyAxNTEuNkM1Ny42Mzc0IDE1MS40NTYgNTcuNjYzNSAxNTEuMzExIDU3LjY2MzUgMTUxLjE2MkM1Ny42NjM1IDE1MC45NzUgNTcuNjI0NCAxNTAuODA1IDU3LjU0NjMgMTUwLjY1NEM1Ny40NzA4IDE1MC41MDEgNTcuMzU4OCAxNTAuMzc4IDU3LjIxMDMgMTUwLjI4N0M1Ny4wNjE5IDE1MC4xOTYgNTYuODgyMiAxNTAuMTUgNTYuNjcxMyAxNTAuMTVDNTYuNDE4NyAxNTAuMTUgNTYuMjA3NyAxNTAuMiA1Ni4wMzg1IDE1MC4yOTlDNTUuODcxOCAxNTAuMzk1IDU1Ljc0NjggMTUwLjUzMSA1NS42NjM1IDE1MC43MDVDNTUuNTgwMSAxNTAuODggNTUuNTM4NSAxNTEuMDggNTUuNTM4NSAxNTEuMzA3SDU0LjgxNThDNTQuODE1OCAxNTAuOTg2IDU0Ljg4NjEgMTUwLjY5MyA1NS4wMjY3IDE1MC40MjhDNTUuMTY3NCAxNTAuMTYyIDU1LjM3NTcgMTQ5Ljk1MSA1NS42NTE3IDE0OS43OTVDNTUuOTI3OCAxNDkuNjM2IDU2LjI2NzYgMTQ5LjU1NyA1Ni42NzEzIDE0OS41NTdDNTcuMDMwNyAxNDkuNTU3IDU3LjMzNzkgMTQ5LjYyIDU3LjU5MzIgMTQ5Ljc0OEM1Ny44NDg0IDE0OS44NzMgNTguMDQzNyAxNTAuMDUgNTguMTc5MSAxNTAuMjc5QzU4LjMxNzEgMTUwLjUwNiA1OC4zODYxIDE1MC43NzEgNTguMzg2MSAxNTEuMDc2QzU4LjM4NjEgMTUxLjI0MyA1OC4zNTc1IDE1MS40MTIgNTguMzAwMiAxNTEuNTg0QzU4LjI0NTUgMTUxLjc1MyA1OC4xNjg3IDE1MS45MjMgNTguMDY5NyAxNTIuMDkyQzU3Ljk3MzQgMTUyLjI2MSA1Ny44NjAxIDE1Mi40MjggNTcuNzI5OSAxNTIuNTkyQzU3LjYwMjMgMTUyLjc1NiA1Ny40NjU1IDE1Mi45MTcgNTcuMzE5NyAxNTMuMDc2TDU1Ljc5NjMgMTU0LjcyOUg1OC42NDc4Wk02Mi41MTIzIDE0OS42MzVWMTU1LjMyMkg2MS43NTg0VjE0OS42MzVINjIuNTEyM1pNNjQuODk1MSAxNTIuMTkzVjE1Mi44MTFINjIuMzQ4MlYxNTIuMTkzSDY0Ljg5NTFaTTY1LjI4MTggMTQ5LjYzNVYxNTAuMjUySDYyLjM0ODJWMTQ5LjYzNUg2NS4yODE4Wk02Ny44MjE1IDE1NS40QzY3LjUyNzIgMTU1LjQgNjcuMjYwMyAxNTUuMzUxIDY3LjAyMDcgMTU1LjI1MkM2Ni43ODM3IDE1NS4xNSA2Ni41NzkzIDE1NS4wMDggNjYuNDA3NCAxNTQuODI2QzY2LjIzODIgMTU0LjY0NCA2Ni4xMDggMTU0LjQyOCA2Ni4wMTY4IDE1NC4xNzhDNjUuOTI1NyAxNTMuOTI4IDY1Ljg4MDEgMTUzLjY1NCA2NS44ODAxIDE1My4zNTdWMTUzLjE5M0M2NS44ODAxIDE1Mi44NSA2NS45MzA5IDE1Mi41NDQgNjYuMDMyNCAxNTIuMjc1QzY2LjEzNCAxNTIuMDA1IDY2LjI3MiAxNTEuNzc1IDY2LjQ0NjUgMTUxLjU4OEM2Ni42MjEgMTUxLjQgNjYuODE4OSAxNTEuMjU4IDY3LjA0MDIgMTUxLjE2MkM2Ny4yNjE2IDE1MS4wNjYgNjcuNDkwOCAxNTEuMDE4IDY3LjcyNzcgMTUxLjAxOEM2OC4wMjk4IDE1MS4wMTggNjguMjkwMiAxNTEuMDcgNjguNTA5IDE1MS4xNzRDNjguNzMwNCAxNTEuMjc4IDY4LjkxMTMgMTUxLjQyNCA2OS4wNTIgMTUxLjYxMUM2OS4xOTI2IDE1MS43OTYgNjkuMjk2OCAxNTIuMDE1IDY5LjM2NDUgMTUyLjI2OEM2OS40MzIyIDE1Mi41MTggNjkuNDY2IDE1Mi43OTEgNjkuNDY2IDE1My4wODhWMTUzLjQxMkg2Ni4zMDk4VjE1Mi44MjJINjguNzQzNFYxNTIuNzY4QzY4LjczMyAxNTIuNTggNjguNjkzOSAxNTIuMzk4IDY4LjYyNjIgMTUyLjIyMUM2OC41NjExIDE1Mi4wNDQgNjguNDU2OSAxNTEuODk4IDY4LjMxMzcgMTUxLjc4M0M2OC4xNzA1IDE1MS42NjkgNjcuOTc1MSAxNTEuNjExIDY3LjcyNzcgMTUxLjYxMUM2Ny41NjM3IDE1MS42MTEgNjcuNDEyNiAxNTEuNjQ2IDY3LjI3NDYgMTUxLjcxN0M2Ny4xMzY2IDE1MS43ODUgNjcuMDE4MSAxNTEuODg2IDY2LjkxOTIgMTUyLjAyMUM2Ni44MjAyIDE1Mi4xNTcgNjYuNzQzNCAxNTIuMzIyIDY2LjY4ODcgMTUyLjUxOEM2Ni42MzQgMTUyLjcxMyA2Ni42MDY3IDE1Mi45MzggNjYuNjA2NyAxNTMuMTkzVjE1My4zNTdDNjYuNjA2NyAxNTMuNTU4IDY2LjYzNCAxNTMuNzQ3IDY2LjY4ODcgMTUzLjkyNEM2Ni43NDYgMTU0LjA5OCA2Ni44MjggMTU0LjI1MiA2Ni45MzQ4IDE1NC4zODVDNjcuMDQ0MiAxNTQuNTE4IDY3LjE3NTcgMTU0LjYyMiA2Ny4zMjkzIDE1NC42OTdDNjcuNDg1NiAxNTQuNzczIDY3LjY2MjYgMTU0LjgxMSA2Ny44NjA2IDE1NC44MTFDNjguMTE1OCAxNTQuODExIDY4LjMzMTkgMTU0Ljc1OCA2OC41MDkgMTU0LjY1NEM2OC42ODYxIDE1NC41NSA2OC44NDEgMTU0LjQxMSA2OC45NzM4IDE1NC4yMzZMNjkuNDExMyAxNTQuNTg0QzY5LjMyMDIgMTU0LjcyMiA2OS4yMDQzIDE1NC44NTQgNjkuMDYzNyAxNTQuOTc5QzY4LjkyMzEgMTU1LjEwNCA2OC43NDk5IDE1NS4yMDUgNjguNTQ0MiAxNTUuMjgzQzY4LjM0MSAxNTUuMzYxIDY4LjEwMDEgMTU1LjQgNjcuODIxNSAxNTUuNFpNNzAuMzg4NSAxNDkuMzIySDcxLjExNTFWMTU0LjUwMkw3MS4wNTI2IDE1NS4zMjJINzAuMzg4NVYxNDkuMzIyWk03My45NzA1IDE1My4xNzRWMTUzLjI1NkM3My45NzA1IDE1My41NjMgNzMuOTM0MSAxNTMuODQ4IDczLjg2MTIgMTU0LjExMUM3My43ODgyIDE1NC4zNzIgNzMuNjgxNSAxNTQuNTk4IDczLjU0MDggMTU0Ljc5MUM3My40MDAyIDE1NC45ODQgNzMuMjI4MyAxNTUuMTMzIDczLjAyNTIgMTU1LjI0QzcyLjgyMjEgMTU1LjM0NyA3Mi41ODkgMTU1LjQgNzIuMzI2IDE1NS40QzcyLjA1NzggMTU1LjQgNzEuODIyMSAxNTUuMzU1IDcxLjYxOSAxNTUuMjY0QzcxLjQxODUgMTU1LjE3IDcxLjI0OTIgMTU1LjAzNiA3MS4xMTEyIDE1NC44NjFDNzAuOTczMSAxNTQuNjg3IDcwLjg2MjUgMTU0LjQ3NiA3MC43NzkxIDE1NC4yMjlDNzAuNjk4NCAxNTMuOTgxIDcwLjY0MjQgMTUzLjcwMiA3MC42MTEyIDE1My4zOTNWMTUzLjAzM0M3MC42NDI0IDE1Mi43MjEgNzAuNjk4NCAxNTIuNDQxIDcwLjc3OTEgMTUyLjE5M0M3MC44NjI1IDE1MS45NDYgNzAuOTczMSAxNTEuNzM1IDcxLjExMTIgMTUxLjU2MUM3MS4yNDkyIDE1MS4zODMgNzEuNDE4NSAxNTEuMjQ5IDcxLjYxOSAxNTEuMTU4QzcxLjgxOTUgMTUxLjA2NCA3Mi4wNTI2IDE1MS4wMTggNzIuMzE4MiAxNTEuMDE4QzcyLjU4MzggMTUxLjAxOCA3Mi44MTk1IDE1MS4wNyA3My4wMjUyIDE1MS4xNzRDNzMuMjMxIDE1MS4yNzUgNzMuNDAyOCAxNTEuNDIxIDczLjU0MDggMTUxLjYxMUM3My42ODE1IDE1MS44MDEgNzMuNzg4MiAxNTIuMDI5IDczLjg2MTIgMTUyLjI5NUM3My45MzQxIDE1Mi41NTggNzMuOTcwNSAxNTIuODUxIDczLjk3MDUgMTUzLjE3NFpNNzMuMjQ0IDE1My4yNTZWMTUzLjE3NEM3My4yNDQgMTUyLjk2MyA3My4yMjQ0IDE1Mi43NjUgNzMuMTg1NCAxNTIuNThDNzMuMTQ2MyAxNTIuMzkzIDczLjA4MzggMTUyLjIyOSA3Mi45OTc5IDE1Mi4wODhDNzIuOTExOSAxNTEuOTQ1IDcyLjc5ODcgMTUxLjgzMyA3Mi42NTggMTUxLjc1MkM3Mi41MTc0IDE1MS42NjkgNzIuMzQ0MiAxNTEuNjI3IDcyLjEzODUgMTUxLjYyN0M3MS45NTYyIDE1MS42MjcgNzEuNzk3NCAxNTEuNjU4IDcxLjY2MTkgMTUxLjcyMUM3MS41MjkxIDE1MS43ODMgNzEuNDE1OCAxNTEuODY4IDcxLjMyMjEgMTUxLjk3NUM3MS4yMjgzIDE1Mi4wNzkgNzEuMTUxNSAxNTIuMTk5IDcxLjA5MTYgMTUyLjMzNEM3MS4wMzQzIDE1Mi40NjcgNzAuOTkxNCAxNTIuNjA1IDcwLjk2MjcgMTUyLjc0OFYxNTMuNjg5QzcxLjAwNDQgMTUzLjg3MiA3MS4wNzIxIDE1NC4wNDggNzEuMTY1OCAxNTQuMjE3QzcxLjI2MjIgMTU0LjM4MyA3MS4zODk4IDE1NC41MiA3MS41NDg3IDE1NC42MjdDNzEuNzEwMSAxNTQuNzM0IDcxLjkwOTMgMTU0Ljc4NyA3Mi4xNDYzIDE1NC43ODdDNzIuMzQxNiAxNTQuNzg3IDcyLjUwODMgMTU0Ljc0OCA3Mi42NDYzIDE1NC42N0M3Mi43ODY5IDE1NC41ODkgNzIuOTAwMiAxNTQuNDc5IDcyLjk4NjIgMTU0LjMzOEM3My4wNzQ3IDE1NC4xOTcgNzMuMTM5OCAxNTQuMDM1IDczLjE4MTUgMTUzLjg1QzczLjIyMzEgMTUzLjY2NSA3My4yNDQgMTUzLjQ2NyA3My4yNDQgMTUzLjI1NloiIGZpbGw9ImJsYWNrIiBmaWxsLW9wYWNpdHk9IjAuNTQiLz4KPC9nPgo8ZyBjbGlwLXBhdGg9InVybCgjY2xpcDJfNDQzN185Njg1OCkiPgo8bGluZSB4MT0iOTcuMjUiIHkxPSIxNDcuMDcyIiB4Mj0iOTcuMjUiIHkyPSIxNDYuMjUiIHN0cm9rZT0iYmxhY2siIHN0cm9rZS1vcGFjaXR5PSIwLjUiIHN0cm9rZS13aWR0aD0iMC41IiBzdHJva2UtbGluZWNhcD0ic3F1YXJlIi8+CjxwYXRoIGQ9Ik04OS4zMDg5IDE1Mi4wMjVWMTUyLjg5M0M4OS4zMDg5IDE1My4zNTkgODkuMjY3MiAxNTMuNzUyIDg5LjE4MzkgMTU0LjA3MkM4OS4xMDA1IDE1NC4zOTMgODguOTgwNyAxNTQuNjUgODguODI0NSAxNTQuODQ2Qzg4LjY2ODIgMTU1LjA0MSA4OC40Nzk0IDE1NS4xODMgODguMjU4MSAxNTUuMjcxQzg4LjAzOTMgMTU1LjM1NyA4Ny43OTE5IDE1NS40IDg3LjUxNTkgMTU1LjRDODcuMjk3MSAxNTUuNCA4Ny4wOTUzIDE1NS4zNzMgODYuOTEwNCAxNTUuMzE4Qzg2LjcyNTUgMTU1LjI2NCA4Ni41NTg5IDE1NS4xNzYgODYuNDEwNCAxNTUuMDU3Qzg2LjI2NDYgMTU0LjkzNCA4Ni4xMzk2IDE1NC43NzUgODYuMDM1NCAxNTQuNThDODUuOTMxMyAxNTQuMzg1IDg1Ljg1MTggMTU0LjE0OCA4NS43OTcxIDE1My44NjlDODUuNzQyNSAxNTMuNTkgODUuNzE1MSAxNTMuMjY1IDg1LjcxNTEgMTUyLjg5M1YxNTIuMDI1Qzg1LjcxNTEgMTUxLjU1OSA4NS43NTY4IDE1MS4xNjkgODUuODQwMSAxNTAuODU0Qzg1LjkyNjEgMTUwLjUzOCA4Ni4wNDcxIDE1MC4yODYgODYuMjAzNCAxNTAuMDk2Qzg2LjM1OTYgMTQ5LjkwMyA4Ni41NDcxIDE0OS43NjUgODYuNzY1OSAxNDkuNjgyQzg2Ljk4NzIgMTQ5LjU5OCA4Ny4yMzQ2IDE0OS41NTcgODcuNTA4MSAxNDkuNTU3Qzg3LjcyOTQgMTQ5LjU1NyA4Ny45MzI2IDE0OS41ODQgODguMTE3NSAxNDkuNjM5Qzg4LjMwNSAxNDkuNjkxIDg4LjQ3MTYgMTQ5Ljc3NSA4OC42MTc1IDE0OS44OTNDODguNzYzMyAxNTAuMDA3IDg4Ljg4NyAxNTAuMTYxIDg4Ljk4ODYgMTUwLjM1NEM4OS4wOTI3IDE1MC41NDQgODkuMTcyMSAxNTAuNzc3IDg5LjIyNjggMTUxLjA1M0M4OS4yODE1IDE1MS4zMjkgODkuMzA4OSAxNTEuNjUzIDg5LjMwODkgMTUyLjAyNVpNODguNTgyMyAxNTMuMDFWMTUxLjkwNEM4OC41ODIzIDE1MS42NDkgODguNTY2NyAxNTEuNDI1IDg4LjUzNTQgMTUxLjIzMkM4OC41MDY4IDE1MS4wMzcgODguNDYzOCAxNTAuODcgODguNDA2NSAxNTAuNzMyQzg4LjM0OTIgMTUwLjU5NCA4OC4yNzYzIDE1MC40ODIgODguMTg3OCAxNTAuMzk2Qzg4LjEwMTggMTUwLjMxMSA4OC4wMDE2IDE1MC4yNDggODcuODg3IDE1MC4yMDlDODcuNzc1IDE1MC4xNjcgODcuNjQ4NyAxNTAuMTQ2IDg3LjUwODEgMTUwLjE0NkM4Ny4zMzYyIDE1MC4xNDYgODcuMTgzOSAxNTAuMTc5IDg3LjA1MTEgMTUwLjI0NEM4Ni45MTgyIDE1MC4zMDcgODYuODA2MyAxNTAuNDA3IDg2LjcxNTEgMTUwLjU0NUM4Ni42MjY2IDE1MC42ODMgODYuNTU4OSAxNTAuODY0IDg2LjUxMiAxNTEuMDg4Qzg2LjQ2NTEgMTUxLjMxMiA4Ni40NDE3IDE1MS41ODQgODYuNDQxNyAxNTEuOTA0VjE1My4wMUM4Ni40NDE3IDE1My4yNjUgODYuNDU2IDE1My40OSA4Ni40ODQ2IDE1My42ODZDODYuNTE1OSAxNTMuODgxIDg2LjU2MTUgMTU0LjA1IDg2LjYyMTQgMTU0LjE5M0M4Ni42ODEzIDE1NC4zMzQgODYuNzU0MiAxNTQuNDUgODYuODQwMSAxNTQuNTQxQzg2LjkyNjEgMTU0LjYzMiA4Ny4wMjUgMTU0LjcgODcuMTM3IDE1NC43NDRDODcuMjUxNiAxNTQuNzg2IDg3LjM3NzkgMTU0LjgwNyA4Ny41MTU5IDE1NC44MDdDODcuNjkzIDE1NC44MDcgODcuODQ3OSAxNTQuNzczIDg3Ljk4MDcgMTU0LjcwNUM4OC4xMTM2IDE1NC42MzcgODguMjI0MiAxNTQuNTMyIDg4LjMxMjggMTU0LjM4OUM4OC40MDM5IDE1NC4yNDMgODguNDcxNiAxNTQuMDU3IDg4LjUxNTkgMTUzLjgzQzg4LjU2MDIgMTUzLjYwMSA4OC41ODIzIDE1My4zMjcgODguNTgyMyAxNTMuMDFaTTkxLjM3NTkgMTUyLjEyM0g5MS44OTE1QzkyLjE0NDEgMTUyLjEyMyA5Mi4zNTI0IDE1Mi4wODEgOTIuNTE2NSAxNTEuOTk4QzkyLjY4MzIgMTUxLjkxMiA5Mi44MDY5IDE1MS43OTYgOTIuODg3NiAxNTEuNjVDOTIuOTcwOSAxNTEuNTAyIDkzLjAxMjYgMTUxLjMzNSA5My4wMTI2IDE1MS4xNUM5My4wMTI2IDE1MC45MzIgOTIuOTc2MSAxNTAuNzQ4IDkyLjkwMzIgMTUwLjZDOTIuODMwMyAxNTAuNDUxIDkyLjcyMDkgMTUwLjMzOSA5Mi41NzUxIDE1MC4yNjRDOTIuNDI5MyAxNTAuMTg4IDkyLjI0NDQgMTUwLjE1IDkyLjAyMDQgMTUwLjE1QzkxLjgxNzMgMTUwLjE1IDkxLjYzNzYgMTUwLjE5MSA5MS40ODEzIDE1MC4yNzFDOTEuMzI3NyAxNTAuMzUgOTEuMjA2NiAxNTAuNDYyIDkxLjExODEgMTUwLjYwN0M5MS4wMzIxIDE1MC43NTMgOTAuOTg5MSAxNTAuOTI1IDkwLjk4OTEgMTUxLjEyM0g5MC4yNjY1QzkwLjI2NjUgMTUwLjgzNCA5MC4zMzk0IDE1MC41NzEgOTAuNDg1MiAxNTAuMzM0QzkwLjYzMTEgMTUwLjA5NyA5MC44MzU1IDE0OS45MDggOTEuMDk4NSAxNDkuNzY4QzkxLjM2NDEgMTQ5LjYyNyA5MS42NzE0IDE0OS41NTcgOTIuMDIwNCAxNDkuNTU3QzkyLjM2NDEgMTQ5LjU1NyA5Mi42NjQ5IDE0OS42MTggOTIuOTIyNyAxNDkuNzRDOTMuMTgwNiAxNDkuODYgOTMuMzgxMSAxNTAuMDQgOTMuNTI0MyAxNTAuMjc5QzkzLjY2NzUgMTUwLjUxNiA5My43MzkxIDE1MC44MTIgOTMuNzM5MSAxNTEuMTY2QzkzLjczOTEgMTUxLjMwOSA5My43MDUzIDE1MS40NjMgOTMuNjM3NiAxNTEuNjI3QzkzLjU3MjUgMTUxLjc4OCA5My40Njk2IDE1MS45MzkgOTMuMzI5IDE1Mi4wOEM5My4xOTEgMTUyLjIyMSA5My4wMTEzIDE1Mi4zMzcgOTIuNzg5OSAxNTIuNDI4QzkyLjU2ODYgMTUyLjUxNiA5Mi4zMDI5IDE1Mi41NjEgOTEuOTkzMSAxNTIuNTYxSDkxLjM3NTlWMTUyLjEyM1pNOTEuMzc1OSAxNTIuNzE3VjE1Mi4yODNIOTEuOTkzMUM5Mi4zNTUgMTUyLjI4MyA5Mi42NTQ1IDE1Mi4zMjYgOTIuODkxNSAxNTIuNDEyQzkzLjEyODUgMTUyLjQ5OCA5My4zMTQ3IDE1Mi42MTMgOTMuNDUwMSAxNTIuNzU2QzkzLjU4ODEgMTUyLjg5OSA5My42ODQ1IDE1My4wNTcgOTMuNzM5MSAxNTMuMjI5QzkzLjc5NjQgMTUzLjM5OCA5My44MjUxIDE1My41NjcgOTMuODI1MSAxNTMuNzM2QzkzLjgyNTEgMTU0LjAwMiA5My43Nzk1IDE1NC4yMzggOTMuNjg4NCAxNTQuNDQzQzkzLjU5OTggMTU0LjY0OSA5My40NzM1IDE1NC44MjQgOTMuMzA5NSAxNTQuOTY3QzkzLjE0OCAxNTUuMTEgOTIuOTU3OSAxNTUuMjE4IDkyLjczOTEgMTU1LjI5MUM5Mi41MjA0IDE1NS4zNjQgOTIuMjgyMSAxNTUuNCA5Mi4wMjQzIDE1NS40QzkxLjc3NjkgMTU1LjQgOTEuNTQzOCAxNTUuMzY1IDkxLjMyNTEgMTU1LjI5NUM5MS4xMDg5IDE1NS4yMjUgOTAuOTE3NSAxNTUuMTIzIDkwLjc1MDkgMTU0Ljk5QzkwLjU4NDIgMTU0Ljg1NSA5MC40NTQgMTU0LjY4OSA5MC4zNjAyIDE1NC40OTRDOTAuMjY2NSAxNTQuMjk2IDkwLjIxOTYgMTU0LjA3MSA5MC4yMTk2IDE1My44MThIOTAuOTQyM0M5MC45NDIzIDE1NC4wMTYgOTAuOTg1MiAxNTQuMTg5IDkxLjA3MTIgMTU0LjMzOEM5MS4xNTk3IDE1NC40ODYgOTEuMjg0NyAxNTQuNjAyIDkxLjQ0NjIgMTU0LjY4NkM5MS42MTAyIDE1NC43NjYgOTEuODAyOSAxNTQuODA3IDkyLjAyNDMgMTU0LjgwN0M5Mi4yNDU3IDE1NC44MDcgOTIuNDM1OCAxNTQuNzY5IDkyLjU5NDYgMTU0LjY5M0M5Mi43NTYxIDE1NC42MTUgOTIuODc5OCAxNTQuNDk4IDkyLjk2NTcgMTU0LjM0MkM5My4wNTQzIDE1NC4xODYgOTMuMDk4NSAxNTMuOTg5IDkzLjA5ODUgMTUzLjc1MkM5My4wOTg1IDE1My41MTUgOTMuMDQ5IDE1My4zMjEgOTIuOTUwMSAxNTMuMTdDOTIuODUxMSAxNTMuMDE2IDkyLjcxMDUgMTUyLjkwMyA5Mi41MjgyIDE1Mi44M0M5Mi4zNDg1IDE1Mi43NTUgOTIuMTM2MyAxNTIuNzE3IDkxLjg5MTUgMTUyLjcxN0g5MS4zNzU5Wk05Ny45MTIyIDE0OS42MzVWMTU1LjMyMkg5Ny4xNTgzVjE0OS42MzVIOTcuOTEyMlpNMTAwLjI5NSAxNTIuMTkzVjE1Mi44MTFIOTcuNzQ4MVYxNTIuMTkzSDEwMC4yOTVaTTEwMC42ODIgMTQ5LjYzNVYxNTAuMjUySDk3Ljc0ODFWMTQ5LjYzNUgxMDAuNjgyWk0xMDMuMjIxIDE1NS40QzEwMi45MjcgMTU1LjQgMTAyLjY2IDE1NS4zNTEgMTAyLjQyMSAxNTUuMjUyQzEwMi4xODQgMTU1LjE1IDEwMS45NzkgMTU1LjAwOCAxMDEuODA3IDE1NC44MjZDMTAxLjYzOCAxNTQuNjQ0IDEwMS41MDggMTU0LjQyOCAxMDEuNDE3IDE1NC4xNzhDMTAxLjMyNiAxNTMuOTI4IDEwMS4yOCAxNTMuNjU0IDEwMS4yOCAxNTMuMzU3VjE1My4xOTNDMTAxLjI4IDE1Mi44NSAxMDEuMzMxIDE1Mi41NDQgMTAxLjQzMiAxNTIuMjc1QzEwMS41MzQgMTUyLjAwNSAxMDEuNjcyIDE1MS43NzUgMTAxLjg0NiAxNTEuNTg4QzEwMi4wMjEgMTUxLjQgMTAyLjIxOSAxNTEuMjU4IDEwMi40NCAxNTEuMTYyQzEwMi42NjIgMTUxLjA2NiAxMDIuODkxIDE1MS4wMTggMTAzLjEyOCAxNTEuMDE4QzEwMy40MyAxNTEuMDE4IDEwMy42OSAxNTEuMDcgMTAzLjkwOSAxNTEuMTc0QzEwNC4xMyAxNTEuMjc4IDEwNC4zMTEgMTUxLjQyNCAxMDQuNDUyIDE1MS42MTFDMTA0LjU5MiAxNTEuNzk2IDEwNC42OTcgMTUyLjAxNSAxMDQuNzY0IDE1Mi4yNjhDMTA0LjgzMiAxNTIuNTE4IDEwNC44NjYgMTUyLjc5MSAxMDQuODY2IDE1My4wODhWMTUzLjQxMkgxMDEuNzFWMTUyLjgyMkgxMDQuMTQzVjE1Mi43NjhDMTA0LjEzMyAxNTIuNTggMTA0LjA5NCAxNTIuMzk4IDEwNC4wMjYgMTUyLjIyMUMxMDMuOTYxIDE1Mi4wNDQgMTAzLjg1NyAxNTEuODk4IDEwMy43MTQgMTUxLjc4M0MxMDMuNTcgMTUxLjY2OSAxMDMuMzc1IDE1MS42MTEgMTAzLjEyOCAxNTEuNjExQzEwMi45NjQgMTUxLjYxMSAxMDIuODEzIDE1MS42NDYgMTAyLjY3NSAxNTEuNzE3QzEwMi41MzcgMTUxLjc4NSAxMDIuNDE4IDE1MS44ODYgMTAyLjMxOSAxNTIuMDIxQzEwMi4yMiAxNTIuMTU3IDEwMi4xNDMgMTUyLjMyMiAxMDIuMDg5IDE1Mi41MThDMTAyLjAzNCAxNTIuNzEzIDEwMi4wMDcgMTUyLjkzOCAxMDIuMDA3IDE1My4xOTNWMTUzLjM1N0MxMDIuMDA3IDE1My41NTggMTAyLjAzNCAxNTMuNzQ3IDEwMi4wODkgMTUzLjkyNEMxMDIuMTQ2IDE1NC4wOTggMTAyLjIyOCAxNTQuMjUyIDEwMi4zMzUgMTU0LjM4NUMxMDIuNDQ0IDE1NC41MTggMTAyLjU3NiAxNTQuNjIyIDEwMi43MjkgMTU0LjY5N0MxMDIuODg1IDE1NC43NzMgMTAzLjA2MyAxNTQuODExIDEwMy4yNiAxNTQuODExQzEwMy41MTYgMTU0LjgxMSAxMDMuNzMyIDE1NC43NTggMTAzLjkwOSAxNTQuNjU0QzEwNC4wODYgMTU0LjU1IDEwNC4yNDEgMTU0LjQxMSAxMDQuMzc0IDE1NC4yMzZMMTA0LjgxMSAxNTQuNTg0QzEwNC43MiAxNTQuNzIyIDEwNC42MDQgMTU0Ljg1NCAxMDQuNDY0IDE1NC45NzlDMTA0LjMyMyAxNTUuMTA0IDEwNC4xNSAxNTUuMjA1IDEwMy45NDQgMTU1LjI4M0MxMDMuNzQxIDE1NS4zNjEgMTAzLjUgMTU1LjQgMTAzLjIyMSAxNTUuNFpNMTA1Ljc4OCAxNDkuMzIySDEwNi41MTVWMTU0LjUwMkwxMDYuNDUyIDE1NS4zMjJIMTA1Ljc4OFYxNDkuMzIyWk0xMDkuMzcgMTUzLjE3NFYxNTMuMjU2QzEwOS4zNyAxNTMuNTYzIDEwOS4zMzQgMTUzLjg0OCAxMDkuMjYxIDE1NC4xMTFDMTA5LjE4OCAxNTQuMzcyIDEwOS4wODEgMTU0LjU5OCAxMDguOTQxIDE1NC43OTFDMTA4LjggMTU0Ljk4NCAxMDguNjI4IDE1NS4xMzMgMTA4LjQyNSAxNTUuMjRDMTA4LjIyMiAxNTUuMzQ3IDEwNy45ODkgMTU1LjQgMTA3LjcyNiAxNTUuNEMxMDcuNDU4IDE1NS40IDEwNy4yMjIgMTU1LjM1NSAxMDcuMDE5IDE1NS4yNjRDMTA2LjgxOCAxNTUuMTcgMTA2LjY0OSAxNTUuMDM2IDEwNi41MTEgMTU0Ljg2MUMxMDYuMzczIDE1NC42ODcgMTA2LjI2MiAxNTQuNDc2IDEwNi4xNzkgMTU0LjIyOUMxMDYuMDk4IDE1My45ODEgMTA2LjA0MiAxNTMuNzAyIDEwNi4wMTEgMTUzLjM5M1YxNTMuMDMzQzEwNi4wNDIgMTUyLjcyMSAxMDYuMDk4IDE1Mi40NDEgMTA2LjE3OSAxNTIuMTkzQzEwNi4yNjIgMTUxLjk0NiAxMDYuMzczIDE1MS43MzUgMTA2LjUxMSAxNTEuNTYxQzEwNi42NDkgMTUxLjM4MyAxMDYuODE4IDE1MS4yNDkgMTA3LjAxOSAxNTEuMTU4QzEwNy4yMTkgMTUxLjA2NCAxMDcuNDUyIDE1MS4wMTggMTA3LjcxOCAxNTEuMDE4QzEwNy45ODQgMTUxLjAxOCAxMDguMjE5IDE1MS4wNyAxMDguNDI1IDE1MS4xNzRDMTA4LjYzMSAxNTEuMjc1IDEwOC44MDMgMTUxLjQyMSAxMDguOTQxIDE1MS42MTFDMTA5LjA4MSAxNTEuODAxIDEwOS4xODggMTUyLjAyOSAxMDkuMjYxIDE1Mi4yOTVDMTA5LjMzNCAxNTIuNTU4IDEwOS4zNyAxNTIuODUxIDEwOS4zNyAxNTMuMTc0Wk0xMDguNjQ0IDE1My4yNTZWMTUzLjE3NEMxMDguNjQ0IDE1Mi45NjMgMTA4LjYyNCAxNTIuNzY1IDEwOC41ODUgMTUyLjU4QzEwOC41NDYgMTUyLjM5MyAxMDguNDg0IDE1Mi4yMjkgMTA4LjM5OCAxNTIuMDg4QzEwOC4zMTIgMTUxLjk0NSAxMDguMTk5IDE1MS44MzMgMTA4LjA1OCAxNTEuNzUyQzEwNy45MTcgMTUxLjY2OSAxMDcuNzQ0IDE1MS42MjcgMTA3LjUzOCAxNTEuNjI3QzEwNy4zNTYgMTUxLjYyNyAxMDcuMTk3IDE1MS42NTggMTA3LjA2MiAxNTEuNzIxQzEwNi45MjkgMTUxLjc4MyAxMDYuODE2IDE1MS44NjggMTA2LjcyMiAxNTEuOTc1QzEwNi42MjggMTUyLjA3OSAxMDYuNTUxIDE1Mi4xOTkgMTA2LjQ5MiAxNTIuMzM0QzEwNi40MzQgMTUyLjQ2NyAxMDYuMzkxIDE1Mi42MDUgMTA2LjM2MyAxNTIuNzQ4VjE1My42ODlDMTA2LjQwNCAxNTMuODcyIDEwNi40NzIgMTU0LjA0OCAxMDYuNTY2IDE1NC4yMTdDMTA2LjY2MiAxNTQuMzgzIDEwNi43OSAxNTQuNTIgMTA2Ljk0OSAxNTQuNjI3QzEwNy4xMSAxNTQuNzM0IDEwNy4zMDkgMTU0Ljc4NyAxMDcuNTQ2IDE1NC43ODdDMTA3Ljc0MiAxNTQuNzg3IDEwNy45MDggMTU0Ljc0OCAxMDguMDQ2IDE1NC42N0MxMDguMTg3IDE1NC41ODkgMTA4LjMgMTU0LjQ3OSAxMDguMzg2IDE1NC4zMzhDMTA4LjQ3NSAxNTQuMTk3IDEwOC41NCAxNTQuMDM1IDEwOC41ODEgMTUzLjg1QzEwOC42MjMgMTUzLjY2NSAxMDguNjQ0IDE1My40NjcgMTA4LjY0NCAxNTMuMjU2WiIgZmlsbD0iYmxhY2siIGZpbGwtb3BhY2l0eT0iMC41NCIvPgo8L2c+CjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwM180NDM3Xzk2ODU4KSI+CjxsaW5lIHgxPSIxMzIuNjUiIHkxPSIxNDcuMDcyIiB4Mj0iMTMyLjY1IiB5Mj0iMTQ2LjI1IiBzdHJva2U9ImJsYWNrIiBzdHJva2Utb3BhY2l0eT0iMC41IiBzdHJva2Utd2lkdGg9IjAuNSIgc3Ryb2tlLWxpbmVjYXA9InNxdWFyZSIvPgo8cGF0aCBkPSJNMTI0LjcwOSAxNTIuMDI1VjE1Mi44OTNDMTI0LjcwOSAxNTMuMzU5IDEyNC42NjggMTUzLjc1MiAxMjQuNTg0IDE1NC4wNzJDMTI0LjUwMSAxNTQuMzkzIDEyNC4zODEgMTU0LjY1IDEyNC4yMjUgMTU0Ljg0NkMxMjQuMDY5IDE1NS4wNDEgMTIzLjg4IDE1NS4xODMgMTIzLjY1OCAxNTUuMjcxQzEyMy40NCAxNTUuMzU3IDEyMy4xOTIgMTU1LjQgMTIyLjkxNiAxNTUuNEMxMjIuNjk4IDE1NS40IDEyMi40OTYgMTU1LjM3MyAxMjIuMzExIDE1NS4zMThDMTIyLjEyNiAxNTUuMjY0IDEyMS45NTkgMTU1LjE3NiAxMjEuODExIDE1NS4wNTdDMTIxLjY2NSAxNTQuOTM0IDEyMS41NCAxNTQuNzc1IDEyMS40MzYgMTU0LjU4QzEyMS4zMzIgMTU0LjM4NSAxMjEuMjUyIDE1NC4xNDggMTIxLjE5OCAxNTMuODY5QzEyMS4xNDMgMTUzLjU5IDEyMS4xMTYgMTUzLjI2NSAxMjEuMTE2IDE1Mi44OTNWMTUyLjAyNUMxMjEuMTE2IDE1MS41NTkgMTIxLjE1NyAxNTEuMTY5IDEyMS4yNDEgMTUwLjg1NEMxMjEuMzI2IDE1MC41MzggMTIxLjQ0OCAxNTAuMjg2IDEyMS42MDQgMTUwLjA5NkMxMjEuNzYgMTQ5LjkwMyAxMjEuOTQ4IDE0OS43NjUgMTIyLjE2NiAxNDkuNjgyQzEyMi4zODggMTQ5LjU5OCAxMjIuNjM1IDE0OS41NTcgMTIyLjkwOCAxNDkuNTU3QzEyMy4xMyAxNDkuNTU3IDEyMy4zMzMgMTQ5LjU4NCAxMjMuNTE4IDE0OS42MzlDMTIzLjcwNSAxNDkuNjkxIDEyMy44NzIgMTQ5Ljc3NSAxMjQuMDE4IDE0OS44OTNDMTI0LjE2NCAxNTAuMDA3IDEyNC4yODcgMTUwLjE2MSAxMjQuMzg5IDE1MC4zNTRDMTI0LjQ5MyAxNTAuNTQ0IDEyNC41NzMgMTUwLjc3NyAxMjQuNjI3IDE1MS4wNTNDMTI0LjY4MiAxNTEuMzI5IDEyNC43MDkgMTUxLjY1MyAxMjQuNzA5IDE1Mi4wMjVaTTEyMy45ODMgMTUzLjAxVjE1MS45MDRDMTIzLjk4MyAxNTEuNjQ5IDEyMy45NjcgMTUxLjQyNSAxMjMuOTM2IDE1MS4yMzJDMTIzLjkwNyAxNTEuMDM3IDEyMy44NjQgMTUwLjg3IDEyMy44MDcgMTUwLjczMkMxMjMuNzUgMTUwLjU5NCAxMjMuNjc3IDE1MC40ODIgMTIzLjU4OCAxNTAuMzk2QzEyMy41MDIgMTUwLjMxMSAxMjMuNDAyIDE1MC4yNDggMTIzLjI4NyAxNTAuMjA5QzEyMy4xNzUgMTUwLjE2NyAxMjMuMDQ5IDE1MC4xNDYgMTIyLjkwOCAxNTAuMTQ2QzEyMi43MzcgMTUwLjE0NiAxMjIuNTg0IDE1MC4xNzkgMTIyLjQ1MSAxNTAuMjQ0QzEyMi4zMTkgMTUwLjMwNyAxMjIuMjA3IDE1MC40MDcgMTIyLjExNiAxNTAuNTQ1QzEyMi4wMjcgMTUwLjY4MyAxMjEuOTU5IDE1MC44NjQgMTIxLjkxMiAxNTEuMDg4QzEyMS44NjYgMTUxLjMxMiAxMjEuODQyIDE1MS41ODQgMTIxLjg0MiAxNTEuOTA0VjE1My4wMUMxMjEuODQyIDE1My4yNjUgMTIxLjg1NiAxNTMuNDkgMTIxLjg4NSAxNTMuNjg2QzEyMS45MTYgMTUzLjg4MSAxMjEuOTYyIDE1NC4wNSAxMjIuMDIyIDE1NC4xOTNDMTIyLjA4MiAxNTQuMzM0IDEyMi4xNTUgMTU0LjQ1IDEyMi4yNDEgMTU0LjU0MUMxMjIuMzI2IDE1NC42MzIgMTIyLjQyNSAxNTQuNyAxMjIuNTM3IDE1NC43NDRDMTIyLjY1MiAxNTQuNzg2IDEyMi43NzggMTU0LjgwNyAxMjIuOTE2IDE1NC44MDdDMTIzLjA5MyAxNTQuODA3IDEyMy4yNDggMTU0Ljc3MyAxMjMuMzgxIDE1NC43MDVDMTIzLjUxNCAxNTQuNjM3IDEyMy42MjUgMTU0LjUzMiAxMjMuNzEzIDE1NC4zODlDMTIzLjgwNCAxNTQuMjQzIDEyMy44NzIgMTU0LjA1NyAxMjMuOTE2IDE1My44M0MxMjMuOTYxIDE1My42MDEgMTIzLjk4MyAxNTMuMzI3IDEyMy45ODMgMTUzLjAxWk0xMjkuNTY1IDE1My40MDhWMTU0LjAwMkgxMjUuNDU2VjE1My41NzZMMTI4LjAwMyAxNDkuNjM1SDEyOC41OTNMMTI3Ljk2IDE1MC43NzVMMTI2LjI3NiAxNTMuNDA4SDEyOS41NjVaTTEyOC43NzIgMTQ5LjYzNVYxNTUuMzIySDEyOC4wNVYxNDkuNjM1SDEyOC43NzJaTTEzMy4zMTMgMTQ5LjYzNVYxNTUuMzIySDEzMi41NTlWMTQ5LjYzNUgxMzMuMzEzWk0xMzUuNjk1IDE1Mi4xOTNWMTUyLjgxMUgxMzMuMTQ5VjE1Mi4xOTNIMTM1LjY5NVpNMTM2LjA4MiAxNDkuNjM1VjE1MC4yNTJIMTMzLjE0OVYxNDkuNjM1SDEzNi4wODJaTTEzOC42MjIgMTU1LjRDMTM4LjMyOCAxNTUuNCAxMzguMDYxIDE1NS4zNTEgMTM3LjgyMSAxNTUuMjUyQzEzNy41ODQgMTU1LjE1IDEzNy4zOCAxNTUuMDA4IDEzNy4yMDggMTU0LjgyNkMxMzcuMDM4IDE1NC42NDQgMTM2LjkwOCAxNTQuNDI4IDEzNi44MTcgMTU0LjE3OEMxMzYuNzI2IDE1My45MjggMTM2LjY4IDE1My42NTQgMTM2LjY4IDE1My4zNTdWMTUzLjE5M0MxMzYuNjggMTUyLjg1IDEzNi43MzEgMTUyLjU0NCAxMzYuODMzIDE1Mi4yNzVDMTM2LjkzNCAxNTIuMDA1IDEzNy4wNzIgMTUxLjc3NSAxMzcuMjQ3IDE1MS41ODhDMTM3LjQyMSAxNTEuNCAxMzcuNjE5IDE1MS4yNTggMTM3Ljg0MSAxNTEuMTYyQzEzOC4wNjIgMTUxLjA2NiAxMzguMjkxIDE1MS4wMTggMTM4LjUyOCAxNTEuMDE4QzEzOC44MyAxNTEuMDE4IDEzOS4wOTEgMTUxLjA3IDEzOS4zMDkgMTUxLjE3NEMxMzkuNTMxIDE1MS4yNzggMTM5LjcxMiAxNTEuNDI0IDEzOS44NTIgMTUxLjYxMUMxMzkuOTkzIDE1MS43OTYgMTQwLjA5NyAxNTIuMDE1IDE0MC4xNjUgMTUyLjI2OEMxNDAuMjMyIDE1Mi41MTggMTQwLjI2NiAxNTIuNzkxIDE0MC4yNjYgMTUzLjA4OFYxNTMuNDEySDEzNy4xMVYxNTIuODIySDEzOS41NDRWMTUyLjc2OEMxMzkuNTMzIDE1Mi41OCAxMzkuNDk0IDE1Mi4zOTggMTM5LjQyNiAxNTIuMjIxQzEzOS4zNjEgMTUyLjA0NCAxMzkuMjU3IDE1MS44OTggMTM5LjExNCAxNTEuNzgzQzEzOC45NzEgMTUxLjY2OSAxMzguNzc1IDE1MS42MTEgMTM4LjUyOCAxNTEuNjExQzEzOC4zNjQgMTUxLjYxMSAxMzguMjEzIDE1MS42NDYgMTM4LjA3NSAxNTEuNzE3QzEzNy45MzcgMTUxLjc4NSAxMzcuODE4IDE1MS44ODYgMTM3LjcxOSAxNTIuMDIxQzEzNy42MiAxNTIuMTU3IDEzNy41NDQgMTUyLjMyMiAxMzcuNDg5IDE1Mi41MThDMTM3LjQzNCAxNTIuNzEzIDEzNy40MDcgMTUyLjkzOCAxMzcuNDA3IDE1My4xOTNWMTUzLjM1N0MxMzcuNDA3IDE1My41NTggMTM3LjQzNCAxNTMuNzQ3IDEzNy40ODkgMTUzLjkyNEMxMzcuNTQ2IDE1NC4wOTggMTM3LjYyOCAxNTQuMjUyIDEzNy43MzUgMTU0LjM4NUMxMzcuODQ0IDE1NC41MTggMTM3Ljk3NiAxNTQuNjIyIDEzOC4xMyAxNTQuNjk3QzEzOC4yODYgMTU0Ljc3MyAxMzguNDYzIDE1NC44MTEgMTM4LjY2MSAxNTQuODExQzEzOC45MTYgMTU0LjgxMSAxMzkuMTMyIDE1NC43NTggMTM5LjMwOSAxNTQuNjU0QzEzOS40ODYgMTU0LjU1IDEzOS42NDEgMTU0LjQxMSAxMzkuNzc0IDE1NC4yMzZMMTQwLjIxMiAxNTQuNTg0QzE0MC4xMiAxNTQuNzIyIDE0MC4wMDUgMTU0Ljg1NCAxMzkuODY0IDE1NC45NzlDMTM5LjcyMyAxNTUuMTA0IDEzOS41NSAxNTUuMjA1IDEzOS4zNDQgMTU1LjI4M0MxMzkuMTQxIDE1NS4zNjEgMTM4LjkgMTU1LjQgMTM4LjYyMiAxNTUuNFpNMTQxLjE4OSAxNDkuMzIySDE0MS45MTVWMTU0LjUwMkwxNDEuODUzIDE1NS4zMjJIMTQxLjE4OVYxNDkuMzIyWk0xNDQuNzcxIDE1My4xNzRWMTUzLjI1NkMxNDQuNzcxIDE1My41NjMgMTQ0LjczNCAxNTMuODQ4IDE0NC42NjEgMTU0LjExMUMxNDQuNTg5IDE1NC4zNzIgMTQ0LjQ4MiAxNTQuNTk4IDE0NC4zNDEgMTU0Ljc5MUMxNDQuMjAxIDE1NC45ODQgMTQ0LjAyOSAxNTUuMTMzIDE0My44MjYgMTU1LjI0QzE0My42MjIgMTU1LjM0NyAxNDMuMzg5IDE1NS40IDE0My4xMjYgMTU1LjRDMTQyLjg1OCAxNTUuNCAxNDIuNjIyIDE1NS4zNTUgMTQyLjQxOSAxNTUuMjY0QzE0Mi4yMTkgMTU1LjE3IDE0Mi4wNDkgMTU1LjAzNiAxNDEuOTExIDE1NC44NjFDMTQxLjc3MyAxNTQuNjg3IDE0MS42NjMgMTU0LjQ3NiAxNDEuNTc5IDE1NC4yMjlDMTQxLjQ5OSAxNTMuOTgxIDE0MS40NDMgMTUzLjcwMiAxNDEuNDExIDE1My4zOTNWMTUzLjAzM0MxNDEuNDQzIDE1Mi43MjEgMTQxLjQ5OSAxNTIuNDQxIDE0MS41NzkgMTUyLjE5M0MxNDEuNjYzIDE1MS45NDYgMTQxLjc3MyAxNTEuNzM1IDE0MS45MTEgMTUxLjU2MUMxNDIuMDQ5IDE1MS4zODMgMTQyLjIxOSAxNTEuMjQ5IDE0Mi40MTkgMTUxLjE1OEMxNDIuNjIgMTUxLjA2NCAxNDIuODUzIDE1MS4wMTggMTQzLjExOCAxNTEuMDE4QzE0My4zODQgMTUxLjAxOCAxNDMuNjIgMTUxLjA3IDE0My44MjYgMTUxLjE3NEMxNDQuMDMxIDE1MS4yNzUgMTQ0LjIwMyAxNTEuNDIxIDE0NC4zNDEgMTUxLjYxMUMxNDQuNDgyIDE1MS44MDEgMTQ0LjU4OSAxNTIuMDI5IDE0NC42NjEgMTUyLjI5NUMxNDQuNzM0IDE1Mi41NTggMTQ0Ljc3MSAxNTIuODUxIDE0NC43NzEgMTUzLjE3NFpNMTQ0LjA0NCAxNTMuMjU2VjE1My4xNzRDMTQ0LjA0NCAxNTIuOTYzIDE0NC4wMjUgMTUyLjc2NSAxNDMuOTg2IDE1Mi41OEMxNDMuOTQ3IDE1Mi4zOTMgMTQzLjg4NCAxNTIuMjI5IDE0My43OTggMTUyLjA4OEMxNDMuNzEyIDE1MS45NDUgMTQzLjU5OSAxNTEuODMzIDE0My40NTggMTUxLjc1MkMxNDMuMzE4IDE1MS42NjkgMTQzLjE0NSAxNTEuNjI3IDE0Mi45MzkgMTUxLjYyN0MxNDIuNzU3IDE1MS42MjcgMTQyLjU5OCAxNTEuNjU4IDE0Mi40NjIgMTUxLjcyMUMxNDIuMzI5IDE1MS43ODMgMTQyLjIxNiAxNTEuODY4IDE0Mi4xMjIgMTUxLjk3NUMxNDIuMDI5IDE1Mi4wNzkgMTQxLjk1MiAxNTIuMTk5IDE0MS44OTIgMTUyLjMzNEMxNDEuODM1IDE1Mi40NjcgMTQxLjc5MiAxNTIuNjA1IDE0MS43NjMgMTUyLjc0OFYxNTMuNjg5QzE0MS44MDUgMTUzLjg3MiAxNDEuODcyIDE1NC4wNDggMTQxLjk2NiAxNTQuMjE3QzE0Mi4wNjIgMTU0LjM4MyAxNDIuMTkgMTU0LjUyIDE0Mi4zNDkgMTU0LjYyN0MxNDIuNTEgMTU0LjczNCAxNDIuNzEgMTU0Ljc4NyAxNDIuOTQ3IDE1NC43ODdDMTQzLjE0MiAxNTQuNzg3IDE0My4zMDkgMTU0Ljc0OCAxNDMuNDQ3IDE1NC42N0MxNDMuNTg3IDE1NC41ODkgMTQzLjcwMSAxNTQuNDc5IDE0My43ODYgMTU0LjMzOEMxNDMuODc1IDE1NC4xOTcgMTQzLjk0IDE1NC4wMzUgMTQzLjk4MiAxNTMuODVDMTQ0LjAyMyAxNTMuNjY1IDE0NC4wNDQgMTUzLjQ2NyAxNDQuMDQ0IDE1My4yNTZaIiBmaWxsPSJibGFjayIgZmlsbC1vcGFjaXR5PSIwLjU0Ii8+CjwvZz4KPGcgY2xpcC1wYXRoPSJ1cmwoI2NsaXA0XzQ0MzdfOTY4NTgpIj4KPGxpbmUgeDE9IjE2OC4wNSIgeTE9IjE0Ny4wNzIiIHgyPSIxNjguMDUiIHkyPSIxNDYuMjUiIHN0cm9rZT0iYmxhY2siIHN0cm9rZS1vcGFjaXR5PSIwLjUiIHN0cm9rZS13aWR0aD0iMC41IiBzdHJva2UtbGluZWNhcD0ic3F1YXJlIi8+CjxwYXRoIGQ9Ik0xNjAuMTA5IDE1Mi4wMjVWMTUyLjg5M0MxNjAuMTA5IDE1My4zNTkgMTYwLjA2NyAxNTMuNzUyIDE1OS45ODQgMTU0LjA3MkMxNTkuOTAxIDE1NC4zOTMgMTU5Ljc4MSAxNTQuNjUgMTU5LjYyNSAxNTQuODQ2QzE1OS40NjkgMTU1LjA0MSAxNTkuMjggMTU1LjE4MyAxNTkuMDU4IDE1NS4yNzFDMTU4Ljg0IDE1NS4zNTcgMTU4LjU5MiAxNTUuNCAxNTguMzE2IDE1NS40QzE1OC4wOTcgMTU1LjQgMTU3Ljg5NiAxNTUuMzczIDE1Ny43MTEgMTU1LjMxOEMxNTcuNTI2IDE1NS4yNjQgMTU3LjM1OSAxNTUuMTc2IDE1Ny4yMTEgMTU1LjA1N0MxNTcuMDY1IDE1NC45MzQgMTU2Ljk0IDE1NC43NzUgMTU2LjgzNiAxNTQuNThDMTU2LjczMiAxNTQuMzg1IDE1Ni42NTIgMTU0LjE0OCAxNTYuNTk3IDE1My44NjlDMTU2LjU0MyAxNTMuNTkgMTU2LjUxNSAxNTMuMjY1IDE1Ni41MTUgMTUyLjg5M1YxNTIuMDI1QzE1Ni41MTUgMTUxLjU1OSAxNTYuNTU3IDE1MS4xNjkgMTU2LjY0IDE1MC44NTRDMTU2LjcyNiAxNTAuNTM4IDE1Ni44NDcgMTUwLjI4NiAxNTcuMDA0IDE1MC4wOTZDMTU3LjE2IDE0OS45MDMgMTU3LjM0NyAxNDkuNzY1IDE1Ny41NjYgMTQ5LjY4MkMxNTcuNzg4IDE0OS41OTggMTU4LjAzNSAxNDkuNTU3IDE1OC4zMDggMTQ5LjU1N0MxNTguNTMgMTQ5LjU1NyAxNTguNzMzIDE0OS41ODQgMTU4LjkxOCAxNDkuNjM5QzE1OS4xMDUgMTQ5LjY5MSAxNTkuMjcyIDE0OS43NzUgMTU5LjQxOCAxNDkuODkzQzE1OS41NjQgMTUwLjAwNyAxNTkuNjg3IDE1MC4xNjEgMTU5Ljc4OSAxNTAuMzU0QzE1OS44OTMgMTUwLjU0NCAxNTkuOTcyIDE1MC43NzcgMTYwLjAyNyAxNTEuMDUzQzE2MC4wODIgMTUxLjMyOSAxNjAuMTA5IDE1MS42NTMgMTYwLjEwOSAxNTIuMDI1Wk0xNTkuMzgzIDE1My4wMVYxNTEuOTA0QzE1OS4zODMgMTUxLjY0OSAxNTkuMzY3IDE1MS40MjUgMTU5LjMzNiAxNTEuMjMyQzE1OS4zMDcgMTUxLjAzNyAxNTkuMjY0IDE1MC44NyAxNTkuMjA3IDE1MC43MzJDMTU5LjE1IDE1MC41OTQgMTU5LjA3NyAxNTAuNDgyIDE1OC45ODggMTUwLjM5NkMxNTguOTAyIDE1MC4zMTEgMTU4LjgwMiAxNTAuMjQ4IDE1OC42ODcgMTUwLjIwOUMxNTguNTc1IDE1MC4xNjcgMTU4LjQ0OSAxNTAuMTQ2IDE1OC4zMDggMTUwLjE0NkMxNTguMTM2IDE1MC4xNDYgMTU3Ljk4NCAxNTAuMTc5IDE1Ny44NTEgMTUwLjI0NEMxNTcuNzE5IDE1MC4zMDcgMTU3LjYwNyAxNTAuNDA3IDE1Ny41MTUgMTUwLjU0NUMxNTcuNDI3IDE1MC42ODMgMTU3LjM1OSAxNTAuODY0IDE1Ny4zMTIgMTUxLjA4OEMxNTcuMjY1IDE1MS4zMTIgMTU3LjI0MiAxNTEuNTg0IDE1Ny4yNDIgMTUxLjkwNFYxNTMuMDFDMTU3LjI0MiAxNTMuMjY1IDE1Ny4yNTYgMTUzLjQ5IDE1Ny4yODUgMTUzLjY4NkMxNTcuMzE2IDE1My44ODEgMTU3LjM2MiAxNTQuMDUgMTU3LjQyMiAxNTQuMTkzQzE1Ny40ODIgMTU0LjMzNCAxNTcuNTU0IDE1NC40NSAxNTcuNjQgMTU0LjU0MUMxNTcuNzI2IDE1NC42MzIgMTU3LjgyNSAxNTQuNyAxNTcuOTM3IDE1NC43NDRDMTU4LjA1MiAxNTQuNzg2IDE1OC4xNzggMTU0LjgwNyAxNTguMzE2IDE1NC44MDdDMTU4LjQ5MyAxNTQuODA3IDE1OC42NDggMTU0Ljc3MyAxNTguNzgxIDE1NC43MDVDMTU4LjkxNCAxNTQuNjM3IDE1OS4wMjUgMTU0LjUzMiAxNTkuMTEzIDE1NC4zODlDMTU5LjIwNCAxNTQuMjQzIDE1OS4yNzIgMTU0LjA1NyAxNTkuMzE2IDE1My44M0MxNTkuMzYgMTUzLjYwMSAxNTkuMzgzIDE1My4zMjcgMTU5LjM4MyAxNTMuMDFaTTE2Mi4wMzYgMTUyLjYxNUwxNjEuNDU3IDE1Mi40NjdMMTYxLjc0MyAxNDkuNjM1SDE2NC42NjFWMTUwLjMwM0gxNjIuMzU2TDE2Mi4xODQgMTUxLjg1QzE2Mi4yODggMTUxLjc5IDE2Mi40MiAxNTEuNzM0IDE2Mi41NzkgMTUxLjY4MkMxNjIuNzQgMTUxLjYzIDE2Mi45MjUgMTUxLjYwNCAxNjMuMTMzIDE1MS42MDRDMTYzLjM5NiAxNTEuNjA0IDE2My42MzIgMTUxLjY0OSAxNjMuODQgMTUxLjc0QzE2NC4wNDkgMTUxLjgyOSAxNjQuMjI2IDE1MS45NTYgMTY0LjM3MSAxNTIuMTIzQzE2NC41MiAxNTIuMjkgMTY0LjYzMyAxNTIuNDkgMTY0LjcxMSAxNTIuNzI1QzE2NC43ODkgMTUyLjk1OSAxNjQuODI5IDE1My4yMjEgMTY0LjgyOSAxNTMuNTFDMTY0LjgyOSAxNTMuNzgzIDE2NC43OTEgMTU0LjAzNSAxNjQuNzE1IDE1NC4yNjRDMTY0LjY0MiAxNTQuNDkzIDE2NC41MzIgMTU0LjY5MyAxNjQuMzgzIDE1NC44NjVDMTY0LjIzNSAxNTUuMDM1IDE2NC4wNDcgMTU1LjE2NiAxNjMuODIxIDE1NS4yNkMxNjMuNTk3IDE1NS4zNTQgMTYzLjMzMiAxNTUuNCAxNjMuMDI4IDE1NS40QzE2Mi43OTkgMTU1LjQgMTYyLjU4MSAxNTUuMzY5IDE2Mi4zNzUgMTU1LjMwN0MxNjIuMTcyIDE1NS4yNDIgMTYxLjk5IDE1NS4xNDQgMTYxLjgyOSAxNTUuMDE0QzE2MS42NyAxNTQuODgxIDE2MS41MzkgMTU0LjcxNyAxNjEuNDM4IDE1NC41MjFDMTYxLjMzOSAxNTQuMzI0IDE2MS4yNzYgMTU0LjA5MiAxNjEuMjUgMTUzLjgyNkgxNjEuOTM4QzE2MS45NjkgMTU0LjA0IDE2Mi4wMzIgMTU0LjIxOSAxNjIuMTI1IDE1NC4zNjVDMTYyLjIxOSAxNTQuNTExIDE2Mi4zNDIgMTU0LjYyMiAxNjIuNDkzIDE1NC42OTdDMTYyLjY0NiAxNTQuNzcgMTYyLjgyNSAxNTQuODA3IDE2My4wMjggMTU0LjgwN0MxNjMuMiAxNTQuODA3IDE2My4zNTIgMTU0Ljc3NyAxNjMuNDg1IDE1NC43MTdDMTYzLjYxOCAxNTQuNjU3IDE2My43MyAxNTQuNTcxIDE2My44MjEgMTU0LjQ1OUMxNjMuOTEyIDE1NC4zNDcgMTYzLjk4MSAxNTQuMjEyIDE2NC4wMjggMTU0LjA1M0MxNjQuMDc3IDE1My44OTQgMTY0LjEwMiAxNTMuNzE1IDE2NC4xMDIgMTUzLjUxOEMxNjQuMTAyIDE1My4zMzggMTY0LjA3NyAxNTMuMTcxIDE2NC4wMjggMTUzLjAxOEMxNjMuOTc4IDE1Mi44NjQgMTYzLjkwNCAxNTIuNzMgMTYzLjgwNSAxNTIuNjE1QzE2My43MDkgMTUyLjUwMSAxNjMuNTkgMTUyLjQxMiAxNjMuNDUgMTUyLjM1QzE2My4zMDkgMTUyLjI4NSAxNjMuMTQ4IDE1Mi4yNTIgMTYyLjk2NSAxNTIuMjUyQzE2Mi43MjMgMTUyLjI1MiAxNjIuNTM5IDE1Mi4yODUgMTYyLjQxNCAxNTIuMzVDMTYyLjI5MiAxNTIuNDE1IDE2Mi4xNjYgMTUyLjUwMyAxNjIuMDM2IDE1Mi42MTVaTTE2OC43MTMgMTQ5LjYzNVYxNTUuMzIySDE2Ny45NTlWMTQ5LjYzNUgxNjguNzEzWk0xNzEuMDk1IDE1Mi4xOTNWMTUyLjgxMUgxNjguNTQ4VjE1Mi4xOTNIMTcxLjA5NVpNMTcxLjQ4MiAxNDkuNjM1VjE1MC4yNTJIMTY4LjU0OFYxNDkuNjM1SDE3MS40ODJaTTE3NC4wMjIgMTU1LjRDMTczLjcyNyAxNTUuNCAxNzMuNDYgMTU1LjM1MSAxNzMuMjIxIDE1NS4yNTJDMTcyLjk4NCAxNTUuMTUgMTcyLjc4IDE1NS4wMDggMTcyLjYwOCAxNTQuODI2QzE3Mi40MzggMTU0LjY0NCAxNzIuMzA4IDE1NC40MjggMTcyLjIxNyAxNTQuMTc4QzE3Mi4xMjYgMTUzLjkyOCAxNzIuMDggMTUzLjY1NCAxNzIuMDggMTUzLjM1N1YxNTMuMTkzQzE3Mi4wOCAxNTIuODUgMTcyLjEzMSAxNTIuNTQ0IDE3Mi4yMzMgMTUyLjI3NUMxNzIuMzM0IDE1Mi4wMDUgMTcyLjQ3MiAxNTEuNzc1IDE3Mi42NDcgMTUxLjU4OEMxNzIuODIxIDE1MS40IDE3My4wMTkgMTUxLjI1OCAxNzMuMjQgMTUxLjE2MkMxNzMuNDYyIDE1MS4wNjYgMTczLjY5MSAxNTEuMDE4IDE3My45MjggMTUxLjAxOEMxNzQuMjMgMTUxLjAxOCAxNzQuNDkgMTUxLjA3IDE3NC43MDkgMTUxLjE3NEMxNzQuOTMxIDE1MS4yNzggMTc1LjExMiAxNTEuNDI0IDE3NS4yNTIgMTUxLjYxMUMxNzUuMzkzIDE1MS43OTYgMTc1LjQ5NyAxNTIuMDE1IDE3NS41NjUgMTUyLjI2OEMxNzUuNjMyIDE1Mi41MTggMTc1LjY2NiAxNTIuNzkxIDE3NS42NjYgMTUzLjA4OFYxNTMuNDEySDE3Mi41MVYxNTIuODIySDE3NC45NDRWMTUyLjc2OEMxNzQuOTMzIDE1Mi41OCAxNzQuODk0IDE1Mi4zOTggMTc0LjgyNiAxNTIuMjIxQzE3NC43NjEgMTUyLjA0NCAxNzQuNjU3IDE1MS44OTggMTc0LjUxNCAxNTEuNzgzQzE3NC4zNzEgMTUxLjY2OSAxNzQuMTc1IDE1MS42MTEgMTczLjkyOCAxNTEuNjExQzE3My43NjQgMTUxLjYxMSAxNzMuNjEzIDE1MS42NDYgMTczLjQ3NSAxNTEuNzE3QzE3My4zMzcgMTUxLjc4NSAxNzMuMjE4IDE1MS44ODYgMTczLjExOSAxNTIuMDIxQzE3My4wMiAxNTIuMTU3IDE3Mi45NDQgMTUyLjMyMiAxNzIuODg5IDE1Mi41MThDMTcyLjgzNCAxNTIuNzEzIDE3Mi44MDcgMTUyLjkzOCAxNzIuODA3IDE1My4xOTNWMTUzLjM1N0MxNzIuODA3IDE1My41NTggMTcyLjgzNCAxNTMuNzQ3IDE3Mi44ODkgMTUzLjkyNEMxNzIuOTQ2IDE1NC4wOTggMTczLjAyOCAxNTQuMjUyIDE3My4xMzUgMTU0LjM4NUMxNzMuMjQ0IDE1NC41MTggMTczLjM3NiAxNTQuNjIyIDE3My41MyAxNTQuNjk3QzE3My42ODYgMTU0Ljc3MyAxNzMuODYzIDE1NC44MTEgMTc0LjA2MSAxNTQuODExQzE3NC4zMTYgMTU0LjgxMSAxNzQuNTMyIDE1NC43NTggMTc0LjcwOSAxNTQuNjU0QzE3NC44ODYgMTU0LjU1IDE3NS4wNDEgMTU0LjQxMSAxNzUuMTc0IDE1NC4yMzZMMTc1LjYxMiAxNTQuNTg0QzE3NS41MiAxNTQuNzIyIDE3NS40MDUgMTU0Ljg1NCAxNzUuMjY0IDE1NC45NzlDMTc1LjEyMyAxNTUuMTA0IDE3NC45NSAxNTUuMjA1IDE3NC43NDQgMTU1LjI4M0MxNzQuNTQxIDE1NS4zNjEgMTc0LjMgMTU1LjQgMTc0LjAyMiAxNTUuNFpNMTc2LjU4OSAxNDkuMzIySDE3Ny4zMTVWMTU0LjUwMkwxNzcuMjUzIDE1NS4zMjJIMTc2LjU4OVYxNDkuMzIyWk0xODAuMTcxIDE1My4xNzRWMTUzLjI1NkMxODAuMTcxIDE1My41NjMgMTgwLjEzNCAxNTMuODQ4IDE4MC4wNjEgMTU0LjExMUMxNzkuOTg4IDE1NC4zNzIgMTc5Ljg4MiAxNTQuNTk4IDE3OS43NDEgMTU0Ljc5MUMxNzkuNiAxNTQuOTg0IDE3OS40MjkgMTU1LjEzMyAxNzkuMjI1IDE1NS4yNEMxNzkuMDIyIDE1NS4zNDcgMTc4Ljc4OSAxNTUuNCAxNzguNTI2IDE1NS40QzE3OC4yNTggMTU1LjQgMTc4LjAyMiAxNTUuMzU1IDE3Ny44MTkgMTU1LjI2NEMxNzcuNjE5IDE1NS4xNyAxNzcuNDQ5IDE1NS4wMzYgMTc3LjMxMSAxNTQuODYxQzE3Ny4xNzMgMTU0LjY4NyAxNzcuMDYzIDE1NC40NzYgMTc2Ljk3OSAxNTQuMjI5QzE3Ni44OTkgMTUzLjk4MSAxNzYuODQzIDE1My43MDIgMTc2LjgxMSAxNTMuMzkzVjE1My4wMzNDMTc2Ljg0MyAxNTIuNzIxIDE3Ni44OTkgMTUyLjQ0MSAxNzYuOTc5IDE1Mi4xOTNDMTc3LjA2MyAxNTEuOTQ2IDE3Ny4xNzMgMTUxLjczNSAxNzcuMzExIDE1MS41NjFDMTc3LjQ0OSAxNTEuMzgzIDE3Ny42MTkgMTUxLjI0OSAxNzcuODE5IDE1MS4xNThDMTc4LjAyIDE1MS4wNjQgMTc4LjI1MyAxNTEuMDE4IDE3OC41MTggMTUxLjAxOEMxNzguNzg0IDE1MS4wMTggMTc5LjAyIDE1MS4wNyAxNzkuMjI1IDE1MS4xNzRDMTc5LjQzMSAxNTEuMjc1IDE3OS42MDMgMTUxLjQyMSAxNzkuNzQxIDE1MS42MTFDMTc5Ljg4MiAxNTEuODAxIDE3OS45ODggMTUyLjAyOSAxODAuMDYxIDE1Mi4yOTVDMTgwLjEzNCAxNTIuNTU4IDE4MC4xNzEgMTUyLjg1MSAxODAuMTcxIDE1My4xNzRaTTE3OS40NDQgMTUzLjI1NlYxNTMuMTc0QzE3OS40NDQgMTUyLjk2MyAxNzkuNDI1IDE1Mi43NjUgMTc5LjM4NiAxNTIuNThDMTc5LjM0NyAxNTIuMzkzIDE3OS4yODQgMTUyLjIyOSAxNzkuMTk4IDE1Mi4wODhDMTc5LjExMiAxNTEuOTQ1IDE3OC45OTkgMTUxLjgzMyAxNzguODU4IDE1MS43NTJDMTc4LjcxOCAxNTEuNjY5IDE3OC41NDQgMTUxLjYyNyAxNzguMzM5IDE1MS42MjdDMTc4LjE1NiAxNTEuNjI3IDE3Ny45OTggMTUxLjY1OCAxNzcuODYyIDE1MS43MjFDMTc3LjcyOSAxNTEuNzgzIDE3Ny42MTYgMTUxLjg2OCAxNzcuNTIyIDE1MS45NzVDMTc3LjQyOSAxNTIuMDc5IDE3Ny4zNTIgMTUyLjE5OSAxNzcuMjkyIDE1Mi4zMzRDMTc3LjIzNSAxNTIuNDY3IDE3Ny4xOTIgMTUyLjYwNSAxNzcuMTYzIDE1Mi43NDhWMTUzLjY4OUMxNzcuMjA1IDE1My44NzIgMTc3LjI3MiAxNTQuMDQ4IDE3Ny4zNjYgMTU0LjIxN0MxNzcuNDYyIDE1NC4zODMgMTc3LjU5IDE1NC41MiAxNzcuNzQ5IDE1NC42MjdDMTc3LjkxIDE1NC43MzQgMTc4LjExIDE1NC43ODcgMTc4LjM0NyAxNTQuNzg3QzE3OC41NDIgMTU0Ljc4NyAxNzguNzA4IDE1NC43NDggMTc4Ljg0NyAxNTQuNjdDMTc4Ljk4NyAxNTQuNTg5IDE3OS4xIDE1NC40NzkgMTc5LjE4NiAxNTQuMzM4QzE3OS4yNzUgMTU0LjE5NyAxNzkuMzQgMTU0LjAzNSAxNzkuMzgyIDE1My44NUMxNzkuNDIzIDE1My42NjUgMTc5LjQ0NCAxNTMuNDY3IDE3OS40NDQgMTUzLjI1NloiIGZpbGw9ImJsYWNrIiBmaWxsLW9wYWNpdHk9IjAuNTQiLz4KPC9nPgo8cGF0aCBkPSJNMTQuNSAxNy41SDI3SDYwLjVDNjEuMDUyMyAxNy41IDYxLjUgMTcuOTQ3NyA2MS41IDE4LjVWMjZDNjEuNSAyNi41NTIzIDYxLjk0NzcgMjcgNjIuNSAyN0g5NkM5Ni41NTIzIDI3IDk3IDI2LjU1MjMgOTcgMjZWMTIuNUM5NyAxMS45NDc3IDk3LjQ0NzcgMTEuNSA5OCAxMS41SDEzMkMxMzIuNTUyIDExLjUgMTMzIDExLjk0NzcgMTMzIDEyLjVWMzIuNUMxMzMgMzMuMDUyMyAxMzMuNDQ4IDMzLjUgMTM0IDMzLjVIMTY3LjVDMTY4LjA1MiAzMy41IDE2OC41IDMzLjA1MjMgMTY4LjUgMzIuNVYyNEMxNjguNSAyMy40NDc3IDE2OC45NDggMjMgMTY5LjUgMjNIMTg2IiBzdHJva2U9IiNGRkMxMDciIHN0cm9rZS13aWR0aD0iMiIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIi8+CjxwYXRoIGQ9Ik0xMSAxMTdIMjdWMTI3LjVINjJWNzkuMTYxMUg5N1YxMDFIMTMyVjg5LjI2NzVIMTY4LjVWMTIxLjVIMTg2IiBzdHJva2U9IiM0Q0FGNTAiIHN0cm9rZS13aWR0aD0iMiIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIi8+CjxwYXRoIGQ9Ik0xMiA2MC41SDI4VjY5LjVINjIuNVY0N0w5NyA0Ny4wMDAyVjU3LjAwMDJMMTMzIDU3VjY5LjVIMTY4SDE4NS41IiBzdHJva2U9IiMyMTk2RjMiIHN0cm9rZS13aWR0aD0iMiIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIi8+CjwvZz4KPGRlZnM+CjxjbGlwUGF0aCBpZD0iY2xpcDBfNDQzN185Njg1OCI+CjxyZWN0IHdpZHRoPSIyMDAiIGhlaWdodD0iMTYwIiBmaWxsPSJ3aGl0ZSIvPgo8L2NsaXBQYXRoPgo8Y2xpcFBhdGggaWQ9ImNsaXAxXzQ0MzdfOTY4NTgiPgo8cmVjdCB3aWR0aD0iMzUuNCIgaGVpZ2h0PSIxMC4zMjIiIGZpbGw9IndoaXRlIiB0cmFuc2Zvcm09InRyYW5zbGF0ZSg0NC4zOTk5IDE0NikiLz4KPC9jbGlwUGF0aD4KPGNsaXBQYXRoIGlkPSJjbGlwMl80NDM3Xzk2ODU4Ij4KPHJlY3Qgd2lkdGg9IjM1LjQiIGhlaWdodD0iMTAuMzIyIiBmaWxsPSJ3aGl0ZSIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoNzkuNzk5OCAxNDYpIi8+CjwvY2xpcFBhdGg+CjxjbGlwUGF0aCBpZD0iY2xpcDNfNDQzN185Njg1OCI+CjxyZWN0IHdpZHRoPSIzNS40IiBoZWlnaHQ9IjEwLjMyMiIgZmlsbD0id2hpdGUiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDExNS4yIDE0NikiLz4KPC9jbGlwUGF0aD4KPGNsaXBQYXRoIGlkPSJjbGlwNF80NDM3Xzk2ODU4Ij4KPHJlY3Qgd2lkdGg9IjM1LjQiIGhlaWdodD0iMTAuMzIyIiBmaWxsPSJ3aGl0ZSIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMTUwLjYgMTQ2KSIvPgo8L2NsaXBQYXRoPgo8L2RlZnM+Cjwvc3ZnPgo=", "description": "Displays changes to the state of the entity over time. For example, online and offline.", "descriptor": { "type": "timeseries", "sizeX": 8, "sizeY": 5, "resources": [], - "templateHtml": "\n", - "templateCss": ".legend {\n font-size: 13px;\n line-height: 10px;\n}\n\n.legend table { \n border-spacing: 0px;\n border-collapse: separate;\n}\n\n.mouse-events .flot-overlay {\n cursor: crosshair; \n}\n\n", - "controllerScript": "self.onInit = function() {\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.flotWidget.onDataUpdated();\n}\n\nself.onLatestDataUpdated = function() {\n self.ctx.$scope.flotWidget.onLatestDataUpdated();\n}\n\nself.onResize = function() {\n self.ctx.$scope.flotWidget.onResize();\n}\n\nself.onEditModeChanged = function() {\n self.ctx.$scope.flotWidget.onEditModeChanged();\n}\n\nself.onDestroy = function() {\n self.ctx.$scope.flotWidget.onDestroy();\n}\n\nself.typeParameters = function() {\n return {\n stateData: true,\n hasAdditionalLatestDataKeys: true,\n defaultDataKeysFunction: function() {\n return [{ name: 'temperature', label: 'Temperature', type: 'timeseries', units: '°C', decimals: 0 }];\n }\n };\n}\n\n", + "templateHtml": "\n", + "templateCss": "", + "controllerScript": "self.onInit = function() {\n self.ctx.$scope.timeSeriesChartWidget.onInit();\n};\n\nself.onDataUpdated = function() {\n self.ctx.$scope.timeSeriesChartWidget.onDataUpdated();\n}\n\nself.onLatestDataUpdated = function() {\n self.ctx.$scope.timeSeriesChartWidget.onLatestDataUpdated();\n}\n\nself.typeParameters = function() {\n return {\n stateData: true,\n chartType: 'state',\n previewWidth: '80%',\n embedTitlePanel: true,\n hasAdditionalLatestDataKeys: true,\n dataKeySettingsFunction: TbTimeSeriesChart.dataKeySettings('state'),\n defaultDataKeysFunction: function() {\n return [{ name: 'state', label: 'State', type: 'timeseries', units: '', decimals: 0 }];\n }\n };\n}\n", "settingsSchema": "{}", "dataKeySettingsSchema": "{}", - "settingsDirective": "tb-flot-line-widget-settings", - "dataKeySettingsDirective": "tb-flot-line-key-settings", - "latestDataKeySettingsDirective": "tb-flot-latest-key-settings", + "latestDataKeySettingsSchema": "{}", + "settingsDirective": "tb-time-series-chart-widget-settings", + "dataKeySettingsDirective": "tb-time-series-chart-key-settings", + "latestDataKeySettingsDirective": "", "hasBasicMode": true, - "basicModeDirective": "tb-flot-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Switch 1\",\"color\":\"#2196f3\",\"settings\":{\"showLines\":true,\"fillLines\":true,\"showPoints\":false,\"axisPosition\":\"left\",\"showSeparateAxis\":false},\"_hash\":0.8587686344902596,\"funcBody\":\"return Math.random() > 0.5 ? 1 : 0;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Switch 2\",\"color\":\"#ffc107\",\"settings\":{\"showLines\":true,\"fillLines\":false,\"showPoints\":false,\"axisPosition\":\"left\"},\"_hash\":0.12775350966079668,\"funcBody\":\"return Math.random() <= 0.5 ? 1 : 0;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"stack\":false,\"fontSize\":10,\"fontColor\":\"#545454\",\"showTooltip\":true,\"tooltipIndividual\":false,\"tooltipCumulative\":false,\"hideZeros\":false,\"tooltipValueFormatter\":\"if (value > 0 && value <= 1) {\\n return 'On';\\n} else if (value === 0) {\\n return 'Off';\\n} else {\\n return '';\\n}\",\"grid\":{\"verticalLines\":true,\"horizontalLines\":true,\"outlineWidth\":1,\"color\":\"#545454\",\"backgroundColor\":null,\"tickColor\":\"#DDDDDD\"},\"xaxis\":{\"title\":null,\"showLabels\":true,\"color\":\"#545454\"},\"yaxis\":{\"min\":0,\"max\":1.2,\"title\":null,\"showLabels\":true,\"color\":\"#545454\",\"tickSize\":null,\"tickDecimals\":0,\"ticksFormatter\":\"if (value > 0 && value <= 1) {\\n return 'On';\\n} else if (value === 0) {\\n return 'Off';\\n} else {\\n return '';\\n}\"},\"shadowSize\":4,\"smoothLines\":false,\"comparisonEnabled\":false,\"timeForComparison\":\"previousInterval\",\"comparisonCustomIntervalValue\":7200000,\"xaxisSecond\":{\"axisPosition\":\"top\",\"title\":null,\"showLabels\":true},\"showLegend\":true,\"legendConfig\":{\"direction\":\"column\",\"position\":\"right\",\"sortDataKeys\":false,\"showMin\":false,\"showMax\":false,\"showAvg\":false,\"showTotal\":false,\"showLatest\":false},\"customLegendEnabled\":false,\"dataKeysListForLabels\":[]},\"title\":\"State Chart\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"mobileHeight\":null,\"widgetStyle\":{},\"useDashboardTimewindow\":true,\"showLegend\":true,\"actions\":{},\"configMode\":\"basic\",\"showTitleIcon\":false,\"titleIcon\":\"waterfall_chart\",\"iconColor\":\"#1F6BDD\"}" + "basicModeDirective": "tb-time-series-chart-basic-config", + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Switch 1\",\"color\":\"#2196f3\",\"settings\":{\"yAxisId\":\"default\",\"showInLegend\":true,\"dataHiddenByDefault\":false,\"type\":\"line\",\"lineSettings\":{\"showLine\":true,\"step\":true,\"stepType\":\"end\",\"smooth\":false,\"lineType\":\"solid\",\"lineWidth\":2,\"showPoints\":false,\"showPointLabel\":false,\"pointLabelPosition\":\"top\",\"pointLabelFont\":{\"family\":\"Roboto\",\"size\":11,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"400\",\"lineHeight\":\"1\"},\"pointLabelColor\":\"rgba(0, 0, 0, 0.76)\",\"enablePointLabelBackground\":false,\"pointLabelBackground\":\"rgba(255,255,255,0.56)\",\"pointShape\":\"circle\",\"pointSize\":12,\"fillAreaSettings\":{\"type\":\"opacity\",\"opacity\":0.4,\"gradient\":{\"start\":100,\"end\":0}}},\"barSettings\":{\"showBorder\":false,\"borderWidth\":2,\"borderRadius\":0,\"showLabel\":false,\"labelPosition\":\"top\",\"labelFont\":{\"family\":\"Roboto\",\"size\":11,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"400\",\"lineHeight\":\"1\"},\"labelColor\":\"rgba(0, 0, 0, 0.76)\",\"enableLabelBackground\":false,\"labelBackground\":\"rgba(255,255,255,0.56)\",\"backgroundSettings\":{\"type\":\"none\",\"opacity\":0.4,\"gradient\":{\"start\":100,\"end\":0}}},\"tooltipValueFormatter\":\"\"},\"_hash\":0.676226248393859,\"funcBody\":\"return Math.random() > 0.5 ? true : false;\",\"decimals\":0,\"aggregationType\":null,\"usePostProcessing\":null,\"postFuncBody\":null,\"units\":null},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Switch 2\",\"color\":\"#FFC107\",\"settings\":{\"yAxisId\":\"default\",\"showInLegend\":true,\"dataHiddenByDefault\":false,\"type\":\"line\",\"lineSettings\":{\"showLine\":true,\"step\":true,\"stepType\":\"end\",\"smooth\":false,\"lineType\":\"solid\",\"lineWidth\":2,\"showPoints\":false,\"showPointLabel\":false,\"pointLabelPosition\":\"top\",\"pointLabelFont\":{\"family\":\"Roboto\",\"size\":11,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"400\",\"lineHeight\":\"1\"},\"pointLabelColor\":\"rgba(0, 0, 0, 0.76)\",\"enablePointLabelBackground\":false,\"pointLabelBackground\":\"rgba(255,255,255,0.56)\",\"pointShape\":\"circle\",\"pointSize\":12,\"fillAreaSettings\":{\"type\":\"none\",\"opacity\":0.4,\"gradient\":{\"start\":100,\"end\":0}}},\"barSettings\":{\"showBorder\":false,\"borderWidth\":2,\"borderRadius\":0,\"showLabel\":false,\"labelPosition\":\"top\",\"labelFont\":{\"family\":\"Roboto\",\"size\":11,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"400\",\"lineHeight\":\"1\"},\"labelColor\":\"rgba(0, 0, 0, 0.76)\",\"enableLabelBackground\":false,\"labelBackground\":\"rgba(255,255,255,0.56)\",\"backgroundSettings\":{\"type\":\"none\",\"opacity\":0.4,\"gradient\":{\"start\":100,\"end\":0}}},\"tooltipValueFormatter\":null},\"_hash\":0.1106990458957191,\"funcBody\":\"return Math.random() <= 0.5 ? true : false;\",\"decimals\":0,\"aggregationType\":null,\"usePostProcessing\":null,\"postFuncBody\":null,\"units\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]},\"latestDataKeys\":null}],\"timewindow\":{\"hideInterval\":false,\"hideLastInterval\":false,\"hideQuickInterval\":false,\"hideAggregation\":false,\"hideAggInterval\":false,\"hideTimezone\":false,\"selectedTab\":0,\"realtime\":{\"realtimeType\":0,\"timewindowMs\":60000,\"quickInterval\":\"CURRENT_DAY\",\"interval\":1000},\"aggregation\":{\"type\":\"NONE\",\"limit\":25000},\"timezone\":null},\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"yAxes\":{\"default\":{\"units\":null,\"decimals\":0,\"show\":true,\"label\":\"\",\"labelFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"600\",\"lineHeight\":\"1\"},\"labelColor\":\"rgba(0, 0, 0, 0.54)\",\"position\":\"left\",\"showTickLabels\":true,\"tickLabelFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"400\",\"lineHeight\":\"1\"},\"tickLabelColor\":\"rgba(0, 0, 0, 0.54)\",\"ticksFormatter\":\"\",\"showTicks\":true,\"ticksColor\":\"rgba(0, 0, 0, 0.54)\",\"showLine\":true,\"lineColor\":\"rgba(0, 0, 0, 0.54)\",\"showSplitLines\":true,\"splitLinesColor\":\"rgba(0, 0, 0, 0.12)\",\"id\":\"default\",\"order\":0,\"interval\":null,\"splitNumber\":null,\"min\":null,\"max\":null,\"ticksGenerator\":\"\"}},\"thresholds\":[],\"dataZoom\":true,\"stack\":false,\"xAxis\":{\"show\":true,\"label\":\"\",\"labelFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"600\",\"lineHeight\":\"1\"},\"labelColor\":\"rgba(0, 0, 0, 0.54)\",\"position\":\"bottom\",\"showTickLabels\":true,\"tickLabelFont\":{\"family\":\"Roboto\",\"size\":10,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"400\",\"lineHeight\":\"1\"},\"tickLabelColor\":\"rgba(0, 0, 0, 0.54)\",\"ticksFormat\":{},\"showTicks\":true,\"ticksColor\":\"rgba(0, 0, 0, 0.54)\",\"showLine\":true,\"lineColor\":\"rgba(0, 0, 0, 0.54)\",\"showSplitLines\":true,\"splitLinesColor\":\"rgba(0, 0, 0, 0.12)\"},\"noAggregationBarWidthSettings\":{\"strategy\":\"group\",\"groupWidth\":{\"relative\":true,\"relativeWidth\":2,\"absoluteWidth\":1000},\"barWidth\":{\"relative\":true,\"relativeWidth\":2,\"absoluteWidth\":1000}},\"showLegend\":true,\"legendLabelFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"400\",\"lineHeight\":\"16px\"},\"legendLabelColor\":\"rgba(0, 0, 0, 0.76)\",\"legendConfig\":{\"direction\":\"column\",\"position\":\"right\",\"sortDataKeys\":false,\"showMin\":false,\"showMax\":false,\"showAvg\":false,\"showTotal\":false,\"showLatest\":false},\"showTooltip\":true,\"tooltipTrigger\":\"axis\",\"tooltipValueFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"16px\"},\"tooltipValueColor\":\"rgba(0, 0, 0, 0.76)\",\"tooltipValueFormatter\":\"\",\"tooltipShowDate\":true,\"tooltipDateFormat\":{\"format\":null,\"lastUpdateAgo\":false,\"custom\":false,\"auto\":true,\"autoDateFormatSettings\":{\"millisecond\":\"MMM dd yyyy HH:mm:ss\"}},\"tooltipDateFont\":{\"family\":\"Roboto\",\"size\":11,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"400\",\"lineHeight\":\"16px\"},\"tooltipDateColor\":\"rgba(0, 0, 0, 0.76)\",\"tooltipDateInterval\":true,\"tooltipBackgroundColor\":\"rgba(255, 255, 255, 0.76)\",\"tooltipBackgroundBlur\":4,\"animation\":{\"animation\":true,\"animationThreshold\":2000,\"animationDuration\":500,\"animationEasing\":\"cubicOut\",\"animationDelay\":0,\"animationDurationUpdate\":300,\"animationEasingUpdate\":\"cubicOut\",\"animationDelayUpdate\":0},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"padding\":\"12px\",\"states\":[{\"label\":\"Off\",\"value\":0,\"sourceType\":\"constant\",\"sourceValue\":false},{\"label\":\"On\",\"value\":1,\"sourceType\":\"constant\",\"sourceValue\":true}]},\"title\":\"State chart\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":null,\"mobileHeight\":null,\"configMode\":\"basic\",\"actions\":{},\"showTitleIcon\":false,\"titleIcon\":\"thermostat\",\"iconColor\":\"#1F6BDD\",\"useDashboardTimewindow\":false,\"displayTimewindow\":true,\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"units\":\"\",\"decimals\":null,\"noDataDisplayMessage\":\"\",\"timewindowStyle\":{\"showIcon\":false,\"iconSize\":\"24px\",\"icon\":null,\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"color\":\"rgba(0, 0, 0, 0.38)\",\"displayTypePrefix\":true},\"margin\":\"0px\",\"borderRadius\":\"0px\",\"iconSize\":\"0px\"}" }, - "externalId": null, - "tags": null + "tags": [ + "chart", + "time series", + "time-series", + "state", + "state chart", + "online", + "offline" + ] } \ No newline at end of file diff --git a/application/src/main/data/json/system/widget_types/state_chart_deprecated.json b/application/src/main/data/json/system/widget_types/state_chart_deprecated.json new file mode 100644 index 0000000000..90fc886d62 --- /dev/null +++ b/application/src/main/data/json/system/widget_types/state_chart_deprecated.json @@ -0,0 +1,25 @@ +{ + "fqn": "charts.state_chart", + "name": "State Chart", + "deprecated": true, + "image": "tb-image:c3RhdGVfY2hhcnRfc3lzdGVtX3dpZGdldF9pbWFnZS5wbmc=:IlN0YXRlIENoYXJ0IiBzeXN0ZW0gd2lkZ2V0IGltYWdl;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAACgCAMAAAB+IdObAAAB9VBMVEUAAAAhlvMilvMymeE+nNVDoetInslNq/VZqOdpuPd3d3d5p5Z5suB6enp8fHyBgYGDg4ODqoqEhISIiIiKioqKuN2MjIyNjY2Ojo6QkJCRkZGSkpKUlJSVlZWWlpaXl5eYmJiZmZmampqcnJydnZ2enp6goKCgr3KhoaGioqKisXSjo6OjvsmkpKSlpaWlwdempqanp6eoqKiosGOo1vqpqampsGOqqqqqsWOrq6usrKysw9Wurq6wsLCysrK0tLS1tbW1wbK2tra3t7e4wau5ubm6urq7u7u8vLy9vb2+vr6/xbTAwMDAxbjCwsLC3ejDw8PExMTFxcXGxsbHx8fIv3jIyMjJycnKysrK5vzMzc7Nzc3Ozs7Pz8/QuDnRzcHS0tLT09PT39HU1NTU6/3VzLLV1dXV3sjW1tbX19fYuTHY2NjZ2dna0Ira2trb29vc3Nzex4De3t7f39/guyvhvCfhvCvh4eHh6Nbi4uLjvCTj4+PkyXbk5OTlvCTm5ubm693o6Ojpxl7q6urr6+vswTTs7Ozt7e3uvhju7u7vvhjv7+/wvxjw8PDx8fHyvxX0yTv09PT19fX29vb39/f4wyL4+Pj5+fn6+vr75J37+/v8whP8/Pz9/f39/v/+/v7/wQf/xRb/3HT/5JH/9tz/++/////APs7XAAAAAWJLR0Smt7AblQAAA1RJREFUeNrt3dlT01AUBvAEd8UNl2oLrdrFolZArUul1hWlVhQXFAUF1xaxIqi4FUQUrDsUClZi4nb+Th96S9NQkjDOOKZ+3wuZw8md++M25OXMlKOccJQnTUYocdRqdw8UAqTfOjG4lvr7uowOqbtAtG6o5OiGFoNDapuJHO8tFK4xOKS9msTVgoUiaQjPclnSl01snZ/y4ly2yBNZbRarbc+3yvdpt/hLawOPJp9ur7O0mRiEmzFkY1M6N/4I8qJpulzR2sBx1sgRjQuyA2pk+STdylw2VqR/FPFfWdcCvjhduiM7kVF2db1Nuspu7JGes+I76SRba7f0Wfnn/6F6It+mfo7m8TvYvh7KTiT3PQIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggPwXkFa7e7AQIP3WiUFzdl7LuBDFvJZxIYp5LeNCFPNaBn7Yc+e1KlheSYcrFCniL7LZqPl8cbp0TjavNcqu9rZJB9gdPdIJVnwjbWG1ndI95UzWM9V5rS9Ti3P4VWy1+5jXAgQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAMK+FeS3Ma2FeC/NamNfCCxEQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBA/hZkTKSkwSGn7VUJOmOLV241NiRSRZ2uYcvt+Io+Y0N83USm+MqGWEnU2JCqXqI1ZE+QhYiIy36tXR5INpOQbB5kftcmK85mtdey2iJekeWqX6/3kc+TLCQTrq6eEuZCgKTsbnNXBsIZOERJMedNQnry73XpW8UAKVhIyBPScVfQE9RqEaP7iMT6g+pdw7vKbxKl3IJqV8K7OUqXPHvG9UMGykk2lz1d4g5yvNXo6QqZiA41aHT5OwQTUWBJSrWrvlMwj1jFU2f1Q8K1dCysCUmVhdenNLvMRKZt3hGNriEnxfxlGqt9aPYRkb9bP6Q1SMFrmltMukKuMT2QpckWjc/WhP2lYB/TgjzdHyCKVM/gGXnsp+qY5hbba+hIVA/ETL0+9SepsoNiTs/igGpXZIhKqVv9QVJARIfXIWqfiM1nS+qBnHdae1V7Qss8ngEijRO5a6sMCAtdqv+HfgPwpNPbU6ipOwAAAABJRU5ErkJggg==", + "description": "Displays changes to the state of the entity over time. For example, online and offline.", + "descriptor": { + "type": "timeseries", + "sizeX": 8, + "sizeY": 5, + "resources": [], + "templateHtml": "\n", + "templateCss": ".legend {\n font-size: 13px;\n line-height: 10px;\n}\n\n.legend table { \n border-spacing: 0px;\n border-collapse: separate;\n}\n\n.mouse-events .flot-overlay {\n cursor: crosshair; \n}\n\n", + "controllerScript": "self.onInit = function() {\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.flotWidget.onDataUpdated();\n}\n\nself.onLatestDataUpdated = function() {\n self.ctx.$scope.flotWidget.onLatestDataUpdated();\n}\n\nself.onResize = function() {\n self.ctx.$scope.flotWidget.onResize();\n}\n\nself.onEditModeChanged = function() {\n self.ctx.$scope.flotWidget.onEditModeChanged();\n}\n\nself.onDestroy = function() {\n self.ctx.$scope.flotWidget.onDestroy();\n}\n\nself.typeParameters = function() {\n return {\n stateData: true,\n hasAdditionalLatestDataKeys: true,\n defaultDataKeysFunction: function() {\n return [{ name: 'temperature', label: 'Temperature', type: 'timeseries', units: '°C', decimals: 0 }];\n }\n };\n}\n\n", + "settingsSchema": "{}", + "dataKeySettingsSchema": "{}", + "settingsDirective": "tb-flot-line-widget-settings", + "dataKeySettingsDirective": "tb-flot-line-key-settings", + "latestDataKeySettingsDirective": "tb-flot-latest-key-settings", + "hasBasicMode": true, + "basicModeDirective": "tb-flot-basic-config", + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Switch 1\",\"color\":\"#2196f3\",\"settings\":{\"showLines\":true,\"fillLines\":true,\"showPoints\":false,\"axisPosition\":\"left\",\"showSeparateAxis\":false},\"_hash\":0.8587686344902596,\"funcBody\":\"return Math.random() > 0.5 ? 1 : 0;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Switch 2\",\"color\":\"#ffc107\",\"settings\":{\"showLines\":true,\"fillLines\":false,\"showPoints\":false,\"axisPosition\":\"left\"},\"_hash\":0.12775350966079668,\"funcBody\":\"return Math.random() <= 0.5 ? 1 : 0;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"stack\":false,\"fontSize\":10,\"fontColor\":\"#545454\",\"showTooltip\":true,\"tooltipIndividual\":false,\"tooltipCumulative\":false,\"hideZeros\":false,\"tooltipValueFormatter\":\"if (value > 0 && value <= 1) {\\n return 'On';\\n} else if (value === 0) {\\n return 'Off';\\n} else {\\n return '';\\n}\",\"grid\":{\"verticalLines\":true,\"horizontalLines\":true,\"outlineWidth\":1,\"color\":\"#545454\",\"backgroundColor\":null,\"tickColor\":\"#DDDDDD\"},\"xaxis\":{\"title\":null,\"showLabels\":true,\"color\":\"#545454\"},\"yaxis\":{\"min\":0,\"max\":1.2,\"title\":null,\"showLabels\":true,\"color\":\"#545454\",\"tickSize\":null,\"tickDecimals\":0,\"ticksFormatter\":\"if (value > 0 && value <= 1) {\\n return 'On';\\n} else if (value === 0) {\\n return 'Off';\\n} else {\\n return '';\\n}\"},\"shadowSize\":4,\"smoothLines\":false,\"comparisonEnabled\":false,\"timeForComparison\":\"previousInterval\",\"comparisonCustomIntervalValue\":7200000,\"xaxisSecond\":{\"axisPosition\":\"top\",\"title\":null,\"showLabels\":true},\"showLegend\":true,\"legendConfig\":{\"direction\":\"column\",\"position\":\"right\",\"sortDataKeys\":false,\"showMin\":false,\"showMax\":false,\"showAvg\":false,\"showTotal\":false,\"showLatest\":false},\"customLegendEnabled\":false,\"dataKeysListForLabels\":[]},\"title\":\"State Chart\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"mobileHeight\":null,\"widgetStyle\":{},\"useDashboardTimewindow\":true,\"showLegend\":true,\"actions\":{},\"configMode\":\"basic\",\"showTitleIcon\":false,\"titleIcon\":\"waterfall_chart\",\"iconColor\":\"#1F6BDD\"}" + }, + "tags": null +} \ No newline at end of file diff --git a/ui-ngx/src/app/core/api/data-aggregator.ts b/ui-ngx/src/app/core/api/data-aggregator.ts index 61cb1f1a85..5bce9115e8 100644 --- a/ui-ngx/src/app/core/api/data-aggregator.ts +++ b/ui-ngx/src/app/core/api/data-aggregator.ts @@ -378,6 +378,7 @@ export class DataAggregator { private updateStateBounds(keyData: DataSet, lastPrevKvPair: DataEntry) { if (lastPrevKvPair) { lastPrevKvPair[0] = this.startTs; + lastPrevKvPair[2] = [this.startTs, this.startTs]; } let firstKvPair: DataEntry; if (!keyData.length) { @@ -398,6 +399,7 @@ export class DataAggregator { if (lastKvPair[0] < this.endTs) { lastKvPair = deepClone(lastKvPair); lastKvPair[0] = this.endTs; + lastKvPair[2] = [this.endTs, this.endTs]; keyData.push(lastKvPair); } } diff --git a/ui-ngx/src/app/core/api/widget-api.models.ts b/ui-ngx/src/app/core/api/widget-api.models.ts index 54552c714f..1848733006 100644 --- a/ui-ngx/src/app/core/api/widget-api.models.ts +++ b/ui-ngx/src/app/core/api/widget-api.models.ts @@ -30,7 +30,7 @@ import { import { TimeService } from '../services/time.service'; import { DeviceService } from '../http/device.service'; import { UtilsService } from '@core/services/utils.service'; -import { Timewindow, WidgetTimewindow } from '@shared/models/time/time.models'; +import { SubscriptionTimewindow, Timewindow, WidgetTimewindow } from '@shared/models/time/time.models'; import { EntityType } from '@shared/models/entity-type.models'; import { HttpErrorResponse } from '@angular/common/http'; import { RafService } from '@core/services/raf.service'; @@ -303,6 +303,7 @@ export interface IWidgetSubscription { hiddenData?: Array<{data: DataSet}>; timeWindowConfig?: Timewindow; timeWindow?: WidgetTimewindow; + subscriptionTimewindow: SubscriptionTimewindow; onTimewindowChangeFunction?: (timewindow: Timewindow) => Timewindow; widgetTimewindowChanged$: Observable; comparisonEnabled?: boolean; diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/time-series-chart-basic-config.component.html b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/time-series-chart-basic-config.component.html index 94866da201..80e13a96a2 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/time-series-chart-basic-config.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/time-series-chart-basic-config.component.html @@ -41,6 +41,10 @@ [entityAliasId]="datasource?.entityAliasId" formControlName="series"> + + diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/time-series-chart-basic-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/time-series-chart-basic-config.component.ts index 31dca75c7e..8ab44db11d 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/time-series-chart-basic-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/time-series-chart-basic-config.component.ts @@ -46,8 +46,9 @@ import { } from '@home/components/widget/lib/chart/time-series-chart-widget.models'; import { EChartsTooltipTrigger } from '@home/components/widget/lib/chart/echarts-widget.models'; import { - TimeSeriesChartKeySettings, TimeSeriesChartThreshold, - TimeSeriesChartType, TimeSeriesChartYAxes, + TimeSeriesChartKeySettings, + TimeSeriesChartType, + TimeSeriesChartYAxes, TimeSeriesChartYAxisId } from '@home/components/widget/lib/chart/time-series-chart.models'; @@ -178,6 +179,9 @@ export class TimeSeriesChartBasicConfigComponent extends BasicWidgetConfigCompon actions: [configData.config.actions || {}, []] }); + if (this.chartType === TimeSeriesChartType.state) { + this.timeSeriesChartWidgetConfigForm.addControl('states', this.fb.control(settings.states, [])); + } } protected prepareOutputConfig(config: any): WidgetConfigComponentData { @@ -233,6 +237,10 @@ export class TimeSeriesChartBasicConfigComponent extends BasicWidgetConfigCompon this.widgetConfig.config.settings.padding = config.padding; this.widgetConfig.config.actions = config.actions; + + if (this.chartType === TimeSeriesChartType.state) { + this.widgetConfig.config.settings.states = config.states; + } return this.widgetConfig; } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/echarts-widget.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/chart/echarts-widget.models.ts index 81ffb457b9..0a1bb673cb 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/chart/echarts-widget.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/echarts-widget.models.ts @@ -17,7 +17,7 @@ import * as echarts from 'echarts/core'; import AxisModel from 'echarts/types/src/coord/cartesian/AxisModel'; import { estimateLabelUnionRect } from 'echarts/lib/coord/axisHelper'; -import { formatValue, isDefinedAndNotNull, isNumber } from '@core/utils'; +import { isDefinedAndNotNull, isFunction, isNumber } from '@core/utils'; import { DataZoomComponent, DataZoomComponentOption, @@ -42,7 +42,7 @@ import { } from 'echarts/charts'; import { LabelLayout } from 'echarts/features'; import { CanvasRenderer, SVGRenderer } from 'echarts/renderers'; -import { DataEntry, DataKey, DataSet } from '@shared/models/widget.models'; +import { DataEntry, DataKey, DataSet, Datasource, FormattedData } from '@shared/models/widget.models'; import { calculateAggIntervalWithWidgetTimeWindow, Interval, @@ -56,6 +56,7 @@ import GlobalModel from 'echarts/types/src/model/Global'; import Axis2D from 'echarts/types/src/coord/cartesian/Axis2D'; import SeriesModel from 'echarts/types/src/model/Series'; import { MarkLine2DDataItemOption } from 'echarts/types/src/component/marker/MarkLineModel'; +import { TimeAxisBaseOption } from 'echarts/types/src/coord/axisCommonTypes'; class EChartsModule { private initialized = false; @@ -101,14 +102,19 @@ export type EChartsDataItem = [number, any, number, number]; export type NamedDataSet = {name: string; value: EChartsDataItem}[]; +export type EChartsTooltipValueFormatFunction = (value: any, latestData: FormattedData, units?: string, decimals?: number) => string; + export type EChartsSeriesItem = { id: string; + datasource: Datasource; dataKey: DataKey; data: NamedDataSet; dataSet?: DataSet; enabled: boolean; units?: string; decimals?: number; + latestData?: FormattedData; + tooltipValueFormatFunction?: EChartsTooltipValueFormatFunction; }; export enum EChartsShape { @@ -270,7 +276,7 @@ const measureSymbolOffset = (symbol: string, symbolSize: any): number => { } else { return 0; } -} +}; export const measureThresholdOffset = (chart: ECharts, axisId: string, thresholdId: string, value: any): [number, number] => { const offset: [number, number] = [0,0]; @@ -395,7 +401,7 @@ export const getFocusedSeriesIndex = (chart: ECharts): number => { return -1; }; -export const toNamedData = (data: DataSet): NamedDataSet => { +export const toNamedData = (data: DataSet, valueConverter?: (value: any) => any): NamedDataSet => { if (!data?.length) { return []; } else { @@ -403,14 +409,43 @@ export const toNamedData = (data: DataSet): NamedDataSet => { const ts = isDefinedAndNotNull(d[2]) ? d[2][0] : d[0]; return { name: ts + '', - value: toEChartsDataItem(d) + value: toEChartsDataItem(d, valueConverter) }; }); } }; -const toEChartsDataItem = (entry: DataEntry): EChartsDataItem => { - const item: EChartsDataItem = [entry[0], entry[1], entry[0], entry[0]]; +const minDataTs = (dataSet: NamedDataSet): number => dataSet.length ? dataSet.map(data => + Number(data.name)).reduce((a, b) => Math.min(a, b)) : undefined; + +const maxDataTs = (dataSet: NamedDataSet): number => dataSet.length ? dataSet.map(data => + Number(data.name)).reduce((a, b) => Math.max(a, b)) : undefined; + +export const adjustTimeAxisExtentToData = (timeAxisOption: TimeAxisBaseOption, + dataItems: EChartsSeriesItem[], + defaultMin: number, + defaultMax: number): void => { + let min: number; + let max: number; + for (const item of dataItems) { + if (item.enabled) { + const minTs = minDataTs(item.data); + if (typeof minTs !== 'undefined') { + min = (typeof min !== 'undefined') ? Math.min(min, minTs) : minTs; + } + const maxTs = maxDataTs(item.data); + if (typeof maxTs !== 'undefined') { + max = (typeof max !== 'undefined') ? Math.max(max, maxTs) : maxTs; + } + } + } + timeAxisOption.min = (typeof min !== 'undefined') ? min : defaultMin; + timeAxisOption.max = (typeof max !== 'undefined') ? max : defaultMax; +}; + +const toEChartsDataItem = (entry: DataEntry, valueConverter?: (value: any) => any): EChartsDataItem => { + const value = valueConverter ? valueConverter(entry[1]) : entry[1]; + const item: EChartsDataItem = [entry[0], value, entry[0], entry[0]]; if (isDefinedAndNotNull(entry[2])) { item[2] = entry[2][0]; item[3] = entry[2][1]; @@ -436,6 +471,7 @@ export interface EChartsTooltipWidgetSettings { tooltipShowFocusedSeries?: boolean; tooltipValueFont: Font; tooltipValueColor: string; + tooltipValueFormatter?: string | EChartsTooltipValueFormatFunction; tooltipShowDate: boolean; tooltipDateInterval?: boolean; tooltipDateFormat: DateFormatSettings; @@ -445,12 +481,25 @@ export interface EChartsTooltipWidgetSettings { tooltipBackgroundBlur: number; } +export const createTooltipValueFormatFunction = + (tooltipValueFormatter: string | EChartsTooltipValueFormatFunction): EChartsTooltipValueFormatFunction => { + let tooltipValueFormatFunction: EChartsTooltipValueFormatFunction; + if (isFunction(tooltipValueFormatter)) { + tooltipValueFormatFunction = tooltipValueFormatter as EChartsTooltipValueFormatFunction; + } else if (typeof tooltipValueFormatter === 'string' && tooltipValueFormatter.length) { + try { + tooltipValueFormatFunction = + new Function('value', 'latestData', tooltipValueFormatter) as EChartsTooltipValueFormatFunction; + } catch (e) {} + } + return tooltipValueFormatFunction; +}; + export const echartsTooltipFormatter = (renderer: Renderer2, tooltipDateFormat: DateFormatProcessor, settings: EChartsTooltipWidgetSettings, params: CallbackDataParams[] | CallbackDataParams, - decimals: number, - units: string, + valueFormatFunction: EChartsTooltipValueFormatFunction, focusedSeriesIndex: number, series?: EChartsSeriesItem[], interval?: Interval): null | HTMLElement => { @@ -499,10 +548,12 @@ export const echartsTooltipFormatter = (renderer: Renderer2, seriesParams = params; } if (seriesParams) { - renderer.appendChild(tooltipElement, constructEchartsTooltipSeriesElement(renderer, settings, seriesParams, decimals, units, series)); + renderer.appendChild(tooltipElement, + constructEchartsTooltipSeriesElement(renderer, settings, seriesParams, valueFormatFunction, series)); } else if (Array.isArray(params)) { for (seriesParams of params) { - renderer.appendChild(tooltipElement, constructEchartsTooltipSeriesElement(renderer, settings, seriesParams, decimals, units, series)); + renderer.appendChild(tooltipElement, + constructEchartsTooltipSeriesElement(renderer, settings, seriesParams, valueFormatFunction, series)); } } return tooltipElement; @@ -511,8 +562,7 @@ export const echartsTooltipFormatter = (renderer: Renderer2, const constructEchartsTooltipSeriesElement = (renderer: Renderer2, settings: EChartsTooltipWidgetSettings, seriesParams: CallbackDataParams, - decimals: number, - units: string, + valueFormatFunction: EChartsTooltipValueFormatFunction, series?: EChartsSeriesItem[]): HTMLElement => { const labelValueElement: HTMLElement = renderer.createElement('div'); renderer.setStyle(labelValueElement, 'display', 'flex'); @@ -542,16 +592,25 @@ const constructEchartsTooltipSeriesElement = (renderer: Renderer2, renderer.setStyle(labelTextElement, 'color', 'rgba(0, 0, 0, 0.76)'); renderer.appendChild(labelElement, labelTextElement); const valueElement: HTMLElement = renderer.createElement('div'); - let formatDecimals = decimals; - let formatUnits = units; + let formatFunction = valueFormatFunction; + let latestData: FormattedData; + let units = ''; + let decimals = 0; if (series) { const item = series.find(s => s.id === seriesParams.seriesId); if (item) { - formatDecimals = item.decimals; - formatUnits = item.units; + if (item.tooltipValueFormatFunction) { + formatFunction = item.tooltipValueFormatFunction; + } + latestData = item.latestData; + units = item.units; + decimals = item.decimals; } } - const value = formatValue(seriesParams.value[1], formatDecimals, formatUnits, false); + if (!latestData) { + latestData = {} as FormattedData; + } + const value = formatFunction(seriesParams.value[1], latestData, units, decimals); renderer.appendChild(valueElement, renderer.createText(value)); renderer.setStyle(valueElement, 'flex', '1'); renderer.setStyle(valueElement, 'text-align', 'end'); diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart-state.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart-state.models.ts new file mode 100644 index 0000000000..9a72361632 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart-state.models.ts @@ -0,0 +1,109 @@ +/// +/// 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 { + TimeSeriesChartStateSettings, + TimeSeriesChartStateSourceType, + TimeSeriesChartTicksFormatter, + TimeSeriesChartTicksGenerator +} from '@home/components/widget/lib/chart/time-series-chart.models'; +import { UtilsService } from '@core/services/utils.service'; +import { EChartsTooltipValueFormatFunction } from '@home/components/widget/lib/chart/echarts-widget.models'; +import { FormattedData } from '@shared/models/widget.models'; +import { formatValue, isDefinedAndNotNull, isNumber, isNumeric } from '@core/utils'; +import { LabelFormatterCallback } from 'echarts'; + +export class TimeSeriesChartStateValueConverter { + + private readonly constantsMap = new Map(); + private readonly rangeStates: TimeSeriesChartStateSettings[] = []; + private readonly ticks: {value: number}[] = []; + private readonly labelsMap = new Map(); + + public readonly ticksGenerator: TimeSeriesChartTicksGenerator; + public readonly ticksFormatter: TimeSeriesChartTicksFormatter; + public readonly tooltipFormatter: EChartsTooltipValueFormatFunction; + public readonly labelFormatter: LabelFormatterCallback; + public readonly valueConverter: (value: any) => any; + + constructor(utils: UtilsService, + states: TimeSeriesChartStateSettings[]) { + const ticks: number[] = []; + for (const state of states) { + if (state.sourceType === TimeSeriesChartStateSourceType.constant) { + this.constantsMap.set(state.sourceValue, state.value); + } else { + this.rangeStates.push(state); + } + if (!ticks.includes(state.value)) { + ticks.push(state.value); + const label = utils.customTranslation(state.label, state.label); + this.labelsMap.set(state.value, label); + } + } + this.ticks = ticks.map(val => ({value: val})); + this.ticksGenerator = () => this.ticks; + this.ticksFormatter = (value: any) => { + const result = this.labelsMap.get(value); + return result || ''; + }; + this.tooltipFormatter = (value: any, latestData: FormattedData, units?: string, decimals?: number) => { + const result = this.labelsMap.get(value); + if (typeof result === 'string') { + return result; + } else { + return formatValue(value, decimals, units, false); + } + }; + this.labelFormatter = (params) => { + const value = params.value[1]; + const result = this.labelsMap.get(value); + if (typeof result === 'string') { + return `{value|${result}}`; + } else { + return undefined; + } + }; + this.valueConverter = (value: any) => { + let key = value; + if (key === 'true') { + key = true; + } else if (key === 'false') { + key = false; + } + const result = this.constantsMap.get(key); + if (typeof result === 'number') { + return result; + } else if (this.rangeStates.length && isDefinedAndNotNull(value) && isNumeric(value)) { + for (const state of this.rangeStates) { + const num = Number(value); + if (TimeSeriesChartStateValueConverter.constantRange(state) && state.sourceRangeFrom === num) { + return state.value; + } else if ((!isNumber(state.sourceRangeFrom) || num >= state.sourceRangeFrom) && + (!isNumber(state.sourceRangeTo) || num < state.sourceRangeTo)) { + return state.value; + } + } + } + return value; + }; + } + + static constantRange(state: TimeSeriesChartStateSettings): boolean { + return isNumber(state.sourceRangeFrom) && isNumber(state.sourceRangeTo) && state.sourceRangeFrom === state.sourceRangeTo; + } + +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.models.ts index 3589d69d4f..252a284aaa 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.models.ts @@ -17,8 +17,10 @@ import { ECharts, EChartsOption, - EChartsSeriesItem, EChartsShape, + EChartsSeriesItem, + EChartsShape, EChartsTooltipTrigger, + EChartsTooltipValueFormatFunction, EChartsTooltipWidgetSettings, measureThresholdOffset, timeAxisBandWidthCalculator @@ -41,7 +43,8 @@ import { CustomSeriesOption, LineSeriesOption } from 'echarts/charts'; import { formatValue, isDefinedAndNotNull, - isFunction, isNumber, + isFunction, + isNumber, isNumeric, isUndefined, isUndefinedOrNull, @@ -51,7 +54,8 @@ import { import { LinearGradientObject } from 'zrender/lib/graphic/LinearGradient'; import tinycolor from 'tinycolor2'; import { ValueAxisBaseOption } from 'echarts/types/src/coord/axisCommonTypes'; -import { LabelFormatterCallback, LabelLayoutOption, SeriesLabelOption } from 'echarts/types/src/util/types'; +import { LabelLayoutOption, SeriesLabelOption } from 'echarts/types/src/util/types'; +import { LabelFormatterCallback } from 'echarts'; import { BarRenderContext, BarRenderSharedContext, @@ -70,7 +74,8 @@ export enum TimeSeriesChartType { default = 'default', line = 'line', bar = 'bar', - point = 'point' + point = 'point', + state = 'state' } export const timeSeriesChartTypeTranslations = new Map( @@ -200,6 +205,21 @@ export const timeSeriesThresholdTypeTranslations = new Map( + [ + [TimeSeriesChartStateSourceType.constant, 'widgets.time-series-chart.state.type-constant'], + [TimeSeriesChartStateSourceType.range, 'widgets.time-series-chart.state.type-range'] + ] +); + export enum SeriesFillType { none = 'none', opacity = 'opacity', @@ -639,6 +659,39 @@ export interface TimeSeriesChartVisualMapSettings { pieces: TimeSeriesChartVisualMapPiece[]; } +export interface TimeSeriesChartStateSettings { + label: string; + value: number; + sourceType: TimeSeriesChartStateSourceType; + sourceValue?: any; + sourceRangeFrom?: number; + sourceRangeTo?: number; +} + +export const timeSeriesChartStateValid = (state: TimeSeriesChartStateSettings): boolean => { + if (isUndefinedOrNull(state.value) || !state.sourceType) { + return false; + } + switch (state.sourceType) { + case TimeSeriesChartStateSourceType.constant: + if (isUndefinedOrNull(state.sourceValue)) { + return false; + } + break; + } + return true; +}; + +export const timeSeriesChartStateValidator = (control: AbstractControl): ValidationErrors | null => { + const state: TimeSeriesChartStateSettings = control.value; + if (!timeSeriesChartStateValid(state)) { + return { + state: true + }; + } + return null; +}; + export interface TimeSeriesChartSettings extends EChartsTooltipWidgetSettings { thresholds: TimeSeriesChartThreshold[]; darkMode: boolean; @@ -650,6 +703,7 @@ export interface TimeSeriesChartSettings extends EChartsTooltipWidgetSettings { barWidthSettings: TimeSeriesChartBarWidthSettings; noAggregationBarWidthSettings: TimeSeriesChartNoAggregationBarWidthSettings; visualMapSettings?: TimeSeriesChartVisualMapSettings; + states?: TimeSeriesChartStateSettings[]; } export const timeSeriesChartDefaultSettings: TimeSeriesChartSettings = { @@ -751,6 +805,7 @@ export interface TimeSeriesChartKeySettings { type: TimeSeriesChartSeriesType; lineSettings: LineSeriesSettings; barSettings: BarSeriesSettings; + tooltipValueFormatter?: string | EChartsTooltipValueFormatFunction; } export const timeSeriesChartKeyDefaultSettings: TimeSeriesChartKeySettings = { @@ -1359,28 +1414,28 @@ const createSeriesLabelOption = (item: TimeSeriesChartDataItem, show: boolean, if (show) { labelStyle = createChartTextStyle(labelFont, labelColor, darkMode, 'series.label', labelColorFill); } - let formatter: LabelFormatterCallback; - if (isFunction(labelFormatter)) { - formatter = labelFormatter as LabelFormatterCallback; - } else if (labelFormatter?.length) { - const formatFunction = parseFunction(labelFormatter, ['value']); - formatter = (params): string => { - let result: string; - try { - result = formatFunction(params.value[1]); - } catch (_e) { - } - if (isUndefined(result)) { - result = formatValue(params.value[1], item.decimals, item.units, false); - } - return `{value|${result}}`; - }; - } else { - formatter = (params): string => { - const value = formatValue(params.value[1], item.decimals, item.units, false); - return `{value|${value}}`; - }; + let formatFunction: (...args: any[]) => any; + if (typeof labelFormatter === 'string' && labelFormatter.length) { + formatFunction = parseFunction(labelFormatter, ['value']); } + const formatter: LabelFormatterCallback = (params): string => { + let result: string; + if (typeof labelFormatter === 'string') { + if (formatFunction) { + try { + result = formatFunction(params.value[1]); + } catch (_e) { + } + } + } else if (isFunction(labelFormatter)) { + result = labelFormatter(params); + } + if (isUndefined(result)) { + result = formatValue(params.value[1], item.decimals, item.units, false); + result = `{value|${result}}`; + } + return result; + }; const labelOption: SeriesLabelOption = { show, position, diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.ts b/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.ts index 00f2c55d3f..320a683363 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.ts @@ -23,6 +23,7 @@ import { createTimeSeriesYAxis, defaultTimeSeriesChartYAxisSettings, generateChartData, + LineSeriesStepType, parseThresholdData, SeriesLabelPosition, TimeSeriesChartDataItem, @@ -44,13 +45,17 @@ import { } from '@home/components/widget/lib/chart/time-series-chart.models'; import { ResizeObserver } from '@juggle/resize-observer'; import { + adjustTimeAxisExtentToData, calculateXAxisHeight, calculateYAxisWidth, + createTooltipValueFormatFunction, ECharts, echartsModule, - EChartsOption, EChartsShape, + EChartsOption, + EChartsShape, echartsTooltipFormatter, EChartsTooltipTrigger, + EChartsTooltipValueFormatFunction, getAxisExtent, getFocusedSeriesIndex, measureXAxisNameHeight, @@ -58,12 +63,11 @@ import { toNamedData } from '@home/components/widget/lib/chart/echarts-widget.models'; import { DateFormatProcessor } from '@shared/models/widget-settings.models'; -import { isDefinedAndNotNull, isEqual, mergeDeep } from '@core/utils'; -import { DataKey, Datasource, DatasourceType, widgetType } from '@shared/models/widget.models'; +import { formattedDataFormDatasourceData, formatValue, isDefinedAndNotNull, isEqual, mergeDeep } from '@core/utils'; +import { DataKey, Datasource, DatasourceType, FormattedData, widgetType } from '@shared/models/widget.models'; import * as echarts from 'echarts/core'; import { CallbackDataParams, PiecewiseVisualMapOption } from 'echarts/types/dist/shared'; import { Renderer2 } from '@angular/core'; -import { CustomSeriesOption, LineSeriesOption } from 'echarts/charts'; import { BehaviorSubject } from 'rxjs'; import { AggregationType } from '@shared/models/time/time.models'; import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; @@ -71,6 +75,7 @@ import { WidgetSubscriptionOptions } from '@core/api/widget-api.models'; import { DataKeySettingsFunction } from '@home/components/widget/config/data-keys.component.models'; import { DeepPartial } from '@shared/models/common'; import { BarRenderSharedContext } from '@home/components/widget/lib/chart/time-series-chart-bar.models'; +import { TimeSeriesChartStateValueConverter } from '@home/components/widget/lib/chart/time-series-chart-state.models'; export class TbTimeSeriesChart { @@ -89,6 +94,13 @@ export class TbTimeSeriesChart { settings.lineSettings.showPoints = true; settings.lineSettings.pointShape = EChartsShape.circle; settings.lineSettings.pointSize = 8; + } else if (type === TimeSeriesChartType.state) { + settings.type = TimeSeriesChartSeriesType.line; + settings.lineSettings.showLine = true; + settings.lineSettings.step = true; + settings.lineSettings.stepType = LineSeriesStepType.end; + settings.lineSettings.pointShape = EChartsShape.circle; + settings.lineSettings.pointSize = 12; } return settings; } @@ -97,7 +109,11 @@ export class TbTimeSeriesChart { } private get noAggregation(): boolean { - return this.ctx.defaultSubscription.timeWindowConfig?.aggregation?.type === AggregationType.NONE; + return this.ctx.defaultSubscription.subscriptionTimewindow?.aggregation?.type === AggregationType.NONE; + } + + private get stateData(): boolean { + return this.ctx.defaultSubscription.subscriptionTimewindow?.aggregation?.stateData === true; } private readonly shapeResize$: ResizeObserver; @@ -115,6 +131,8 @@ export class TbTimeSeriesChart { private timeSeriesChartOptions: EChartsOption; private readonly tooltipDateFormat: DateFormatProcessor; + private readonly tooltipValueFormatFunction: EChartsTooltipValueFormatFunction; + private readonly stateValueConverter: TimeSeriesChartStateValueConverter; private yMinSubject = new BehaviorSubject(-1); private yMaxSubject = new BehaviorSubject(1); @@ -131,6 +149,8 @@ export class TbTimeSeriesChart { private barRenderSharedContext: BarRenderSharedContext; + private latestData: FormattedData[] = []; + yMin$ = this.yMinSubject.asObservable(); yMax$ = this.yMaxSubject.asObservable(); @@ -143,6 +163,10 @@ export class TbTimeSeriesChart { this.settings = mergeDeep({} as TimeSeriesChartSettings, timeSeriesChartDefaultSettings, this.inputSettings as TimeSeriesChartSettings); + if (this.settings.states && this.settings.states.length) { + this.stateValueConverter = new TimeSeriesChartStateValueConverter(this.ctx.dashboard.utils, this.settings.states); + this.tooltipValueFormatFunction = this.stateValueConverter.tooltipFormatter; + } const $dashboardPageElement = this.ctx.$containerParent.parents('.tb-dashboard-page'); const dashboardPageElement = $dashboardPageElement.length ? $($dashboardPageElement[$dashboardPageElement.length-1]) : null; this.darkMode = this.settings.darkMode || dashboardPageElement?.hasClass('dark'); @@ -150,8 +174,17 @@ export class TbTimeSeriesChart { this.setupData(); this.setupThresholds(); this.setupVisualMap(); - if (this.settings.showTooltip && this.settings.tooltipShowDate) { - this.tooltipDateFormat = DateFormatProcessor.fromSettings(this.ctx.$injector, this.settings.tooltipDateFormat); + if (this.settings.showTooltip) { + if (this.settings.tooltipShowDate) { + this.tooltipDateFormat = DateFormatProcessor.fromSettings(this.ctx.$injector, this.settings.tooltipDateFormat); + } + if (!this.tooltipValueFormatFunction) { + this.tooltipValueFormatFunction = + createTooltipValueFormatFunction(this.settings.tooltipValueFormatter); + if (!this.tooltipValueFormatFunction) { + this.tooltipValueFormatFunction = (value, latestData, units, decimals) => formatValue(value, decimals, units, false); + } + } } this.onResize(); if (this.autoResize) { @@ -178,7 +211,7 @@ export class TbTimeSeriesChart { const datasourceData = this.ctx.data ? this.ctx.data.find(d => d.dataKey === item.dataKey) : null; if (!isEqual(item.dataSet, datasourceData?.data)) { item.dataSet = datasourceData?.data; - item.data = datasourceData?.data ? toNamedData(datasourceData.data) : []; + item.data = datasourceData?.data ? toNamedData(datasourceData.data, this.stateValueConverter?.valueConverter) : []; } } this.onResize(); @@ -205,6 +238,14 @@ export class TbTimeSeriesChart { public latestUpdated() { let update = false; if (this.ctx.latestData) { + this.latestData = formattedDataFormDatasourceData(this.ctx.latestData); + for (const item of this.dataItems) { + let latestData = this.latestData.find(data => data.$datasource === item.datasource); + if (!latestData) { + latestData = {} as FormattedData; + } + item.latestData = latestData; + } for (const item of this.thresholdItems) { if (item.settings.type === TimeSeriesChartThresholdType.latestKey && item.latestDataKey) { const data = this.ctx.latestData.find(d => d.dataKey === item.latestDataKey); @@ -253,7 +294,7 @@ export class TbTimeSeriesChart { seriesId: dataItem.id }); } - this.timeSeriesChartOptions.series = this.updateSeries(); + this.updateSeries(); const mergeList = ['series']; if (this.updateYAxisScale(this.yAxisList)) { this.timeSeriesChartOptions.yAxis = this.yAxisList.map(axis => axis.option); @@ -338,9 +379,12 @@ export class TbTimeSeriesChart { .includes(keySettings.barSettings.labelPosition as SeriesLabelPosition))) { this.topPointLabels = true; } + if (this.stateValueConverter && keySettings.type === TimeSeriesChartSeriesType.line) { + keySettings.lineSettings.pointLabelFormatter = this.stateValueConverter.labelFormatter; + } dataKey.settings = keySettings; const datasourceData = this.ctx.data ? this.ctx.data.find(d => d.dataKey === dataKey) : null; - const namedData = datasourceData?.data ? toNamedData(datasourceData.data) : []; + const namedData = datasourceData?.data ? toNamedData(datasourceData.data, this.stateValueConverter?.valueConverter) : []; const units = dataKey.units && dataKey.units.length ? dataKey.units : this.ctx.units; const decimals = isDefinedAndNotNull(dataKey.decimals) ? dataKey.decimals : (isDefinedAndNotNull(this.ctx.decimals) ? this.ctx.decimals : 2); @@ -354,9 +398,11 @@ export class TbTimeSeriesChart { decimals, yAxisId, yAxisIndex: this.getYAxisIndex(yAxisId), + datasource, dataKey, data: namedData, - enabled: !keySettings.dataHiddenByDefault + enabled: !keySettings.dataHiddenByDefault, + tooltipValueFormatFunction: createTooltipValueFormatFunction(keySettings.tooltipValueFormatter) }); } } @@ -451,6 +497,10 @@ export class TbTimeSeriesChart { const units = axisSettings.units && axisSettings.units.length ? axisSettings.units : this.ctx.units; const decimals = isDefinedAndNotNull(axisSettings.decimals) ? axisSettings.decimals : (isDefinedAndNotNull(this.ctx.decimals) ? this.ctx.decimals : 2); + if (this.stateValueConverter) { + axisSettings.ticksGenerator = this.stateValueConverter.ticksGenerator; + axisSettings.ticksFormatter = this.stateValueConverter.ticksFormatter; + } const yAxis = createTimeSeriesYAxis(units, decimals, axisSettings, this.darkMode); this.yAxisList.push(yAxis); } @@ -528,7 +578,7 @@ export class TbTimeSeriesChart { }, formatter: (params: CallbackDataParams[]) => this.settings.showTooltip ? echartsTooltipFormatter(this.renderer, this.tooltipDateFormat, - this.settings, params, 0, '', + this.settings, params, this.tooltipValueFormatFunction, this.settings.tooltipShowFocusedSeries ? getFocusedSeriesIndex(this.timeSeriesChart) : -1, this.dataItems, this.noAggregation ? null : this.ctx.timeWindow.interval) : undefined, padding: [8, 12], @@ -577,7 +627,7 @@ export class TbTimeSeriesChart { this.timeSeriesChartOptions.xAxis[0].tbTimeWindow = this.ctx.defaultSubscription.timeWindow; - this.timeSeriesChartOptions.series = this.updateSeries(); + this.updateSeries(); if (this.updateYAxisScale(this.yAxisList)) { this.timeSeriesChartOptions.yAxis = this.yAxisList.map(axis => axis.option); } @@ -593,7 +643,7 @@ export class TbTimeSeriesChart { } private updateSeriesData(updateScale = false): void { - this.timeSeriesChartOptions.series = this.updateSeries(); + this.updateSeries(); if (updateScale && this.updateYAxisScale(this.yAxisList)) { this.timeSeriesChartOptions.yAxis = this.yAxisList.map(axis => axis.option); } @@ -601,11 +651,16 @@ export class TbTimeSeriesChart { this.updateAxes(); } - private updateSeries(): Array { - return generateChartData(this.dataItems, this.thresholdItems, + private updateSeries(): void { + this.timeSeriesChartOptions.series = generateChartData(this.dataItems, this.thresholdItems, this.settings.stack, this.noAggregation, this.barRenderSharedContext, this.darkMode); + if (this.stateData) { + adjustTimeAxisExtentToData(this.timeSeriesChartOptions.xAxis[0], this.dataItems, + this.ctx.defaultSubscription.timeWindow.minTime, + this.ctx.defaultSubscription.timeWindow.maxTime); + } } private updateAxes(lazy = true) { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-key-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-key-settings.component.html index 5f0fbb9e37..6d015e04fa 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-key-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-key-settings.component.html @@ -56,6 +56,16 @@ formControlName="barSettings"> +

+
widgets.chart.tooltip-settings
+ + +
{{ timeSeriesChartTypeTranslations.get(chartType) | translate }}
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-key-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-key-settings.component.ts index d1ac428396..3b49476a59 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-key-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-key-settings.component.ts @@ -29,6 +29,7 @@ import { } from '@home/components/widget/lib/chart/time-series-chart.models'; import { WidgetConfigComponentData } from '@home/models/widget-component.models'; import { TimeSeriesChartWidgetSettings } from '@home/components/widget/lib/chart/time-series-chart-widget.models'; +import { WidgetService } from '@core/http/widget.service'; @Component({ selector: 'tb-time-series-chart-key-settings', @@ -53,7 +54,10 @@ export class TimeSeriesChartKeySettingsComponent extends WidgetSettingsComponent yAxisIds: TimeSeriesChartYAxisId[]; + functionScopeVariables = this.widgetService.getWidgetScopeVariables(); + constructor(protected store: Store, + private widgetService: WidgetService, private fb: UntypedFormBuilder) { super(store); } @@ -88,7 +92,8 @@ export class TimeSeriesChartKeySettingsComponent extends WidgetSettingsComponent dataHiddenByDefault: [seriesSettings.dataHiddenByDefault, []], type: [seriesSettings.type, []], lineSettings: [seriesSettings.lineSettings, []], - barSettings: [seriesSettings.barSettings, []] + barSettings: [seriesSettings.barSettings, []], + tooltipValueFormatter: [seriesSettings.tooltipValueFormatter, []], }); } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-line-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-line-settings.component.html index cc3102c475..f20f1323b3 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-line-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-line-settings.component.html @@ -18,7 +18,7 @@
widgets.time-series-chart.series.line.line
-
+
{{ 'widgets.time-series-chart.series.line.show-line' | translate }} @@ -35,7 +35,7 @@
-
+
{{ 'widgets.time-series-chart.series.line.smooth-line' | translate }} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-line-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-line-settings.component.ts index 5b4701bf07..1d57220b4e 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-line-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-line-settings.component.ts @@ -147,12 +147,22 @@ export class TimeSeriesChartLineSettingsComponent implements OnInit, ControlValu } private updateValidators() { + const state = this.chartType === TimeSeriesChartType.state; const showLine: boolean = this.lineSettingsFormGroup.get('showLine').value; const step: boolean = this.lineSettingsFormGroup.get('step').value; const showPointLabel: boolean = this.lineSettingsFormGroup.get('showPointLabel').value; const enablePointLabelBackground: boolean = this.lineSettingsFormGroup.get('enablePointLabelBackground').value; + if (state) { + this.lineSettingsFormGroup.get('showLine').disable({emitEvent: false}); + } else { + this.lineSettingsFormGroup.get('showLine').enable({emitEvent: false}); + } if (showLine) { - this.lineSettingsFormGroup.get('step').enable({emitEvent: false}); + if (state) { + this.lineSettingsFormGroup.get('step').disable({emitEvent: false}); + } else { + this.lineSettingsFormGroup.get('step').enable({emitEvent: false}); + } if (step) { this.lineSettingsFormGroup.get('stepType').enable({emitEvent: false}); this.lineSettingsFormGroup.get('smooth').disable({emitEvent: false}); diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-widget-settings.component.html index fbd960be18..d98f9ee319 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-widget-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-widget-settings.component.html @@ -16,6 +16,10 @@ --> + +
+ +
{{ 'tooltip.date' | translate }} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-widget-settings.component.ts index 6edab7ecf2..01ecca2944 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-widget-settings.component.ts @@ -39,6 +39,7 @@ import { TimeSeriesChartYAxisId } from '@home/components/widget/lib/chart/time-series-chart.models'; import { WidgetConfigComponentData } from '@home/models/widget-component.models'; +import { WidgetService } from '@core/http/widget.service'; @Component({ selector: 'tb-time-series-chart-widget-settings', @@ -77,8 +78,11 @@ export class TimeSeriesChartWidgetSettingsComponent extends WidgetSettingsCompon chartType: TimeSeriesChartType = TimeSeriesChartType.default; + functionScopeVariables = this.widgetService.getWidgetScopeVariables(); + constructor(protected store: Store, private $injector: Injector, + private widgetService: WidgetService, private fb: UntypedFormBuilder) { super(store); } @@ -129,6 +133,7 @@ export class TimeSeriesChartWidgetSettingsComponent extends WidgetSettingsCompon tooltipTrigger: [settings.tooltipTrigger, []], tooltipValueFont: [settings.tooltipValueFont, []], tooltipValueColor: [settings.tooltipValueColor, []], + tooltipValueFormatter: [settings.tooltipValueFormatter, []], tooltipShowDate: [settings.tooltipShowDate, []], tooltipDateFormat: [settings.tooltipDateFormat, []], tooltipDateFont: [settings.tooltipDateFont, []], @@ -143,6 +148,9 @@ export class TimeSeriesChartWidgetSettingsComponent extends WidgetSettingsCompon background: [settings.background, []], padding: [settings.padding, []] }); + if (this.chartType === TimeSeriesChartType.state) { + this.timeSeriesChartWidgetSettingsForm.addControl('states', this.fb.control(settings.states, [])); + } } protected validatorTriggers(): string[] { @@ -168,6 +176,7 @@ export class TimeSeriesChartWidgetSettingsComponent extends WidgetSettingsCompon this.timeSeriesChartWidgetSettingsForm.get('tooltipTrigger').enable(); this.timeSeriesChartWidgetSettingsForm.get('tooltipValueFont').enable(); this.timeSeriesChartWidgetSettingsForm.get('tooltipValueColor').enable(); + this.timeSeriesChartWidgetSettingsForm.get('tooltipValueFormatter').enable(); this.timeSeriesChartWidgetSettingsForm.get('tooltipShowDate').enable({emitEvent: false}); this.timeSeriesChartWidgetSettingsForm.get('tooltipBackgroundColor').enable(); this.timeSeriesChartWidgetSettingsForm.get('tooltipBackgroundBlur').enable(); @@ -185,6 +194,7 @@ export class TimeSeriesChartWidgetSettingsComponent extends WidgetSettingsCompon } else { this.timeSeriesChartWidgetSettingsForm.get('tooltipValueFont').disable(); this.timeSeriesChartWidgetSettingsForm.get('tooltipValueColor').disable(); + this.timeSeriesChartWidgetSettingsForm.get('tooltipValueFormatter').disable(); this.timeSeriesChartWidgetSettingsForm.get('tooltipShowDate').disable({emitEvent: false}); this.timeSeriesChartWidgetSettingsForm.get('tooltipDateFormat').disable(); this.timeSeriesChartWidgetSettingsForm.get('tooltipDateFont').disable(); diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-axis-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-axis-settings.component.html index 66b6c9896d..cd240ba8e4 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-axis-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-axis-settings.component.html @@ -68,6 +68,15 @@
+
+ + +
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-axis-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-axis-settings.component.ts index ac49b8137d..91fe61caa3 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-axis-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-axis-settings.component.ts @@ -85,7 +85,7 @@ export class TimeSeriesChartAxisSettingsComponent implements OnInit, ControlValu public axisSettingsFormGroup: UntypedFormGroup; constructor(private fb: UntypedFormBuilder, - private widgetService: WidgetService,) { + private widgetService: WidgetService) { } ngOnInit(): void { @@ -113,6 +113,7 @@ export class TimeSeriesChartAxisSettingsComponent implements OnInit, ControlValu this.axisSettingsFormGroup.addControl('units', this.fb.control(null, [])); this.axisSettingsFormGroup.addControl('decimals', this.fb.control(null, [Validators.min(0)])); this.axisSettingsFormGroup.addControl('ticksFormatter', this.fb.control(null, [])); + this.axisSettingsFormGroup.addControl('ticksGenerator', this.fb.control(null, [])); this.axisSettingsFormGroup.addControl('interval', this.fb.control(null, [Validators.min(0)])); this.axisSettingsFormGroup.addControl('splitNumber', this.fb.control(null, [Validators.min(1)])); this.axisSettingsFormGroup.addControl('min', this.fb.control(null, [])); diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-state-row.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-state-row.component.html new file mode 100644 index 0000000000..a2e11c3a8e --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-state-row.component.html @@ -0,0 +1,62 @@ + +
+ + + + + + + + + + {{ timeSeriesStateSourceTypeTranslations.get(type) | translate }} + + + +
+ + + + + + + + +
+
+ +
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-state-row.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-state-row.component.scss new file mode 100644 index 0000000000..247eb5d28f --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-state-row.component.scss @@ -0,0 +1,51 @@ +/** + * 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 '../../../../../../../../../scss/constants'; + +.tb-form-table-row.tb-time-series-state-row { + @media #{$mat-lt-md} { + align-items: flex-start; + } + .tb-state-label-field { + flex: 1; + min-width: 70px; + } + .tb-state-value-field { + width: 80px; + min-width: 80px; + } + .tb-state-source-field { + width: 100px; + min-width: 100px; + } + .tb-state-source-value-field { + flex: 1; + min-width: 200px; + display: flex; + flex-direction: column; + gap: 8px; + @media #{$mat-gt-sm} { + min-width: 358px; + flex-direction: row; + align-items: center; + gap: 12px; + } + .tb-inline-field { + flex: 1; + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-state-row.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-state-row.component.ts new file mode 100644 index 0000000000..97c87432c2 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-state-row.component.ts @@ -0,0 +1,138 @@ +/// +/// 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 { + ChangeDetectorRef, + Component, + EventEmitter, + forwardRef, + Input, + OnInit, + Output, + ViewEncapsulation +} from '@angular/core'; +import { + ControlValueAccessor, + NG_VALUE_ACCESSOR, + UntypedFormBuilder, + UntypedFormGroup, + Validators +} from '@angular/forms'; +import { + TimeSeriesChartStateSettings, + TimeSeriesChartStateSourceType, + timeSeriesStateSourceTypes, + timeSeriesStateSourceTypeTranslations +} from '@home/components/widget/lib/chart/time-series-chart.models'; + +@Component({ + selector: 'tb-time-series-chart-state-row', + templateUrl: './time-series-chart-state-row.component.html', + styleUrls: ['./time-series-chart-state-row.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => TimeSeriesChartStateRowComponent), + multi: true + } + ], + encapsulation: ViewEncapsulation.None +}) +export class TimeSeriesChartStateRowComponent implements ControlValueAccessor, OnInit { + + TimeSeriesChartStateSourceType = TimeSeriesChartStateSourceType; + + timeSeriesStateSourceTypes = timeSeriesStateSourceTypes; + + timeSeriesStateSourceTypeTranslations = timeSeriesStateSourceTypeTranslations; + + @Input() + disabled: boolean; + + @Output() + stateRemoved = new EventEmitter(); + + stateFormGroup: UntypedFormGroup; + + modelValue: TimeSeriesChartStateSettings; + + private propagateChange = (_val: any) => {}; + + constructor(private fb: UntypedFormBuilder, + private cd: ChangeDetectorRef) { + } + + ngOnInit() { + this.stateFormGroup = this.fb.group({ + label: [null, []], + value: [null, [Validators.required]], + sourceType: [null, [Validators.required]], + sourceValue: [null, [Validators.required]], + sourceRangeFrom: [null, []], + sourceRangeTo: [null, []] + }); + this.stateFormGroup.valueChanges.subscribe( + () => this.updateModel() + ); + this.stateFormGroup.get('sourceType').valueChanges.subscribe(() => { + this.updateValidators(); + }); + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (isDisabled) { + this.stateFormGroup.disable({emitEvent: false}); + } else { + this.stateFormGroup.enable({emitEvent: false}); + this.updateValidators(); + } + } + + writeValue(value: TimeSeriesChartStateSettings): void { + this.modelValue = value; + this.stateFormGroup.patchValue( + value, {emitEvent: false} + ); + this.updateValidators(); + this.cd.markForCheck(); + } + + private updateValidators() { + const sourceType: TimeSeriesChartStateSourceType = this.stateFormGroup.get('sourceType').value; + if (sourceType === TimeSeriesChartStateSourceType.constant) { + this.stateFormGroup.get('sourceValue').enable({emitEvent: false}); + this.stateFormGroup.get('sourceRangeFrom').disable({emitEvent: false}); + this.stateFormGroup.get('sourceRangeTo').disable({emitEvent: false}); + } else if (sourceType === TimeSeriesChartStateSourceType.range) { + this.stateFormGroup.get('sourceValue').disable({emitEvent: false}); + this.stateFormGroup.get('sourceRangeFrom').enable({emitEvent: false}); + this.stateFormGroup.get('sourceRangeTo').enable({emitEvent: false}); + } + } + + private updateModel() { + this.modelValue = this.stateFormGroup.value; + this.propagateChange(this.modelValue); + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-states-panel.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-states-panel.component.html new file mode 100644 index 0000000000..5db76d1493 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-states-panel.component.html @@ -0,0 +1,47 @@ + +
+
{{ 'widgets.time-series-chart.state.states' | translate }}
+
+
+
widgets.time-series-chart.state.label
+
widgets.time-series-chart.state.ticks-value
+
widgets.time-series-chart.state.source
+
widgets.time-series-chart.state.value-range
+
+
+
+
+ + + +
+
+
+
+ +
+
+ + {{ 'widgets.time-series-chart.state.no-states' | translate }} + diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-states-panel.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-states-panel.component.scss new file mode 100644 index 0000000000..3177b8cdc1 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-states-panel.component.scss @@ -0,0 +1,60 @@ +/** + * 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 '../../../../../../../../../scss/constants'; + +.tb-time-series-states-panel { + .tb-form-table { + overflow-x: auto; + } + .tb-form-table-header-cell { + &.tb-state-label-header { + flex: 1; + min-width: 80px; + } + &.tb-state-value-header { + width: 80px; + min-width: 80px; + } + &.tb-state-source-header { + width: 100px; + min-width: 100px; + } + &.tb-state-source-value-header { + flex: 1; + min-width: 200px; + @media #{$mat-gt-sm} { + min-width: 358px; + } + } + &.tb-actions-header { + width: 40px; + min-width: 40px; + } + } + .tb-form-table-header { + min-width: fit-content; + } + .tb-form-table-body { + min-width: fit-content; + .mat-divider { + margin-top: 8px; + @media #{$mat-gt-sm} { + display: none; + } + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-states-panel.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-states-panel.component.ts new file mode 100644 index 0000000000..8c30582425 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-states-panel.component.ts @@ -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. +/// + +import { Component, forwardRef, Input, OnInit, ViewEncapsulation } from '@angular/core'; +import { + AbstractControl, + ControlValueAccessor, + NG_VALIDATORS, + NG_VALUE_ACCESSOR, + UntypedFormArray, + UntypedFormBuilder, + UntypedFormControl, + UntypedFormGroup, + Validator +} from '@angular/forms'; +import { + TimeSeriesChartStateSettings, + TimeSeriesChartStateSourceType, + timeSeriesChartStateValid, + timeSeriesChartStateValidator +} from '@home/components/widget/lib/chart/time-series-chart.models'; + +@Component({ + selector: 'tb-time-series-chart-states-panel', + templateUrl: './time-series-chart-states-panel.component.html', + styleUrls: ['./time-series-chart-states-panel.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => TimeSeriesChartStatesPanelComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => TimeSeriesChartStatesPanelComponent), + multi: true + } + ], + encapsulation: ViewEncapsulation.None +}) +export class TimeSeriesChartStatesPanelComponent implements ControlValueAccessor, OnInit, Validator { + + @Input() + disabled: boolean; + + statesFormGroup: UntypedFormGroup; + + private propagateChange = (_val: any) => {}; + + constructor(private fb: UntypedFormBuilder) { + } + + ngOnInit() { + this.statesFormGroup = this.fb.group({ + states: [this.fb.array([]), []] + }); + this.statesFormGroup.valueChanges.subscribe( + () => { + let states: TimeSeriesChartStateSettings[] = this.statesFormGroup.get('states').value; + if (states) { + states = states.filter(s => timeSeriesChartStateValid(s)); + } + this.propagateChange(states); + } + ); + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (isDisabled) { + this.statesFormGroup.disable({emitEvent: false}); + } else { + this.statesFormGroup.enable({emitEvent: false}); + } + } + + writeValue(value: TimeSeriesChartStateSettings[] | undefined): void { + const states = value || []; + this.statesFormGroup.setControl('states', this.prepareStatesFormArray(states), {emitEvent: false}); + } + + public validate(c: UntypedFormControl) { + const valid = this.statesFormGroup.valid; + return valid ? null : { + states: { + valid: false, + }, + }; + } + + statesFormArray(): UntypedFormArray { + return this.statesFormGroup.get('states') as UntypedFormArray; + } + + trackByState(index: number, stateControl: AbstractControl): any { + return stateControl; + } + + removeState(index: number) { + (this.statesFormGroup.get('states') as UntypedFormArray).removeAt(index); + } + + addState() { + const state: TimeSeriesChartStateSettings = { + label: '', + value: 0, + sourceType: TimeSeriesChartStateSourceType.constant + }; + const statesArray = this.statesFormGroup.get('states') as UntypedFormArray; + const stateControl = this.fb.control(state, [timeSeriesChartStateValidator]); + statesArray.push(stateControl); + } + + private prepareStatesFormArray(states: TimeSeriesChartStateSettings[] | undefined): UntypedFormArray { + const statesControls: Array = []; + if (states) { + states.forEach((state) => { + statesControls.push(this.fb.control(state, [timeSeriesChartStateValidator])); + }); + } + return this.fb.array(statesControls); + } + +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/widget-settings-common.module.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/widget-settings-common.module.ts index 86e460ff9d..e25282bb4c 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/widget-settings-common.module.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/widget-settings-common.module.ts @@ -133,6 +133,12 @@ import { import { TimeSeriesChartThresholdSettingsComponent } from '@home/components/widget/lib/settings/common/chart/time-series-chart-threshold-settings.component'; +import { + TimeSeriesChartStateRowComponent +} from '@home/components/widget/lib/settings/common/chart/time-series-chart-state-row.component'; +import { + TimeSeriesChartStatesPanelComponent +} from '@home/components/widget/lib/settings/common/chart/time-series-chart-states-panel.component'; @NgModule({ declarations: [ @@ -182,6 +188,8 @@ import { TimeSeriesChartAnimationSettingsComponent, TimeSeriesChartFillSettingsComponent, TimeSeriesChartThresholdSettingsComponent, + TimeSeriesChartStatesPanelComponent, + TimeSeriesChartStateRowComponent, DataKeyInputComponent, EntityAliasInputComponent ], @@ -237,6 +245,8 @@ import { TimeSeriesChartAnimationSettingsComponent, TimeSeriesChartFillSettingsComponent, TimeSeriesChartThresholdSettingsComponent, + TimeSeriesChartStatesPanelComponent, + TimeSeriesChartStateRowComponent, DataKeyInputComponent, EntityAliasInputComponent ], diff --git a/ui-ngx/src/assets/help/en_US/widget/lib/chart/ticks_generator_fn.md b/ui-ngx/src/assets/help/en_US/widget/lib/chart/ticks_generator_fn.md new file mode 100644 index 0000000000..e3b5bf817a --- /dev/null +++ b/ui-ngx/src/assets/help/en_US/widget/lib/chart/ticks_generator_fn.md @@ -0,0 +1,61 @@ +#### Ticks generator function + +
+
+ +*function (extent): {value: number}[]* + +A JavaScript function used to generate Y axis ticks. + +**Parameters:** + +
    +
  • extent: number[] - An array of two numbers holding axis min and max values [axisMin, axisMax]. +
  • +
+ +**Returns:** + +An array of tick values with the following structure: + +```typescript +{ + value: number +} +``` + +
+ +##### Examples + +* Always display only one tick in the middle: + +```javascript +return extent ? [{ value: (extent[0] + extent[1]) / 2}] : []; +{:copy-code} +``` + +* Display only min and max ticks: + +```javascript +if (extent) { + return [ {value: extent[0]}, {value: extent[1]} ]; +} else { + return []; +} +{:copy-code} +``` + +* Disable ticks: + +```javascript +return []; +{:copy-code} +``` + +* Constant ticks (1,2,3): + +```javascript +return [ {value: 1}, {value: 2}, {value: 3} ]; +{:copy-code} +``` 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 0b94659acb..1e21f28cda 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -6783,6 +6783,20 @@ "label-position-inside-end-bottom": "Inside end bottom", "label-background": "Label background" }, + "state": { + "states": "States", + "label": "Label", + "ticks-value": "Ticks value", + "source": "Source", + "value-range": "Value / Range", + "no-states": "No states configured", + "add-state": "Add state", + "type-constant": "Constant", + "type-range": "Range", + "from": "From", + "to": "To", + "remove-state": "Remove state" + }, "axis": { "axes": "Axes", "x-axis": "X axis", @@ -6798,6 +6812,7 @@ "position-bottom": "Bottom", "tick-labels": "Tick labels", "ticks-formatter-function": "Ticks formatter function", + "ticks-generator-function": "Ticks generator function", "show-ticks": "Show ticks", "show-line": "Show line", "show-split-lines": "Show split lines", From d2d68faee7706854ffd3e1377707b8c1be724b39 Mon Sep 17 00:00:00 2001 From: nick Date: Wed, 10 Apr 2024 16:51:51 +0300 Subject: [PATCH 39/80] fix bug: lwm2m tests dif Port --- .../AbstractLwM2MIntegrationDiffPortTest.java | 30 ++++++++++--------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/diffPort/AbstractLwM2MIntegrationDiffPortTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/diffPort/AbstractLwM2MIntegrationDiffPortTest.java index e61b1afb6c..1439140733 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/diffPort/AbstractLwM2MIntegrationDiffPortTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/diffPort/AbstractLwM2MIntegrationDiffPortTest.java @@ -59,23 +59,25 @@ public abstract class AbstractLwM2MIntegrationDiffPortTest extends AbstractSecur .until(() -> lwM2MTestClient.getClientStates().contains(ON_REGISTRATION_SUCCESS) || lwM2MTestClient.getClientStates().contains(ON_REGISTRATION_STARTED)); Assert.assertTrue(lwM2MTestClient.getClientStates().containsAll(expectedStatusesRegistrationLwm2mSuccess)); + doAnswer(new Answer() { + @Override + public Object answer(InvocationOnMock invocation) throws Throwable { + Object[] arguments = invocation.getArguments(); + if (arguments.length > 0 && arguments[0] instanceof RegistrationUpdate) { + log.error("RegistrationUpdate arguments [{}]", arguments); + int portOld = ((RegistrationUpdate) arguments[0]).getPort(); + int portValueChange = 5; + arguments[0] = registrationUpdateNewPort((RegistrationUpdate) arguments[0], portValueChange); + int portNew = ((RegistrationUpdate) arguments[0]).getPort(); + Assert.assertEquals((portNew - portOld), portValueChange); + } + return invocation.callRealMethod(); + } + }).when(registrationStoreTest).updateRegistration(any(RegistrationUpdate.class)); + await(awaitAlias) .atMost(40, TimeUnit.SECONDS) .until(() -> { - doAnswer(new Answer() { - @Override - public Object answer(InvocationOnMock invocation) throws Throwable { - Object[] arguments = invocation.getArguments(); - if (arguments.length > 0 && arguments[0] instanceof RegistrationUpdate) { - int portOld = ((RegistrationUpdate) arguments[0]).getPort(); - int portValueChange = 5; - arguments[0] = registrationUpdateNewPort((RegistrationUpdate) arguments[0], portValueChange); - int portNew = ((RegistrationUpdate) arguments[0]).getPort(); - Assert.assertEquals((portNew - portOld), portValueChange); - } - return invocation.callRealMethod(); - } - }).when(registrationStoreTest).updateRegistration(any(RegistrationUpdate.class)); return lwM2MTestClient.getClientStates().contains(ON_UPDATE_SUCCESS); }); Assert.assertTrue(lwM2MTestClient.getClientStates().containsAll(expectedStatusesRegistrationLwm2mSuccessUpdate)); From d876fc8bfe223c88e127dab3ea9a4ed36c0ff1e3 Mon Sep 17 00:00:00 2001 From: nick Date: Wed, 10 Apr 2024 17:14:45 +0300 Subject: [PATCH 40/80] fix bug: lwm2m tests dif Port 3 --- .../AbstractLwM2MIntegrationDiffPortTest.java | 36 ++++++++----------- .../src/test/resources/logback-test.xml | 3 ++ .../store/TbInMemoryRegistrationStore.java | 2 ++ .../store/TbLwM2mRedisRegistrationStore.java | 2 ++ 4 files changed, 22 insertions(+), 21 deletions(-) diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/diffPort/AbstractLwM2MIntegrationDiffPortTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/diffPort/AbstractLwM2MIntegrationDiffPortTest.java index 1439140733..44da58a4ef 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/diffPort/AbstractLwM2MIntegrationDiffPortTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/diffPort/AbstractLwM2MIntegrationDiffPortTest.java @@ -22,8 +22,6 @@ import org.eclipse.leshan.core.peer.SocketIdentity; import org.eclipse.leshan.server.registration.RegistrationStore; import org.eclipse.leshan.server.registration.RegistrationUpdate; import org.junit.Assert; -import org.mockito.invocation.InvocationOnMock; -import org.mockito.stubbing.Answer; import org.springframework.boot.test.mock.mockito.SpyBean; import org.thingsboard.server.common.data.device.profile.Lwm2mDeviceProfileTransportConfiguration; import org.thingsboard.server.dao.service.DaoSqlTest; @@ -50,6 +48,19 @@ public abstract class AbstractLwM2MIntegrationDiffPortTest extends AbstractSecur protected void basicTestConnectionDifferentPort(Lwm2mDeviceProfileTransportConfiguration transportConfiguration, String awaitAlias) throws Exception { + doAnswer((invocation) -> { + Object[] arguments = invocation.getArguments(); + log.warn("doAnswer for registrationStoreTest.updateRegistration with args {}", arguments); +// if (arguments.length > 0 && arguments[0] instanceof RegistrationUpdate) { + int portOld = ((RegistrationUpdate) arguments[0]).getPort(); + int portValueChange = 5; + arguments[0] = registrationUpdateNewPort((RegistrationUpdate) arguments[0], portValueChange); + int portNew = ((RegistrationUpdate) arguments[0]).getPort(); + Assert.assertEquals((portNew - portOld), portValueChange); +// } + return invocation.callRealMethod(); + }).when(registrationStoreTest).updateRegistration(any(RegistrationUpdate.class)); + createDeviceProfile(transportConfiguration); createDevice(deviceCredentials, clientEndpoint); createNewClient(security, null, true, clientEndpoint); @@ -59,27 +70,10 @@ public abstract class AbstractLwM2MIntegrationDiffPortTest extends AbstractSecur .until(() -> lwM2MTestClient.getClientStates().contains(ON_REGISTRATION_SUCCESS) || lwM2MTestClient.getClientStates().contains(ON_REGISTRATION_STARTED)); Assert.assertTrue(lwM2MTestClient.getClientStates().containsAll(expectedStatusesRegistrationLwm2mSuccess)); - doAnswer(new Answer() { - @Override - public Object answer(InvocationOnMock invocation) throws Throwable { - Object[] arguments = invocation.getArguments(); - if (arguments.length > 0 && arguments[0] instanceof RegistrationUpdate) { - log.error("RegistrationUpdate arguments [{}]", arguments); - int portOld = ((RegistrationUpdate) arguments[0]).getPort(); - int portValueChange = 5; - arguments[0] = registrationUpdateNewPort((RegistrationUpdate) arguments[0], portValueChange); - int portNew = ((RegistrationUpdate) arguments[0]).getPort(); - Assert.assertEquals((portNew - portOld), portValueChange); - } - return invocation.callRealMethod(); - } - }).when(registrationStoreTest).updateRegistration(any(RegistrationUpdate.class)); - await(awaitAlias) .atMost(40, TimeUnit.SECONDS) - .until(() -> { - return lwM2MTestClient.getClientStates().contains(ON_UPDATE_SUCCESS); - }); + .until(() -> lwM2MTestClient.getClientStates().contains(ON_UPDATE_SUCCESS)); + Assert.assertTrue(lwM2MTestClient.getClientStates().containsAll(expectedStatusesRegistrationLwm2mSuccessUpdate)); } diff --git a/application/src/test/resources/logback-test.xml b/application/src/test/resources/logback-test.xml index d72bccb7a6..96386c00ff 100644 --- a/application/src/test/resources/logback-test.xml +++ b/application/src/test/resources/logback-test.xml @@ -37,6 +37,9 @@ + + + diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbInMemoryRegistrationStore.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbInMemoryRegistrationStore.java index 60afd76ffb..fa9816c52d 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbInMemoryRegistrationStore.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbInMemoryRegistrationStore.java @@ -147,6 +147,8 @@ public class TbInMemoryRegistrationStore implements RegistrationStore, Startable @Override public UpdatedRegistration updateRegistration(RegistrationUpdate update) { + // test fix bug Diff port + log.warn("updateRegistration inMemory {}", update); try { lock.writeLock().lock(); diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbLwM2mRedisRegistrationStore.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbLwM2mRedisRegistrationStore.java index 898fef7b2f..87dedc52fc 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbLwM2mRedisRegistrationStore.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbLwM2mRedisRegistrationStore.java @@ -222,6 +222,8 @@ public class TbLwM2mRedisRegistrationStore implements RegistrationStore, Startab @Override public UpdatedRegistration updateRegistration(RegistrationUpdate update) { + // test fix bug Diff port + log.warn("updateRegistration Redis {}", update); Lock lock = null; try (var connection = connectionFactory.getConnection()) { From 5c7ad51641352255548f12cddb304fbff4043122 Mon Sep 17 00:00:00 2001 From: Dmytro Skarzhynets Date: Wed, 10 Apr 2024 17:17:24 +0300 Subject: [PATCH 41/80] Replace usages of deprecated @Schema(required = ...) with @Schema(requiredMode = ...) --- .../ComponentDescriptorController.java | 2 +- .../server/controller/QueueController.java | 4 +-- .../controller/TbResourceController.java | 6 ++-- .../controller/TelemetryController.java | 8 ++--- .../TwoFactorAuthConfigController.java | 2 +- .../security/auth/rest/LoginRequest.java | 4 +-- .../security/auth/rest/LoginResponse.java | 4 +-- .../server/common/data/Customer.java | 4 +-- .../server/common/data/DashboardInfo.java | 2 +- .../server/common/data/Device.java | 4 +-- .../server/common/data/EntityView.java | 6 ++-- .../SaveDeviceWithCredentialsRequest.java | 4 +-- .../server/common/data/Tenant.java | 4 +-- .../thingsboard/server/common/data/User.java | 4 +-- .../server/common/data/alarm/Alarm.java | 14 ++++----- .../AlarmCreateOrUpdateActiveRequest.java | 6 ++-- .../common/data/alarm/AlarmUpdateRequest.java | 2 +- .../server/common/data/asset/Asset.java | 2 +- .../server/common/data/edge/Edge.java | 8 ++--- .../server/common/data/event/EventFilter.java | 2 +- .../server/common/data/id/AlarmId.java | 2 +- .../common/data/id/ApiUsageStateId.java | 2 +- .../server/common/data/id/AssetId.java | 2 +- .../server/common/data/id/AssetProfileId.java | 2 +- .../server/common/data/id/CustomerId.java | 2 +- .../server/common/data/id/DashboardId.java | 2 +- .../server/common/data/id/DeviceId.java | 2 +- .../common/data/id/DeviceProfileId.java | 2 +- .../server/common/data/id/EdgeId.java | 2 +- .../server/common/data/id/EntityViewId.java | 2 +- .../server/common/data/id/NotificationId.java | 2 +- .../common/data/id/NotificationRequestId.java | 2 +- .../common/data/id/NotificationRuleId.java | 2 +- .../common/data/id/NotificationTargetId.java | 2 +- .../data/id/NotificationTemplateId.java | 2 +- .../server/common/data/id/OtaPackageId.java | 2 +- .../server/common/data/id/QueueId.java | 4 +-- .../server/common/data/id/QueueStatsId.java | 4 +-- .../server/common/data/id/RpcId.java | 2 +- .../server/common/data/id/RuleChainId.java | 2 +- .../server/common/data/id/RuleNodeId.java | 2 +- .../server/common/data/id/TbResourceId.java | 2 +- .../server/common/data/id/TenantId.java | 2 +- .../common/data/id/TenantProfileId.java | 2 +- .../server/common/data/id/UUIDBased.java | 2 +- .../server/common/data/id/WidgetTypeId.java | 2 +- .../common/data/id/WidgetsBundleId.java | 2 +- .../data/oauth2/OAuth2BasicMapperConfig.java | 2 +- .../OAuth2ClientRegistrationTemplate.java | 2 +- .../common/data/oauth2/OAuth2DomainInfo.java | 4 +-- .../server/common/data/oauth2/OAuth2Info.java | 2 +- .../data/oauth2/OAuth2MapperConfig.java | 2 +- .../common/data/oauth2/OAuth2MobileInfo.java | 4 +-- .../common/data/oauth2/OAuth2ParamsInfo.java | 6 ++-- .../data/oauth2/OAuth2RegistrationInfo.java | 18 +++++------ .../data/objects/AttributesEntityView.java | 6 ++-- .../data/objects/TelemetryEntityView.java | 4 +-- .../rule/DefaultRuleChainCreateRequest.java | 2 +- .../common/data/rule/NodeConnectionInfo.java | 6 ++-- .../server/common/data/rule/RuleChain.java | 4 +-- .../data/rule/RuleChainConnectionInfo.java | 8 ++--- .../common/data/rule/RuleChainData.java | 4 +-- .../common/data/rule/RuleChainMetaData.java | 10 +++---- .../data/rule/RuleChainOutputLabelsUsage.java | 10 +++---- .../data/security/DeviceCredentials.java | 6 ++-- .../config/SmppSmsProviderConfiguration.java | 30 +++++++++---------- 66 files changed, 140 insertions(+), 140 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/controller/ComponentDescriptorController.java b/application/src/main/java/org/thingsboard/server/controller/ComponentDescriptorController.java index 595d6feef1..5271de2329 100644 --- a/application/src/main/java/org/thingsboard/server/controller/ComponentDescriptorController.java +++ b/application/src/main/java/org/thingsboard/server/controller/ComponentDescriptorController.java @@ -68,7 +68,7 @@ public class ComponentDescriptorController extends BaseController { @RequestMapping(value = "/components/{componentType}", method = RequestMethod.GET) @ResponseBody public List getComponentDescriptorsByType( - @Parameter(description = "Type of the Rule Node", schema = @Schema(allowableValues = "ENRICHMENT,FILTER,TRANSFORMATION,ACTION,EXTERNAL", required = true)) + @Parameter(description = "Type of the Rule Node", schema = @Schema(allowableValues = "ENRICHMENT,FILTER,TRANSFORMATION,ACTION,EXTERNAL", requiredMode = Schema.RequiredMode.REQUIRED)) @PathVariable("componentType") String strComponentType, @Parameter(description = "Type of the Rule Chain", schema = @Schema(allowableValues = "CORE,EDGE")) @RequestParam(value = "ruleChainType", required = false) String strRuleChainType) throws ThingsboardException { diff --git a/application/src/main/java/org/thingsboard/server/controller/QueueController.java b/application/src/main/java/org/thingsboard/server/controller/QueueController.java index 089ba9bcca..6b90f8f8fd 100644 --- a/application/src/main/java/org/thingsboard/server/controller/QueueController.java +++ b/application/src/main/java/org/thingsboard/server/controller/QueueController.java @@ -67,7 +67,7 @@ public class QueueController extends BaseController { @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") @RequestMapping(value = "/queues", params = {"serviceType", "pageSize", "page"}, method = RequestMethod.GET) @ResponseBody - public PageData getTenantQueuesByServiceType(@Parameter(description = QUEUE_SERVICE_TYPE_DESCRIPTION, schema = @Schema(allowableValues = {"TB-RULE-ENGINE", "TB-CORE", "TB-TRANSPORT", "JS-EXECUTOR"}, required = true)) + public PageData getTenantQueuesByServiceType(@Parameter(description = QUEUE_SERVICE_TYPE_DESCRIPTION, schema = @Schema(allowableValues = {"TB-RULE-ENGINE", "TB-CORE", "TB-TRANSPORT", "JS-EXECUTOR"}, requiredMode = Schema.RequiredMode.REQUIRED)) @RequestParam String serviceType, @Parameter(description = PAGE_SIZE_DESCRIPTION, required = true) @RequestParam int pageSize, @@ -126,7 +126,7 @@ public class QueueController extends BaseController { @ResponseBody public Queue saveQueue(@Parameter(description = "A JSON value representing the queue.") @RequestBody Queue queue, - @Parameter(description = QUEUE_SERVICE_TYPE_DESCRIPTION, schema = @Schema(allowableValues = {"TB-RULE-ENGINE", "TB-CORE", "TB-TRANSPORT", "JS-EXECUTOR"}, required = true)) + @Parameter(description = QUEUE_SERVICE_TYPE_DESCRIPTION, schema = @Schema(allowableValues = {"TB-RULE-ENGINE", "TB-CORE", "TB-TRANSPORT", "JS-EXECUTOR"}, requiredMode = Schema.RequiredMode.REQUIRED)) @RequestParam String serviceType) throws ThingsboardException { checkParameter("serviceType", serviceType); queue.setTenantId(getCurrentUser().getTenantId()); diff --git a/application/src/main/java/org/thingsboard/server/controller/TbResourceController.java b/application/src/main/java/org/thingsboard/server/controller/TbResourceController.java index 1908459200..e646c34e4f 100644 --- a/application/src/main/java/org/thingsboard/server/controller/TbResourceController.java +++ b/application/src/main/java/org/thingsboard/server/controller/TbResourceController.java @@ -268,9 +268,9 @@ public class TbResourceController extends BaseController { "You can specify parameters to filter the results. " + LWM2M_OBJECT_DESCRIPTION + TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')") @GetMapping(value = "/resource/lwm2m") - public List getLwm2mListObjects(@Parameter(description = SORT_ORDER_DESCRIPTION, schema = @Schema(allowableValues = {"ASC", "DESC"}, required = true)) + public List getLwm2mListObjects(@Parameter(description = SORT_ORDER_DESCRIPTION, schema = @Schema(allowableValues = {"ASC", "DESC"}, requiredMode = Schema.RequiredMode.REQUIRED)) @RequestParam String sortOrder, - @Parameter(description = SORT_PROPERTY_DESCRIPTION, schema = @Schema(allowableValues = {"id", "name"}, required = true)) + @Parameter(description = SORT_PROPERTY_DESCRIPTION, schema = @Schema(allowableValues = {"id", "name"}, requiredMode = Schema.RequiredMode.REQUIRED)) @RequestParam String sortProperty, @Parameter(description = "LwM2M Object ids.", required = true) @RequestParam(required = false) String[] objectIds) throws ThingsboardException { @@ -315,4 +315,4 @@ public class TbResourceController extends BaseController { .body(resource); } -} \ No newline at end of file +} diff --git a/application/src/main/java/org/thingsboard/server/controller/TelemetryController.java b/application/src/main/java/org/thingsboard/server/controller/TelemetryController.java index 65bcc2c969..cea449af31 100644 --- a/application/src/main/java/org/thingsboard/server/controller/TelemetryController.java +++ b/application/src/main/java/org/thingsboard/server/controller/TelemetryController.java @@ -236,7 +236,7 @@ public class TelemetryController extends BaseController { public DeferredResult getAttributesByScope( @Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true, schema = @Schema(defaultValue = "DEVICE")) @PathVariable("entityType") String entityType, @Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @PathVariable("entityId") String entityIdStr, - @Parameter(description = ATTRIBUTES_SCOPE_DESCRIPTION, schema = @Schema(allowableValues = {"SERVER_SCOPE", "SHARED_SCOPE", "CLIENT_SCOPE"}, required = true)) @PathVariable("scope") AttributeScope scope, + @Parameter(description = ATTRIBUTES_SCOPE_DESCRIPTION, schema = @Schema(allowableValues = {"SERVER_SCOPE", "SHARED_SCOPE", "CLIENT_SCOPE"}, requiredMode = Schema.RequiredMode.REQUIRED)) @PathVariable("scope") AttributeScope scope, @Parameter(description = ATTRIBUTES_KEYS_DESCRIPTION) @RequestParam(name = "keys", required = false) String keysStr) throws ThingsboardException { SecurityUser user = getCurrentUser(); return accessValidator.validateEntityAndCallback(getCurrentUser(), Operation.READ_ATTRIBUTES, entityType, entityIdStr, @@ -353,7 +353,7 @@ public class TelemetryController extends BaseController { @ResponseBody public DeferredResult saveDeviceAttributes( @Parameter(description = DEVICE_ID_PARAM_DESCRIPTION, required = true) @PathVariable("deviceId") String deviceIdStr, - @Parameter(description = ATTRIBUTES_SCOPE_DESCRIPTION, schema = @Schema(allowableValues = {"SERVER_SCOPE", "SHARED_SCOPE"}, required = true)) @PathVariable("scope") AttributeScope scope, + @Parameter(description = ATTRIBUTES_SCOPE_DESCRIPTION, schema = @Schema(allowableValues = {"SERVER_SCOPE", "SHARED_SCOPE"}, requiredMode = Schema.RequiredMode.REQUIRED)) @PathVariable("scope") AttributeScope scope, @io.swagger.v3.oas.annotations.parameters.RequestBody(description = ATTRIBUTES_JSON_REQUEST_DESCRIPTION, required = true) @RequestBody JsonNode request) throws ThingsboardException { EntityId entityId = EntityIdFactory.getByTypeAndUuid(EntityType.DEVICE, deviceIdStr); return saveAttributes(getTenantId(), entityId, scope, request); @@ -399,7 +399,7 @@ public class TelemetryController extends BaseController { public DeferredResult saveEntityAttributesV2( @Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true, schema = @Schema(defaultValue = "DEVICE")) @PathVariable("entityType") String entityType, @Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @PathVariable("entityId") String entityIdStr, - @Parameter(description = ATTRIBUTES_SCOPE_DESCRIPTION, schema = @Schema(allowableValues = {"SERVER_SCOPE", "SHARED_SCOPE"}, required = true)) @PathVariable("scope")AttributeScope scope, + @Parameter(description = ATTRIBUTES_SCOPE_DESCRIPTION, schema = @Schema(allowableValues = {"SERVER_SCOPE", "SHARED_SCOPE"}, requiredMode = Schema.RequiredMode.REQUIRED)) @PathVariable("scope")AttributeScope scope, @io.swagger.v3.oas.annotations.parameters.RequestBody(description = ATTRIBUTES_JSON_REQUEST_DESCRIPTION, required = true) @RequestBody JsonNode request) throws ThingsboardException { EntityId entityId = EntityIdFactory.getByTypeAndId(entityType, entityIdStr); return saveAttributes(getTenantId(), entityId, scope, request); @@ -550,7 +550,7 @@ public class TelemetryController extends BaseController { @ResponseBody public DeferredResult deleteDeviceAttributes( @Parameter(description = DEVICE_ID_PARAM_DESCRIPTION, required = true) @PathVariable(DEVICE_ID) String deviceIdStr, - @Parameter(description = ATTRIBUTES_SCOPE_DESCRIPTION, schema = @Schema(allowableValues = {"SERVER_SCOPE", "SHARED_SCOPE", "CLIENT_SCOPE"}, required = true)) @PathVariable("scope")AttributeScope scope, + @Parameter(description = ATTRIBUTES_SCOPE_DESCRIPTION, schema = @Schema(allowableValues = {"SERVER_SCOPE", "SHARED_SCOPE", "CLIENT_SCOPE"}, requiredMode = Schema.RequiredMode.REQUIRED)) @PathVariable("scope")AttributeScope scope, @Parameter(description = ATTRIBUTES_KEYS_DESCRIPTION, required = true)@RequestParam(name = "keys")String keysStr) throws ThingsboardException { EntityId entityId = EntityIdFactory.getByTypeAndUuid(EntityType.DEVICE, deviceIdStr); return deleteAttributes(entityId, scope, keysStr); diff --git a/application/src/main/java/org/thingsboard/server/controller/TwoFactorAuthConfigController.java b/application/src/main/java/org/thingsboard/server/controller/TwoFactorAuthConfigController.java index 7468e683ab..01487b20a0 100644 --- a/application/src/main/java/org/thingsboard/server/controller/TwoFactorAuthConfigController.java +++ b/application/src/main/java/org/thingsboard/server/controller/TwoFactorAuthConfigController.java @@ -100,7 +100,7 @@ public class TwoFactorAuthConfigController extends BaseController { ControllerConstants.AVAILABLE_FOR_ANY_AUTHORIZED_USER) @PostMapping("/account/config/generate") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") - public TwoFaAccountConfig generateTwoFaAccountConfig(@Parameter(description = "2FA provider type to generate new account config for", schema = @Schema(defaultValue = "TOTP", required = true)) + public TwoFaAccountConfig generateTwoFaAccountConfig(@Parameter(description = "2FA provider type to generate new account config for", schema = @Schema(defaultValue = "TOTP", requiredMode = Schema.RequiredMode.REQUIRED)) @RequestParam TwoFaProviderType providerType) throws Exception { SecurityUser user = getCurrentUser(); return twoFactorAuthService.generateNewAccountConfig(user, providerType); diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/rest/LoginRequest.java b/application/src/main/java/org/thingsboard/server/service/security/auth/rest/LoginRequest.java index 5d001ccf6a..dbb930755d 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/rest/LoginRequest.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/rest/LoginRequest.java @@ -32,12 +32,12 @@ public class LoginRequest { this.password = password; } - @Schema(required = true, description = "User email", example = "tenant@thingsboard.org") + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "User email", example = "tenant@thingsboard.org") public String getUsername() { return username; } - @Schema(required = true, description = "User password", example = "tenant") + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "User password", example = "tenant") public String getPassword() { return password; } diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/rest/LoginResponse.java b/application/src/main/java/org/thingsboard/server/service/security/auth/rest/LoginResponse.java index 239186cf45..503767106a 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/rest/LoginResponse.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/rest/LoginResponse.java @@ -22,11 +22,11 @@ import lombok.Data; @Data public class LoginResponse { - @Schema(required = true, description = "JWT token", + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "JWT token", example = "eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJ0ZW5hbnRAdGhpbmdzYm9hcmQub3JnIi...") private String token; - @Schema(required = true, description = "Refresh token", + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "Refresh token", example = "eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJ0ZW5hbnRAdGhpbmdzYm9hcmQub3JnIi...") private String refreshToken; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/Customer.java b/common/data/src/main/java/org/thingsboard/server/common/data/Customer.java index 9f0100dbb1..398cd17aa1 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/Customer.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/Customer.java @@ -36,7 +36,7 @@ public class Customer extends ContactBased implements HasTenantId, E @NoXss @Length(fieldName = "title") - @Schema(required = true, description = "Title of the customer", example = "Company A") + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "Title of the customer", example = "Company A") private String title; @Schema(description = "JSON object with Tenant Id") private TenantId tenantId; @@ -132,7 +132,7 @@ public class Customer extends ContactBased implements HasTenantId, E return super.getPhone(); } - @Schema(required = true, description = "Email", example = "example@company.com") + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "Email", example = "example@company.com") @Override public String getEmail() { return super.getEmail(); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/DashboardInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/DashboardInfo.java index be0f7ce109..238ef8744c 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/DashboardInfo.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/DashboardInfo.java @@ -85,7 +85,7 @@ public class DashboardInfo extends BaseData implements HasName, Has this.tenantId = tenantId; } - @Schema(required = true, description = "Title of the dashboard.") + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "Title of the dashboard.") public String getTitle() { return title; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/Device.java b/common/data/src/main/java/org/thingsboard/server/common/data/Device.java index 13df99ca50..e7c37d3ed1 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/Device.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/Device.java @@ -136,7 +136,7 @@ public class Device extends BaseDataWithAdditionalInfo implements HasL this.customerId = customerId; } - @Schema(required = true, description = "Unique Device Name in scope of Tenant", example = "A4B72CCDFF33") + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "Unique Device Name in scope of Tenant", example = "A4B72CCDFF33") @Override public String getName() { return name; @@ -164,7 +164,7 @@ public class Device extends BaseDataWithAdditionalInfo implements HasL this.label = label; } - @Schema(required = true, description = "JSON object with Device Profile Id.") + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "JSON object with Device Profile Id.") public DeviceProfileId getDeviceProfileId() { return deviceProfileId; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/EntityView.java b/common/data/src/main/java/org/thingsboard/server/common/data/EntityView.java index 4a4e785dac..344045134e 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/EntityView.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/EntityView.java @@ -40,17 +40,17 @@ public class EntityView extends BaseDataWithAdditionalInfo private static final long serialVersionUID = 5582010124562018986L; - @Schema(required = true, description = "JSON object with the referenced Entity Id (Device or Asset).") + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "JSON object with the referenced Entity Id (Device or Asset).") private EntityId entityId; private TenantId tenantId; private CustomerId customerId; @NoXss @Length(fieldName = "name") - @Schema(required = true, description = "Entity View name", example = "A4B72CCDFF33") + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "Entity View name", example = "A4B72CCDFF33") private String name; @NoXss @Length(fieldName = "type") - @Schema(required = true, description = "Device Profile Name", example = "Temperature Sensor") + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "Device Profile Name", example = "Temperature Sensor") private String type; @Schema(description = "Set of telemetry and attribute keys to expose via Entity View.") private TelemetryEntityView keys; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/SaveDeviceWithCredentialsRequest.java b/common/data/src/main/java/org/thingsboard/server/common/data/SaveDeviceWithCredentialsRequest.java index bc40710aed..24991bb71d 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/SaveDeviceWithCredentialsRequest.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/SaveDeviceWithCredentialsRequest.java @@ -25,10 +25,10 @@ import jakarta.validation.constraints.NotNull;; @Data public class SaveDeviceWithCredentialsRequest { - @Schema(description = "The JSON with device entity.", required = true) + @Schema(description = "The JSON with device entity.", requiredMode = Schema.RequiredMode.REQUIRED) @NotNull private final Device device; - @Schema(description = "The JSON with credentials entity.", required = true) + @Schema(description = "The JSON with credentials entity.", requiredMode = Schema.RequiredMode.REQUIRED) @NotNull private final DeviceCredentials credentials; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/Tenant.java b/common/data/src/main/java/org/thingsboard/server/common/data/Tenant.java index 1f649f69e6..7cb7e15395 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/Tenant.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/Tenant.java @@ -33,7 +33,7 @@ public class Tenant extends ContactBased implements HasTenantId, HasTi @Length(fieldName = "title") @NoXss - @Schema(required = true, description = "Title of the tenant", example = "Company A") + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "Title of the tenant", example = "Company A") private String title; @NoXss @Length(fieldName = "region") @@ -152,7 +152,7 @@ public class Tenant extends ContactBased implements HasTenantId, HasTi return super.getPhone(); } - @Schema(required = true, description = "Email", example = "example@company.com") + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "Email", example = "example@company.com") @Override public String getEmail() { return super.getEmail(); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/User.java b/common/data/src/main/java/org/thingsboard/server/common/data/User.java index 2027de62be..678b7cdac6 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/User.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/User.java @@ -103,7 +103,7 @@ public class User extends BaseDataWithAdditionalInfo implements HasName, this.customerId = customerId; } - @Schema(required = true, description = "Email of the user", example = "user@example.com") + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "Email of the user", example = "user@example.com") public String getEmail() { return email; } @@ -119,7 +119,7 @@ public class User extends BaseDataWithAdditionalInfo implements HasName, return email; } - @Schema(required = true, description = "Authority", example = "SYS_ADMIN, TENANT_ADMIN or CUSTOMER_USER") + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "Authority", example = "SYS_ADMIN, TENANT_ADMIN or CUSTOMER_USER") public Authority getAuthority() { return authority; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/Alarm.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/Alarm.java index 04d2b9231c..19e0bc29b1 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/Alarm.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/Alarm.java @@ -59,16 +59,16 @@ public class Alarm extends BaseData implements HasName, HasTenantId, Ha private CustomerId customerId; @NoXss - @Schema(required = true, description = "representing type of the Alarm", example = "High Temperature Alarm") + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "representing type of the Alarm", example = "High Temperature Alarm") @Length(fieldName = "type") private String type; - @Schema(required = true, description = "JSON object with alarm originator id") + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "JSON object with alarm originator id") private EntityId originator; - @Schema(required = true, description = "Alarm severity", example = "CRITICAL") + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "Alarm severity", example = "CRITICAL") private AlarmSeverity severity; - @Schema(required = true, description = "Acknowledged", example = "true") + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "Acknowledged", example = "true") private boolean acknowledged; - @Schema(required = true, description = "Cleared", example = "false") + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "Cleared", example = "false") private boolean cleared; @Schema(description = "Alarm assignee user id") private UserId assigneeId; @@ -128,7 +128,7 @@ public class Alarm extends BaseData implements HasName, HasTenantId, Ha @Override @JsonProperty(access = JsonProperty.Access.READ_ONLY) - @Schema(required = true, description = "representing type of the Alarm", example = "High Temperature Alarm") + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "representing type of the Alarm", example = "High Temperature Alarm") public String getName() { return type; } @@ -150,7 +150,7 @@ public class Alarm extends BaseData implements HasName, HasTenantId, Ha } @JsonProperty(access = JsonProperty.Access.READ_ONLY) - @Schema(required = true, description = "status of the Alarm", example = "ACTIVE_UNACK", accessMode = Schema.AccessMode.READ_ONLY) + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "status of the Alarm", example = "ACTIVE_UNACK", accessMode = Schema.AccessMode.READ_ONLY) public AlarmStatus getStatus() { return toStatus(cleared, acknowledged); } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmCreateOrUpdateActiveRequest.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmCreateOrUpdateActiveRequest.java index 54dd8ba71b..c39c603a9e 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmCreateOrUpdateActiveRequest.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmCreateOrUpdateActiveRequest.java @@ -40,14 +40,14 @@ public class AlarmCreateOrUpdateActiveRequest implements AlarmModificationReques @Schema(description = "JSON object with Customer Id", accessMode = Schema.AccessMode.READ_ONLY) private CustomerId customerId; @NotNull - @Schema(required = true, description = "representing type of the Alarm", example = "High Temperature Alarm") + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "representing type of the Alarm", example = "High Temperature Alarm") @Length(fieldName = "type") private String type; @NotNull - @Schema(required = true, description = "JSON object with alarm originator id") + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "JSON object with alarm originator id") private EntityId originator; @NotNull - @Schema(required = true, description = "Alarm severity", example = "CRITICAL") + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "Alarm severity", example = "CRITICAL") private AlarmSeverity severity; @Schema(description = "Timestamp of the alarm start time, in milliseconds", example = "1634058704565") private long startTs; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmUpdateRequest.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmUpdateRequest.java index 544d200a27..8eb418d6dc 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmUpdateRequest.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmUpdateRequest.java @@ -41,7 +41,7 @@ public class AlarmUpdateRequest implements AlarmModificationRequest { "Omit this field to create new alarm.") private AlarmId alarmId; @NotNull - @Schema(required = true, description = "Alarm severity", example = "CRITICAL") + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "Alarm severity", example = "CRITICAL") private AlarmSeverity severity; @Schema(description = "Timestamp of the alarm start time, in milliseconds", example = "1634058704565") private long startTs; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/asset/Asset.java b/common/data/src/main/java/org/thingsboard/server/common/data/asset/Asset.java index 753a83fbaf..16518bc618 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/asset/Asset.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/asset/Asset.java @@ -120,7 +120,7 @@ public class Asset extends BaseDataWithAdditionalInfo implements HasLab this.customerId = customerId; } - @Schema(required = true, description = "Unique Asset Name in scope of Tenant", example = "Empire State Building") + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "Unique Asset Name in scope of Tenant", example = "Empire State Building") @Override public String getName() { return name; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/edge/Edge.java b/common/data/src/main/java/org/thingsboard/server/common/data/edge/Edge.java index d281589734..67d32d6e89 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/edge/Edge.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/edge/Edge.java @@ -120,13 +120,13 @@ public class Edge extends BaseDataWithAdditionalInfo implements HasLabel return this.rootRuleChainId; } - @Schema(required = true, description = "Unique Edge Name in scope of Tenant", example = "Silo_A_Edge") + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "Unique Edge Name in scope of Tenant", example = "Silo_A_Edge") @Override public String getName() { return this.name; } - @Schema(required = true, description = "Edge type", example = "Silos") + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "Edge type", example = "Silos") public String getType() { return this.type; } @@ -136,12 +136,12 @@ public class Edge extends BaseDataWithAdditionalInfo implements HasLabel return this.label; } - @Schema(required = true, description = "Edge routing key ('username') to authorize on cloud") + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "Edge routing key ('username') to authorize on cloud") public String getRoutingKey() { return this.routingKey; } - @Schema(required = true, description = "Edge secret ('password') to authorize on cloud") + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "Edge secret ('password') to authorize on cloud") public String getSecret() { return this.secret; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/event/EventFilter.java b/common/data/src/main/java/org/thingsboard/server/common/data/event/EventFilter.java index dc7f632037..6d2a110cf7 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/event/EventFilter.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/event/EventFilter.java @@ -33,7 +33,7 @@ import io.swagger.v3.oas.annotations.media.Schema; }) public interface EventFilter { - @Schema(required = true, description = "String value representing the event type", example = "STATS") + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "String value representing the event type", example = "STATS") EventType getEventType(); boolean isNotEmpty(); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/AlarmId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/AlarmId.java index aae2e3726a..02b4d602ce 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/AlarmId.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/AlarmId.java @@ -36,7 +36,7 @@ public class AlarmId extends UUIDBased implements EntityId { return new AlarmId(UUID.fromString(alarmId)); } - @Schema(required = true, description = "string", example = "ALARM", allowableValues = "ALARM") + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "string", example = "ALARM", allowableValues = "ALARM") @Override public EntityType getEntityType() { return EntityType.ALARM; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/ApiUsageStateId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/ApiUsageStateId.java index 228b4160a0..ec942731f3 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/ApiUsageStateId.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/ApiUsageStateId.java @@ -34,7 +34,7 @@ public class ApiUsageStateId extends UUIDBased implements EntityId { return new ApiUsageStateId(UUID.fromString(userId)); } - @Schema(required = true, description = "string", example = "API_USAGE_STATE", allowableValues = "API_USAGE_STATE") + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "string", example = "API_USAGE_STATE", allowableValues = "API_USAGE_STATE") @Override public EntityType getEntityType() { return EntityType.API_USAGE_STATE; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/AssetId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/AssetId.java index 2c70920aeb..a45668e9bd 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/AssetId.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/AssetId.java @@ -36,7 +36,7 @@ public class AssetId extends UUIDBased implements EntityId { return new AssetId(UUID.fromString(assetId)); } - @Schema(required = true, description = "string", example = "ASSET", allowableValues = "ASSET") + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "string", example = "ASSET", allowableValues = "ASSET") @Override public EntityType getEntityType() { return EntityType.ASSET; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/AssetProfileId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/AssetProfileId.java index 0efd6dcf71..d2377c19e6 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/AssetProfileId.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/AssetProfileId.java @@ -35,7 +35,7 @@ public class AssetProfileId extends UUIDBased implements EntityId { return new AssetProfileId(UUID.fromString(assetProfileId)); } - @Schema(required = true, description = "string", example = "ASSET_PROFILE", allowableValues = "ASSET_PROFILE") + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "string", example = "ASSET_PROFILE", allowableValues = "ASSET_PROFILE") @Override public EntityType getEntityType() { return EntityType.ASSET_PROFILE; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/CustomerId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/CustomerId.java index 72f380a4bd..d8ef7fd09e 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/CustomerId.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/CustomerId.java @@ -32,7 +32,7 @@ public final class CustomerId extends UUIDBased implements EntityId { super(id); } - @Schema(required = true, description = "string", example = "CUSTOMER", allowableValues = "CUSTOMER") + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "string", example = "CUSTOMER", allowableValues = "CUSTOMER") @Override public EntityType getEntityType() { return EntityType.CUSTOMER; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/DashboardId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/DashboardId.java index 7220919b14..a7c21821ff 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/DashboardId.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/DashboardId.java @@ -34,7 +34,7 @@ public class DashboardId extends UUIDBased implements EntityId { return new DashboardId(UUID.fromString(dashboardId)); } - @Schema(required = true, description = "string", example = "DASHBOARD", allowableValues = "DASHBOARD") + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "string", example = "DASHBOARD", allowableValues = "DASHBOARD") @Override public EntityType getEntityType() { return EntityType.DASHBOARD; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/DeviceId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/DeviceId.java index d59de6cb07..8fe48b9836 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/DeviceId.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/DeviceId.java @@ -37,7 +37,7 @@ public class DeviceId extends UUIDBased implements EntityId { } @Override - @Schema(required = true, description = "string", example = "DEVICE", allowableValues = "DEVICE") + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "string", example = "DEVICE", allowableValues = "DEVICE") public EntityType getEntityType() { return EntityType.DEVICE; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/DeviceProfileId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/DeviceProfileId.java index 11679436f5..7050fa7ff3 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/DeviceProfileId.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/DeviceProfileId.java @@ -35,7 +35,7 @@ public class DeviceProfileId extends UUIDBased implements EntityId { return new DeviceProfileId(UUID.fromString(deviceProfileId)); } - @Schema(required = true, description = "string", example = "DEVICE_PROFILE", allowableValues = "DEVICE_PROFILE") + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "string", example = "DEVICE_PROFILE", allowableValues = "DEVICE_PROFILE") @Override public EntityType getEntityType() { return EntityType.DEVICE_PROFILE; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/EdgeId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/EdgeId.java index 1d6c1dad49..5c29c210df 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/EdgeId.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/EdgeId.java @@ -41,7 +41,7 @@ public class EdgeId extends UUIDBased implements EntityId { return new EdgeId(UUID.fromString(edgeId)); } - @Schema(required = true, description = "string", example = "EDGE", allowableValues = "EDGE") + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "string", example = "EDGE", allowableValues = "EDGE") @Override public EntityType getEntityType() { return EntityType.EDGE; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityViewId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityViewId.java index a44f35e5d3..7c5f7be1e8 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityViewId.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityViewId.java @@ -38,7 +38,7 @@ public class EntityViewId extends UUIDBased implements EntityId { return new EntityViewId(UUID.fromString(entityViewID)); } - @Schema(required = true, description = "string", example = "ENTITY_VIEW", allowableValues = "ENTITY_VIEW") + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "string", example = "ENTITY_VIEW", allowableValues = "ENTITY_VIEW") @Override public EntityType getEntityType() { return EntityType.ENTITY_VIEW; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/NotificationId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/NotificationId.java index be8aa8e9cd..8fc12993b2 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/NotificationId.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/NotificationId.java @@ -29,7 +29,7 @@ public class NotificationId extends UUIDBased implements EntityId { super(id); } - @Schema(required = true, description = "string", example = "NOTIFICATION", allowableValues = "NOTIFICATION") + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "string", example = "NOTIFICATION", allowableValues = "NOTIFICATION") @Override public EntityType getEntityType() { return EntityType.NOTIFICATION; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/NotificationRequestId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/NotificationRequestId.java index afcb27f0c9..30c7336b79 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/NotificationRequestId.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/NotificationRequestId.java @@ -29,7 +29,7 @@ public class NotificationRequestId extends UUIDBased implements EntityId { super(id); } - @Schema(required = true, description = "string", example = "NOTIFICATION_REQUEST", allowableValues = "NOTIFICATION_REQUEST") + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "string", example = "NOTIFICATION_REQUEST", allowableValues = "NOTIFICATION_REQUEST") @Override public EntityType getEntityType() { return EntityType.NOTIFICATION_REQUEST; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/NotificationRuleId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/NotificationRuleId.java index dc5e6c8671..c17e42a759 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/NotificationRuleId.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/NotificationRuleId.java @@ -29,7 +29,7 @@ public class NotificationRuleId extends UUIDBased implements EntityId { super(id); } - @Schema(required = true, description = "string", example = "NOTIFICATION_RULE", allowableValues = "NOTIFICATION_RULE") + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "string", example = "NOTIFICATION_RULE", allowableValues = "NOTIFICATION_RULE") @Override public EntityType getEntityType() { return EntityType.NOTIFICATION_RULE; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/NotificationTargetId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/NotificationTargetId.java index 3966dbbbd7..2984bc7644 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/NotificationTargetId.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/NotificationTargetId.java @@ -29,7 +29,7 @@ public class NotificationTargetId extends UUIDBased implements EntityId { super(id); } - @Schema(required = true, description = "string", example = "NOTIFICATION_TARGET", allowableValues = "NOTIFICATION_TARGET") + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "string", example = "NOTIFICATION_TARGET", allowableValues = "NOTIFICATION_TARGET") @Override public EntityType getEntityType() { return EntityType.NOTIFICATION_TARGET; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/NotificationTemplateId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/NotificationTemplateId.java index 65f312b7e0..9fb99a578e 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/NotificationTemplateId.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/NotificationTemplateId.java @@ -29,7 +29,7 @@ public class NotificationTemplateId extends UUIDBased implements EntityId { super(id); } - @Schema(required = true, description = "string", example = "NOTIFICATION_TEMPLATE", allowableValues = "NOTIFICATION_TEMPLATE") + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "string", example = "NOTIFICATION_TEMPLATE", allowableValues = "NOTIFICATION_TEMPLATE") @Override public EntityType getEntityType() { return EntityType.NOTIFICATION_TEMPLATE; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/OtaPackageId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/OtaPackageId.java index 3225a72f6f..bc042883ce 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/OtaPackageId.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/OtaPackageId.java @@ -35,7 +35,7 @@ public class OtaPackageId extends UUIDBased implements EntityId { return new OtaPackageId(UUID.fromString(firmwareId)); } - @Schema(required = true, description = "string", example = "OTA_PACKAGE", allowableValues = "OTA_PACKAGE") + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "string", example = "OTA_PACKAGE", allowableValues = "OTA_PACKAGE") @Override public EntityType getEntityType() { return EntityType.OTA_PACKAGE; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/QueueId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/QueueId.java index 7f415fc451..48987a0539 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/QueueId.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/QueueId.java @@ -35,9 +35,9 @@ public class QueueId extends UUIDBased implements EntityId { return new QueueId(UUID.fromString(queueId)); } - @Schema(required = true, description = "string", example = "QUEUE", allowableValues = "QUEUE") + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "string", example = "QUEUE", allowableValues = "QUEUE") @Override public EntityType getEntityType() { return EntityType.QUEUE; } -} \ No newline at end of file +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/QueueStatsId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/QueueStatsId.java index ae5a4843ed..dd36f09313 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/QueueStatsId.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/QueueStatsId.java @@ -35,9 +35,9 @@ public class QueueStatsId extends UUIDBased implements EntityId { return new QueueStatsId(UUID.fromString(queueId)); } - @Schema(required = true, description = "string", example = "QUEUE_STATS", allowableValues = "QUEUE_STATS") + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "string", example = "QUEUE_STATS", allowableValues = "QUEUE_STATS") @Override public EntityType getEntityType() { return EntityType.QUEUE_STATS; } -} \ No newline at end of file +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/RpcId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/RpcId.java index b544971d98..c6bf1aff87 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/RpcId.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/RpcId.java @@ -31,7 +31,7 @@ public final class RpcId extends UUIDBased implements EntityId { super(id); } - @Schema(required = true, description = "string", example = "RPC", allowableValues = "RPC") + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "string", example = "RPC", allowableValues = "RPC") @Override public EntityType getEntityType() { return EntityType.RPC; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/RuleChainId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/RuleChainId.java index 379d1f608e..1557b053c2 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/RuleChainId.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/RuleChainId.java @@ -29,7 +29,7 @@ public class RuleChainId extends UUIDBased implements EntityId { super(id); } - @Schema(required = true, description = "string", example = "RULE_CHAIN", allowableValues = "RULE_CHAIN") + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "string", example = "RULE_CHAIN", allowableValues = "RULE_CHAIN") @Override public EntityType getEntityType() { return EntityType.RULE_CHAIN; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/RuleNodeId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/RuleNodeId.java index b0c1d1c8e5..945c99a89a 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/RuleNodeId.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/RuleNodeId.java @@ -29,7 +29,7 @@ public class RuleNodeId extends UUIDBased implements EntityId { super(id); } - @Schema(required = true, description = "string", example = "RULE_NODE", allowableValues = "RULE_NODE") + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "string", example = "RULE_NODE", allowableValues = "RULE_NODE") @Override public EntityType getEntityType() { return EntityType.RULE_NODE; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/TbResourceId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/TbResourceId.java index 28d22d04f5..a2ef70b75b 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/TbResourceId.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/TbResourceId.java @@ -31,7 +31,7 @@ public class TbResourceId extends UUIDBased implements EntityId { super(id); } - @Schema(required = true, description = "string", example = "TB_RESOURCE", allowableValues = "TB_RESOURCE") + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "string", example = "TB_RESOURCE", allowableValues = "TB_RESOURCE") @Override public EntityType getEntityType() { return EntityType.TB_RESOURCE; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/TenantId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/TenantId.java index aead099818..e160eb59d0 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/TenantId.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/TenantId.java @@ -50,7 +50,7 @@ public final class TenantId extends UUIDBased implements EntityId { return this.equals(SYS_TENANT_ID); } - @Schema(required = true, description = "string", example = "TENANT", allowableValues = "TENANT") + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "string", example = "TENANT", allowableValues = "TENANT") @Override public EntityType getEntityType() { return EntityType.TENANT; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/TenantProfileId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/TenantProfileId.java index ea2aaa1ac1..7b40eb01ed 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/TenantProfileId.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/TenantProfileId.java @@ -35,7 +35,7 @@ public class TenantProfileId extends UUIDBased implements EntityId { return new TenantProfileId(UUID.fromString(tenantProfileId)); } - @Schema(required = true, description = "string", example = "TENANT_PROFILE", allowableValues = "TENANT_PROFILE") + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "string", example = "TENANT_PROFILE", allowableValues = "TENANT_PROFILE") @Override public EntityType getEntityType() { return EntityType.TENANT_PROFILE; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/UUIDBased.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/UUIDBased.java index b9fd9d30ee..e0a1abf9ff 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/UUIDBased.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/UUIDBased.java @@ -36,7 +36,7 @@ public abstract class UUIDBased implements HasUUID, Serializable { this.id = id; } - @Schema(required = true, description = "string", example = "784f394c-42b6-435a-983c-b7beff2784f9") + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "string", example = "784f394c-42b6-435a-983c-b7beff2784f9") public UUID getId() { return id; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/WidgetTypeId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/WidgetTypeId.java index dfa1934499..76a631d4fb 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/WidgetTypeId.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/WidgetTypeId.java @@ -31,7 +31,7 @@ public final class WidgetTypeId extends UUIDBased implements EntityId { super(id); } - @Schema(required = true, description = "string", example = "WIDGET_TYPE", allowableValues = "WIDGET_TYPE") + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "string", example = "WIDGET_TYPE", allowableValues = "WIDGET_TYPE") @Override public EntityType getEntityType() { return EntityType.WIDGET_TYPE; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/WidgetsBundleId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/WidgetsBundleId.java index eaa679bc6a..fd9d3e9db7 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/WidgetsBundleId.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/WidgetsBundleId.java @@ -31,7 +31,7 @@ public final class WidgetsBundleId extends UUIDBased implements EntityId { super(id); } - @Schema(required = true, description = "string", example = "WIDGETS_BUNDLE", allowableValues = "WIDGETS_BUNDLE") + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "string", example = "WIDGETS_BUNDLE", allowableValues = "WIDGETS_BUNDLE") @Override public EntityType getEntityType() { return EntityType.WIDGETS_BUNDLE; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2BasicMapperConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2BasicMapperConfig.java index 3d419caea8..926e063f8d 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2BasicMapperConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2BasicMapperConfig.java @@ -38,7 +38,7 @@ public class OAuth2BasicMapperConfig { @Length(fieldName = "lastNameAttributeKey", max = 31) @Schema(description = "Last name attribute key") private final String lastNameAttributeKey; - @Schema(description = "Tenant naming strategy. For DOMAIN type, domain for tenant name will be taken from the email (substring before '@')", required = true) + @Schema(description = "Tenant naming strategy. For DOMAIN type, domain for tenant name will be taken from the email (substring before '@')", requiredMode = Schema.RequiredMode.REQUIRED) private final TenantNameStrategyType tenantNameStrategy; @Length(fieldName = "tenantNamePattern") @Schema(description = "Tenant name pattern for CUSTOM naming strategy. " + diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2ClientRegistrationTemplate.java b/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2ClientRegistrationTemplate.java index 0f9f580264..aa15466d0e 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2ClientRegistrationTemplate.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2ClientRegistrationTemplate.java @@ -36,7 +36,7 @@ import java.util.List; public class OAuth2ClientRegistrationTemplate extends BaseDataWithAdditionalInfo implements HasName { @Length(fieldName = "providerId") - @Schema(description = "OAuth2 provider identifier (e.g. its name)", required = true) + @Schema(description = "OAuth2 provider identifier (e.g. its name)", requiredMode = Schema.RequiredMode.REQUIRED) private String providerId; @Valid @Schema(description = "Default config for mapping OAuth2 log in response to platform entities") diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2DomainInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2DomainInfo.java index 9ddc3c8492..dc70213e95 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2DomainInfo.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2DomainInfo.java @@ -31,8 +31,8 @@ import lombok.ToString; @Builder @Schema public class OAuth2DomainInfo { - @Schema(description = "Domain scheme. Mixed scheme means than both HTTP and HTTPS are going to be used", required = true) + @Schema(description = "Domain scheme. Mixed scheme means than both HTTP and HTTPS are going to be used", requiredMode = Schema.RequiredMode.REQUIRED) private SchemeType scheme; - @Schema(description = "Domain name. Cannot be empty", required = true) + @Schema(description = "Domain name. Cannot be empty", requiredMode = Schema.RequiredMode.REQUIRED) private String name; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2Info.java b/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2Info.java index 034070f57c..0a2ace050c 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2Info.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2Info.java @@ -37,6 +37,6 @@ public class OAuth2Info { private boolean enabled; @Schema(description = "Whether OAuth2 settings are enabled on Edge or not") private boolean edgeEnabled; - @Schema(description = "List of configured OAuth2 clients. Cannot contain null values", required = true) + @Schema(description = "List of configured OAuth2 clients. Cannot contain null values", requiredMode = Schema.RequiredMode.REQUIRED) private List oauth2ParamsInfos; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2MapperConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2MapperConfig.java index ad39cdad04..be13a032e2 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2MapperConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2MapperConfig.java @@ -32,7 +32,7 @@ public class OAuth2MapperConfig { private boolean allowUserCreation; @Schema(description = "Whether user credentials should be activated when user is created after successful authentication") private boolean activateUser; - @Schema(description = "Type of OAuth2 mapper. Depending on this param, different mapper config fields must be specified", required = true) + @Schema(description = "Type of OAuth2 mapper. Depending on this param, different mapper config fields must be specified", requiredMode = Schema.RequiredMode.REQUIRED) private MapperType type; @Valid @Schema(description = "Mapper config for BASIC and GITHUB mapper types") diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2MobileInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2MobileInfo.java index 8aaffb80aa..053762bdf0 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2MobileInfo.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2MobileInfo.java @@ -31,8 +31,8 @@ import lombok.ToString; @Builder @Schema public class OAuth2MobileInfo { - @Schema(description = "Application package name. Cannot be empty", required = true) + @Schema(description = "Application package name. Cannot be empty", requiredMode = Schema.RequiredMode.REQUIRED) private String pkgName; - @Schema(description = "Application secret. The length must be at least 16 characters", required = true) + @Schema(description = "Application secret. The length must be at least 16 characters", requiredMode = Schema.RequiredMode.REQUIRED) private String appSecret; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2ParamsInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2ParamsInfo.java index 76dca282cf..c1b6124b8f 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2ParamsInfo.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2ParamsInfo.java @@ -36,11 +36,11 @@ public class OAuth2ParamsInfo { @Schema(description = "List of configured domains where OAuth2 platform will redirect a user after successful " + "authentication. Cannot be empty. There have to be only one domain with specific name with scheme type 'MIXED'. " + - "Configured domains with the same name must have different scheme types", required = true) + "Configured domains with the same name must have different scheme types", requiredMode = Schema.RequiredMode.REQUIRED) private List domainInfos; - @Schema(description = "Mobile applications settings. Application package name must be unique within the list", required = true) + @Schema(description = "Mobile applications settings. Application package name must be unique within the list", requiredMode = Schema.RequiredMode.REQUIRED) private List mobileInfos; - @Schema(description = "List of OAuth2 provider settings. Cannot be empty", required = true) + @Schema(description = "List of OAuth2 provider settings. Cannot be empty", requiredMode = Schema.RequiredMode.REQUIRED) private List clientRegistrations; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2RegistrationInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2RegistrationInfo.java index 1773583ffc..36bae12b23 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2RegistrationInfo.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2RegistrationInfo.java @@ -34,17 +34,17 @@ import java.util.List; @Builder @Schema public class OAuth2RegistrationInfo { - @Schema(description = "Config for mapping OAuth2 log in response to platform entities", required = true) + @Schema(description = "Config for mapping OAuth2 log in response to platform entities", requiredMode = Schema.RequiredMode.REQUIRED) private OAuth2MapperConfig mapperConfig; - @Schema(description = "OAuth2 client ID. Cannot be empty", required = true) + @Schema(description = "OAuth2 client ID. Cannot be empty", requiredMode = Schema.RequiredMode.REQUIRED) private String clientId; - @Schema(description = "OAuth2 client secret. Cannot be empty", required = true) + @Schema(description = "OAuth2 client secret. Cannot be empty", requiredMode = Schema.RequiredMode.REQUIRED) private String clientSecret; - @Schema(description = "Authorization URI of the OAuth2 provider. Cannot be empty", required = true) + @Schema(description = "Authorization URI of the OAuth2 provider. Cannot be empty", requiredMode = Schema.RequiredMode.REQUIRED) private String authorizationUri; - @Schema(description = "Access token URI of the OAuth2 provider. Cannot be empty", required = true) + @Schema(description = "Access token URI of the OAuth2 provider. Cannot be empty", requiredMode = Schema.RequiredMode.REQUIRED) private String accessTokenUri; - @Schema(description = "OAuth scopes that will be requested from OAuth2 platform. Cannot be empty", required = true) + @Schema(description = "OAuth scopes that will be requested from OAuth2 platform. Cannot be empty", requiredMode = Schema.RequiredMode.REQUIRED) private List scope; @Schema(description = "User info URI of the OAuth2 provider") private String userInfoUri; @@ -52,14 +52,14 @@ public class OAuth2RegistrationInfo { private String userNameAttributeName; @Schema(description = "JSON Web Key URI of the OAuth2 provider") private String jwkSetUri; - @Schema(description = "Client authentication method to use: 'BASIC' or 'POST'. Cannot be empty", required = true) + @Schema(description = "Client authentication method to use: 'BASIC' or 'POST'. Cannot be empty", requiredMode = Schema.RequiredMode.REQUIRED) private String clientAuthenticationMethod; - @Schema(description = "OAuth2 provider label. Cannot be empty", required = true) + @Schema(description = "OAuth2 provider label. Cannot be empty", requiredMode = Schema.RequiredMode.REQUIRED) private String loginButtonLabel; @Schema(description = "Log in button icon for OAuth2 provider") private String loginButtonIcon; @Schema(description = "List of platforms for which usage of the OAuth2 client is allowed (empty for all allowed)") private List platforms; - @Schema(description = "Additional info of OAuth2 client (e.g. providerName)", required = true) + @Schema(description = "Additional info of OAuth2 client (e.g. providerName)", requiredMode = Schema.RequiredMode.REQUIRED) private JsonNode additionalInfo; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/objects/AttributesEntityView.java b/common/data/src/main/java/org/thingsboard/server/common/data/objects/AttributesEntityView.java index a6f504577b..309385d5e4 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/objects/AttributesEntityView.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/objects/AttributesEntityView.java @@ -31,11 +31,11 @@ import java.util.List; @NoArgsConstructor public class AttributesEntityView implements Serializable { - @Schema(required = true, description = "List of client-side attribute keys to expose", example = "currentConfiguration") + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "List of client-side attribute keys to expose", example = "currentConfiguration") private List cs = new ArrayList<>(); - @Schema(required = true, description = "List of server-side attribute keys to expose", example = "model") + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "List of server-side attribute keys to expose", example = "model") private List ss = new ArrayList<>(); - @Schema(required = true, description = "List of shared attribute keys to expose", example = "targetConfiguration") + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "List of shared attribute keys to expose", example = "targetConfiguration") private List sh = new ArrayList<>(); public AttributesEntityView(List cs, diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/objects/TelemetryEntityView.java b/common/data/src/main/java/org/thingsboard/server/common/data/objects/TelemetryEntityView.java index 609d185f9c..a2484b0707 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/objects/TelemetryEntityView.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/objects/TelemetryEntityView.java @@ -31,9 +31,9 @@ import java.util.List; @NoArgsConstructor public class TelemetryEntityView implements Serializable { - @Schema(required = true, description = "List of time-series data keys to expose", example = "temperature, humidity") + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "List of time-series data keys to expose", example = "temperature, humidity") private List timeseries; - @Schema(required = true, description = "JSON object with attributes to expose") + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "JSON object with attributes to expose") private AttributesEntityView attributes; public TelemetryEntityView(List timeseries, AttributesEntityView attributes) { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/rule/DefaultRuleChainCreateRequest.java b/common/data/src/main/java/org/thingsboard/server/common/data/rule/DefaultRuleChainCreateRequest.java index d984fd244d..cda3072101 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/rule/DefaultRuleChainCreateRequest.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/rule/DefaultRuleChainCreateRequest.java @@ -28,7 +28,7 @@ public class DefaultRuleChainCreateRequest implements Serializable { private static final long serialVersionUID = 5600333716030561537L; - @Schema(required = true, description = "Name of the new rule chain", example = "Root Rule Chain") + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "Name of the new rule chain", example = "Root Rule Chain") private String name; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/rule/NodeConnectionInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/rule/NodeConnectionInfo.java index 82be10c121..fe80293d16 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/rule/NodeConnectionInfo.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/rule/NodeConnectionInfo.java @@ -24,10 +24,10 @@ import lombok.Data; @Schema @Data public class NodeConnectionInfo { - @Schema(required = true, description = "Index of rule node in the 'nodes' array of the RuleChainMetaData. Indicates the 'from' part of the connection.") + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "Index of rule node in the 'nodes' array of the RuleChainMetaData. Indicates the 'from' part of the connection.") private int fromIndex; - @Schema(required = true, description = "Index of rule node in the 'nodes' array of the RuleChainMetaData. Indicates the 'to' part of the connection.") + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "Index of rule node in the 'nodes' array of the RuleChainMetaData. Indicates the 'to' part of the connection.") private int toIndex; - @Schema(required = true, description = "Type of the relation. Typically indicated the result of processing by the 'from' rule node. For example, 'Success' or 'Failure'") + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "Type of the relation. Typically indicated the result of processing by the 'from' rule node. For example, 'Success' or 'Failure'") private String type; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChain.java b/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChain.java index be901a9176..c293e6f570 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChain.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChain.java @@ -39,11 +39,11 @@ public class RuleChain extends BaseDataWithAdditionalInfo implement private static final long serialVersionUID = -5656679015121935465L; - @Schema(required = true, description = "JSON object with Tenant Id.", accessMode = Schema.AccessMode.READ_ONLY) + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "JSON object with Tenant Id.", accessMode = Schema.AccessMode.READ_ONLY) private TenantId tenantId; @NoXss @Length(fieldName = "name") - @Schema(required = true, description = "Rule Chain name", example = "Humidity data processing") + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "Rule Chain name", example = "Humidity data processing") private String name; @Schema(description = "Rule Chain type. 'EDGE' rule chains are processing messages on the edge devices only.", example = "A4B72CCDFF33") private RuleChainType type; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChainConnectionInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChainConnectionInfo.java index 654f296c63..029cfb9976 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChainConnectionInfo.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChainConnectionInfo.java @@ -26,12 +26,12 @@ import org.thingsboard.server.common.data.id.RuleChainId; @Schema @Data public class RuleChainConnectionInfo { - @Schema(required = true, description = "Index of rule node in the 'nodes' array of the RuleChainMetaData. Indicates the 'from' part of the connection.") + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "Index of rule node in the 'nodes' array of the RuleChainMetaData. Indicates the 'from' part of the connection.") private int fromIndex; - @Schema(required = true, description = "JSON object with the Rule Chain Id.") + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "JSON object with the Rule Chain Id.") private RuleChainId targetRuleChainId; - @Schema(required = true, description = "JSON object with the additional information about the connection.") + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "JSON object with the additional information about the connection.") private JsonNode additionalInfo; - @Schema(required = true, description = "Type of the relation. Typically indicated the result of processing by the 'from' rule node. For example, 'Success' or 'Failure'") + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "Type of the relation. Typically indicated the result of processing by the 'from' rule node. For example, 'Success' or 'Failure'") private String type; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChainData.java b/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChainData.java index e9899d6019..e75d830e84 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChainData.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChainData.java @@ -24,8 +24,8 @@ import java.util.List; @Data public class RuleChainData { - @Schema(required = true, description = "List of the Rule Chain objects.", accessMode = Schema.AccessMode.READ_ONLY) + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "List of the Rule Chain objects.", accessMode = Schema.AccessMode.READ_ONLY) List ruleChains; - @Schema(required = true, description = "List of the Rule Chain metadata objects.", accessMode = Schema.AccessMode.READ_ONLY) + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "List of the Rule Chain metadata objects.", accessMode = Schema.AccessMode.READ_ONLY) List metadata; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChainMetaData.java b/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChainMetaData.java index 9a7bc70a7f..eb18e28c8c 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChainMetaData.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChainMetaData.java @@ -29,19 +29,19 @@ import java.util.List; @Data public class RuleChainMetaData { - @Schema(required = true, description = "JSON object with Rule Chain Id.", accessMode = Schema.AccessMode.READ_ONLY) + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "JSON object with Rule Chain Id.", accessMode = Schema.AccessMode.READ_ONLY) private RuleChainId ruleChainId; - @Schema(required = true, description = "Index of the first rule node in the 'nodes' list") + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "Index of the first rule node in the 'nodes' list") private Integer firstNodeIndex; - @Schema(required = true, description = "List of rule node JSON objects") + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "List of rule node JSON objects") private List nodes; - @Schema(required = true, description = "List of JSON objects that represent connections between rule nodes") + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "List of JSON objects that represent connections between rule nodes") private List connections; - @Schema(required = true, description = "List of JSON objects that represent connections between rule nodes and other rule chains.") + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "List of JSON objects that represent connections between rule nodes and other rule chains.") private List ruleChainConnections; public void addConnectionInfo(int fromIndex, int toIndex, String type) { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChainOutputLabelsUsage.java b/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChainOutputLabelsUsage.java index d0c7186501..b9fb1fc6d8 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChainOutputLabelsUsage.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChainOutputLabelsUsage.java @@ -28,16 +28,16 @@ import java.util.Set; @Slf4j public class RuleChainOutputLabelsUsage { - @Schema(required = true, description = "Rule Chain Id", accessMode = Schema.AccessMode.READ_ONLY) + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "Rule Chain Id", accessMode = Schema.AccessMode.READ_ONLY) private RuleChainId ruleChainId; - @Schema(required = true, description = "Rule Node Id", accessMode = Schema.AccessMode.READ_ONLY) + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "Rule Node Id", accessMode = Schema.AccessMode.READ_ONLY) private RuleNodeId ruleNodeId; - @Schema(required = true, description = "Rule Chain Name", accessMode = Schema.AccessMode.READ_ONLY) + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "Rule Chain Name", accessMode = Schema.AccessMode.READ_ONLY) private String ruleChainName; - @Schema(required = true, description = "Rule Node Name", accessMode = Schema.AccessMode.READ_ONLY) + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "Rule Node Name", accessMode = Schema.AccessMode.READ_ONLY) private String ruleNodeName; - @Schema(required = true, description = "Output labels", accessMode = Schema.AccessMode.READ_ONLY) + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "Output labels", accessMode = Schema.AccessMode.READ_ONLY) private Set labels; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/security/DeviceCredentials.java b/common/data/src/main/java/org/thingsboard/server/common/data/security/DeviceCredentials.java index f59b86af63..24d8cc1175 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/security/DeviceCredentials.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/security/DeviceCredentials.java @@ -47,7 +47,7 @@ public class DeviceCredentials extends BaseData implements this.credentialsValue = deviceCredentials.getCredentialsValue(); } - @Schema(required = true, accessMode = Schema.AccessMode.READ_ONLY, description = "The Id is automatically generated during device creation. " + + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, accessMode = Schema.AccessMode.READ_ONLY, description = "The Id is automatically generated during device creation. " + "Use 'getDeviceCredentialsByDeviceId' to obtain the id based on device id. " + "Use 'updateDeviceCredentials' to update device credentials. ", example = "784f394c-42b6-435a-983c-b7beff2784f9") @Override @@ -61,7 +61,7 @@ public class DeviceCredentials extends BaseData implements return super.getCreatedTime(); } - @Schema(required = true, description = "JSON object with the device Id.") + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "JSON object with the device Id.") public DeviceId getDeviceId() { return deviceId; } @@ -80,7 +80,7 @@ public class DeviceCredentials extends BaseData implements this.credentialsType = credentialsType; } - @Schema(required = true, description = "Unique Credentials Id per platform instance. " + + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "Unique Credentials Id per platform instance. " + "Used to lookup credentials from the database. " + "By default, new access token for your device. " + "Depends on the type of the credentials." diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/sms/config/SmppSmsProviderConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/sms/config/SmppSmsProviderConfiguration.java index 8ed16a1c1c..5fc0c14d61 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/sms/config/SmppSmsProviderConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/sms/config/SmppSmsProviderConfiguration.java @@ -20,27 +20,27 @@ import lombok.Data; @Data public class SmppSmsProviderConfiguration implements SmsProviderConfiguration { - @Schema(description = "SMPP version", allowableValues = "3.3, 3.4", required = true) + @Schema(description = "SMPP version", allowableValues = "3.3, 3.4", requiredMode = Schema.RequiredMode.REQUIRED) private String protocolVersion; - @Schema(description = "SMPP host", required = true) + @Schema(description = "SMPP host", requiredMode = Schema.RequiredMode.REQUIRED) private String host; - @Schema(description = "SMPP port", required = true) + @Schema(description = "SMPP port", requiredMode = Schema.RequiredMode.REQUIRED) private Integer port; - @Schema(description = "System ID", required = true) + @Schema(description = "System ID", requiredMode = Schema.RequiredMode.REQUIRED) private String systemId; - @Schema(description = "Password", required = true) + @Schema(description = "Password", requiredMode = Schema.RequiredMode.REQUIRED) private String password; - @Schema(description = "System type", required = false) + @Schema(description = "System type", requiredMode = Schema.RequiredMode.NOT_REQUIRED) private String systemType; - @Schema(description = "TX - Transmitter, RX - Receiver, TRX - Transciever. By default TX is used", required = false) + @Schema(description = "TX - Transmitter, RX - Receiver, TRX - Transciever. By default TX is used", requiredMode = Schema.RequiredMode.NOT_REQUIRED) private SmppBindType bindType; - @Schema(description = "Service type", required = false) + @Schema(description = "Service type", requiredMode = Schema.RequiredMode.NOT_REQUIRED) private String serviceType; - @Schema(description = "Source address", required = false) + @Schema(description = "Source address", requiredMode = Schema.RequiredMode.NOT_REQUIRED) private String sourceAddress; @Schema(description = "Source TON (Type of Number). Needed is source address is set. 5 by default.\n" + "0 - Unknown\n" + @@ -49,7 +49,7 @@ public class SmppSmsProviderConfiguration implements SmsProviderConfiguration { "3 - Network Specific\n" + "4 - Subscriber Number\n" + "5 - Alphanumeric\n" + - "6 - Abbreviated", required = false) + "6 - Abbreviated", requiredMode = Schema.RequiredMode.NOT_REQUIRED) private Byte sourceTon; @Schema(description = "Source NPI (Numbering Plan Identification). Needed is source address is set. 0 by default.\n" + "0 - Unknown\n" + @@ -61,7 +61,7 @@ public class SmppSmsProviderConfiguration implements SmsProviderConfiguration { "9 - Private numbering plan\n" + "10 - ERMES numbering plan (ETSI DE/PS 3 01-3)\n" + "13 - Internet (IP)\n" + - "18 - WAP Client Id (to be defined by WAP Forum)", required = false) + "18 - WAP Client Id (to be defined by WAP Forum)", requiredMode = Schema.RequiredMode.NOT_REQUIRED) private Byte sourceNpi; @Schema(description = "Destination TON (Type of Number). 5 by default.\n" + @@ -71,7 +71,7 @@ public class SmppSmsProviderConfiguration implements SmsProviderConfiguration { "3 - Network Specific\n" + "4 - Subscriber Number\n" + "5 - Alphanumeric\n" + - "6 - Abbreviated", required = false) + "6 - Abbreviated", requiredMode = Schema.RequiredMode.NOT_REQUIRED) private Byte destinationTon; @Schema(description = "Destination NPI (Numbering Plan Identification). 0 by default.\n" + "0 - Unknown\n" + @@ -83,10 +83,10 @@ public class SmppSmsProviderConfiguration implements SmsProviderConfiguration { "9 - Private numbering plan\n" + "10 - ERMES numbering plan (ETSI DE/PS 3 01-3)\n" + "13 - Internet (IP)\n" + - "18 - WAP Client Id (to be defined by WAP Forum)", required = false) + "18 - WAP Client Id (to be defined by WAP Forum)", requiredMode = Schema.RequiredMode.NOT_REQUIRED) private Byte destinationNpi; - @Schema(description = "Address range", required = false) + @Schema(description = "Address range", requiredMode = Schema.RequiredMode.NOT_REQUIRED) private String addressRange; @Schema(allowableValues = "0-10,13-14", @@ -102,7 +102,7 @@ public class SmppSmsProviderConfiguration implements SmsProviderConfiguration { "9 - Pictogram Encoding\n" + "10 - Music Codes (ISO-2022-JP)\n" + "13 - Extended Kanji JIS (X 0212-1990)\n" + - "14 - Korean Graphic Character Set (KS C 5601/KS X 1001)", required = false) + "14 - Korean Graphic Character Set (KS C 5601/KS X 1001)", requiredMode = Schema.RequiredMode.NOT_REQUIRED) private Byte codingScheme; @Override From 6c3c9f99b9e1547145eac1769690cec79b234435 Mon Sep 17 00:00:00 2001 From: nick Date: Wed, 10 Apr 2024 17:18:15 +0300 Subject: [PATCH 42/80] fix bug: lwm2m tests dif Port 4 --- .../lwm2m/server/store/TbInMemoryRegistrationStore.java | 2 +- .../lwm2m/server/store/TbLwM2mRedisRegistrationStore.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbInMemoryRegistrationStore.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbInMemoryRegistrationStore.java index fa9816c52d..ba49da33e5 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbInMemoryRegistrationStore.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbInMemoryRegistrationStore.java @@ -148,7 +148,7 @@ public class TbInMemoryRegistrationStore implements RegistrationStore, Startable @Override public UpdatedRegistration updateRegistration(RegistrationUpdate update) { // test fix bug Diff port - log.warn("updateRegistration inMemory {}", update); + log.trace("updateRegistration inMemory {}", update); try { lock.writeLock().lock(); diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbLwM2mRedisRegistrationStore.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbLwM2mRedisRegistrationStore.java index 87dedc52fc..5e60c4498a 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbLwM2mRedisRegistrationStore.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbLwM2mRedisRegistrationStore.java @@ -223,7 +223,7 @@ public class TbLwM2mRedisRegistrationStore implements RegistrationStore, Startab @Override public UpdatedRegistration updateRegistration(RegistrationUpdate update) { // test fix bug Diff port - log.warn("updateRegistration Redis {}", update); + log.trace("updateRegistration Redis {}", update); Lock lock = null; try (var connection = connectionFactory.getConnection()) { From decd12565eee14071833c2cf23ae77017ca508ee Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Wed, 10 Apr 2024 18:14:53 +0300 Subject: [PATCH 43/80] UI: State chart - not filter data on data zoom. --- .../home/components/widget/lib/chart/time-series-chart.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.ts b/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.ts index 320a683363..700f07da15 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.ts @@ -601,13 +601,15 @@ export class TbTimeSeriesChart { { type: 'inside', disabled: !this.settings.dataZoom, - realtime: true + realtime: true, + filterMode: this.stateData ? 'none' : 'filter' }, { type: 'slider', show: this.settings.dataZoom, showDetail: false, realtime: true, + filterMode: this.stateData ? 'none' : 'filter', bottom: 10 } ], From 6ea06fecd89d2d289a382ca0afc554329e7d3c3f Mon Sep 17 00:00:00 2001 From: nick Date: Thu, 11 Apr 2024 06:53:45 +0300 Subject: [PATCH 44/80] fix bug: lwm2m tests dif Port 5 --- docker/docker-compose.yml | 1 + docker/tb-lwm2m-transport.env | 2 ++ 2 files changed, 3 insertions(+) diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 9ed8e015f1..18e80b425d 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -220,6 +220,7 @@ services: image: "${DOCKER_REPO}/${LWM2M_TRANSPORT_DOCKER_NAME}:${TB_VERSION}" ports: - "5685:5685/udp" + - "5686:5686/udp" environment: TB_SERVICE_ID: tb-lwm2m-transport JAVA_OPTS: "${JAVA_OPTS}" diff --git a/docker/tb-lwm2m-transport.env b/docker/tb-lwm2m-transport.env index f284803a46..bf1d612410 100644 --- a/docker/tb-lwm2m-transport.env +++ b/docker/tb-lwm2m-transport.env @@ -3,6 +3,8 @@ ZOOKEEPER_URL=zookeeper:2181 LWM2M_BIND_ADDRESS=0.0.0.0 LWM2M_BIND_PORT=5685 +LWM2M_SECURITY_BIND_ADDRESS=0.0.0.0 +LWM2M_SECURITY_BIND_PORT=5686 LWM2M_TIMEOUT=10000 METRICS_ENABLED=true From 934287a380679a93336054578824f812d3b80ea1 Mon Sep 17 00:00:00 2001 From: nick Date: Thu, 11 Apr 2024 12:25:44 +0300 Subject: [PATCH 45/80] fix bug: lwm2m tests dif Port Comments 1 --- .../AbstractLwM2MIntegrationDiffPortTest.java | 14 ++++++-------- .../server/store/TbInMemoryRegistrationStore.java | 3 +-- .../store/TbLwM2mRedisRegistrationStore.java | 3 +-- 3 files changed, 8 insertions(+), 12 deletions(-) diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/diffPort/AbstractLwM2MIntegrationDiffPortTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/diffPort/AbstractLwM2MIntegrationDiffPortTest.java index 44da58a4ef..9e9a38925c 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/diffPort/AbstractLwM2MIntegrationDiffPortTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/diffPort/AbstractLwM2MIntegrationDiffPortTest.java @@ -50,14 +50,12 @@ public abstract class AbstractLwM2MIntegrationDiffPortTest extends AbstractSecur doAnswer((invocation) -> { Object[] arguments = invocation.getArguments(); - log.warn("doAnswer for registrationStoreTest.updateRegistration with args {}", arguments); -// if (arguments.length > 0 && arguments[0] instanceof RegistrationUpdate) { - int portOld = ((RegistrationUpdate) arguments[0]).getPort(); - int portValueChange = 5; - arguments[0] = registrationUpdateNewPort((RegistrationUpdate) arguments[0], portValueChange); - int portNew = ((RegistrationUpdate) arguments[0]).getPort(); - Assert.assertEquals((portNew - portOld), portValueChange); -// } + log.trace("doAnswer for registrationStoreTest.updateRegistration with args {}", arguments); + int portOld = ((RegistrationUpdate) arguments[0]).getPort(); + int portValueChange = 5; + arguments[0] = registrationUpdateNewPort((RegistrationUpdate) arguments[0], portValueChange); + int portNew = ((RegistrationUpdate) arguments[0]).getPort(); + Assert.assertEquals((portNew - portOld), portValueChange); return invocation.callRealMethod(); }).when(registrationStoreTest).updateRegistration(any(RegistrationUpdate.class)); diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbInMemoryRegistrationStore.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbInMemoryRegistrationStore.java index ba49da33e5..138d0d6af8 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbInMemoryRegistrationStore.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbInMemoryRegistrationStore.java @@ -147,8 +147,7 @@ public class TbInMemoryRegistrationStore implements RegistrationStore, Startable @Override public UpdatedRegistration updateRegistration(RegistrationUpdate update) { - // test fix bug Diff port - log.trace("updateRegistration inMemory {}", update); + log.trace("updateRegistration [{}]", update); try { lock.writeLock().lock(); diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbLwM2mRedisRegistrationStore.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbLwM2mRedisRegistrationStore.java index 5e60c4498a..436fb479e9 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbLwM2mRedisRegistrationStore.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbLwM2mRedisRegistrationStore.java @@ -222,8 +222,7 @@ public class TbLwM2mRedisRegistrationStore implements RegistrationStore, Startab @Override public UpdatedRegistration updateRegistration(RegistrationUpdate update) { - // test fix bug Diff port - log.trace("updateRegistration Redis {}", update); + log.trace("updateRegistration [{}]", update); Lock lock = null; try (var connection = connectionFactory.getConnection()) { From faf172bdb2257791a519a724fcb6976b14739eaa Mon Sep 17 00:00:00 2001 From: ShvaykaD Date: Thu, 11 Apr 2024 13:11:28 +0300 Subject: [PATCH 46/80] fixed claim device flaky tests --- .../device/DeviceActorMessageProcessor.java | 19 +++++++-- .../device/ClaimDevicesServiceImpl.java | 17 ++++---- .../server/controller/AbstractWebTest.java | 17 ++++++++ .../coap/claim/CoapClaimDeviceTest.java | 34 ++-------------- .../coap/claim/CoapClaimJsonDeviceTest.java | 1 - .../coap/claim/CoapClaimProtoDeviceTest.java | 1 - .../mqtt/AbstractMqttIntegrationTest.java | 8 ++-- ...tClaimBackwardCompatibilityDeviceTest.java | 1 - .../mqttv3/claim/MqttClaimDeviceTest.java | 40 ++++--------------- .../mqttv3/claim/MqttClaimJsonDeviceTest.java | 1 - .../claim/MqttClaimProtoDeviceTest.java | 1 - 11 files changed, 56 insertions(+), 84 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessor.java b/application/src/main/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessor.java index 341fa8765e..87d5c8a63a 100644 --- a/application/src/main/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessor.java +++ b/application/src/main/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessor.java @@ -460,7 +460,7 @@ public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcesso handleSessionActivity(sessionInfo, msg.getSubscriptionInfo()); } if (msg.hasClaimDevice()) { - handleClaimDeviceMsg(msg.getClaimDevice()); + handleClaimDeviceMsg(sessionInfo, msg.getClaimDevice()); } if (msg.hasRpcResponseStatusMsg()) { processRpcResponseStatus(sessionInfo, msg.getRpcResponseStatusMsg()); @@ -485,9 +485,22 @@ public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcesso }); } - private void handleClaimDeviceMsg(ClaimDeviceMsg msg) { + private void handleClaimDeviceMsg(SessionInfoProto sessionInfo, ClaimDeviceMsg msg) { + UUID sessionId = getSessionId(sessionInfo); DeviceId deviceId = new DeviceId(new UUID(msg.getDeviceIdMSB(), msg.getDeviceIdLSB())); - systemContext.getClaimDevicesService().registerClaimingInfo(tenantId, deviceId, msg.getSecretKey(), msg.getDurationMs()); + ListenableFuture registrationFuture = systemContext.getClaimDevicesService() + .registerClaimingInfo(tenantId, deviceId, msg.getSecretKey(), msg.getDurationMs()); + Futures.addCallback(registrationFuture, new FutureCallback<>() { + @Override + public void onSuccess(Void result) { + log.debug("[{}][{}] Successfully processed register claiming info request!", sessionId, deviceId); + } + + @Override + public void onFailure(Throwable t) { + log.error("[{}][{}] Failed to process register claiming info request due to: ", sessionId, deviceId, t); + } + }, MoreExecutors.directExecutor()); } private void reportSessionOpen() { diff --git a/application/src/main/java/org/thingsboard/server/service/device/ClaimDevicesServiceImpl.java b/application/src/main/java/org/thingsboard/server/service/device/ClaimDevicesServiceImpl.java index 974bc06a40..a77b7d0874 100644 --- a/application/src/main/java/org/thingsboard/server/service/device/ClaimDevicesServiceImpl.java +++ b/application/src/main/java/org/thingsboard/server/service/device/ClaimDevicesServiceImpl.java @@ -20,6 +20,7 @@ import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.MoreExecutors; import com.google.common.util.concurrent.SettableFuture; +import jakarta.annotation.Nullable; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; @@ -28,7 +29,6 @@ import org.springframework.cache.CacheManager; import org.springframework.stereotype.Service; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.rule.engine.api.RuleEngineTelemetryService; -import org.thingsboard.server.cluster.TbClusterService; import org.thingsboard.server.common.data.AttributeScope; import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.Device; @@ -51,9 +51,7 @@ import org.thingsboard.server.dao.device.claim.ReclaimResult; import org.thingsboard.server.dao.model.ModelConstants; import org.thingsboard.server.queue.util.TbCoreComponent; -import jakarta.annotation.Nullable; import java.util.Arrays; -import java.util.Collections; import java.util.List; import java.util.Optional; @@ -89,16 +87,16 @@ public class ClaimDevicesServiceImpl implements ClaimDevicesService { Device device = deviceService.findDeviceById(tenantId, deviceId); Cache cache = cacheManager.getCache(CLAIM_DEVICES_CACHE); List key = constructCacheKey(device.getId()); + String deviceName = device.getName(); if (isAllowedClaimingByDefault) { if (device.getCustomerId().getId().equals(ModelConstants.NULL_UUID)) { persistInCache(secretKey, durationMs, cache, key); return Futures.immediateFuture(null); } - log.warn("The device [{}] has been already claimed!", device.getName()); - return Futures.immediateFailedFuture(new IllegalArgumentException()); + return Futures.immediateFailedFuture(new IllegalArgumentException("Device [" + deviceName + "] has been already claimed!")); } else { ListenableFuture> claimingAllowedFuture = attributesService.find(tenantId, device.getId(), - AttributeScope.SERVER_SCOPE, Collections.singletonList(CLAIM_ATTRIBUTE_NAME)); + AttributeScope.SERVER_SCOPE, List.of(CLAIM_ATTRIBUTE_NAME)); return Futures.transform(claimingAllowedFuture, list -> { if (list != null && !list.isEmpty()) { Optional claimingAllowedOptional = list.get(0).getBooleanValue(); @@ -108,8 +106,7 @@ public class ClaimDevicesServiceImpl implements ClaimDevicesService { return null; } } - log.warn("Failed to find claimingAllowed attribute for device or it is already claimed![{}]", device.getName()); - throw new IllegalArgumentException(); + throw new IllegalArgumentException("Failed to find claimingAllowed attribute for device [" + deviceName + "] or it is already claimed!"); }, MoreExecutors.directExecutor()); } } @@ -182,7 +179,7 @@ public class ClaimDevicesServiceImpl implements ClaimDevicesService { } SettableFuture result = SettableFuture.create(); telemetryService.saveAndNotify( - tenantId, savedDevice.getId(), AttributeScope.SERVER_SCOPE, Collections.singletonList( + tenantId, savedDevice.getId(), AttributeScope.SERVER_SCOPE, List.of( new BaseAttributeKvEntry(new BooleanDataEntry(CLAIM_ATTRIBUTE_NAME, true), System.currentTimeMillis()) ), new FutureCallback<>() { @@ -203,7 +200,7 @@ public class ClaimDevicesServiceImpl implements ClaimDevicesService { } private List constructCacheKey(DeviceId deviceId) { - return Collections.singletonList(deviceId); + return List.of(deviceId); } private void persistInCache(String secretKey, long durationMs, Cache cache, List key) { diff --git a/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java b/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java index 49c26b5c38..e14b041e62 100644 --- a/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java @@ -41,6 +41,8 @@ import org.mockito.invocation.InvocationOnMock; import org.mockito.stubbing.Answer; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.mock.mockito.SpyBean; +import org.springframework.cache.Cache; +import org.springframework.cache.CacheManager; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.http.converter.HttpMessageConverter; @@ -112,6 +114,7 @@ import org.thingsboard.server.common.data.tenant.profile.TenantProfileData; import org.thingsboard.server.common.msg.session.FeatureType; import org.thingsboard.server.config.ThingsboardSecurityConfiguration; import org.thingsboard.server.dao.Dao; +import org.thingsboard.server.dao.device.ClaimDevicesService; import org.thingsboard.server.dao.tenant.TenantProfileService; import org.thingsboard.server.dao.timeseries.TimeseriesService; import org.thingsboard.server.service.entitiy.tenant.profile.TbTenantProfileService; @@ -149,6 +152,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers. import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.request; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import static org.springframework.test.web.servlet.setup.MockMvcBuilders.webAppContextSetup; +import static org.thingsboard.server.common.data.CacheConstants.CLAIM_DEVICES_CACHE; @Slf4j public abstract class AbstractWebTest extends AbstractInMemoryStorageTest { @@ -230,6 +234,9 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest { @Autowired protected DefaultActorService actorService; + @Autowired + protected ClaimDevicesService claimDevicesService; + @SpyBean protected MailService mailService; @@ -1055,6 +1062,16 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest { }); } + protected void awaitForClaimingInfoToBeRegistered(DeviceId deviceId) { + CacheManager cacheManager = (CacheManager) ReflectionTestUtils.getField(claimDevicesService, "cacheManager"); + Cache cache = cacheManager.getCache(CLAIM_DEVICES_CACHE); + Awaitility.await("Claiming request from the transport was registered").atMost(5, TimeUnit.SECONDS).until(() -> { + Cache.ValueWrapper value = cache.get(List.of(deviceId)); + log.warn("device {}, claimingRequest registered: {}", deviceId, value); + return value != null; + }); + } + protected static String getMapName(FeatureType featureType) { switch (featureType) { case ATTRIBUTES: diff --git a/application/src/test/java/org/thingsboard/server/transport/coap/claim/CoapClaimDeviceTest.java b/application/src/test/java/org/thingsboard/server/transport/coap/claim/CoapClaimDeviceTest.java index 924792fca1..26bef40c48 100644 --- a/application/src/test/java/org/thingsboard/server/transport/coap/claim/CoapClaimDeviceTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/coap/claim/CoapClaimDeviceTest.java @@ -23,10 +23,7 @@ import org.junit.After; import org.junit.Before; import org.junit.Test; import org.thingsboard.server.common.data.ClaimRequest; -import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.Device; -import org.thingsboard.server.common.data.User; -import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.common.msg.session.FeatureType; import org.thingsboard.server.dao.device.claim.ClaimResponse; import org.thingsboard.server.dao.device.claim.ClaimResult; @@ -45,37 +42,12 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers. @DaoSqlTest public class CoapClaimDeviceTest extends AbstractCoapIntegrationTest { - protected static final String CUSTOMER_USER_PASSWORD = "customerUser123!"; - - protected User customerAdmin; - protected Customer savedCustomer; - @Before public void beforeTest() throws Exception { CoapTestConfigProperties configProperties = CoapTestConfigProperties.builder() .deviceName("Test Claim device") .build(); processBeforeTest(configProperties); - createCustomerAndUser(); - } - - protected void createCustomerAndUser() throws Exception { - Customer customer = new Customer(); - customer.setTenantId(tenantId); - customer.setTitle("Test Claiming Customer"); - savedCustomer = doPost("/api/customer", customer, Customer.class); - assertNotNull(savedCustomer); - assertEquals(tenantId, savedCustomer.getTenantId()); - - User user = new User(); - user.setAuthority(Authority.CUSTOMER_USER); - user.setTenantId(tenantId); - user.setCustomerId(savedCustomer.getId()); - user.setEmail("customer@thingsboard.org"); - - customerAdmin = createUser(user, CUSTOMER_USER_PASSWORD); - assertNotNull(customerAdmin); - assertEquals(customerAdmin.getCustomerId(), savedCustomer.getId()); } @After @@ -110,8 +82,9 @@ public class CoapClaimDeviceTest extends AbstractCoapIntegrationTest { protected void validateClaimResponse(boolean emptyPayload, CoapTestClient client, byte[] payloadBytes, byte[] failurePayloadBytes) throws Exception { postClaimRequest(client, failurePayloadBytes); + awaitForClaimingInfoToBeRegistered(savedDevice.getId()); - loginUser(customerAdmin.getName(), CUSTOMER_USER_PASSWORD); + loginCustomerUser(); ClaimRequest claimRequest; if (!emptyPayload) { claimRequest = new ClaimRequest("value"); @@ -128,6 +101,7 @@ public class CoapClaimDeviceTest extends AbstractCoapIntegrationTest { assertEquals(claimResponse, ClaimResponse.FAILURE); postClaimRequest(client, payloadBytes); + awaitForClaimingInfoToBeRegistered(savedDevice.getId()); ClaimResult claimResult = doExecuteWithRetriesAndInterval( () -> doPostClaimAsync("/api/customer/device/" + savedDevice.getName() + "/claim", claimRequest, ClaimResult.class, status().isOk()), @@ -138,7 +112,7 @@ public class CoapClaimDeviceTest extends AbstractCoapIntegrationTest { Device claimedDevice = claimResult.getDevice(); assertNotNull(claimedDevice); assertNotNull(claimedDevice.getCustomerId()); - assertEquals(customerAdmin.getCustomerId(), claimedDevice.getCustomerId()); + assertEquals(customerId, claimedDevice.getCustomerId()); claimResponse = doPostClaimAsync("/api/customer/device/" + savedDevice.getName() + "/claim", claimRequest, ClaimResponse.class, status().isBadRequest()); assertEquals(claimResponse, ClaimResponse.CLAIMED); diff --git a/application/src/test/java/org/thingsboard/server/transport/coap/claim/CoapClaimJsonDeviceTest.java b/application/src/test/java/org/thingsboard/server/transport/coap/claim/CoapClaimJsonDeviceTest.java index 89ef5e57fe..8e1d663565 100644 --- a/application/src/test/java/org/thingsboard/server/transport/coap/claim/CoapClaimJsonDeviceTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/coap/claim/CoapClaimJsonDeviceTest.java @@ -36,7 +36,6 @@ public class CoapClaimJsonDeviceTest extends CoapClaimDeviceTest { .transportPayloadType(TransportPayloadType.JSON) .build(); processBeforeTest(configProperties); - createCustomerAndUser(); } @After diff --git a/application/src/test/java/org/thingsboard/server/transport/coap/claim/CoapClaimProtoDeviceTest.java b/application/src/test/java/org/thingsboard/server/transport/coap/claim/CoapClaimProtoDeviceTest.java index 7dbebac847..eb66e63190 100644 --- a/application/src/test/java/org/thingsboard/server/transport/coap/claim/CoapClaimProtoDeviceTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/coap/claim/CoapClaimProtoDeviceTest.java @@ -39,7 +39,6 @@ public class CoapClaimProtoDeviceTest extends CoapClaimDeviceTest { .transportPayloadType(TransportPayloadType.PROTOBUF) .build(); processBeforeTest(configProperties); - createCustomerAndUser(); } @After diff --git a/application/src/test/java/org/thingsboard/server/transport/mqtt/AbstractMqttIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/mqtt/AbstractMqttIntegrationTest.java index 510854d759..c0aeaf37ef 100644 --- a/application/src/test/java/org/thingsboard/server/transport/mqtt/AbstractMqttIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/mqtt/AbstractMqttIntegrationTest.java @@ -189,9 +189,9 @@ public abstract class AbstractMqttIntegrationTest extends AbstractTransportInteg subscribeAndWait(client, attrSubTopic, deviceId, featureType, MqttQoS.AT_MOST_ONCE); } - protected void subscribeAndWait(MqttTestClient client, String attrSubTopic, DeviceId deviceId, FeatureType featureType, MqttQoS mqttQoS) throws MqttException { + protected void subscribeAndWait(MqttTestClient client, String subTopic, DeviceId deviceId, FeatureType featureType, MqttQoS mqttQoS) throws MqttException { int subscriptionCount = getDeviceActorSubscriptionCount(deviceId, featureType); - client.subscribeAndWait(attrSubTopic, mqttQoS); + client.subscribeAndWait(subTopic, mqttQoS); // TODO: This test awaits for the device actor to receive the subscription. Ideally it should not happen. See details below: // The transport layer acknowledge subscription request once the message about subscription is in the queue. // Test sends data immediately after acknowledgement. @@ -200,8 +200,8 @@ public abstract class AbstractMqttIntegrationTest extends AbstractTransportInteg awaitForDeviceActorToReceiveSubscription(deviceId, featureType, subscriptionCount + 1); } - protected void subscribeAndCheckSubscription(MqttTestClient client, String attrSubTopic, DeviceId deviceId, FeatureType featureType) throws MqttException { - client.subscribeAndWait(attrSubTopic, MqttQoS.AT_MOST_ONCE); + protected void subscribeAndCheckSubscription(MqttTestClient client, String subTopic, DeviceId deviceId, FeatureType featureType) throws MqttException { + client.subscribeAndWait(subTopic, MqttQoS.AT_MOST_ONCE); // TODO: This test awaits for the device actor to receive the subscription. Ideally it should not happen. See details below: // The transport layer acknowledge subscription request once the message about subscription is in the queue. // Test sends data immediately after acknowledgement. diff --git a/application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/claim/MqttClaimBackwardCompatibilityDeviceTest.java b/application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/claim/MqttClaimBackwardCompatibilityDeviceTest.java index feab658a9b..96e45d51c0 100644 --- a/application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/claim/MqttClaimBackwardCompatibilityDeviceTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/claim/MqttClaimBackwardCompatibilityDeviceTest.java @@ -36,7 +36,6 @@ public class MqttClaimBackwardCompatibilityDeviceTest extends MqttClaimDeviceTes .useJsonPayloadFormatForDefaultDownlinkTopics(true) .build(); processBeforeTest(configProperties); - createCustomerAndUser(); } @Test diff --git a/application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/claim/MqttClaimDeviceTest.java b/application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/claim/MqttClaimDeviceTest.java index b253136b6e..fbc12e48ba 100644 --- a/application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/claim/MqttClaimDeviceTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/claim/MqttClaimDeviceTest.java @@ -19,10 +19,7 @@ import lombok.extern.slf4j.Slf4j; import org.junit.Before; import org.junit.Test; import org.thingsboard.server.common.data.ClaimRequest; -import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.Device; -import org.thingsboard.server.common.data.User; -import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.dao.device.claim.ClaimResponse; import org.thingsboard.server.dao.device.claim.ClaimResult; import org.thingsboard.server.dao.service.DaoSqlTest; @@ -41,11 +38,6 @@ import static org.thingsboard.server.common.data.device.profile.MqttTopics.GATEW @DaoSqlTest public class MqttClaimDeviceTest extends AbstractMqttIntegrationTest { - protected static final String CUSTOMER_USER_PASSWORD = "customerUser123!"; - - protected User customerAdmin; - protected Customer savedCustomer; - @Before public void beforeTest() throws Exception { MqttTestConfigProperties configProperties = MqttTestConfigProperties.builder() @@ -53,26 +45,6 @@ public class MqttClaimDeviceTest extends AbstractMqttIntegrationTest { .gatewayName("Test Claim gateway") .build(); processBeforeTest(configProperties); - createCustomerAndUser(); - } - - protected void createCustomerAndUser() throws Exception { - Customer customer = new Customer(); - customer.setTenantId(tenantId); - customer.setTitle("Test Claiming Customer"); - savedCustomer = doPost("/api/customer", customer, Customer.class); - assertNotNull(savedCustomer); - assertEquals(tenantId, savedCustomer.getTenantId()); - - User user = new User(); - user.setAuthority(Authority.CUSTOMER_USER); - user.setTenantId(tenantId); - user.setCustomerId(savedCustomer.getId()); - user.setEmail("customer@thingsboard.org"); - - customerAdmin = createUser(user, CUSTOMER_USER_PASSWORD); - assertNotNull(customerAdmin); - assertEquals(customerAdmin.getCustomerId(), savedCustomer.getId()); } @Test @@ -113,8 +85,9 @@ public class MqttClaimDeviceTest extends AbstractMqttIntegrationTest { protected void validateClaimResponse(boolean emptyPayload, MqttTestClient client, byte[] payloadBytes, byte[] failurePayloadBytes) throws Exception { client.publishAndWait(DEVICE_CLAIM_TOPIC, failurePayloadBytes); + awaitForClaimingInfoToBeRegistered(savedDevice.getId()); - loginUser(customerAdmin.getName(), CUSTOMER_USER_PASSWORD); + loginCustomerUser(); ClaimRequest claimRequest; if (!emptyPayload) { claimRequest = new ClaimRequest("value"); @@ -132,6 +105,7 @@ public class MqttClaimDeviceTest extends AbstractMqttIntegrationTest { client.publishAndWait(DEVICE_CLAIM_TOPIC, payloadBytes); client.disconnect(); + awaitForClaimingInfoToBeRegistered(savedDevice.getId()); ClaimResult claimResult = doExecuteWithRetriesAndInterval( () -> doPostClaimAsync("/api/customer/device/" + savedDevice.getName() + "/claim", claimRequest, ClaimResult.class, status().isOk()), @@ -142,7 +116,7 @@ public class MqttClaimDeviceTest extends AbstractMqttIntegrationTest { Device claimedDevice = claimResult.getDevice(); assertNotNull(claimedDevice); assertNotNull(claimedDevice.getCustomerId()); - assertEquals(customerAdmin.getCustomerId(), claimedDevice.getCustomerId()); + assertEquals(customerId, claimedDevice.getCustomerId()); claimResponse = doPostClaimAsync("/api/customer/device/" + savedDevice.getName() + "/claim", claimRequest, ClaimResponse.class, status().isBadRequest()); assertEquals(claimResponse, ClaimResponse.CLAIMED); @@ -158,8 +132,9 @@ public class MqttClaimDeviceTest extends AbstractMqttIntegrationTest { ); assertNotNull(savedDevice); + awaitForClaimingInfoToBeRegistered(savedDevice.getId()); - loginUser(customerAdmin.getName(), CUSTOMER_USER_PASSWORD); + loginCustomerUser(); ClaimRequest claimRequest; if (!emptyPayload) { claimRequest = new ClaimRequest("value"); @@ -172,6 +147,7 @@ public class MqttClaimDeviceTest extends AbstractMqttIntegrationTest { client.publishAndWait(GATEWAY_CLAIM_TOPIC, payloadBytes); client.disconnect(); + awaitForClaimingInfoToBeRegistered(savedDevice.getId()); ClaimResult claimResult = doExecuteWithRetriesAndInterval( () -> doPostClaimAsync("/api/customer/device/" + deviceName + "/claim", claimRequest, ClaimResult.class, status().isOk()), @@ -183,7 +159,7 @@ public class MqttClaimDeviceTest extends AbstractMqttIntegrationTest { Device claimedDevice = claimResult.getDevice(); assertNotNull(claimedDevice); assertNotNull(claimedDevice.getCustomerId()); - assertEquals(customerAdmin.getCustomerId(), claimedDevice.getCustomerId()); + assertEquals(customerId, claimedDevice.getCustomerId()); claimResponse = doPostClaimAsync("/api/customer/device/" + deviceName + "/claim", claimRequest, ClaimResponse.class, status().isBadRequest()); assertEquals(claimResponse, ClaimResponse.CLAIMED); diff --git a/application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/claim/MqttClaimJsonDeviceTest.java b/application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/claim/MqttClaimJsonDeviceTest.java index a591933cf7..c567ef25d1 100644 --- a/application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/claim/MqttClaimJsonDeviceTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/claim/MqttClaimJsonDeviceTest.java @@ -34,7 +34,6 @@ public class MqttClaimJsonDeviceTest extends MqttClaimDeviceTest { .transportPayloadType(TransportPayloadType.JSON) .build(); processBeforeTest(configProperties); - createCustomerAndUser(); } @Test diff --git a/application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/claim/MqttClaimProtoDeviceTest.java b/application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/claim/MqttClaimProtoDeviceTest.java index d4276eb211..a8bc81cee5 100644 --- a/application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/claim/MqttClaimProtoDeviceTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/claim/MqttClaimProtoDeviceTest.java @@ -36,7 +36,6 @@ public class MqttClaimProtoDeviceTest extends MqttClaimDeviceTest { .transportPayloadType(TransportPayloadType.PROTOBUF) .build(); processBeforeTest(configProperties); - createCustomerAndUser(); } @Test From 2d1a3a9b05390f46e7064196b6f8d86144136a91 Mon Sep 17 00:00:00 2001 From: Volodymyr Babak Date: Thu, 11 Apr 2024 13:28:47 +0300 Subject: [PATCH 47/80] Set edge client version to 3.6.4 --- .../src/main/java/org/thingsboard/edge/rpc/EdgeGrpcClient.java | 2 +- common/edge-api/src/main/proto/edge.proto | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/common/edge-api/src/main/java/org/thingsboard/edge/rpc/EdgeGrpcClient.java b/common/edge-api/src/main/java/org/thingsboard/edge/rpc/EdgeGrpcClient.java index 3c4a1e6be9..698c321625 100644 --- a/common/edge-api/src/main/java/org/thingsboard/edge/rpc/EdgeGrpcClient.java +++ b/common/edge-api/src/main/java/org/thingsboard/edge/rpc/EdgeGrpcClient.java @@ -111,7 +111,7 @@ public class EdgeGrpcClient implements EdgeRpcClient { .setConnectRequestMsg(ConnectRequestMsg.newBuilder() .setEdgeRoutingKey(edgeKey) .setEdgeSecret(edgeSecret) - .setEdgeVersion(EdgeVersion.V_3_6_2) + .setEdgeVersion(EdgeVersion.V_3_6_4) .setMaxInboundMessageSize(maxInboundMessageSize) .build()) .build()); diff --git a/common/edge-api/src/main/proto/edge.proto b/common/edge-api/src/main/proto/edge.proto index 34a8c6f093..00cf838fd7 100644 --- a/common/edge-api/src/main/proto/edge.proto +++ b/common/edge-api/src/main/proto/edge.proto @@ -37,6 +37,7 @@ enum EdgeVersion { V_3_6_0 = 3; V_3_6_1 = 4; V_3_6_2 = 5; + V_3_6_4 = 6; } /** From 211226e23046c6e141dea5c499b4c4e396fe629b Mon Sep 17 00:00:00 2001 From: ShvaykaD Date: Thu, 11 Apr 2024 13:40:49 +0300 Subject: [PATCH 48/80] added fix for mqttv5 test --- .../mqttv5/claim/AbstractMqttV5ClaimTest.java | 31 +++---------------- .../mqtt/mqttv5/claim/MqttV5ClaimTest.java | 1 - 2 files changed, 4 insertions(+), 28 deletions(-) diff --git a/application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv5/claim/AbstractMqttV5ClaimTest.java b/application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv5/claim/AbstractMqttV5ClaimTest.java index 7213ecafb5..00a190f6d0 100644 --- a/application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv5/claim/AbstractMqttV5ClaimTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv5/claim/AbstractMqttV5ClaimTest.java @@ -17,10 +17,7 @@ package org.thingsboard.server.transport.mqtt.mqttv5.claim; import lombok.extern.slf4j.Slf4j; import org.thingsboard.server.common.data.ClaimRequest; -import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.Device; -import org.thingsboard.server.common.data.User; -import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.dao.device.claim.ClaimResponse; import org.thingsboard.server.dao.device.claim.ClaimResult; import org.thingsboard.server.transport.mqtt.mqttv5.AbstractMqttV5Test; @@ -33,10 +30,6 @@ import static org.thingsboard.server.common.data.device.profile.MqttTopics.DEVIC @Slf4j public abstract class AbstractMqttV5ClaimTest extends AbstractMqttV5Test { - protected static final String CUSTOMER_USER_PASSWORD = "customerUser123!"; - - protected User customerAdmin; - protected Customer savedCustomer; protected void processTestClaimingDevice() throws Exception { MqttV5TestClient client = new MqttV5TestClient(); @@ -50,8 +43,9 @@ public abstract class AbstractMqttV5ClaimTest extends AbstractMqttV5Test { protected void validateClaimResponse(MqttV5TestClient client, byte[] payloadBytes, byte[] failurePayloadBytes) throws Exception { client.publishAndWait(DEVICE_CLAIM_TOPIC, failurePayloadBytes); + awaitForClaimingInfoToBeRegistered(savedDevice.getId()); - loginUser(customerAdmin.getName(), CUSTOMER_USER_PASSWORD); + loginCustomerUser(); ClaimRequest claimRequest = new ClaimRequest("value"); ClaimResponse claimResponse = doExecuteWithRetriesAndInterval( @@ -64,6 +58,7 @@ public abstract class AbstractMqttV5ClaimTest extends AbstractMqttV5Test { client.publishAndWait(DEVICE_CLAIM_TOPIC, payloadBytes); client.disconnect(); + awaitForClaimingInfoToBeRegistered(savedDevice.getId()); ClaimResult claimResult = doExecuteWithRetriesAndInterval( () -> doPostClaimAsync("/api/customer/device/" + savedDevice.getName() + "/claim", claimRequest, ClaimResult.class, status().isOk()), @@ -74,28 +69,10 @@ public abstract class AbstractMqttV5ClaimTest extends AbstractMqttV5Test { Device claimedDevice = claimResult.getDevice(); assertNotNull(claimedDevice); assertNotNull(claimedDevice.getCustomerId()); - assertEquals(customerAdmin.getCustomerId(), claimedDevice.getCustomerId()); + assertEquals(customerId, claimedDevice.getCustomerId()); claimResponse = doPostClaimAsync("/api/customer/device/" + savedDevice.getName() + "/claim", claimRequest, ClaimResponse.class, status().isBadRequest()); assertEquals(claimResponse, ClaimResponse.CLAIMED); } - protected void createCustomerAndUser() throws Exception { - Customer customer = new Customer(); - customer.setTenantId(tenantId); - customer.setTitle("Test Claiming Customer"); - savedCustomer = doPost("/api/customer", customer, Customer.class); - assertNotNull(savedCustomer); - assertEquals(tenantId, savedCustomer.getTenantId()); - - User user = new User(); - user.setAuthority(Authority.CUSTOMER_USER); - user.setTenantId(tenantId); - user.setCustomerId(savedCustomer.getId()); - user.setEmail("customer@thingsboard.org"); - - customerAdmin = createUser(user, CUSTOMER_USER_PASSWORD); - assertNotNull(customerAdmin); - assertEquals(customerAdmin.getCustomerId(), savedCustomer.getId()); - } } diff --git a/application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv5/claim/MqttV5ClaimTest.java b/application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv5/claim/MqttV5ClaimTest.java index dcabe07de1..75e884f9fd 100644 --- a/application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv5/claim/MqttV5ClaimTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv5/claim/MqttV5ClaimTest.java @@ -29,7 +29,6 @@ public class MqttV5ClaimTest extends AbstractMqttV5ClaimTest { .deviceName("Test Claim device") .build(); processBeforeTest(configProperties); - createCustomerAndUser(); } @Test From 3e527f2634f3954ffdb7ef171b684805f7ac0287 Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Thu, 11 Apr 2024 13:53:00 +0300 Subject: [PATCH 49/80] UI: Implement comparison support for new time series charts. --- ui-ngx/src/app/core/api/widget-api.models.ts | 9 +- .../src/app/core/api/widget-subscription.ts | 14 +- ...ime-series-chart-basic-config.component.ts | 9 + .../widget/lib/chart/echarts-widget.models.ts | 219 +++++++++++------- .../lib/chart/time-series-chart.models.ts | 198 ++++++++++------ .../widget/lib/chart/time-series-chart.ts | 142 ++++++------ .../widget/lib/flot-widget.models.ts | 12 +- ...ime-series-chart-key-settings.component.ts | 5 + ui-ngx/src/app/shared/models/widget.models.ts | 16 +- 9 files changed, 379 insertions(+), 245 deletions(-) diff --git a/ui-ngx/src/app/core/api/widget-api.models.ts b/ui-ngx/src/app/core/api/widget-api.models.ts index 1848733006..1fcd18bd93 100644 --- a/ui-ngx/src/app/core/api/widget-api.models.ts +++ b/ui-ngx/src/app/core/api/widget-api.models.ts @@ -30,7 +30,12 @@ import { import { TimeService } from '../services/time.service'; import { DeviceService } from '../http/device.service'; import { UtilsService } from '@core/services/utils.service'; -import { SubscriptionTimewindow, Timewindow, WidgetTimewindow } from '@shared/models/time/time.models'; +import { + ComparisonDuration, + SubscriptionTimewindow, + Timewindow, + WidgetTimewindow +} from '@shared/models/time/time.models'; import { EntityType } from '@shared/models/entity-type.models'; import { HttpErrorResponse } from '@angular/common/http'; import { RafService } from '@core/services/raf.service'; @@ -265,7 +270,7 @@ export interface WidgetSubscriptionOptions { onTimewindowChangeFunction?: (timewindow: Timewindow) => Timewindow; legendConfig?: LegendConfig; comparisonEnabled?: boolean; - timeForComparison?: moment_.unitOfTime.DurationConstructor; + timeForComparison?: ComparisonDuration; comparisonCustomIntervalValue?: number; decimals?: number; units?: string; diff --git a/ui-ngx/src/app/core/api/widget-subscription.ts b/ui-ngx/src/app/core/api/widget-subscription.ts index 933d836ffd..dc29f36ee2 100644 --- a/ui-ngx/src/app/core/api/widget-subscription.ts +++ b/ui-ngx/src/app/core/api/widget-subscription.ts @@ -23,13 +23,13 @@ import { WidgetSubscriptionOptions } from '@core/api/widget-api.models'; import { - DataKey, + DataKey, DataKeySettingsWithComparison, DataSet, DataSetHolder, Datasource, DatasourceData, datasourcesHasAggregation, - DatasourceType, + DatasourceType, isDataKeySettingsWithComparison, LegendConfig, LegendData, LegendKey, @@ -513,7 +513,7 @@ export class WidgetSubscription implements IWidgetSubscription { this.configuredDatasources.forEach((datasource, datasourceIndex) => { const additionalDataKeys: DataKey[] = []; datasource.dataKeys.forEach((dataKey, dataKeyIndex) => { - if (dataKey.settings.comparisonSettings && dataKey.settings.comparisonSettings.showValuesForComparison) { + if (isDataKeySettingsWithComparison(dataKey.settings) && dataKey.settings.comparisonSettings.showValuesForComparison) { const additionalDataKey = deepClone(dataKey); additionalDataKey.isAdditional = true; additionalDataKey.origDataKeyIndex = dataKeyIndex; @@ -1468,11 +1468,12 @@ export class WidgetSubscription implements IWidgetSubscription { if (datasource.isAdditional) { const origDatasource = this.datasourcePages[datasource.origDatasourceIndex].data[dIndex]; datasource.dataKeys.forEach((dataKey) => { - if (dataKey.settings.comparisonSettings.color) { + const settings: DataKeySettingsWithComparison = dataKey.settings; + if (settings.comparisonSettings.color) { dataKey.color = dataKey.settings.comparisonSettings.color; } const origDataKey = origDatasource.dataKeys[dataKey.origDataKeyIndex]; - origDataKey.settings.comparisonSettings.color = dataKey.color; + (origDataKey.settings as DataKeySettingsWithComparison).comparisonSettings.color = dataKey.color; }); } }); @@ -1523,7 +1524,8 @@ export class WidgetSubscription implements IWidgetSubscription { const formattedData = flatFormattedData(formattedDataArray); datasource.dataKeys.forEach((dataKey) => { - if (this.comparisonEnabled && dataKey.isAdditional && dataKey.settings.comparisonSettings.comparisonValuesLabel) { + if (this.comparisonEnabled && dataKey.isAdditional && isDataKeySettingsWithComparison(dataKey.settings) && + dataKey.settings.comparisonSettings.comparisonValuesLabel) { dataKey.label = createLabelFromPattern(dataKey.settings.comparisonSettings.comparisonValuesLabel, formattedData); } else { if (this.comparisonEnabled && dataKey.isAdditional) { diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/time-series-chart-basic-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/time-series-chart-basic-config.component.ts index 8ab44db11d..508709a3f9 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/time-series-chart-basic-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/time-series-chart-basic-config.component.ts @@ -131,6 +131,11 @@ export class TimeSeriesChartBasicConfigComponent extends BasicWidgetConfigCompon yAxes: [settings.yAxes, []], series: [this.getSeries(configData.config.datasources), []], + + comparisonEnabled: [settings.comparisonEnabled, []], + timeForComparison: [settings.timeForComparison, []], + comparisonCustomIntervalValue: [settings.comparisonCustomIntervalValue, [Validators.min(0)]], + thresholds: [settings.thresholds, []], showTitle: [configData.config.showTitle, []], @@ -201,6 +206,10 @@ export class TimeSeriesChartBasicConfigComponent extends BasicWidgetConfigCompon this.widgetConfig.config.settings = this.widgetConfig.config.settings || {}; + this.widgetConfig.config.settings.comparisonEnabled = config.comparisonEnabled; + this.widgetConfig.config.settings.timeForComparison = config.timeForComparison; + this.widgetConfig.config.settings.comparisonCustomIntervalValue = config.comparisonCustomIntervalValue; + this.widgetConfig.config.settings.thresholds = config.thresholds; this.widgetConfig.config.settings.dataZoom = config.dataZoom; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/echarts-widget.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/chart/echarts-widget.models.ts index 0a1bb673cb..35b929319b 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/chart/echarts-widget.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/echarts-widget.models.ts @@ -115,6 +115,7 @@ export type EChartsSeriesItem = { decimals?: number; latestData?: FormattedData; tooltipValueFormatFunction?: EChartsTooltipValueFormatFunction; + comparisonItem?: boolean; }; export enum EChartsShape { @@ -200,9 +201,9 @@ export const timeAxisBandWidthCalculator: TimeAxisBandWidthCalculator = (model) } }; -export const getXAxis = (chart: ECharts): Axis2D => { +export const getAxis = (chart: ECharts, mainType: string, axisId: string): Axis2D => { const model: GlobalModel = (chart as any).getModel(); - const models = model.queryComponents({mainType: 'xAxis'}); + const models = model.queryComponents({mainType, id: axisId}); if (models?.length) { const axisModel = models[0] as AxisModel; return axisModel.axis; @@ -210,27 +211,20 @@ export const getXAxis = (chart: ECharts): Axis2D => { return null; }; -export const getYAxis = (chart: ECharts, axisId: string): Axis2D => { - const model: GlobalModel = (chart as any).getModel(); - const models = model.queryComponents({mainType: 'yAxis', id: axisId}); - if (models?.length) { - const axisModel = models[0] as AxisModel; - return axisModel.axis; - } - return null; -}; - -export const calculateYAxisWidth = (chart: ECharts, axisId: string): number => { - const axis = getYAxis(chart, axisId); - return calculateAxisSize(axis); +export const calculateAxisSize = (chart: ECharts, mainType: string, axisId: string): number => { + const axis = getAxis(chart, mainType, axisId); + return _calculateAxisSize(axis); }; -export const calculateXAxisHeight = (chart: ECharts): number => { - const axis = getXAxis(chart); - return calculateAxisSize(axis); +export const measureAxisNameSize = (chart: ECharts, mainType: string, axisId: string, name: string): number => { + const axis = getAxis(chart, mainType, axisId); + if (axis) { + return axis.model.getModel('nameTextStyle').getTextRect(name).height; + } + return 0; }; -const calculateAxisSize = (axis: Axis2D): number => { +const _calculateAxisSize = (axis: Axis2D): number => { let size = 0; if (axis && axis.model.option.show) { const labelUnionRect = estimateLabelUnionRect(axis); @@ -247,22 +241,6 @@ const calculateAxisSize = (axis: Axis2D): number => { return size; }; -export const measureYAxisNameWidth = (chart: ECharts, axisId: string, name: string): number => { - const axis = getYAxis(chart, axisId); - if (axis) { - return axis.model.getModel('nameTextStyle').getTextRect(name).height; - } - return 0; -}; - -export const measureXAxisNameHeight = (chart: ECharts, name: string): number => { - const axis = getXAxis(chart); - if (axis) { - return axis.model.getModel('nameTextStyle').getTextRect(name).height; - } - return 0; -}; - const measureSymbolOffset = (symbol: string, symbolSize: any): number => { if (isNumber(symbolSize)) { if (symbol) { @@ -280,7 +258,7 @@ const measureSymbolOffset = (symbol: string, symbolSize: any): number => { export const measureThresholdOffset = (chart: ECharts, axisId: string, thresholdId: string, value: any): [number, number] => { const offset: [number, number] = [0,0]; - const axis = getYAxis(chart, axisId); + const axis = getAxis(chart, 'yAxis', axisId); if (axis && !axis.scale.isBlank()) { const extent = axis.scale.getExtent(); const model: GlobalModel = (chart as any).getModel(); @@ -352,7 +330,7 @@ export const measureThresholdOffset = (chart: ECharts, axisId: string, threshold }; export const getAxisExtent = (chart: ECharts, axisId: string): [number, number] => { - const axis = getYAxis(chart, axisId); + const axis = getAxis(chart, 'yAxis', axisId); if (axis) { return axis.scale.getExtent(); } @@ -503,43 +481,72 @@ export const echartsTooltipFormatter = (renderer: Renderer2, focusedSeriesIndex: number, series?: EChartsSeriesItem[], interval?: Interval): null | HTMLElement => { - if (!params || Array.isArray(params) && !params[0]) { - return null; - } - const firstParam = Array.isArray(params) ? params[0] : params; - if (!firstParam.value) { + + const tooltipParams = mapTooltipParams(params, series, focusedSeriesIndex); + if (!tooltipParams.items.length && !tooltipParams.comparisonItems.length) { return null; } + const tooltipElement: HTMLElement = renderer.createElement('div'); renderer.setStyle(tooltipElement, 'display', 'flex'); renderer.setStyle(tooltipElement, 'flex-direction', 'column'); renderer.setStyle(tooltipElement, 'align-items', 'flex-start'); - renderer.setStyle(tooltipElement, 'gap', '4px'); - if (settings.tooltipShowDate) { - const dateElement: HTMLElement = renderer.createElement('div'); - let dateText: string; - const startTs = firstParam.value[2]; - const endTs = firstParam.value[3]; - if (settings.tooltipDateInterval && startTs && endTs && (endTs - 1) > startTs) { - const startDateText = tooltipDateFormat.update(startTs, interval); - const endDateText = tooltipDateFormat.update(endTs - 1, interval); - if (startDateText === endDateText) { - dateText = startDateText; - } else { - dateText = startDateText + ' - ' + endDateText; - } - } else { - const ts = firstParam.value[0]; - dateText = tooltipDateFormat.update(ts, interval); + renderer.setStyle(tooltipElement, 'gap', '16px'); + + buildItemsTooltip(tooltipElement, tooltipParams.items, renderer, tooltipDateFormat, settings, valueFormatFunction, interval); + buildItemsTooltip(tooltipElement, tooltipParams.comparisonItems, renderer, tooltipDateFormat, settings, valueFormatFunction, interval); + + return tooltipElement; +}; + +interface TooltipItem { + param: CallbackDataParams; + dataItem: EChartsSeriesItem; +} + +interface TooltipParams { + items: TooltipItem[]; + comparisonItems: TooltipItem[]; +} + +const buildItemsTooltip = (tooltipElement: HTMLElement, + items: TooltipItem[], + renderer: Renderer2, + tooltipDateFormat: DateFormatProcessor, + settings: EChartsTooltipWidgetSettings, + valueFormatFunction: EChartsTooltipValueFormatFunction, + interval?: Interval) => { + if (items.length) { + const tooltipItemsElement: HTMLElement = renderer.createElement('div'); + renderer.setStyle(tooltipItemsElement, 'display', 'flex'); + renderer.setStyle(tooltipItemsElement, 'flex-direction', 'column'); + renderer.setStyle(tooltipItemsElement, 'align-items', 'flex-start'); + renderer.setStyle(tooltipItemsElement, 'gap', '4px'); + renderer.appendChild(tooltipElement, tooltipItemsElement); + if (settings.tooltipShowDate) { + renderer.appendChild(tooltipItemsElement, + constructEchartsTooltipDateElement(renderer, tooltipDateFormat, settings, items[0].param, interval)); + } + for (const item of items) { + renderer.appendChild(tooltipItemsElement, + constructEchartsTooltipSeriesElement(renderer, settings, item, valueFormatFunction)); } - renderer.appendChild(dateElement, renderer.createText(dateText)); - renderer.setStyle(dateElement, 'font-family', settings.tooltipDateFont.family); - renderer.setStyle(dateElement, 'font-size', settings.tooltipDateFont.size + settings.tooltipDateFont.sizeUnit); - renderer.setStyle(dateElement, 'font-style', settings.tooltipDateFont.style); - renderer.setStyle(dateElement, 'font-weight', settings.tooltipDateFont.weight); - renderer.setStyle(dateElement, 'line-height', settings.tooltipDateFont.lineHeight); - renderer.setStyle(dateElement, 'color', settings.tooltipDateColor); - renderer.appendChild(tooltipElement, dateElement); + } +}; + +const mapTooltipParams = (params: CallbackDataParams[] | CallbackDataParams, + series?: EChartsSeriesItem[], + focusedSeriesIndex?: number): TooltipParams => { + const result: TooltipParams = { + items: [], + comparisonItems: [] + }; + if (!params || Array.isArray(params) && !params[0]) { + return result; + } + const firstParam = Array.isArray(params) ? params[0] : params; + if (!firstParam.value) { + return result; } let seriesParams: CallbackDataParams = null; if (Array.isArray(params) && focusedSeriesIndex > -1) { @@ -548,22 +555,63 @@ export const echartsTooltipFormatter = (renderer: Renderer2, seriesParams = params; } if (seriesParams) { - renderer.appendChild(tooltipElement, - constructEchartsTooltipSeriesElement(renderer, settings, seriesParams, valueFormatFunction, series)); + appendTooltipItem(result, seriesParams, series); } else if (Array.isArray(params)) { for (seriesParams of params) { - renderer.appendChild(tooltipElement, - constructEchartsTooltipSeriesElement(renderer, settings, seriesParams, valueFormatFunction, series)); + appendTooltipItem(result, seriesParams, series); } } - return tooltipElement; + return result; +}; + +const appendTooltipItem = (tooltipParams: TooltipParams, seriesParams: CallbackDataParams, series?: EChartsSeriesItem[]) => { + const dataItem = series?.find(s => s.id === seriesParams.seriesId); + const tooltipItem: TooltipItem = { + param: seriesParams, + dataItem + }; + if (dataItem?.comparisonItem) { + tooltipParams.comparisonItems.push(tooltipItem); + } else { + tooltipParams.items.push(tooltipItem); + } +}; + +const constructEchartsTooltipDateElement = (renderer: Renderer2, + tooltipDateFormat: DateFormatProcessor, + settings: EChartsTooltipWidgetSettings, + param: CallbackDataParams, + interval?: Interval): HTMLElement => { + const dateElement: HTMLElement = renderer.createElement('div'); + let dateText: string; + const startTs = param.value[2]; + const endTs = param.value[3]; + if (settings.tooltipDateInterval && startTs && endTs && (endTs - 1) > startTs) { + const startDateText = tooltipDateFormat.update(startTs, interval); + const endDateText = tooltipDateFormat.update(endTs - 1, interval); + if (startDateText === endDateText) { + dateText = startDateText; + } else { + dateText = startDateText + ' - ' + endDateText; + } + } else { + const ts = param.value[0]; + dateText = tooltipDateFormat.update(ts, interval); + } + renderer.appendChild(dateElement, renderer.createText(dateText)); + renderer.setStyle(dateElement, 'font-family', settings.tooltipDateFont.family); + renderer.setStyle(dateElement, 'font-size', settings.tooltipDateFont.size + settings.tooltipDateFont.sizeUnit); + renderer.setStyle(dateElement, 'font-style', settings.tooltipDateFont.style); + renderer.setStyle(dateElement, 'font-weight', settings.tooltipDateFont.weight); + renderer.setStyle(dateElement, 'line-height', settings.tooltipDateFont.lineHeight); + renderer.setStyle(dateElement, 'color', settings.tooltipDateColor); + return dateElement; }; const constructEchartsTooltipSeriesElement = (renderer: Renderer2, settings: EChartsTooltipWidgetSettings, - seriesParams: CallbackDataParams, - valueFormatFunction: EChartsTooltipValueFormatFunction, - series?: EChartsSeriesItem[]): HTMLElement => { + item: TooltipItem, + valueFormatFunction: EChartsTooltipValueFormatFunction): HTMLElement => { const labelValueElement: HTMLElement = renderer.createElement('div'); renderer.setStyle(labelValueElement, 'display', 'flex'); renderer.setStyle(labelValueElement, 'flex-direction', 'row'); @@ -579,10 +627,10 @@ const constructEchartsTooltipSeriesElement = (renderer: Renderer2, renderer.setStyle(circleElement, 'width', '8px'); renderer.setStyle(circleElement, 'height', '8px'); renderer.setStyle(circleElement, 'border-radius', '50%'); - renderer.setStyle(circleElement, 'background', seriesParams.color); + renderer.setStyle(circleElement, 'background', item.param.color); renderer.appendChild(labelElement, circleElement); const labelTextElement: HTMLElement = renderer.createElement('div'); - renderer.appendChild(labelTextElement, renderer.createText(seriesParams.seriesName)); + renderer.appendChild(labelTextElement, renderer.createText(item.param.seriesName)); renderer.setStyle(labelTextElement, 'font-family', 'Roboto'); renderer.setStyle(labelTextElement, 'font-size', '12px'); renderer.setStyle(labelTextElement, 'font-style', 'normal'); @@ -596,21 +644,18 @@ const constructEchartsTooltipSeriesElement = (renderer: Renderer2, let latestData: FormattedData; let units = ''; let decimals = 0; - if (series) { - const item = series.find(s => s.id === seriesParams.seriesId); - if (item) { - if (item.tooltipValueFormatFunction) { - formatFunction = item.tooltipValueFormatFunction; - } - latestData = item.latestData; - units = item.units; - decimals = item.decimals; + if (item.dataItem) { + if (item.dataItem.tooltipValueFormatFunction) { + formatFunction = item.dataItem.tooltipValueFormatFunction; } + latestData = item.dataItem.latestData; + units = item.dataItem.units; + decimals = item.dataItem.decimals; } if (!latestData) { latestData = {} as FormattedData; } - const value = formatFunction(seriesParams.value[1], latestData, units, decimals); + const value = formatFunction(item.param.value[1], latestData, units, decimals); renderer.appendChild(valueElement, renderer.createText(value)); renderer.setStyle(valueElement, 'flex', '1'); renderer.setStyle(valueElement, 'text-align', 'end'); diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.models.ts index 252a284aaa..db3831f6a3 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.models.ts @@ -62,13 +62,15 @@ import { BarVisualSettings, renderTimeSeriesBar } from '@home/components/widget/lib/chart/time-series-chart-bar.models'; -import { DataKey } from '@shared/models/widget.models'; +import { DataKey, DataKeySettingsWithComparison, WidgetComparisonSettings } from '@shared/models/widget.models'; import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; import { TbColorScheme } from '@shared/models/color.models'; import { AbstractControl, ValidationErrors } from '@angular/forms'; import { MarkLine2DDataItemOption } from 'echarts/types/src/component/marker/MarkLineModel'; import { DatePipe } from '@angular/common'; import { BuiltinTextPosition } from 'zrender/src/core/types'; +import { CartesianAxisOption } from 'echarts/types/src/coord/cartesian/AxisModel'; +import { WidgetTimewindow } from '@shared/models/time/time.models'; export enum TimeSeriesChartType { default = 'default', @@ -692,7 +694,11 @@ export const timeSeriesChartStateValidator = (control: AbstractControl): Validat return null; }; -export interface TimeSeriesChartSettings extends EChartsTooltipWidgetSettings { +export interface TimeSeriesChartComparisonSettings extends WidgetComparisonSettings { + comparisonXAxis?: TimeSeriesChartXAxisSettings; +} + +export interface TimeSeriesChartSettings extends EChartsTooltipWidgetSettings, TimeSeriesChartComparisonSettings { thresholds: TimeSeriesChartThreshold[]; darkMode: boolean; dataZoom: boolean; @@ -750,7 +756,13 @@ export const timeSeriesChartDefaultSettings: TimeSeriesChartSettings = { tooltipDateColor: 'rgba(0, 0, 0, 0.76)', tooltipDateInterval: true, tooltipBackgroundColor: 'rgba(255, 255, 255, 0.76)', - tooltipBackgroundBlur: 4 + tooltipBackgroundBlur: 4, + comparisonEnabled: false, + timeForComparison: 'previousInterval', + comparisonCustomIntervalValue: 7200000, + comparisonXAxis: mergeDeep({} as TimeSeriesChartXAxisSettings, + defaultTimeSeriesChartXAxisSettings, + { position: AxisPosition.top } as TimeSeriesChartXAxisSettings) }; export interface SeriesFillSettings { @@ -798,7 +810,7 @@ export interface BarSeriesSettings { backgroundSettings: SeriesFillSettings; } -export interface TimeSeriesChartKeySettings { +export interface TimeSeriesChartKeySettings extends DataKeySettingsWithComparison { yAxisId: TimeSeriesChartYAxisId; showInLegend: boolean; dataHiddenByDefault: boolean; @@ -870,10 +882,16 @@ export const timeSeriesChartKeyDefaultSettings: TimeSeriesChartKeySettings = { end: 0 } } + }, + comparisonSettings: { + showValuesForComparison: false, + comparisonValuesLabel: '', + color: '' } }; export interface TimeSeriesChartDataItem extends EChartsSeriesItem { + xAxisIndex: number; yAxisId: TimeSeriesChartYAxisId; yAxisIndex: number; option?: LineSeriesOption | CustomSeriesOption; @@ -894,13 +912,23 @@ export interface TimeSeriesChartThresholdItem { option?: LineSeriesOption; } -export interface TimeSeriesChartYAxis { +export interface TimeSeriesChartAxis { id: string; + settings: TimeSeriesChartAxisSettings; + option: CartesianAxisOption; +} + +export interface TimeSeriesChartYAxis extends TimeSeriesChartAxis { decimals: number; settings: TimeSeriesChartYAxisSettings; option: YAXisOption & ValueAxisBaseOption; } +export interface TimeSeriesChartXAxis extends TimeSeriesChartAxis { + settings: TimeSeriesChartXAxisSettings; + option: XAXisOption; +} + export const createTimeSeriesYAxis = (units: string, decimals: number, settings: TimeSeriesChartYAxisSettings, @@ -944,6 +972,7 @@ export const createTimeSeriesYAxis = (units: string, decimals, settings, option: { + mainType: 'yAxis', show: settings.show, type: 'value', position: settings.position, @@ -1011,75 +1040,87 @@ export const createTimeSeriesYAxis = (units: string, }; }; -export const createTimeSeriesXAxisOption = (settings: TimeSeriesChartXAxisSettings, - min: number, max: number, - datePipe: DatePipe, - darkMode: boolean): XAXisOption => { +export const createTimeSeriesXAxis = (id: string, + settings: TimeSeriesChartXAxisSettings, + min: number, max: number, + datePipe: DatePipe, + darkMode: boolean): TimeSeriesChartXAxis => { const xAxisTickLabelStyle = createChartTextStyle(settings.tickLabelFont, settings.tickLabelColor, darkMode, 'axis.tickLabel'); const xAxisNameStyle = createChartTextStyle(settings.labelFont, settings.labelColor, darkMode, 'axis.label'); const ticksFormat = mergeDeep({}, defaultXAxisTicksFormat, settings.ticksFormat); return { - show: settings.show, - type: 'time', - scale: true, - position: settings.position, - name: settings.label, - nameLocation: 'middle', - nameTextStyle: { - color: xAxisNameStyle.color, - fontStyle: xAxisNameStyle.fontStyle, - fontWeight: xAxisNameStyle.fontWeight, - fontFamily: xAxisNameStyle.fontFamily, - fontSize: xAxisNameStyle.fontSize - }, - axisTick: { - show: settings.showTicks, - lineStyle: { - color: prepareChartThemeColor(settings.ticksColor, darkMode, 'axis.ticks') - } - }, - axisLabel: { - show: settings.showTickLabels, - color: xAxisTickLabelStyle.color, - fontStyle: xAxisTickLabelStyle.fontStyle, - fontWeight: xAxisTickLabelStyle.fontWeight, - fontFamily: xAxisTickLabelStyle.fontFamily, - fontSize: xAxisTickLabelStyle.fontSize, - hideOverlap: true, - /** Min/Max time label always visible **/ - /* alignMinLabel: 'left', - alignMaxLabel: 'right', - showMinLabel: true, - showMaxLabel: true, */ - formatter: (value: number, _index: number, extra: {level: number}) => { - const unit = tsToFormatTimeUnit(value); - const format = ticksFormat[unit]; - const formatted = datePipe.transform(value, format); - if (extra.level > 0) { - return `{primary|${formatted}}`; - } else { - return formatted; + id, + settings, + option: { + mainType: 'xAxis', + show: settings.show, + type: 'time', + scale: true, + position: settings.position, + id, + name: settings.label, + nameLocation: 'middle', + nameTextStyle: { + color: xAxisNameStyle.color, + fontStyle: xAxisNameStyle.fontStyle, + fontWeight: xAxisNameStyle.fontWeight, + fontFamily: xAxisNameStyle.fontFamily, + fontSize: xAxisNameStyle.fontSize + }, + axisPointer: { + shadowStyle: { + color: id === 'main' ? 'rgba(210,219,238,0.2)' : 'rgba(150,150,150,0.1)' } - } - }, - axisLine: { - show: settings.showLine, - onZero: false, - lineStyle: { - color: prepareChartThemeColor(settings.lineColor, darkMode, 'axis.line') - } - }, - splitLine: { - show: settings.showSplitLines, - lineStyle: { - color: prepareChartThemeColor(settings.splitLinesColor, darkMode, 'axis.splitLine') - } - }, - min, - max, - bandWidthCalculator: timeAxisBandWidthCalculator + }, + axisTick: { + show: settings.showTicks, + lineStyle: { + color: prepareChartThemeColor(settings.ticksColor, darkMode, 'axis.ticks') + } + }, + axisLabel: { + show: settings.showTickLabels, + color: xAxisTickLabelStyle.color, + fontStyle: xAxisTickLabelStyle.fontStyle, + fontWeight: xAxisTickLabelStyle.fontWeight, + fontFamily: xAxisTickLabelStyle.fontFamily, + fontSize: xAxisTickLabelStyle.fontSize, + hideOverlap: true, + /** Min/Max time label always visible **/ + /* alignMinLabel: 'left', + alignMaxLabel: 'right', + showMinLabel: true, + showMaxLabel: true, */ + formatter: (value: number, _index: number, extra: {level: number}) => { + const unit = tsToFormatTimeUnit(value); + const format = ticksFormat[unit]; + const formatted = datePipe.transform(value, format); + if (extra.level > 0) { + return `{primary|${formatted}}`; + } else { + return formatted; + } + } + }, + axisLine: { + show: settings.showLine, + onZero: false, + lineStyle: { + color: prepareChartThemeColor(settings.lineColor, darkMode, 'axis.line') + } + }, + splitLine: { + show: settings.showSplitLines, + lineStyle: { + color: prepareChartThemeColor(settings.splitLinesColor, darkMode, 'axis.splitLine') + } + }, + min, + max, + bandWidthCalculator: timeAxisBandWidthCalculator + } }; }; @@ -1098,6 +1139,13 @@ export const createTimeSeriesVisualMapOption = (settings: TimeSeriesChartVisualM } : undefined }); +export const updateXAxisTimeWindow = (option: XAXisOption, + timeWindow: WidgetTimewindow) => { + option.min = timeWindow.minTime; + option.max = timeWindow.maxTime; + (option as any).tbTimeWindow = timeWindow; +}; + export const generateChartData = (dataItems: TimeSeriesChartDataItem[], thresholdItems: TimeSeriesChartThresholdItem[], stack: boolean, @@ -1262,6 +1310,7 @@ const generateChartSeries = (dataItems: TimeSeriesChartDataItem[], }; export const updateDarkMode = (options: EChartsOption, settings: TimeSeriesChartSettings, + xAxisList: TimeSeriesChartXAxis[], yAxisList: TimeSeriesChartYAxis[], dataItems: TimeSeriesChartDataItem[], darkMode: boolean): EChartsOption => { @@ -1278,12 +1327,14 @@ export const updateDarkMode = (options: EChartsOption, settings: TimeSeriesChart } } if (Array.isArray(options.xAxis)) { - for (const xAxis of options.xAxis) { - xAxis.nameTextStyle.color = prepareChartThemeColor(settings.xAxis.labelColor, darkMode, 'axis.label'); - xAxis.axisLabel.color = prepareChartThemeColor(settings.xAxis.tickLabelColor, darkMode, 'axis.tickLabel'); - xAxis.axisLine.lineStyle.color = prepareChartThemeColor(settings.xAxis.lineColor, darkMode, 'axis.line'); - xAxis.axisTick.lineStyle.color = prepareChartThemeColor(settings.xAxis.ticksColor, darkMode, 'axis.ticks'); - xAxis.splitLine.lineStyle.color = prepareChartThemeColor(settings.xAxis.splitLinesColor, darkMode, 'axis.splitLine'); + for (let i = 0; i < options.xAxis.length; i++) { + const xAxis = options.xAxis[i]; + const xAxisSettings = xAxisList[i].settings; + xAxis.nameTextStyle.color = prepareChartThemeColor(xAxisSettings.labelColor, darkMode, 'axis.label'); + xAxis.axisLabel.color = prepareChartThemeColor(xAxisSettings.tickLabelColor, darkMode, 'axis.tickLabel'); + xAxis.axisLine.lineStyle.color = prepareChartThemeColor(xAxisSettings.lineColor, darkMode, 'axis.line'); + xAxis.axisTick.lineStyle.color = prepareChartThemeColor(xAxisSettings.ticksColor, darkMode, 'axis.ticks'); + xAxis.splitLine.lineStyle.color = prepareChartThemeColor(xAxisSettings.splitLinesColor, darkMode, 'axis.splitLine'); } } for (const item of dataItems) { @@ -1322,6 +1373,7 @@ const createTimeSeriesChartSeries = (item: TimeSeriesChartDataItem, seriesOption = { id: item.id, dataGroupId: item.id, + xAxisIndex: item.xAxisIndex, yAxisIndex: item.yAxisIndex, name: item.dataKey.label, color: seriesColor, diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.ts b/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.ts index 700f07da15..89aa599afb 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.ts @@ -16,16 +16,16 @@ import { WidgetContext } from '@home/models/widget-component.models'; import { - AxisPosition, calculateThresholdsOffset, createTimeSeriesVisualMapOption, - createTimeSeriesXAxisOption, + createTimeSeriesXAxis, createTimeSeriesYAxis, defaultTimeSeriesChartYAxisSettings, generateChartData, LineSeriesStepType, parseThresholdData, SeriesLabelPosition, + TimeSeriesChartAxis, TimeSeriesChartDataItem, timeSeriesChartDefaultSettings, timeSeriesChartKeyDefaultSettings, @@ -38,16 +38,17 @@ import { TimeSeriesChartThresholdItem, TimeSeriesChartThresholdType, TimeSeriesChartType, + TimeSeriesChartXAxis, TimeSeriesChartYAxis, TimeSeriesChartYAxisId, TimeSeriesChartYAxisSettings, - updateDarkMode + updateDarkMode, + updateXAxisTimeWindow } from '@home/components/widget/lib/chart/time-series-chart.models'; import { ResizeObserver } from '@juggle/resize-observer'; import { adjustTimeAxisExtentToData, - calculateXAxisHeight, - calculateYAxisWidth, + calculateAxisSize, createTooltipValueFormatFunction, ECharts, echartsModule, @@ -58,8 +59,7 @@ import { EChartsTooltipValueFormatFunction, getAxisExtent, getFocusedSeriesIndex, - measureXAxisNameHeight, - measureYAxisNameWidth, + measureAxisNameSize, toNamedData } from '@home/components/widget/lib/chart/echarts-widget.models'; import { DateFormatProcessor } from '@shared/models/widget-settings.models'; @@ -120,6 +120,10 @@ export class TbTimeSeriesChart { private readonly settings: TimeSeriesChartSettings; + private readonly comparisonEnabled: boolean; + private readonly stackMode: boolean; + + private xAxisList: TimeSeriesChartXAxis[] = []; private yAxisList: TimeSeriesChartYAxis[] = []; private dataItems: TimeSeriesChartDataItem[] = []; private thresholdItems: TimeSeriesChartThresholdItem[] = []; @@ -163,6 +167,8 @@ export class TbTimeSeriesChart { this.settings = mergeDeep({} as TimeSeriesChartSettings, timeSeriesChartDefaultSettings, this.inputSettings as TimeSeriesChartSettings); + this.comparisonEnabled = !!this.ctx.defaultSubscription.comparisonEnabled; + this.stackMode = !this.comparisonEnabled && this.settings.stack; if (this.settings.states && this.settings.states.length) { this.stateValueConverter = new TimeSeriesChartStateValueConverter(this.ctx.dashboard.utils, this.settings.states); this.tooltipValueFormatFunction = this.stateValueConverter.tooltipFormatter; @@ -170,6 +176,7 @@ export class TbTimeSeriesChart { const $dashboardPageElement = this.ctx.$containerParent.parents('.tb-dashboard-page'); const dashboardPageElement = $dashboardPageElement.length ? $($dashboardPageElement[$dashboardPageElement.length-1]) : null; this.darkMode = this.settings.darkMode || dashboardPageElement?.hasClass('dark'); + this.setupXAxes(); this.setupYAxes(); this.setupData(); this.setupThresholds(); @@ -216,14 +223,16 @@ export class TbTimeSeriesChart { } this.onResize(); if (this.timeSeriesChart) { - this.timeSeriesChartOptions.xAxis[0].min = this.ctx.defaultSubscription.timeWindow.minTime; - this.timeSeriesChartOptions.xAxis[0].max = this.ctx.defaultSubscription.timeWindow.maxTime; - this.timeSeriesChartOptions.xAxis[0].tbTimeWindow = this.ctx.defaultSubscription.timeWindow; + updateXAxisTimeWindow(this.xAxisList[0].option, this.ctx.defaultSubscription.timeWindow); if (this.noAggregation) { this.timeSeriesChartOptions.tooltip[0].axisPointer.type = 'line'; } else { this.timeSeriesChartOptions.tooltip[0].axisPointer.type = 'shadow'; } + if (this.comparisonEnabled) { + updateXAxisTimeWindow(this.xAxisList[1].option, this.ctx.defaultSubscription.comparisonTimeWindow); + } + this.timeSeriesChartOptions.xAxis = this.xAxisList.map(axis => axis.option); if (this.hasVisualMap) { (this.timeSeriesChartOptions.visualMap as PiecewiseVisualMapOption).selected = this.visualMapSelectedRanges; } @@ -300,7 +309,7 @@ export class TbTimeSeriesChart { this.timeSeriesChartOptions.yAxis = this.yAxisList.map(axis => axis.option); mergeList.push('yAxis'); } - this.timeSeriesChart.setOption(this.timeSeriesChartOptions, this.settings.stack ? {notMerge: true} : {replaceMerge: mergeList}); + this.timeSeriesChart.setOption(this.timeSeriesChartOptions, this.stackMode ? {notMerge: true} : {replaceMerge: mergeList}); this.updateAxes(); dataKey.hidden = !enable; if (enable) { @@ -343,7 +352,7 @@ export class TbTimeSeriesChart { this.darkMode = darkMode; if (this.timeSeriesChart) { this.timeSeriesChartOptions = updateDarkMode(this.timeSeriesChartOptions, - this.settings, this.yAxisList, this.dataItems, darkMode); + this.settings, this.xAxisList, this.yAxisList, this.dataItems, darkMode); this.timeSeriesChart.setOption(this.timeSeriesChartOptions); } } @@ -392,12 +401,16 @@ export class TbTimeSeriesChart { if (!Object.keys(this.settings.yAxes).includes(yAxisId)) { yAxisId = 'default'; } + const comparisonItem = this.comparisonEnabled && dataKey.isAdditional; + const xAxisIndex = comparisonItem ? 1 : 0; this.dataItems.push({ id: this.nextComponentId(), units, decimals, + xAxisIndex, yAxisId, yAxisIndex: this.getYAxisIndex(yAxisId), + comparisonItem, datasource, dataKey, data: namedData, @@ -488,6 +501,18 @@ export class TbTimeSeriesChart { this.subscribeForEntityThresholds(thresholdDatasources); } + private setupXAxes(): void { + const mainXAxis = createTimeSeriesXAxis('main', this.settings.xAxis, this.ctx.defaultSubscription.timeWindow.minTime, + this.ctx.defaultSubscription.timeWindow.maxTime, this.ctx.date, this.darkMode); + this.xAxisList.push(mainXAxis); + if (this.comparisonEnabled) { + const comparisonXAxis = createTimeSeriesXAxis('comparison', this.settings.comparisonXAxis, + this.ctx.defaultSubscription.comparisonTimeWindow.minTime, this.ctx.defaultSubscription.comparisonTimeWindow.maxTime, + this.ctx.date, this.darkMode); + this.xAxisList.push(comparisonXAxis); + } + } + private setupYAxes(): void { const yAxisSettingsList = Object.values(this.settings.yAxes); yAxisSettingsList.sort((a1, a2) => a1.order - a2.order); @@ -592,10 +617,7 @@ export class TbTimeSeriesChart { right: this.settings.dataZoom ? 5 : 0, bottom: this.minBottomOffset() }], - xAxis: [ - createTimeSeriesXAxisOption(this.settings.xAxis, this.ctx.defaultSubscription.timeWindow.minTime, - this.ctx.defaultSubscription.timeWindow.maxTime, this.ctx.date, this.darkMode) - ], + xAxis: this.xAxisList.map(axis => axis.option), yAxis: this.yAxisList.map(axis => axis.option), dataZoom: [ { @@ -655,10 +677,10 @@ export class TbTimeSeriesChart { private updateSeries(): void { this.timeSeriesChartOptions.series = generateChartData(this.dataItems, this.thresholdItems, - this.settings.stack, + this.stackMode, this.noAggregation, this.barRenderSharedContext, this.darkMode); - if (this.stateData) { + if (this.stateData && !this.comparisonEnabled) { adjustTimeAxisExtentToData(this.timeSeriesChartOptions.xAxis[0], this.dataItems, this.ctx.defaultSubscription.timeWindow.minTime, this.ctx.defaultSubscription.timeWindow.maxTime); @@ -667,37 +689,26 @@ export class TbTimeSeriesChart { private updateAxes(lazy = true) { const leftAxisList = this.yAxisList.filter(axis => axis.option.position === 'left'); - let res = this.updateYAxisOffset(leftAxisList); + let res = this.updateAxisOffset(leftAxisList); let leftOffset = res.offset + (!res.offset && this.settings.dataZoom ? 5 : 0); let changed = res.changed; const rightAxisList = this.yAxisList.filter(axis => axis.option.position === 'right'); - res = this.updateYAxisOffset(rightAxisList); + res = this.updateAxisOffset(rightAxisList); let rightOffset = res.offset + (!res.offset && this.settings.dataZoom ? 5 : 0); changed = changed || res.changed; + let bottomOffset = this.minBottomOffset(); const minTopOffset = this.minTopOffset(); - let topOffset = minTopOffset; - if (this.timeSeriesChartOptions.xAxis[0].show) { - const xAxisHeight = calculateXAxisHeight(this.timeSeriesChart); - if (this.timeSeriesChartOptions.xAxis[0].position === AxisPosition.bottom) { - bottomOffset += xAxisHeight; - } else { - topOffset = Math.max(minTopOffset, xAxisHeight); - } - if (this.settings.xAxis.label) { - const nameHeight = measureXAxisNameHeight(this.timeSeriesChart, this.timeSeriesChartOptions.xAxis[0].name); - if (this.timeSeriesChartOptions.xAxis[0].position === AxisPosition.bottom) { - bottomOffset += nameHeight; - } else { - topOffset = Math.max(minTopOffset, xAxisHeight + nameHeight); - } - const nameGap = xAxisHeight; - if (this.timeSeriesChartOptions.xAxis[0].nameGap !== nameGap) { - this.timeSeriesChartOptions.xAxis[0].nameGap = nameGap; - changed = true; - } - } - } + + const topAxisList = this.xAxisList.filter(axis => axis.option.position === 'top'); + res = this.updateAxisOffset(topAxisList); + const topOffset = Math.max(res.offset, minTopOffset); + changed = changed || res.changed; + + const bottomAxisList = this.xAxisList.filter(axis => axis.option.position === 'bottom'); + res = this.updateAxisOffset(bottomAxisList); + bottomOffset += res.offset; + changed = changed || res.changed; const thresholdsOffset = calculateThresholdsOffset(this.timeSeriesChart, this.thresholdItems, this.yAxisList); leftOffset = Math.max(leftOffset, thresholdsOffset[0]); @@ -715,6 +726,7 @@ export class TbTimeSeriesChart { } if (changed) { this.timeSeriesChartOptions.yAxis = this.yAxisList.map(axis => axis.option); + this.timeSeriesChartOptions.xAxis = this.xAxisList.map(axis => axis.option); this.timeSeriesChart.setOption(this.timeSeriesChartOptions, {replaceMerge: ['yAxis', 'xAxis', 'grid'], lazyUpdate: lazy}); } if (this.yAxisList.length) { @@ -730,45 +742,45 @@ export class TbTimeSeriesChart { } } - private updateYAxisOffset(axisList: TimeSeriesChartYAxis[]): {offset: number; changed: boolean} { + private updateAxisOffset(axisList: TimeSeriesChartAxis[]): {offset: number; changed: boolean} { const result = {offset: 0, changed: false}; - let width = 0; - for (const yAxis of axisList) { - const newWidth = calculateYAxisWidth(this.timeSeriesChart, yAxis.id); - if (width && newWidth) { + let size = 0; + for (const axis of axisList) { + const newSize = calculateAxisSize(this.timeSeriesChart, axis.option.mainType, axis.id); + if (size && newSize) { result.offset += 5; } - width = newWidth; - const showLine = !!width && yAxis.settings.showLine; - if (yAxis.option.axisLine.show !== showLine) { - yAxis.option.axisLine.show = showLine; + size = newSize; + const showLine = !!size && axis.settings.showLine; + if (axis.option.axisLine.show !== showLine) { + axis.option.axisLine.show = showLine; result.changed = true; } - if (yAxis.option.offset !== result.offset) { - yAxis.option.offset = result.offset; + if (axis.option.offset !== result.offset) { + axis.option.offset = result.offset; result.changed = true; } - if (yAxis.settings.label) { - if (!width) { - if (yAxis.option.name) { - yAxis.option.name = null; + if (axis.settings.label) { + if (!size) { + if (axis.option.name) { + axis.option.name = null; result.changed = true; } } else { - if (!yAxis.option.name) { - yAxis.option.name = yAxis.settings.label; + if (!axis.option.name) { + axis.option.name = axis.settings.label; result.changed = true; } - const nameGap = width; - if (yAxis.option.nameGap !== nameGap) { - yAxis.option.nameGap = nameGap; + const nameGap = size; + if (axis.option.nameGap !== nameGap) { + axis.option.nameGap = nameGap; result.changed = true; } - const nameWidth = measureYAxisNameWidth(this.timeSeriesChart, yAxis.id, yAxis.settings.label); - result.offset += nameWidth; + const nameSize = measureAxisNameSize(this.timeSeriesChart, axis.option.mainType, axis.id, axis.settings.label); + result.offset += nameSize; } } - result.offset += width; + result.offset += size; } return result; } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/flot-widget.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/flot-widget.models.ts index 14596b56a8..cc51bde975 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/flot-widget.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/flot-widget.models.ts @@ -18,11 +18,10 @@ /// import { - DataKey, + DataKey, DataKeySettingsWithComparison, Datasource, DatasourceData, FormattedData, - JsonSettingsSchema, LegendConfig } from '@shared/models/widget.models'; import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; @@ -219,13 +218,7 @@ export interface TbFlotKeyThreshold { color: string; } -export interface TbFlotKeyComparisonSettings { - showValuesForComparison: boolean; - comparisonValuesLabel: string; - color: string; -} - -export interface TbFlotKeySettings { +export interface TbFlotKeySettings extends DataKeySettingsWithComparison { excludeFromStacking: boolean; hideDataByDefault: boolean; disableDataHiding: boolean; @@ -249,7 +242,6 @@ export interface TbFlotKeySettings { axisPosition: TbFlotYAxisPosition; axisTicksFormatter: string; thresholds: TbFlotKeyThreshold[]; - comparisonSettings: TbFlotKeyComparisonSettings; } export interface TbFlotLatestKeySettings { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-key-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-key-settings.component.ts index 3b49476a59..b2495f5dda 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-key-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-key-settings.component.ts @@ -94,6 +94,11 @@ export class TimeSeriesChartKeySettingsComponent extends WidgetSettingsComponent lineSettings: [seriesSettings.lineSettings, []], barSettings: [seriesSettings.barSettings, []], tooltipValueFormatter: [seriesSettings.tooltipValueFormatter, []], + comparisonSettings: this.fb.group({ + showValuesForComparison: [seriesSettings.comparisonSettings?.showValuesForComparison, []], + comparisonValuesLabel: [seriesSettings.comparisonSettings?.comparisonValuesLabel, []], + color: [seriesSettings.comparisonSettings?.color, []] + }) }); } diff --git a/ui-ngx/src/app/shared/models/widget.models.ts b/ui-ngx/src/app/shared/models/widget.models.ts index c736ed7404..47605f108b 100644 --- a/ui-ngx/src/app/shared/models/widget.models.ts +++ b/ui-ngx/src/app/shared/models/widget.models.ts @@ -21,7 +21,6 @@ import { AggregationType, ComparisonDuration, Timewindow } from '@shared/models/ import { EntityType } from '@shared/models/entity-type.models'; import { DataKeyType } from './telemetry/telemetry.models'; import { EntityId } from '@shared/models/id/entity-id'; -import * as moment_ from 'moment'; import { AlarmFilter, AlarmFilterConfig, @@ -710,10 +709,23 @@ export const defaultWidgetAction = (setEntityId = true): WidgetAction => ({ export interface WidgetComparisonSettings { comparisonEnabled?: boolean; - timeForComparison?: moment_.unitOfTime.DurationConstructor; + timeForComparison?: ComparisonDuration; comparisonCustomIntervalValue?: number; } +export interface DataKeyComparisonSettings { + showValuesForComparison: boolean; + comparisonValuesLabel: string; + color: string; +} + +export interface DataKeySettingsWithComparison { + comparisonSettings?: DataKeyComparisonSettings; +} + +export const isDataKeySettingsWithComparison = (settings: any): settings is DataKeySettingsWithComparison => + 'comparisonSettings' in settings; + export interface WidgetSettings { [key: string]: any; } From 5191143c3f8a9d434e7951b57f4ccaa0dad4dfa2 Mon Sep 17 00:00:00 2001 From: Andrii Landiak Date: Thu, 11 Apr 2024 15:50:57 +0300 Subject: [PATCH 50/80] Provide support for Notification to Edge --- .../edge/DefaultEdgeNotificationService.java | 91 ++++---------- .../service/edge/EdgeContextComponent.java | 16 +++ .../service/edge/rpc/EdgeGrpcSession.java | 6 + .../service/edge/rpc/EdgeSyncCursor.java | 7 ++ .../BaseDashboardMsgConstructor.java | 1 + .../dashboard/DashboardMsgConstructor.java | 1 + .../dashboard/DashboardMsgConstructorV2.java | 1 + .../device/DeviceMsgConstructor.java | 1 + .../device/DeviceMsgConstructorV2.java | 1 + .../constructor/edge/EdgeMsgConstructor.java | 1 + .../BaseEntityViewMsgConstructor.java | 1 + .../entityview/EntityViewMsgConstructor.java | 1 + .../EntityViewMsgConstructorV1.java | 14 +-- .../EntityViewMsgConstructorV2.java | 1 + .../NotificationMsgConstructor.java | 43 +++++++ .../NotificationMsgConstructorImpl.java | 81 ++++++++++++ .../fetch/AdminSettingsEdgeEventFetcher.java | 1 + .../fetch/AssetProfilesEdgeEventFetcher.java | 3 +- .../rpc/fetch/AssetsEdgeEventFetcher.java | 2 +- .../fetch/BasePageableEdgeEventFetcher.java | 11 +- .../rpc/fetch/BaseUsersEdgeEventFetcher.java | 3 +- .../BaseWidgetTypesEdgeEventFetcher.java | 6 +- .../BaseWidgetsBundlesEdgeEventFetcher.java | 3 +- .../rpc/fetch/CustomerEdgeEventFetcher.java | 1 + .../fetch/CustomerUsersEdgeEventFetcher.java | 1 + .../rpc/fetch/DashboardsEdgeEventFetcher.java | 3 +- .../DefaultProfilesEdgeEventFetcher.java | 1 + .../fetch/DeviceProfilesEdgeEventFetcher.java | 3 +- .../rpc/fetch/DevicesEdgeEventFetcher.java | 3 +- .../edge/rpc/fetch/EdgeEventFetcher.java | 1 + .../fetch/EntityViewsEdgeEventFetcher.java | 3 +- .../rpc/fetch/GeneralEdgeEventFetcher.java | 1 + .../NotificationRuleEdgeEventFetcher.java | 48 +++++++ .../NotificationTargetEdgeEventFetcher.java | 48 +++++++ .../NotificationTemplateEdgeEventFetcher.java | 51 ++++++++ .../fetch/OtaPackagesEdgeEventFetcher.java | 3 +- .../rpc/fetch/QueuesEdgeEventFetcher.java | 3 +- .../rpc/fetch/RuleChainsEdgeEventFetcher.java | 3 +- .../rpc/fetch/TenantEdgeEventFetcher.java | 3 +- .../TenantResourcesEdgeEventFetcher.java | 3 +- .../edge/rpc/processor/BaseEdgeProcessor.java | 64 +++++----- .../processor/BaseEdgeProcessorFactory.java | 16 +-- .../processor/alarm/AlarmEdgeProcessor.java | 1 + .../processor/alarm/AlarmEdgeProcessorV1.java | 17 ++- .../processor/alarm/AlarmEdgeProcessorV2.java | 1 + .../rpc/processor/alarm/AlarmProcessor.java | 1 + .../processor/alarm/BaseAlarmProcessor.java | 24 ++-- .../processor/asset/AssetEdgeProcessor.java | 2 +- .../processor/asset/AssetEdgeProcessorV1.java | 1 + .../processor/asset/AssetEdgeProcessorV2.java | 1 + .../rpc/processor/asset/AssetProcessor.java | 1 + .../processor/asset/BaseAssetProcessor.java | 1 + .../profile/AssetProfileEdgeProcessor.java | 2 +- .../customer/CustomerEdgeProcessor.java | 10 +- .../dashboard/DashboardEdgeProcessor.java | 2 +- .../processor/device/DeviceEdgeProcessor.java | 3 +- .../profile/DeviceProfileEdgeProcessor.java | 2 +- .../rpc/processor/edge/EdgeProcessor.java | 6 +- .../entityview/EntityViewEdgeProcessor.java | 2 +- .../entityview/EntityViewProcessorV1.java | 1 + .../entityview/EntityViewProcessorV2.java | 1 + .../NotificationEdgeProcessor.java | 119 ++++++++++++++++++ .../ota/OtaPackageEdgeProcessor.java | 10 +- .../processor/queue/QueueEdgeProcessor.java | 10 +- .../relation/BaseRelationProcessor.java | 1 + .../relation/RelationEdgeProcessor.java | 1 + .../relation/RelationEdgeProcessorV1.java | 1 + .../relation/RelationEdgeProcessorV2.java | 1 + .../processor/relation/RelationProcessor.java | 1 + .../resource/BaseResourceProcessor.java | 1 + .../resource/ResourceEdgeProcessor.java | 10 +- .../resource/ResourceEdgeProcessorV1.java | 1 + .../resource/ResourceEdgeProcessorV2.java | 1 + .../processor/resource/ResourceProcessor.java | 1 + .../rule/RuleChainEdgeProcessor.java | 23 ++-- .../settings/AdminSettingsEdgeProcessor.java | 1 + .../telemetry/BaseTelemetryProcessor.java | 20 ++- .../tenant/TenantProfileEdgeProcessor.java | 1 + .../rpc/processor/user/UserEdgeProcessor.java | 20 ++- .../widget/WidgetBundleEdgeProcessor.java | 10 +- .../widget/WidgetTypeEdgeProcessor.java | 10 +- .../rpc/sync/DefaultEdgeRequestsService.java | 7 +- .../DefaultNotificationCenter.java | 13 +- .../rpc/processor/BaseEdgeProcessorTest.java | 20 +++ .../common/data/edge/EdgeEventType.java | 3 + .../common/data/id/EntityIdFactory.java | 6 + common/edge-api/src/main/proto/edge.proto | 24 ++++ .../DefaultNotificationTargetService.java | 21 ++-- .../DefaultNotificationTemplateService.java | 8 +- 89 files changed, 724 insertions(+), 255 deletions(-) create mode 100644 application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/notification/NotificationMsgConstructor.java create mode 100644 application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/notification/NotificationMsgConstructorImpl.java create mode 100644 application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/NotificationRuleEdgeEventFetcher.java create mode 100644 application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/NotificationTargetEdgeEventFetcher.java create mode 100644 application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/NotificationTemplateEdgeEventFetcher.java create mode 100644 application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/notification/NotificationEdgeProcessor.java diff --git a/application/src/main/java/org/thingsboard/server/service/edge/DefaultEdgeNotificationService.java b/application/src/main/java/org/thingsboard/server/service/edge/DefaultEdgeNotificationService.java index 4d1fb8a12a..1573fbb69d 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/DefaultEdgeNotificationService.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/DefaultEdgeNotificationService.java @@ -175,71 +175,31 @@ public class DefaultEdgeNotificationService implements EdgeNotificationService { } EdgeEventType type = EdgeEventType.valueOf(edgeNotificationMsg.getType()); switch (type) { - case EDGE: - edgeProcessor.processEdgeNotification(tenantId, edgeNotificationMsg); - break; - case ASSET: - assetProcessor.processEntityNotification(tenantId, edgeNotificationMsg); - break; - case ASSET_PROFILE: - assetProfileEdgeProcessor.processEntityNotification(tenantId, edgeNotificationMsg); - break; - case DEVICE: - deviceProcessor.processEntityNotification(tenantId, edgeNotificationMsg); - break; - case DEVICE_PROFILE: - deviceProfileEdgeProcessor.processEntityNotification(tenantId, edgeNotificationMsg); - break; - case ENTITY_VIEW: - entityViewProcessor.processEntityNotification(tenantId, edgeNotificationMsg); - break; - case DASHBOARD: - dashboardProcessor.processEntityNotification(tenantId, edgeNotificationMsg); - break; - case RULE_CHAIN: - ruleChainProcessor.processEntityNotification(tenantId, edgeNotificationMsg); - break; - case USER: - userProcessor.processEntityNotification(tenantId, edgeNotificationMsg); - break; - case CUSTOMER: - customerProcessor.processCustomerNotification(tenantId, edgeNotificationMsg); - break; - case OTA_PACKAGE: - otaPackageProcessor.processEntityNotification(tenantId, edgeNotificationMsg); - break; - case WIDGETS_BUNDLE: - widgetBundleProcessor.processEntityNotification(tenantId, edgeNotificationMsg); - break; - case WIDGET_TYPE: - widgetTypeProcessor.processEntityNotification(tenantId, edgeNotificationMsg); - break; - case QUEUE: - queueProcessor.processEntityNotification(tenantId, edgeNotificationMsg); - break; - case ALARM: - alarmProcessor.processAlarmNotification(tenantId, edgeNotificationMsg); - break; - case ALARM_COMMENT: - alarmProcessor.processAlarmCommentNotification(tenantId, edgeNotificationMsg); - break; - case RELATION: - relationProcessor.processRelationNotification(tenantId, edgeNotificationMsg); - break; - case TENANT: - tenantEdgeProcessor.processEntityNotification(tenantId, edgeNotificationMsg); - break; - case TENANT_PROFILE: - tenantProfileEdgeProcessor.processEntityNotification(tenantId, edgeNotificationMsg); - break; - case TB_RESOURCE: - resourceEdgeProcessor.processEntityNotification(tenantId, edgeNotificationMsg); - break; - case OAUTH2: - oAuth2EdgeProcessor.processOAuth2Notification(tenantId, edgeNotificationMsg); - break; - default: - log.warn("[{}] Edge event type [{}] is not designed to be pushed to edge", tenantId, type); + case EDGE -> edgeProcessor.processEdgeNotification(tenantId, edgeNotificationMsg); + case ASSET -> assetProcessor.processEntityNotification(tenantId, edgeNotificationMsg); + case ASSET_PROFILE -> assetProfileEdgeProcessor.processEntityNotification(tenantId, edgeNotificationMsg); + case DEVICE -> deviceProcessor.processEntityNotification(tenantId, edgeNotificationMsg); + case DEVICE_PROFILE -> deviceProfileEdgeProcessor.processEntityNotification(tenantId, edgeNotificationMsg); + case ENTITY_VIEW -> entityViewProcessor.processEntityNotification(tenantId, edgeNotificationMsg); + case DASHBOARD -> dashboardProcessor.processEntityNotification(tenantId, edgeNotificationMsg); + case RULE_CHAIN -> ruleChainProcessor.processEntityNotification(tenantId, edgeNotificationMsg); + case USER -> userProcessor.processEntityNotification(tenantId, edgeNotificationMsg); + case CUSTOMER -> customerProcessor.processCustomerNotification(tenantId, edgeNotificationMsg); + case OTA_PACKAGE -> otaPackageProcessor.processEntityNotification(tenantId, edgeNotificationMsg); + case WIDGETS_BUNDLE -> widgetBundleProcessor.processEntityNotification(tenantId, edgeNotificationMsg); + case WIDGET_TYPE -> widgetTypeProcessor.processEntityNotification(tenantId, edgeNotificationMsg); + case QUEUE -> queueProcessor.processEntityNotification(tenantId, edgeNotificationMsg); + case ALARM -> alarmProcessor.processAlarmNotification(tenantId, edgeNotificationMsg); + case ALARM_COMMENT -> alarmProcessor.processAlarmCommentNotification(tenantId, edgeNotificationMsg); + case RELATION -> relationProcessor.processRelationNotification(tenantId, edgeNotificationMsg); + case TENANT -> tenantEdgeProcessor.processEntityNotification(tenantId, edgeNotificationMsg); + case TENANT_PROFILE -> tenantProfileEdgeProcessor.processEntityNotification(tenantId, edgeNotificationMsg); + case NOTIFICATION_RULE -> System.out.println(); + case NOTIFICATION_TARGET -> System.out.println(); + case NOTIFICATION_TEMPLATE -> System.out.println(); + case TB_RESOURCE -> resourceEdgeProcessor.processEntityNotification(tenantId, edgeNotificationMsg); + case OAUTH2 -> oAuth2EdgeProcessor.processOAuth2Notification(tenantId, edgeNotificationMsg); + default -> log.warn("[{}] Edge event type [{}] is not designed to be pushed to edge", tenantId, type); } } catch (Exception e) { callBackFailure(tenantId, edgeNotificationMsg, callback, e); @@ -255,4 +215,5 @@ public class DefaultEdgeNotificationService implements EdgeNotificationService { log.error("[{}] Can't push to edge updates, edgeNotificationMsg [{}]", tenantId, edgeNotificationMsg, throwable); callback.onFailure(throwable); } + } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/EdgeContextComponent.java b/application/src/main/java/org/thingsboard/server/service/edge/EdgeContextComponent.java index 5ef8819df0..489fc6b6b8 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/EdgeContextComponent.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/EdgeContextComponent.java @@ -32,6 +32,9 @@ import org.thingsboard.server.dao.device.DeviceService; import org.thingsboard.server.dao.edge.EdgeEventService; import org.thingsboard.server.dao.edge.EdgeService; import org.thingsboard.server.dao.entityview.EntityViewService; +import org.thingsboard.server.dao.notification.NotificationRuleService; +import org.thingsboard.server.dao.notification.NotificationTargetService; +import org.thingsboard.server.dao.notification.NotificationTemplateService; import org.thingsboard.server.dao.oauth2.OAuth2Service; import org.thingsboard.server.dao.ota.OtaPackageService; import org.thingsboard.server.dao.queue.QueueService; @@ -62,6 +65,7 @@ import org.thingsboard.server.service.edge.rpc.processor.device.profile.DevicePr import org.thingsboard.server.service.edge.rpc.processor.edge.EdgeProcessor; import org.thingsboard.server.service.edge.rpc.processor.entityview.EntityViewEdgeProcessor; import org.thingsboard.server.service.edge.rpc.processor.entityview.EntityViewProcessorFactory; +import org.thingsboard.server.service.edge.rpc.processor.notification.NotificationEdgeProcessor; import org.thingsboard.server.service.edge.rpc.processor.oauth2.OAuth2EdgeProcessor; import org.thingsboard.server.service.edge.rpc.processor.ota.OtaPackageEdgeProcessor; import org.thingsboard.server.service.edge.rpc.processor.queue.QueueEdgeProcessor; @@ -153,6 +157,15 @@ public class EdgeContextComponent { @Autowired private ResourceService resourceService; + @Autowired + private NotificationRuleService notificationRuleService; + + @Autowired + private NotificationTargetService notificationTargetService; + + @Autowired + private NotificationTemplateService notificationTemplateService; + @Autowired private OAuth2Service oAuth2Service; @@ -225,6 +238,9 @@ public class EdgeContextComponent { @Autowired private ResourceEdgeProcessor resourceEdgeProcessor; + @Autowired + private NotificationEdgeProcessor notificationEdgeProcessor; + @Autowired private OAuth2EdgeProcessor oAuth2EdgeProcessor; 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 7d0a1aaa92..55e8e051ab 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 @@ -683,6 +683,12 @@ public final class EdgeGrpcSession implements Closeable { return ctx.getTenantEdgeProcessor().convertTenantEventToDownlink(edgeEvent, this.edgeVersion); case TENANT_PROFILE: return ctx.getTenantProfileEdgeProcessor().convertTenantProfileEventToDownlink(edgeEvent, this.edgeVersion); + case NOTIFICATION_RULE: + return ctx.getNotificationEdgeProcessor().convertNotificationRuleToDownlink(edgeEvent); + case NOTIFICATION_TARGET: + return ctx.getNotificationEdgeProcessor().convertNotificationTargetToDownlink(edgeEvent); + case NOTIFICATION_TEMPLATE: + return ctx.getNotificationEdgeProcessor().convertNotificationTemplateToDownlink(edgeEvent); case OAUTH2: return ctx.getOAuth2EdgeProcessor().convertOAuth2EventToDownlink(edgeEvent); default: 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 5f7ffc08e3..6321991586 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 @@ -30,6 +30,9 @@ import org.thingsboard.server.service.edge.rpc.fetch.DeviceProfilesEdgeEventFetc import org.thingsboard.server.service.edge.rpc.fetch.DevicesEdgeEventFetcher; import org.thingsboard.server.service.edge.rpc.fetch.EdgeEventFetcher; import org.thingsboard.server.service.edge.rpc.fetch.EntityViewsEdgeEventFetcher; +import org.thingsboard.server.service.edge.rpc.fetch.NotificationRuleEdgeEventFetcher; +import org.thingsboard.server.service.edge.rpc.fetch.NotificationTargetEdgeEventFetcher; +import org.thingsboard.server.service.edge.rpc.fetch.NotificationTemplateEdgeEventFetcher; import org.thingsboard.server.service.edge.rpc.fetch.OAuth2EdgeEventFetcher; import org.thingsboard.server.service.edge.rpc.fetch.OtaPackagesEdgeEventFetcher; import org.thingsboard.server.service.edge.rpc.fetch.QueuesEdgeEventFetcher; @@ -74,6 +77,9 @@ public class EdgeSyncCursor { fetchers.add(new AssetsEdgeEventFetcher(ctx.getAssetService())); fetchers.add(new EntityViewsEdgeEventFetcher(ctx.getEntityViewService())); if (fullSync) { + fetchers.add(new NotificationTemplateEdgeEventFetcher(ctx.getNotificationTemplateService())); + fetchers.add(new NotificationTargetEdgeEventFetcher(ctx.getNotificationTargetService())); + fetchers.add(new NotificationRuleEdgeEventFetcher(ctx.getNotificationRuleService())); fetchers.add(new SystemWidgetTypesEdgeEventFetcher(ctx.getWidgetTypeService())); fetchers.add(new TenantWidgetTypesEdgeEventFetcher(ctx.getWidgetTypeService())); fetchers.add(new SystemWidgetsBundlesEdgeEventFetcher(ctx.getWidgetsBundleService())); @@ -100,4 +106,5 @@ public class EdgeSyncCursor { public int getCurrentIdx() { return currentIdx; } + } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/dashboard/BaseDashboardMsgConstructor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/dashboard/BaseDashboardMsgConstructor.java index a61cc1fee3..f1314d9587 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/dashboard/BaseDashboardMsgConstructor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/dashboard/BaseDashboardMsgConstructor.java @@ -28,4 +28,5 @@ public abstract class BaseDashboardMsgConstructor implements DashboardMsgConstru .setIdMSB(dashboardId.getId().getMostSignificantBits()) .setIdLSB(dashboardId.getId().getLeastSignificantBits()).build(); } + } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/dashboard/DashboardMsgConstructor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/dashboard/DashboardMsgConstructor.java index 6e29ed1630..bdf4968692 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/dashboard/DashboardMsgConstructor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/dashboard/DashboardMsgConstructor.java @@ -26,4 +26,5 @@ public interface DashboardMsgConstructor extends MsgConstructor { DashboardUpdateMsg constructDashboardUpdatedMsg(UpdateMsgType msgType, Dashboard dashboard); DashboardUpdateMsg constructDashboardDeleteMsg(DashboardId dashboardId); + } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/dashboard/DashboardMsgConstructorV2.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/dashboard/DashboardMsgConstructorV2.java index 9af71e8150..d1fb5ae703 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/dashboard/DashboardMsgConstructorV2.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/dashboard/DashboardMsgConstructorV2.java @@ -32,4 +32,5 @@ public class DashboardMsgConstructorV2 extends BaseDashboardMsgConstructor { .setIdMSB(dashboard.getId().getId().getMostSignificantBits()) .setIdLSB(dashboard.getId().getId().getLeastSignificantBits()).build(); } + } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/device/DeviceMsgConstructor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/device/DeviceMsgConstructor.java index 2afdeacc67..854d0c80a8 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/device/DeviceMsgConstructor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/device/DeviceMsgConstructor.java @@ -43,4 +43,5 @@ public interface DeviceMsgConstructor extends MsgConstructor { DeviceProfileUpdateMsg constructDeviceProfileDeleteMsg(DeviceProfileId deviceProfileId); DeviceRpcCallMsg constructDeviceRpcCallMsg(UUID deviceId, JsonNode body); + } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/device/DeviceMsgConstructorV2.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/device/DeviceMsgConstructorV2.java index 3d4e20ab3c..cf951e39a9 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/device/DeviceMsgConstructorV2.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/device/DeviceMsgConstructorV2.java @@ -48,4 +48,5 @@ public class DeviceMsgConstructorV2 extends BaseDeviceMsgConstructor { .setIdMSB(deviceProfile.getId().getId().getMostSignificantBits()) .setIdLSB(deviceProfile.getId().getId().getLeastSignificantBits()).build(); } + } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/edge/EdgeMsgConstructor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/edge/EdgeMsgConstructor.java index 81b1df7ac9..4b45b0427f 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/edge/EdgeMsgConstructor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/edge/EdgeMsgConstructor.java @@ -43,4 +43,5 @@ public class EdgeMsgConstructor { } return builder.build(); } + } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/entityview/BaseEntityViewMsgConstructor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/entityview/BaseEntityViewMsgConstructor.java index ec650849e1..7fa4a03c3e 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/entityview/BaseEntityViewMsgConstructor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/entityview/BaseEntityViewMsgConstructor.java @@ -28,4 +28,5 @@ public abstract class BaseEntityViewMsgConstructor implements EntityViewMsgConst .setIdMSB(entityViewId.getId().getMostSignificantBits()) .setIdLSB(entityViewId.getId().getLeastSignificantBits()).build(); } + } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/entityview/EntityViewMsgConstructor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/entityview/EntityViewMsgConstructor.java index 69ce0f6770..bec1d416d3 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/entityview/EntityViewMsgConstructor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/entityview/EntityViewMsgConstructor.java @@ -26,4 +26,5 @@ public interface EntityViewMsgConstructor extends MsgConstructor { EntityViewUpdateMsg constructEntityViewUpdatedMsg(UpdateMsgType msgType, EntityView entityView); EntityViewUpdateMsg constructEntityViewDeleteMsg(EntityViewId entityViewId); + } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/entityview/EntityViewMsgConstructorV1.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/entityview/EntityViewMsgConstructorV1.java index feb0ab1d35..1b3bba22c0 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/entityview/EntityViewMsgConstructorV1.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/entityview/EntityViewMsgConstructorV1.java @@ -51,13 +51,11 @@ public class EntityViewMsgConstructorV1 extends BaseEntityViewMsgConstructor { } private EdgeEntityType checkEntityType(EntityType entityType) { - switch (entityType) { - case DEVICE: - return EdgeEntityType.DEVICE; - case ASSET: - return EdgeEntityType.ASSET; - default: - throw new RuntimeException("Unsupported entity type [" + entityType + "]"); - } + return switch (entityType) { + case DEVICE -> EdgeEntityType.DEVICE; + case ASSET -> EdgeEntityType.ASSET; + default -> throw new RuntimeException("Unsupported entity type [" + entityType + "]"); + }; } + } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/entityview/EntityViewMsgConstructorV2.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/entityview/EntityViewMsgConstructorV2.java index ba793f621b..6db4818688 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/entityview/EntityViewMsgConstructorV2.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/entityview/EntityViewMsgConstructorV2.java @@ -32,4 +32,5 @@ public class EntityViewMsgConstructorV2 extends BaseEntityViewMsgConstructor { .setIdMSB(entityView.getId().getId().getMostSignificantBits()) .setIdLSB(entityView.getId().getId().getLeastSignificantBits()).build(); } + } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/notification/NotificationMsgConstructor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/notification/NotificationMsgConstructor.java new file mode 100644 index 0000000000..9c8f5a37e6 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/notification/NotificationMsgConstructor.java @@ -0,0 +1,43 @@ +/** + * 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.service.edge.rpc.constructor.notification; + +import org.thingsboard.server.common.data.id.NotificationRuleId; +import org.thingsboard.server.common.data.id.NotificationTargetId; +import org.thingsboard.server.common.data.id.NotificationTemplateId; +import org.thingsboard.server.common.data.notification.rule.NotificationRule; +import org.thingsboard.server.common.data.notification.targets.NotificationTarget; +import org.thingsboard.server.common.data.notification.template.NotificationTemplate; +import org.thingsboard.server.gen.edge.v1.NotificationRuleUpdateMsg; +import org.thingsboard.server.gen.edge.v1.NotificationTargetUpdateMsg; +import org.thingsboard.server.gen.edge.v1.NotificationTemplateUpdateMsg; +import org.thingsboard.server.gen.edge.v1.UpdateMsgType; + +public interface NotificationMsgConstructor { + + NotificationRuleUpdateMsg constructNotificationRuleUpdateMsg(UpdateMsgType msgType, NotificationRule notificationRule); + + NotificationRuleUpdateMsg constructNotificationRuleDeleteMsg(NotificationRuleId notificationRuleId); + + NotificationTargetUpdateMsg constructNotificationTargetUpdateMsg(UpdateMsgType msgType, NotificationTarget notificationTarget); + + NotificationTargetUpdateMsg constructNotificationTargetDeleteMsg(NotificationTargetId notificationTargetId); + + NotificationTemplateUpdateMsg constructNotificationTemplateUpdateMsg(UpdateMsgType msgType, NotificationTemplate notificationTemplate); + + NotificationTemplateUpdateMsg constructNotificationTemplateDeleteMsg(NotificationTemplateId notificationTemplateId); + +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/notification/NotificationMsgConstructorImpl.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/notification/NotificationMsgConstructorImpl.java new file mode 100644 index 0000000000..d7ec8d5c92 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/notification/NotificationMsgConstructorImpl.java @@ -0,0 +1,81 @@ +/** + * 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.service.edge.rpc.constructor.notification; + +import org.springframework.stereotype.Component; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.common.data.id.NotificationRuleId; +import org.thingsboard.server.common.data.id.NotificationTargetId; +import org.thingsboard.server.common.data.id.NotificationTemplateId; +import org.thingsboard.server.common.data.notification.rule.NotificationRule; +import org.thingsboard.server.common.data.notification.targets.NotificationTarget; +import org.thingsboard.server.common.data.notification.template.NotificationTemplate; +import org.thingsboard.server.gen.edge.v1.NotificationRuleUpdateMsg; +import org.thingsboard.server.gen.edge.v1.NotificationTargetUpdateMsg; +import org.thingsboard.server.gen.edge.v1.NotificationTemplateUpdateMsg; +import org.thingsboard.server.gen.edge.v1.UpdateMsgType; +import org.thingsboard.server.queue.util.TbCoreComponent; + +@Component +@TbCoreComponent +public class NotificationMsgConstructorImpl implements NotificationMsgConstructor { + + @Override + public NotificationRuleUpdateMsg constructNotificationRuleUpdateMsg(UpdateMsgType msgType, NotificationRule notificationRule) { + return NotificationRuleUpdateMsg.newBuilder().setMsgType(msgType).setEntity(JacksonUtil.toString(notificationRule)) + .setIdMSB(notificationRule.getId().getId().getMostSignificantBits()) + .setIdLSB(notificationRule.getId().getId().getLeastSignificantBits()).build(); + } + + @Override + public NotificationRuleUpdateMsg constructNotificationRuleDeleteMsg(NotificationRuleId notificationRuleId) { + return NotificationRuleUpdateMsg.newBuilder() + .setMsgType(UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE) + .setIdMSB(notificationRuleId.getId().getMostSignificantBits()) + .setIdLSB(notificationRuleId.getId().getLeastSignificantBits()).build(); + } + + @Override + public NotificationTargetUpdateMsg constructNotificationTargetUpdateMsg(UpdateMsgType msgType, NotificationTarget notificationTarget) { + return NotificationTargetUpdateMsg.newBuilder().setMsgType(msgType).setEntity(JacksonUtil.toString(notificationTarget)) + .setIdMSB(notificationTarget.getId().getId().getMostSignificantBits()) + .setIdLSB(notificationTarget.getId().getId().getLeastSignificantBits()).build(); + } + + @Override + public NotificationTargetUpdateMsg constructNotificationTargetDeleteMsg(NotificationTargetId notificationTargetId) { + return NotificationTargetUpdateMsg.newBuilder() + .setMsgType(UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE) + .setIdMSB(notificationTargetId.getId().getMostSignificantBits()) + .setIdLSB(notificationTargetId.getId().getLeastSignificantBits()).build(); + } + + @Override + public NotificationTemplateUpdateMsg constructNotificationTemplateUpdateMsg(UpdateMsgType msgType, NotificationTemplate notificationTemplate) { + return NotificationTemplateUpdateMsg.newBuilder().setMsgType(msgType).setEntity(JacksonUtil.toString(notificationTemplate)) + .setIdMSB(notificationTemplate.getId().getId().getMostSignificantBits()) + .setIdLSB(notificationTemplate.getId().getId().getLeastSignificantBits()).build(); + } + + @Override + public NotificationTemplateUpdateMsg constructNotificationTemplateDeleteMsg(NotificationTemplateId notificationTemplateId) { + return NotificationTemplateUpdateMsg.newBuilder() + .setMsgType(UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE) + .setIdMSB(notificationTemplateId.getId().getMostSignificantBits()) + .setIdLSB(notificationTemplateId.getId().getLeastSignificantBits()).build(); + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/AdminSettingsEdgeEventFetcher.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/AdminSettingsEdgeEventFetcher.java index 0e48077703..5554680dfe 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/AdminSettingsEdgeEventFetcher.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/AdminSettingsEdgeEventFetcher.java @@ -62,4 +62,5 @@ public class AdminSettingsEdgeEventFetcher implements EdgeEventFetcher { } return result; } + } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/AssetProfilesEdgeEventFetcher.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/AssetProfilesEdgeEventFetcher.java index 240203a7e2..ec9e1f5bd9 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/AssetProfilesEdgeEventFetcher.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/AssetProfilesEdgeEventFetcher.java @@ -35,7 +35,7 @@ public class AssetProfilesEdgeEventFetcher extends BasePageableEdgeEventFetcher< private final AssetProfileService assetProfileService; @Override - PageData fetchPageData(TenantId tenantId, Edge edge, PageLink pageLink) { + PageData fetchEntities(TenantId tenantId, Edge edge, PageLink pageLink) { return assetProfileService.findAssetProfiles(tenantId, pageLink); } @@ -44,4 +44,5 @@ public class AssetProfilesEdgeEventFetcher extends BasePageableEdgeEventFetcher< return EdgeUtils.constructEdgeEvent(tenantId, edge.getId(), EdgeEventType.ASSET_PROFILE, EdgeEventActionType.ADDED, assetProfile.getId(), null); } + } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/AssetsEdgeEventFetcher.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/AssetsEdgeEventFetcher.java index 01765c4196..7f896deb50 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/AssetsEdgeEventFetcher.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/AssetsEdgeEventFetcher.java @@ -35,7 +35,7 @@ public class AssetsEdgeEventFetcher extends BasePageableEdgeEventFetcher private final AssetService assetService; @Override - PageData fetchPageData(TenantId tenantId, Edge edge, PageLink pageLink) { + PageData fetchEntities(TenantId tenantId, Edge edge, PageLink pageLink) { return assetService.findAssetsByTenantIdAndEdgeId(tenantId, edge.getId(), pageLink); } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/BasePageableEdgeEventFetcher.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/BasePageableEdgeEventFetcher.java index 5a5f842eaa..747dc5722f 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/BasePageableEdgeEventFetcher.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/BasePageableEdgeEventFetcher.java @@ -36,17 +36,18 @@ public abstract class BasePageableEdgeEventFetcher implements EdgeEventFetche @Override public PageData fetchEdgeEvents(TenantId tenantId, Edge edge, PageLink pageLink) { log.trace("[{}] start fetching edge events [{}]", tenantId, edge.getId()); - PageData pageData = fetchPageData(tenantId, edge, pageLink); + PageData entities = fetchEntities(tenantId, edge, pageLink); List result = new ArrayList<>(); - if (!pageData.getData().isEmpty()) { - for (T entity : pageData.getData()) { + if (!entities.getData().isEmpty()) { + for (T entity : entities.getData()) { result.add(constructEdgeEvent(tenantId, edge, entity)); } } - return new PageData<>(result, pageData.getTotalPages(), pageData.getTotalElements(), pageData.hasNext()); + return new PageData<>(result, entities.getTotalPages(), entities.getTotalElements(), entities.hasNext()); } - abstract PageData fetchPageData(TenantId tenantId, Edge edge, PageLink pageLink); + abstract PageData fetchEntities(TenantId tenantId, Edge edge, PageLink pageLink); abstract EdgeEvent constructEdgeEvent(TenantId tenantId, Edge edge, T entity); + } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/BaseUsersEdgeEventFetcher.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/BaseUsersEdgeEventFetcher.java index b39d7e1209..51970c8556 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/BaseUsersEdgeEventFetcher.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/BaseUsersEdgeEventFetcher.java @@ -35,7 +35,7 @@ public abstract class BaseUsersEdgeEventFetcher extends BasePageableEdgeEventFet protected final UserService userService; @Override - PageData fetchPageData(TenantId tenantId, Edge edge, PageLink pageLink) { + PageData fetchEntities(TenantId tenantId, Edge edge, PageLink pageLink) { return findUsers(tenantId, pageLink); } @@ -46,4 +46,5 @@ public abstract class BaseUsersEdgeEventFetcher extends BasePageableEdgeEventFet } protected abstract PageData findUsers(TenantId tenantId, PageLink pageLink); + } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/BaseWidgetTypesEdgeEventFetcher.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/BaseWidgetTypesEdgeEventFetcher.java index cd6cb3fe7f..969ef98c7f 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/BaseWidgetTypesEdgeEventFetcher.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/BaseWidgetTypesEdgeEventFetcher.java @@ -25,11 +25,8 @@ import org.thingsboard.server.common.data.edge.EdgeEventType; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; -import org.thingsboard.server.common.data.widget.WidgetTypeDetails; import org.thingsboard.server.common.data.widget.WidgetTypeInfo; -import org.thingsboard.server.common.data.widget.WidgetsBundle; import org.thingsboard.server.dao.widget.WidgetTypeService; -import org.thingsboard.server.dao.widget.WidgetsBundleService; @Slf4j @AllArgsConstructor @@ -38,7 +35,7 @@ public abstract class BaseWidgetTypesEdgeEventFetcher extends BasePageableEdgeEv protected final WidgetTypeService widgetTypeService; @Override - PageData fetchPageData(TenantId tenantId, Edge edge, PageLink pageLink) { + PageData fetchEntities(TenantId tenantId, Edge edge, PageLink pageLink) { return findWidgetTypes(tenantId, pageLink); } @@ -49,4 +46,5 @@ public abstract class BaseWidgetTypesEdgeEventFetcher extends BasePageableEdgeEv } protected abstract PageData findWidgetTypes(TenantId tenantId, PageLink pageLink); + } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/BaseWidgetsBundlesEdgeEventFetcher.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/BaseWidgetsBundlesEdgeEventFetcher.java index c14ad7821a..b12ec3fb79 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/BaseWidgetsBundlesEdgeEventFetcher.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/BaseWidgetsBundlesEdgeEventFetcher.java @@ -35,7 +35,7 @@ public abstract class BaseWidgetsBundlesEdgeEventFetcher extends BasePageableEdg protected final WidgetsBundleService widgetsBundleService; @Override - PageData fetchPageData(TenantId tenantId, Edge edge, PageLink pageLink) { + PageData fetchEntities(TenantId tenantId, Edge edge, PageLink pageLink) { return findWidgetsBundles(tenantId, pageLink); } @@ -46,4 +46,5 @@ public abstract class BaseWidgetsBundlesEdgeEventFetcher extends BasePageableEdg } protected abstract PageData findWidgetsBundles(TenantId tenantId, PageLink pageLink); + } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/CustomerEdgeEventFetcher.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/CustomerEdgeEventFetcher.java index ff34e618bc..fa9fcf0d8a 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/CustomerEdgeEventFetcher.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/CustomerEdgeEventFetcher.java @@ -49,4 +49,5 @@ public class CustomerEdgeEventFetcher implements EdgeEventFetcher { // returns PageData object to be in sync with other fetchers return new PageData<>(result, 1, result.size(), false); } + } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/CustomerUsersEdgeEventFetcher.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/CustomerUsersEdgeEventFetcher.java index da278e8053..a1c1752496 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/CustomerUsersEdgeEventFetcher.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/CustomerUsersEdgeEventFetcher.java @@ -35,4 +35,5 @@ public class CustomerUsersEdgeEventFetcher extends BaseUsersEdgeEventFetcher { protected PageData findUsers(TenantId tenantId, PageLink pageLink) { return userService.findCustomerUsers(tenantId, customerId, pageLink); } + } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/DashboardsEdgeEventFetcher.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/DashboardsEdgeEventFetcher.java index 16f4749478..663ada149b 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/DashboardsEdgeEventFetcher.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/DashboardsEdgeEventFetcher.java @@ -35,7 +35,7 @@ public class DashboardsEdgeEventFetcher extends BasePageableEdgeEventFetcher fetchPageData(TenantId tenantId, Edge edge, PageLink pageLink) { + PageData fetchEntities(TenantId tenantId, Edge edge, PageLink pageLink) { return dashboardService.findDashboardsByTenantIdAndEdgeId(tenantId, edge.getId(), pageLink); } @@ -44,4 +44,5 @@ public class DashboardsEdgeEventFetcher extends BasePageableEdgeEventFetcher(result, 1, result.size(), false); } + } \ No newline at end of file diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/DeviceProfilesEdgeEventFetcher.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/DeviceProfilesEdgeEventFetcher.java index 8852850ca1..f86eb23f7f 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/DeviceProfilesEdgeEventFetcher.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/DeviceProfilesEdgeEventFetcher.java @@ -35,7 +35,7 @@ public class DeviceProfilesEdgeEventFetcher extends BasePageableEdgeEventFetcher private final DeviceProfileService deviceProfileService; @Override - PageData fetchPageData(TenantId tenantId, Edge edge, PageLink pageLink) { + PageData fetchEntities(TenantId tenantId, Edge edge, PageLink pageLink) { return deviceProfileService.findDeviceProfiles(tenantId, pageLink); } @@ -44,4 +44,5 @@ public class DeviceProfilesEdgeEventFetcher extends BasePageableEdgeEventFetcher return EdgeUtils.constructEdgeEvent(tenantId, edge.getId(), EdgeEventType.DEVICE_PROFILE, EdgeEventActionType.ADDED, deviceProfile.getId(), null); } + } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/DevicesEdgeEventFetcher.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/DevicesEdgeEventFetcher.java index 252373cfc1..f62f8edf97 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/DevicesEdgeEventFetcher.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/DevicesEdgeEventFetcher.java @@ -35,7 +35,7 @@ public class DevicesEdgeEventFetcher extends BasePageableEdgeEventFetcher fetchPageData(TenantId tenantId, Edge edge, PageLink pageLink) { + PageData fetchEntities(TenantId tenantId, Edge edge, PageLink pageLink) { return deviceService.findDevicesByTenantIdAndEdgeId(tenantId, edge.getId(), pageLink); } @@ -44,4 +44,5 @@ public class DevicesEdgeEventFetcher extends BasePageableEdgeEventFetcher fetchEdgeEvents(TenantId tenantId, Edge edge, PageLink pageLink) throws Exception; + } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/EntityViewsEdgeEventFetcher.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/EntityViewsEdgeEventFetcher.java index 81e5c537dd..c32ab62229 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/EntityViewsEdgeEventFetcher.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/EntityViewsEdgeEventFetcher.java @@ -35,7 +35,7 @@ public class EntityViewsEdgeEventFetcher extends BasePageableEdgeEventFetcher fetchPageData(TenantId tenantId, Edge edge, PageLink pageLink) { + PageData fetchEntities(TenantId tenantId, Edge edge, PageLink pageLink) { return entityViewService.findEntityViewsByTenantIdAndEdgeId(tenantId, edge.getId(), pageLink); } @@ -44,4 +44,5 @@ public class EntityViewsEdgeEventFetcher extends BasePageableEdgeEventFetcher(); } + } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/NotificationRuleEdgeEventFetcher.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/NotificationRuleEdgeEventFetcher.java new file mode 100644 index 0000000000..de4c34f600 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/NotificationRuleEdgeEventFetcher.java @@ -0,0 +1,48 @@ +/** + * 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.service.edge.rpc.fetch; + +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.thingsboard.server.common.data.EdgeUtils; +import org.thingsboard.server.common.data.edge.Edge; +import org.thingsboard.server.common.data.edge.EdgeEvent; +import org.thingsboard.server.common.data.edge.EdgeEventActionType; +import org.thingsboard.server.common.data.edge.EdgeEventType; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.notification.rule.NotificationRule; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.dao.notification.NotificationRuleService; + +@AllArgsConstructor +@Slf4j +public class NotificationRuleEdgeEventFetcher extends BasePageableEdgeEventFetcher{ + + private NotificationRuleService notificationRuleService; + + @Override + PageData fetchEntities(TenantId tenantId, Edge edge, PageLink pageLink) { + return notificationRuleService.findNotificationRulesByTenantId(tenantId, pageLink); + } + + @Override + EdgeEvent constructEdgeEvent(TenantId tenantId, Edge edge, NotificationRule notificationRule) { + return EdgeUtils.constructEdgeEvent(tenantId, edge.getId(), EdgeEventType.NOTIFICATION_RULE, + EdgeEventActionType.ADDED, notificationRule.getId(), null); + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/NotificationTargetEdgeEventFetcher.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/NotificationTargetEdgeEventFetcher.java new file mode 100644 index 0000000000..ed59b375ab --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/NotificationTargetEdgeEventFetcher.java @@ -0,0 +1,48 @@ +/** + * 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.service.edge.rpc.fetch; + +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.thingsboard.server.common.data.EdgeUtils; +import org.thingsboard.server.common.data.edge.Edge; +import org.thingsboard.server.common.data.edge.EdgeEvent; +import org.thingsboard.server.common.data.edge.EdgeEventActionType; +import org.thingsboard.server.common.data.edge.EdgeEventType; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.notification.targets.NotificationTarget; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.dao.notification.NotificationTargetService; + +@AllArgsConstructor +@Slf4j +public class NotificationTargetEdgeEventFetcher extends BasePageableEdgeEventFetcher { + + private NotificationTargetService notificationTargetService; + + @Override + PageData fetchEntities(TenantId tenantId, Edge edge, PageLink pageLink) { + return notificationTargetService.findNotificationTargetsByTenantId(tenantId, pageLink); + } + + @Override + EdgeEvent constructEdgeEvent(TenantId tenantId, Edge edge, NotificationTarget notificationTarget) { + return EdgeUtils.constructEdgeEvent(tenantId, edge.getId(), EdgeEventType.NOTIFICATION_TARGET, + EdgeEventActionType.ADDED, notificationTarget.getId(), null); + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/NotificationTemplateEdgeEventFetcher.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/NotificationTemplateEdgeEventFetcher.java new file mode 100644 index 0000000000..fefff805d3 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/NotificationTemplateEdgeEventFetcher.java @@ -0,0 +1,51 @@ +/** + * 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.service.edge.rpc.fetch; + +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.thingsboard.server.common.data.EdgeUtils; +import org.thingsboard.server.common.data.edge.Edge; +import org.thingsboard.server.common.data.edge.EdgeEvent; +import org.thingsboard.server.common.data.edge.EdgeEventActionType; +import org.thingsboard.server.common.data.edge.EdgeEventType; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.notification.NotificationType; +import org.thingsboard.server.common.data.notification.template.NotificationTemplate; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.dao.notification.NotificationTemplateService; + +import java.util.List; + +@AllArgsConstructor +@Slf4j +public class NotificationTemplateEdgeEventFetcher extends BasePageableEdgeEventFetcher { + + private NotificationTemplateService notificationTemplateService; + + @Override + PageData fetchEntities(TenantId tenantId, Edge edge, PageLink pageLink) { + return notificationTemplateService.findNotificationTemplatesByTenantIdAndNotificationTypes(tenantId, List.of(NotificationType.values()), pageLink); + } + + @Override + EdgeEvent constructEdgeEvent(TenantId tenantId, Edge edge, NotificationTemplate notificationTemplate) { + return EdgeUtils.constructEdgeEvent(tenantId, edge.getId(), EdgeEventType.NOTIFICATION_TEMPLATE, + EdgeEventActionType.ADDED, notificationTemplate.getId(), null); + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/OtaPackagesEdgeEventFetcher.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/OtaPackagesEdgeEventFetcher.java index 0330a9fec9..9f60c1ebd5 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/OtaPackagesEdgeEventFetcher.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/OtaPackagesEdgeEventFetcher.java @@ -35,7 +35,7 @@ public class OtaPackagesEdgeEventFetcher extends BasePageableEdgeEventFetcher fetchPageData(TenantId tenantId, Edge edge, PageLink pageLink) { + PageData fetchEntities(TenantId tenantId, Edge edge, PageLink pageLink) { return otaPackageService.findTenantOtaPackagesByTenantId(tenantId, pageLink); } @@ -44,4 +44,5 @@ public class OtaPackagesEdgeEventFetcher extends BasePageableEdgeEventFetcher private final QueueService queueService; @Override - PageData fetchPageData(TenantId tenantId, Edge edge, PageLink pageLink) { + PageData fetchEntities(TenantId tenantId, Edge edge, PageLink pageLink) { return queueService.findQueuesByTenantId(tenantId, pageLink); } @@ -44,4 +44,5 @@ public class QueuesEdgeEventFetcher extends BasePageableEdgeEventFetcher return EdgeUtils.constructEdgeEvent(tenantId, edge.getId(), EdgeEventType.QUEUE, EdgeEventActionType.ADDED, queue.getId(), null); } + } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/RuleChainsEdgeEventFetcher.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/RuleChainsEdgeEventFetcher.java index d6e48ec6bd..5569c30018 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/RuleChainsEdgeEventFetcher.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/RuleChainsEdgeEventFetcher.java @@ -39,7 +39,7 @@ public class RuleChainsEdgeEventFetcher extends BasePageableEdgeEventFetcher fetchPageData(TenantId tenantId, Edge edge, PageLink pageLink) { + PageData fetchEntities(TenantId tenantId, Edge edge, PageLink pageLink) { return ruleChainService.findRuleChainsByTenantIdAndEdgeId(tenantId, edge.getId(), pageLink); } @@ -54,4 +54,5 @@ public class RuleChainsEdgeEventFetcher extends BasePageableEdgeEventFetcher private final TenantService tenantService; @Override - PageData fetchPageData(TenantId tenantId, Edge edge, PageLink pageLink) { + PageData fetchEntities(TenantId tenantId, Edge edge, PageLink pageLink) { Tenant tenant = tenantService.findTenantById(tenantId); // returns PageData object to be in sync with other fetchers return new PageData<>(List.of(tenant), 1, 1, false); @@ -48,4 +48,5 @@ public class TenantEdgeEventFetcher extends BasePageableEdgeEventFetcher return EdgeUtils.constructEdgeEvent(tenantId, edge.getId(), EdgeEventType.TENANT, EdgeEventActionType.UPDATED, entity.getId(), null); } + } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/TenantResourcesEdgeEventFetcher.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/TenantResourcesEdgeEventFetcher.java index eef428bf8c..5dcf8d4cec 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/TenantResourcesEdgeEventFetcher.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/TenantResourcesEdgeEventFetcher.java @@ -35,7 +35,7 @@ public class TenantResourcesEdgeEventFetcher extends BasePageableEdgeEventFetche private final ResourceService resourceService; @Override - PageData fetchPageData(TenantId tenantId, Edge edge, PageLink pageLink) { + PageData fetchEntities(TenantId tenantId, Edge edge, PageLink pageLink) { return resourceService.findAllTenantResources(tenantId, pageLink); } @@ -44,4 +44,5 @@ public class TenantResourcesEdgeEventFetcher extends BasePageableEdgeEventFetche return EdgeUtils.constructEdgeEvent(tenantId, edge.getId(), EdgeEventType.TB_RESOURCE, EdgeEventActionType.ADDED, tbResource.getId(), null); } + } 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 db0bbd8aa5..1abf6c025f 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 @@ -73,6 +73,9 @@ import org.thingsboard.server.dao.edge.EdgeEventService; import org.thingsboard.server.dao.edge.EdgeService; import org.thingsboard.server.dao.edge.EdgeSynchronizationManager; import org.thingsboard.server.dao.entityview.EntityViewService; +import org.thingsboard.server.dao.notification.NotificationRuleService; +import org.thingsboard.server.dao.notification.NotificationTargetService; +import org.thingsboard.server.dao.notification.NotificationTemplateService; import org.thingsboard.server.dao.oauth2.OAuth2Service; import org.thingsboard.server.dao.ota.OtaPackageService; import org.thingsboard.server.dao.queue.QueueService; @@ -99,6 +102,7 @@ import org.thingsboard.server.service.edge.rpc.constructor.dashboard.DashboardMs import org.thingsboard.server.service.edge.rpc.constructor.device.DeviceMsgConstructorFactory; import org.thingsboard.server.service.edge.rpc.constructor.edge.EdgeMsgConstructor; import org.thingsboard.server.service.edge.rpc.constructor.entityview.EntityViewMsgConstructorFactory; +import org.thingsboard.server.service.edge.rpc.constructor.notification.NotificationMsgConstructor; import org.thingsboard.server.service.edge.rpc.constructor.oauth2.OAuth2MsgConstructor; import org.thingsboard.server.service.edge.rpc.constructor.ota.OtaPackageMsgConstructorFactory; import org.thingsboard.server.service.edge.rpc.constructor.queue.QueueMsgConstructorFactory; @@ -226,6 +230,15 @@ public abstract class BaseEdgeProcessor { @Autowired protected ResourceService resourceService; + @Autowired + protected NotificationRuleService notificationRuleService; + + @Autowired + protected NotificationTargetService notificationTargetService; + + @Autowired + protected NotificationTemplateService notificationTemplateService; + @Autowired protected OAuth2Service oAuth2Service; @@ -260,9 +273,13 @@ public abstract class BaseEdgeProcessor { @Autowired protected EntityDataMsgConstructor entityDataMsgConstructor; + @Autowired + protected NotificationMsgConstructor notificationMsgConstructor; + @Autowired protected OAuth2MsgConstructor oAuth2MsgConstructor; + @Autowired protected RuleChainMsgConstructorFactory ruleChainMsgConstructorFactory; @@ -417,10 +434,8 @@ public abstract class BaseEdgeProcessor { return switch (actionType) { case UPDATED, CREDENTIALS_UPDATED, ASSIGNED_TO_CUSTOMER, UNASSIGNED_FROM_CUSTOMER, UPDATED_COMMENT -> UpdateMsgType.ENTITY_UPDATED_RPC_MESSAGE; - case ADDED, ASSIGNED_TO_EDGE, RELATION_ADD_OR_UPDATE, ADDED_COMMENT -> - UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE; - case DELETED, UNASSIGNED_FROM_EDGE, RELATION_DELETED, DELETED_COMMENT, ALARM_DELETE -> - UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE; + case ADDED, ASSIGNED_TO_EDGE, RELATION_ADD_OR_UPDATE, ADDED_COMMENT -> UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE; + case DELETED, UNASSIGNED_FROM_EDGE, RELATION_DELETED, DELETED_COMMENT, ALARM_DELETE -> UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE; case ALARM_ACK -> UpdateMsgType.ALARM_ACK_RPC_MESSAGE; case ALARM_CLEAR -> UpdateMsgType.ALARM_CLEAR_RPC_MESSAGE; default -> throw new RuntimeException("Unsupported actionType [" + actionType + "]"); @@ -528,28 +543,21 @@ public abstract class BaseEdgeProcessor { protected EntityId constructEntityId(String entityTypeStr, long entityIdMSB, long entityIdLSB) { EntityType entityType = EntityType.valueOf(entityTypeStr); - switch (entityType) { - case DEVICE: - return new DeviceId(new UUID(entityIdMSB, entityIdLSB)); - case ASSET: - return new AssetId(new UUID(entityIdMSB, entityIdLSB)); - case ENTITY_VIEW: - return new EntityViewId(new UUID(entityIdMSB, entityIdLSB)); - case DASHBOARD: - return new DashboardId(new UUID(entityIdMSB, entityIdLSB)); - case TENANT: - return TenantId.fromUUID(new UUID(entityIdMSB, entityIdLSB)); - case CUSTOMER: - return new CustomerId(new UUID(entityIdMSB, entityIdLSB)); - case USER: - return new UserId(new UUID(entityIdMSB, entityIdLSB)); - case EDGE: - return new EdgeId(new UUID(entityIdMSB, entityIdLSB)); - default: + return switch (entityType) { + case DEVICE -> new DeviceId(new UUID(entityIdMSB, entityIdLSB)); + case ASSET -> new AssetId(new UUID(entityIdMSB, entityIdLSB)); + case ENTITY_VIEW -> new EntityViewId(new UUID(entityIdMSB, entityIdLSB)); + case DASHBOARD -> new DashboardId(new UUID(entityIdMSB, entityIdLSB)); + case TENANT -> TenantId.fromUUID(new UUID(entityIdMSB, entityIdLSB)); + case CUSTOMER -> new CustomerId(new UUID(entityIdMSB, entityIdLSB)); + case USER -> new UserId(new UUID(entityIdMSB, entityIdLSB)); + case EDGE -> new EdgeId(new UUID(entityIdMSB, entityIdLSB)); + default -> { log.warn("Unsupported entity type [{}] during construct of entity id. entityIdMSB [{}], entityIdLSB [{}]", entityTypeStr, entityIdMSB, entityIdLSB); - return null; - } + yield null; + } + }; } protected UUID safeGetUUID(long mSB, long lSB) { @@ -570,8 +578,7 @@ public abstract class BaseEdgeProcessor { case TENANT -> tenantService.findTenantById(tenantId) != null; case DEVICE -> deviceService.findDeviceById(tenantId, new DeviceId(entityId.getId())) != null; case ASSET -> assetService.findAssetById(tenantId, new AssetId(entityId.getId())) != null; - case ENTITY_VIEW -> - entityViewService.findEntityViewById(tenantId, new EntityViewId(entityId.getId())) != null; + case ENTITY_VIEW -> entityViewService.findEntityViewById(tenantId, new EntityViewId(entityId.getId())) != null; case CUSTOMER -> customerService.findCustomerById(tenantId, new CustomerId(entityId.getId())) != null; case USER -> userService.findUserById(tenantId, new UserId(entityId.getId())) != null; case DASHBOARD -> dashboardService.findDashboardById(tenantId, new DashboardId(entityId.getId())) != null; @@ -599,9 +606,8 @@ public abstract class BaseEdgeProcessor { return metaData; } - protected void pushEntityEventToRuleEngine(TenantId tenantId, EntityId entityId, CustomerId customerId, - TbMsgType msgType, String msgData, TbMsgMetaData metaData) { - TbMsg tbMsg = TbMsg.newMsg(msgType, entityId, customerId, metaData, TbMsgDataType.JSON, msgData); + protected void pushEntityEventToRuleEngine(TenantId tenantId, EntityId entityId, CustomerId customerId, String msgData, TbMsgMetaData metaData) { + TbMsg tbMsg = TbMsg.newMsg(TbMsgType.ENTITY_CREATED, entityId, customerId, metaData, TbMsgDataType.JSON, msgData); tbClusterService.pushMsgToRuleEngine(tenantId, entityId, tbMsg, new TbQueueCallback() { @Override public void onSuccess(TbQueueMsgMetadata metadata) { diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessorFactory.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessorFactory.java index a9ec97cf64..a8ca885c7b 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessorFactory.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessorFactory.java @@ -31,16 +31,10 @@ public abstract class BaseEdgeProcessorFactory v1Processor; + default -> v2Processor; + }; } + } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/alarm/AlarmEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/alarm/AlarmEdgeProcessor.java index b578325961..22d76dc86e 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/alarm/AlarmEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/alarm/AlarmEdgeProcessor.java @@ -156,4 +156,5 @@ public abstract class AlarmEdgeProcessor extends BaseAlarmProcessor implements A } return futures; } + } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/alarm/AlarmEdgeProcessorV1.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/alarm/AlarmEdgeProcessorV1.java index 9f8738b075..30ee4efe5f 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/alarm/AlarmEdgeProcessorV1.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/alarm/AlarmEdgeProcessorV1.java @@ -58,15 +58,12 @@ public class AlarmEdgeProcessorV1 extends AlarmEdgeProcessor { } private EntityId getAlarmOriginator(TenantId tenantId, String entityName, EntityType entityType) { - switch (entityType) { - case DEVICE: - return deviceService.findDeviceByTenantIdAndName(tenantId, entityName).getId(); - case ASSET: - return assetService.findAssetByTenantIdAndName(tenantId, entityName).getId(); - case ENTITY_VIEW: - return entityViewService.findEntityViewByTenantIdAndName(tenantId, entityName).getId(); - default: - return null; - } + return switch (entityType) { + case DEVICE -> deviceService.findDeviceByTenantIdAndName(tenantId, entityName).getId(); + case ASSET -> assetService.findAssetByTenantIdAndName(tenantId, entityName).getId(); + case ENTITY_VIEW -> entityViewService.findEntityViewByTenantIdAndName(tenantId, entityName).getId(); + default -> null; + }; } + } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/alarm/AlarmEdgeProcessorV2.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/alarm/AlarmEdgeProcessorV2.java index a1d06e1b3b..dd49af252d 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/alarm/AlarmEdgeProcessorV2.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/alarm/AlarmEdgeProcessorV2.java @@ -40,4 +40,5 @@ public class AlarmEdgeProcessorV2 extends AlarmEdgeProcessor { protected Alarm constructAlarmFromUpdateMsg(TenantId tenantId, AlarmId alarmId, EntityId originatorId, AlarmUpdateMsg alarmUpdateMsg) { return JacksonUtil.fromString(alarmUpdateMsg.getEntity(), Alarm.class, true); } + } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/alarm/AlarmProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/alarm/AlarmProcessor.java index 938269462d..d4f820b6c3 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/alarm/AlarmProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/alarm/AlarmProcessor.java @@ -34,4 +34,5 @@ public interface AlarmProcessor extends EdgeProcessor { ListenableFuture processAlarmCommentMsgFromEdge(TenantId tenantId, EdgeId edgeId, AlarmCommentUpdateMsg alarmCommentUpdateMsg); DownlinkMsg convertAlarmCommentEventToDownlink(EdgeEvent edgeEvent, EdgeVersion edgeVersion); + } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/alarm/BaseAlarmProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/alarm/BaseAlarmProcessor.java index ff2c0ca5f6..f6a05e363c 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/alarm/BaseAlarmProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/alarm/BaseAlarmProcessor.java @@ -144,23 +144,20 @@ public abstract class BaseAlarmProcessor extends BaseEdgeProcessor { AlarmId alarmId = new AlarmId(entityId); UpdateMsgType msgType = getUpdateMsgType(actionType); switch (actionType) { - case ADDED: - case UPDATED: - case ALARM_ACK: - case ALARM_CLEAR: + case ADDED, UPDATED, ALARM_ACK, ALARM_CLEAR -> { Alarm alarm = alarmService.findAlarmById(tenantId, alarmId); if (alarm != null) { return ((AlarmMsgConstructor) alarmMsgConstructorFactory.getMsgConstructorByEdgeVersion(edgeVersion)) .constructAlarmUpdatedMsg(msgType, alarm, findOriginatorEntityName(tenantId, alarm)); } - break; - case ALARM_DELETE: - case DELETED: + } + case ALARM_DELETE, DELETED -> { Alarm deletedAlarm = JacksonUtil.convertValue(body, Alarm.class); if (deletedAlarm != null) { return ((AlarmMsgConstructor) alarmMsgConstructorFactory.getMsgConstructorByEdgeVersion(edgeVersion)) .constructAlarmUpdatedMsg(msgType, deletedAlarm, findOriginatorEntityName(tenantId, deletedAlarm)); } + } } return null; } @@ -168,25 +165,26 @@ public abstract class BaseAlarmProcessor extends BaseEdgeProcessor { private String findOriginatorEntityName(TenantId tenantId, Alarm alarm) { String entityName = null; switch (alarm.getOriginator().getEntityType()) { - case DEVICE: + case DEVICE -> { Device deviceById = deviceService.findDeviceById(tenantId, new DeviceId(alarm.getOriginator().getId())); if (deviceById != null) { entityName = deviceById.getName(); } - break; - case ASSET: + } + case ASSET -> { Asset assetById = assetService.findAssetById(tenantId, new AssetId(alarm.getOriginator().getId())); if (assetById != null) { entityName = assetById.getName(); } - break; - case ENTITY_VIEW: + } + case ENTITY_VIEW -> { EntityView entityViewById = entityViewService.findEntityViewById(tenantId, new EntityViewId(alarm.getOriginator().getId())); if (entityViewById != null) { entityName = entityViewById.getName(); } - break; + } } return entityName; } + } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/asset/AssetEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/asset/AssetEdgeProcessor.java index d8122e1b6c..deac6fbc1f 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/asset/AssetEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/asset/AssetEdgeProcessor.java @@ -97,7 +97,7 @@ public abstract class AssetEdgeProcessor extends BaseAssetProcessor implements A Asset asset = assetService.findAssetById(tenantId, assetId); String assetAsString = JacksonUtil.toString(asset); TbMsgMetaData msgMetaData = getEdgeActionTbMsgMetaData(edge, asset.getCustomerId()); - pushEntityEventToRuleEngine(tenantId, assetId, asset.getCustomerId(), TbMsgType.ENTITY_CREATED, assetAsString, msgMetaData); + pushEntityEventToRuleEngine(tenantId, assetId, asset.getCustomerId(), assetAsString, msgMetaData); } catch (Exception e) { log.warn("[{}][{}] Failed to push asset action to rule engine: {}", tenantId, assetId, TbMsgType.ENTITY_CREATED.name(), e); } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/asset/AssetEdgeProcessorV1.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/asset/AssetEdgeProcessorV1.java index e84b37cc14..8cd2de1cab 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/asset/AssetEdgeProcessorV1.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/asset/AssetEdgeProcessorV1.java @@ -56,4 +56,5 @@ public class AssetEdgeProcessorV1 extends AssetEdgeProcessor { CustomerId customerUUID = safeGetCustomerId(assetUpdateMsg.getCustomerIdMSB(), assetUpdateMsg.getCustomerIdLSB()); asset.setCustomerId(customerUUID != null ? customerUUID : customerId); } + } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/asset/AssetEdgeProcessorV2.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/asset/AssetEdgeProcessorV2.java index a8c558b88e..021507f5d7 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/asset/AssetEdgeProcessorV2.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/asset/AssetEdgeProcessorV2.java @@ -40,4 +40,5 @@ public class AssetEdgeProcessorV2 extends AssetEdgeProcessor { CustomerId customerUUID = asset.getCustomerId() != null ? asset.getCustomerId() : customerId; asset.setCustomerId(customerUUID); } + } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/asset/AssetProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/asset/AssetProcessor.java index 4d2f6ff52b..2685274bbf 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/asset/AssetProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/asset/AssetProcessor.java @@ -30,4 +30,5 @@ public interface AssetProcessor extends EdgeProcessor { ListenableFuture processAssetMsgFromEdge(TenantId tenantId, Edge edge, AssetUpdateMsg assetUpdateMsg); DownlinkMsg convertAssetEventToDownlink(EdgeEvent edgeEvent, EdgeId edgeId, EdgeVersion edgeVersion); + } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/asset/BaseAssetProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/asset/BaseAssetProcessor.java index e84ffb730e..2588e52d5f 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/asset/BaseAssetProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/asset/BaseAssetProcessor.java @@ -72,4 +72,5 @@ public abstract class BaseAssetProcessor extends BaseEdgeProcessor { protected abstract Asset constructAssetFromUpdateMsg(TenantId tenantId, AssetId assetId, AssetUpdateMsg assetUpdateMsg); protected abstract void setCustomerId(TenantId tenantId, CustomerId customerId, Asset asset, AssetUpdateMsg assetUpdateMsg); + } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/asset/profile/AssetProfileEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/asset/profile/AssetProfileEdgeProcessor.java index 9678dbcbad..bcafaf740e 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/asset/profile/AssetProfileEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/asset/profile/AssetProfileEdgeProcessor.java @@ -86,7 +86,7 @@ public abstract class AssetProfileEdgeProcessor extends BaseAssetProfileProcesso AssetProfile assetProfile = assetProfileService.findAssetProfileById(tenantId, assetProfileId); String assetProfileAsString = JacksonUtil.toString(assetProfile); TbMsgMetaData msgMetaData = getEdgeActionTbMsgMetaData(edge, null); - pushEntityEventToRuleEngine(tenantId, assetProfileId, null, TbMsgType.ENTITY_CREATED, assetProfileAsString, msgMetaData); + pushEntityEventToRuleEngine(tenantId, assetProfileId, null, assetProfileAsString, msgMetaData); } catch (Exception e) { log.warn("[{}][{}] Failed to push asset profile action to rule engine: {}", tenantId, assetProfileId, TbMsgType.ENTITY_CREATED.name(), e); } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/customer/CustomerEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/customer/CustomerEdgeProcessor.java index f6403629eb..e13c4c487f 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/customer/CustomerEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/customer/CustomerEdgeProcessor.java @@ -52,8 +52,7 @@ public class CustomerEdgeProcessor extends BaseEdgeProcessor { CustomerId customerId = new CustomerId(edgeEvent.getEntityId()); DownlinkMsg downlinkMsg = null; switch (edgeEvent.getAction()) { - case ADDED: - case UPDATED: + case ADDED, UPDATED -> { Customer customer = customerService.findCustomerById(edgeEvent.getTenantId(), customerId); if (customer != null) { UpdateMsgType msgType = getUpdateMsgType(edgeEvent.getAction()); @@ -64,15 +63,15 @@ public class CustomerEdgeProcessor extends BaseEdgeProcessor { .addCustomerUpdateMsg(customerUpdateMsg) .build(); } - break; - case DELETED: + } + case DELETED -> { CustomerUpdateMsg customerUpdateMsg = ((CustomerMsgConstructor) customerMsgConstructorFactory.getMsgConstructorByEdgeVersion(edgeVersion)).constructCustomerDeleteMsg(customerId); downlinkMsg = DownlinkMsg.newBuilder() .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) .addCustomerUpdateMsg(customerUpdateMsg) .build(); - break; + } } return downlinkMsg; } @@ -97,4 +96,5 @@ public class CustomerEdgeProcessor extends BaseEdgeProcessor { return Futures.immediateFuture(null); } } + } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/dashboard/DashboardEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/dashboard/DashboardEdgeProcessor.java index f50565b177..acf312504b 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/dashboard/DashboardEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/dashboard/DashboardEdgeProcessor.java @@ -90,7 +90,7 @@ public abstract class DashboardEdgeProcessor extends BaseDashboardProcessor impl Dashboard dashboard = dashboardService.findDashboardById(tenantId, dashboardId); String dashboardAsString = JacksonUtil.toString(dashboard); TbMsgMetaData msgMetaData = getEdgeActionTbMsgMetaData(edge, null); - pushEntityEventToRuleEngine(tenantId, dashboardId, null, TbMsgType.ENTITY_CREATED, dashboardAsString, msgMetaData); + pushEntityEventToRuleEngine(tenantId, dashboardId, null, dashboardAsString, msgMetaData); } catch (Exception e) { log.warn("[{}][{}] Failed to push dashboard action to rule engine: {}", tenantId, dashboardId, TbMsgType.ENTITY_CREATED.name(), e); } 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 6981ad5ff3..27f86a34b9 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 @@ -125,7 +125,7 @@ public abstract class DeviceEdgeProcessor extends BaseDeviceProcessor implements Device device = deviceService.findDeviceById(tenantId, deviceId); String deviceAsString = JacksonUtil.toString(device); TbMsgMetaData msgMetaData = getEdgeActionTbMsgMetaData(edge, device.getCustomerId()); - pushEntityEventToRuleEngine(tenantId, deviceId, device.getCustomerId(), TbMsgType.ENTITY_CREATED, deviceAsString, msgMetaData); + pushEntityEventToRuleEngine(tenantId, deviceId, device.getCustomerId(), deviceAsString, msgMetaData); } catch (Exception e) { log.warn("[{}][{}] Failed to push device action to rule engine: {}", tenantId, deviceId, TbMsgType.ENTITY_CREATED.name(), e); } @@ -286,4 +286,5 @@ public abstract class DeviceEdgeProcessor extends BaseDeviceProcessor implements .addDeviceCredentialsRequestMsg(deviceCredentialsRequestMsg); return builder.build(); } + } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/profile/DeviceProfileEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/profile/DeviceProfileEdgeProcessor.java index 23ad24bb62..2f1a60be1a 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/profile/DeviceProfileEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/profile/DeviceProfileEdgeProcessor.java @@ -86,7 +86,7 @@ public abstract class DeviceProfileEdgeProcessor extends BaseDeviceProfileProces DeviceProfile deviceProfile = deviceProfileService.findDeviceProfileById(tenantId, deviceProfileId); String deviceProfileAsString = JacksonUtil.toString(deviceProfile); TbMsgMetaData msgMetaData = getEdgeActionTbMsgMetaData(edge, null); - pushEntityEventToRuleEngine(tenantId, deviceProfileId, null, TbMsgType.ENTITY_CREATED, deviceProfileAsString, msgMetaData); + pushEntityEventToRuleEngine(tenantId, deviceProfileId, null, deviceProfileAsString, msgMetaData); } catch (Exception e) { log.warn("[{}][{}] Failed to push device profile action to rule engine: {}", tenantId, deviceProfileId, TbMsgType.ENTITY_CREATED.name(), e); } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/edge/EdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/edge/EdgeProcessor.java index e4a9189aad..7bb631b330 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/edge/EdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/edge/EdgeProcessor.java @@ -50,8 +50,7 @@ public class EdgeProcessor extends BaseEdgeProcessor { EdgeId edgeId = new EdgeId(edgeEvent.getEntityId()); DownlinkMsg downlinkMsg = null; switch (edgeEvent.getAction()) { - case ASSIGNED_TO_CUSTOMER: - case UNASSIGNED_FROM_CUSTOMER: + case ASSIGNED_TO_CUSTOMER, UNASSIGNED_FROM_CUSTOMER -> { Edge edge = edgeService.findEdgeById(edgeEvent.getTenantId(), edgeId); if (edge != null) { EdgeConfiguration edgeConfigMsg = @@ -61,7 +60,7 @@ public class EdgeProcessor extends BaseEdgeProcessor { .setEdgeConfiguration(edgeConfigMsg) .build(); } - break; + } } return downlinkMsg; } @@ -112,4 +111,5 @@ public class EdgeProcessor extends BaseEdgeProcessor { return Futures.immediateFailedFuture(e); } } + } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/entityview/EntityViewEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/entityview/EntityViewEdgeProcessor.java index c85eb95e98..125b50f60c 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/entityview/EntityViewEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/entityview/EntityViewEdgeProcessor.java @@ -95,7 +95,7 @@ public abstract class EntityViewEdgeProcessor extends BaseEntityViewProcessor im EntityView entityView = entityViewService.findEntityViewById(tenantId, entityViewId); String entityViewAsString = JacksonUtil.toString(entityView); TbMsgMetaData msgMetaData = getEdgeActionTbMsgMetaData(edge, entityView.getCustomerId()); - pushEntityEventToRuleEngine(tenantId, entityViewId, entityView.getCustomerId(), TbMsgType.ENTITY_CREATED, entityViewAsString, msgMetaData); + pushEntityEventToRuleEngine(tenantId, entityViewId, entityView.getCustomerId(), entityViewAsString, msgMetaData); } catch (Exception e) { log.warn("[{}][{}] Failed to push entity view action to rule engine: {}", tenantId, entityViewId, TbMsgType.ENTITY_CREATED.name(), e); } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/entityview/EntityViewProcessorV1.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/entityview/EntityViewProcessorV1.java index 3d1171cbfd..acb8877c03 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/entityview/EntityViewProcessorV1.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/entityview/EntityViewProcessorV1.java @@ -62,4 +62,5 @@ public class EntityViewProcessorV1 extends EntityViewEdgeProcessor { CustomerId customerUUID = safeGetCustomerId(entityViewUpdateMsg.getCustomerIdMSB(), entityViewUpdateMsg.getCustomerIdLSB()); entityView.setCustomerId(customerUUID != null ? customerUUID : customerId); } + } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/entityview/EntityViewProcessorV2.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/entityview/EntityViewProcessorV2.java index 72356276cf..fa4177552e 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/entityview/EntityViewProcessorV2.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/entityview/EntityViewProcessorV2.java @@ -40,4 +40,5 @@ public class EntityViewProcessorV2 extends EntityViewEdgeProcessor { CustomerId customerUUID = entityView.getCustomerId() != null ? entityView.getCustomerId() : customerId; entityView.setCustomerId(customerUUID); } + } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/notification/NotificationEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/notification/NotificationEdgeProcessor.java new file mode 100644 index 0000000000..6af039341f --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/notification/NotificationEdgeProcessor.java @@ -0,0 +1,119 @@ +/** + * 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.service.edge.rpc.processor.notification; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import org.thingsboard.server.common.data.EdgeUtils; +import org.thingsboard.server.common.data.edge.EdgeEvent; +import org.thingsboard.server.common.data.id.NotificationRuleId; +import org.thingsboard.server.common.data.id.NotificationTargetId; +import org.thingsboard.server.common.data.id.NotificationTemplateId; +import org.thingsboard.server.common.data.notification.rule.NotificationRule; +import org.thingsboard.server.common.data.notification.targets.NotificationTarget; +import org.thingsboard.server.common.data.notification.template.NotificationTemplate; +import org.thingsboard.server.gen.edge.v1.DownlinkMsg; +import org.thingsboard.server.gen.edge.v1.NotificationRuleUpdateMsg; +import org.thingsboard.server.gen.edge.v1.NotificationTargetUpdateMsg; +import org.thingsboard.server.gen.edge.v1.NotificationTemplateUpdateMsg; +import org.thingsboard.server.gen.edge.v1.UpdateMsgType; +import org.thingsboard.server.queue.util.TbCoreComponent; +import org.thingsboard.server.service.edge.rpc.processor.BaseEdgeProcessor; + +@Slf4j +@Component +@TbCoreComponent +public class NotificationEdgeProcessor extends BaseEdgeProcessor { + + public DownlinkMsg convertNotificationRuleToDownlink(EdgeEvent edgeEvent) { + NotificationRuleId notificationRuleId = new NotificationRuleId(edgeEvent.getEntityId()); + DownlinkMsg downlinkMsg = null; + switch (edgeEvent.getAction()) { + case ADDED, UPDATED -> { + NotificationRule notificationRule = notificationRuleService.findNotificationRuleById(edgeEvent.getTenantId(), notificationRuleId); + if (notificationRule != null) { + UpdateMsgType msgType = getUpdateMsgType(edgeEvent.getAction()); + NotificationRuleUpdateMsg notificationRuleUpdateMsg = notificationMsgConstructor.constructNotificationRuleUpdateMsg(msgType, notificationRule); + downlinkMsg = DownlinkMsg.newBuilder() + .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) + .addNotificationRuleUpdateMsg(notificationRuleUpdateMsg) + .build(); + } + } + case DELETED -> { + NotificationRuleUpdateMsg notificationRuleUpdateMsg = notificationMsgConstructor.constructNotificationRuleDeleteMsg(notificationRuleId); + downlinkMsg = DownlinkMsg.newBuilder() + .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) + .addNotificationRuleUpdateMsg(notificationRuleUpdateMsg) + .build(); + } + } + return downlinkMsg; + } + + public DownlinkMsg convertNotificationTargetToDownlink(EdgeEvent edgeEvent) { + NotificationTargetId notificationTargetId = new NotificationTargetId(edgeEvent.getEntityId()); + DownlinkMsg downlinkMsg = null; + switch (edgeEvent.getAction()) { + case ADDED, UPDATED -> { + NotificationTarget notificationTarget = notificationTargetService.findNotificationTargetById(edgeEvent.getTenantId(), notificationTargetId); + if (notificationTarget != null) { + UpdateMsgType msgType = getUpdateMsgType(edgeEvent.getAction()); + NotificationTargetUpdateMsg notificationTargetUpdateMsg = notificationMsgConstructor.constructNotificationTargetUpdateMsg(msgType, notificationTarget); + downlinkMsg = DownlinkMsg.newBuilder() + .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) + .addNotificationTargetUpdateMsg(notificationTargetUpdateMsg) + .build(); + } + } + case DELETED -> { + NotificationTargetUpdateMsg notificationTargetUpdateMsg = notificationMsgConstructor.constructNotificationTargetDeleteMsg(notificationTargetId); + downlinkMsg = DownlinkMsg.newBuilder() + .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) + .addNotificationTargetUpdateMsg(notificationTargetUpdateMsg) + .build(); + } + } + return downlinkMsg; + } + + public DownlinkMsg convertNotificationTemplateToDownlink(EdgeEvent edgeEvent) { + NotificationTemplateId notificationTemplateId = new NotificationTemplateId(edgeEvent.getEntityId()); + DownlinkMsg downlinkMsg = null; + switch (edgeEvent.getAction()) { + case ADDED, UPDATED -> { + NotificationTemplate notificationTemplate = notificationTemplateService.findNotificationTemplateById(edgeEvent.getTenantId(), notificationTemplateId); + if (notificationTemplate != null) { + UpdateMsgType msgType = getUpdateMsgType(edgeEvent.getAction()); + NotificationTemplateUpdateMsg notificationTemplateUpdateMsg = notificationMsgConstructor.constructNotificationTemplateUpdateMsg(msgType, notificationTemplate); + downlinkMsg = DownlinkMsg.newBuilder() + .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) + .addNotificationTemplateUpdateMsg(notificationTemplateUpdateMsg) + .build(); + } + } + case DELETED -> { + NotificationTemplateUpdateMsg notificationTemplateUpdateMsg = notificationMsgConstructor.constructNotificationTemplateDeleteMsg(notificationTemplateId); + downlinkMsg = DownlinkMsg.newBuilder() + .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) + .addNotificationTemplateUpdateMsg(notificationTemplateUpdateMsg) + .build(); + } + } + return downlinkMsg; + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/ota/OtaPackageEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/ota/OtaPackageEdgeProcessor.java index e5c9ed1779..34a7724fee 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/ota/OtaPackageEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/ota/OtaPackageEdgeProcessor.java @@ -38,8 +38,7 @@ public class OtaPackageEdgeProcessor extends BaseEdgeProcessor { OtaPackageId otaPackageId = new OtaPackageId(edgeEvent.getEntityId()); DownlinkMsg downlinkMsg = null; switch (edgeEvent.getAction()) { - case ADDED: - case UPDATED: + case ADDED, UPDATED -> { OtaPackage otaPackage = otaPackageService.findOtaPackageById(edgeEvent.getTenantId(), otaPackageId); if (otaPackage != null) { UpdateMsgType msgType = getUpdateMsgType(edgeEvent.getAction()); @@ -50,16 +49,17 @@ public class OtaPackageEdgeProcessor extends BaseEdgeProcessor { .addOtaPackageUpdateMsg(otaPackageUpdateMsg) .build(); } - break; - case DELETED: + } + case DELETED -> { OtaPackageUpdateMsg otaPackageUpdateMsg = ((OtaPackageMsgConstructor) otaPackageMsgConstructorFactory.getMsgConstructorByEdgeVersion(edgeVersion)).constructOtaPackageDeleteMsg(otaPackageId); downlinkMsg = DownlinkMsg.newBuilder() .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) .addOtaPackageUpdateMsg(otaPackageUpdateMsg) .build(); - break; + } } return downlinkMsg; } + } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/queue/QueueEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/queue/QueueEdgeProcessor.java index d529b44850..bfa81361fa 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/queue/QueueEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/queue/QueueEdgeProcessor.java @@ -38,8 +38,7 @@ public class QueueEdgeProcessor extends BaseEdgeProcessor { QueueId queueId = new QueueId(edgeEvent.getEntityId()); DownlinkMsg downlinkMsg = null; switch (edgeEvent.getAction()) { - case ADDED: - case UPDATED: + case ADDED, UPDATED -> { Queue queue = queueService.findQueueById(edgeEvent.getTenantId(), queueId); if (queue != null) { UpdateMsgType msgType = getUpdateMsgType(edgeEvent.getAction()); @@ -50,16 +49,17 @@ public class QueueEdgeProcessor extends BaseEdgeProcessor { .addQueueUpdateMsg(queueUpdateMsg) .build(); } - break; - case DELETED: + } + case DELETED -> { QueueUpdateMsg queueDeleteMsg = ((QueueMsgConstructor) queueMsgConstructorFactory.getMsgConstructorByEdgeVersion(edgeVersion)).constructQueueDeleteMsg(queueId); downlinkMsg = DownlinkMsg.newBuilder() .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) .addQueueUpdateMsg(queueDeleteMsg) .build(); - break; + } } return downlinkMsg; } + } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/relation/BaseRelationProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/relation/BaseRelationProcessor.java index 045e4148e6..a832f0b1e1 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/relation/BaseRelationProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/relation/BaseRelationProcessor.java @@ -58,4 +58,5 @@ public abstract class BaseRelationProcessor extends BaseEdgeProcessor { } protected abstract EntityRelation constructEntityRelationFromUpdateMsg(RelationUpdateMsg relationUpdateMsg); + } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/relation/RelationEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/relation/RelationEdgeProcessor.java index 4c2ba3d04f..780550ed95 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/relation/RelationEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/relation/RelationEdgeProcessor.java @@ -91,4 +91,5 @@ public abstract class RelationEdgeProcessor extends BaseRelationProcessor implem } return Futures.transform(Futures.allAsList(futures), voids -> null, dbCallbackExecutorService); } + } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/relation/RelationEdgeProcessorV1.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/relation/RelationEdgeProcessorV1.java index 10bdd19acb..647cec120a 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/relation/RelationEdgeProcessorV1.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/relation/RelationEdgeProcessorV1.java @@ -49,4 +49,5 @@ public class RelationEdgeProcessorV1 extends RelationEdgeProcessor { entityRelation.setAdditionalInfo(JacksonUtil.toJsonNode(relationUpdateMsg.getAdditionalInfo())); return entityRelation; } + } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/relation/RelationEdgeProcessorV2.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/relation/RelationEdgeProcessorV2.java index 13e2c40f4f..7b31da421d 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/relation/RelationEdgeProcessorV2.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/relation/RelationEdgeProcessorV2.java @@ -31,4 +31,5 @@ public class RelationEdgeProcessorV2 extends RelationEdgeProcessor { protected EntityRelation constructEntityRelationFromUpdateMsg(RelationUpdateMsg relationUpdateMsg) { return JacksonUtil.fromString(relationUpdateMsg.getEntity(), EntityRelation.class, true); } + } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/relation/RelationProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/relation/RelationProcessor.java index f48fbefff8..82a893e9ba 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/relation/RelationProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/relation/RelationProcessor.java @@ -29,4 +29,5 @@ public interface RelationProcessor extends EdgeProcessor { ListenableFuture processRelationMsgFromEdge(TenantId tenantId, Edge edge, RelationUpdateMsg relationUpdateMsg); DownlinkMsg convertRelationEventToDownlink(EdgeEvent edgeEvent, EdgeVersion edgeVersion); + } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/resource/BaseResourceProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/resource/BaseResourceProcessor.java index 1b55658183..7384130baf 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/resource/BaseResourceProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/resource/BaseResourceProcessor.java @@ -72,4 +72,5 @@ public abstract class BaseResourceProcessor extends BaseEdgeProcessor { } protected abstract TbResource constructResourceFromUpdateMsg(TenantId tenantId, TbResourceId tbResourceId, ResourceUpdateMsg resourceUpdateMsg); + } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/resource/ResourceEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/resource/ResourceEdgeProcessor.java index b67ad78888..0a83e439b5 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/resource/ResourceEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/resource/ResourceEdgeProcessor.java @@ -74,8 +74,7 @@ public abstract class ResourceEdgeProcessor extends BaseResourceProcessor implem TbResourceId tbResourceId = new TbResourceId(edgeEvent.getEntityId()); DownlinkMsg downlinkMsg = null; switch (edgeEvent.getAction()) { - case ADDED: - case UPDATED: + case ADDED, UPDATED -> { TbResource tbResource = resourceService.findResourceById(edgeEvent.getTenantId(), tbResourceId); if (tbResource != null) { UpdateMsgType msgType = getUpdateMsgType(edgeEvent.getAction()); @@ -86,16 +85,17 @@ public abstract class ResourceEdgeProcessor extends BaseResourceProcessor implem .addResourceUpdateMsg(resourceUpdateMsg) .build() : null; } - break; - case DELETED: + } + case DELETED -> { ResourceUpdateMsg resourceUpdateMsg = ((ResourceMsgConstructor) resourceMsgConstructorFactory.getMsgConstructorByEdgeVersion(edgeVersion)).constructResourceDeleteMsg(tbResourceId); downlinkMsg = DownlinkMsg.newBuilder() .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) .addResourceUpdateMsg(resourceUpdateMsg) .build(); - break; + } } return downlinkMsg; } + } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/resource/ResourceEdgeProcessorV1.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/resource/ResourceEdgeProcessorV1.java index 0dd87df856..f272d447a4 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/resource/ResourceEdgeProcessorV1.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/resource/ResourceEdgeProcessorV1.java @@ -45,4 +45,5 @@ public class ResourceEdgeProcessorV1 extends ResourceEdgeProcessor { resource.setEtag(resourceUpdateMsg.hasEtag() ? resourceUpdateMsg.getEtag() : null); return resource; } + } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/resource/ResourceEdgeProcessorV2.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/resource/ResourceEdgeProcessorV2.java index ccbc6eec95..108a6c8ecb 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/resource/ResourceEdgeProcessorV2.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/resource/ResourceEdgeProcessorV2.java @@ -33,4 +33,5 @@ public class ResourceEdgeProcessorV2 extends ResourceEdgeProcessor { protected TbResource constructResourceFromUpdateMsg(TenantId tenantId, TbResourceId tbResourceId, ResourceUpdateMsg resourceUpdateMsg) { return JacksonUtil.fromString(resourceUpdateMsg.getEntity(), TbResource.class, true); } + } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/resource/ResourceProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/resource/ResourceProcessor.java index 7b8d777313..a52d31a98b 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/resource/ResourceProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/resource/ResourceProcessor.java @@ -29,4 +29,5 @@ public interface ResourceProcessor extends EdgeProcessor { ListenableFuture processResourceMsgFromEdge(TenantId tenantId, Edge edge, ResourceUpdateMsg resourceUpdateMsg); DownlinkMsg convertResourceEventToDownlink(EdgeEvent edgeEvent, EdgeVersion edgeVersion); + } 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 118cc0ccd4..30f74ae42d 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 @@ -43,16 +43,15 @@ public class RuleChainEdgeProcessor extends BaseEdgeProcessor { RuleChainId ruleChainId = new RuleChainId(edgeEvent.getEntityId()); DownlinkMsg downlinkMsg = null; switch (edgeEvent.getAction()) { - case ADDED: - case UPDATED: - case ASSIGNED_TO_EDGE: + case ADDED, UPDATED, ASSIGNED_TO_EDGE -> { RuleChain ruleChain = ruleChainService.findRuleChainById(edgeEvent.getTenantId(), ruleChainId); if (ruleChain != null) { boolean isRoot = false; if (edgeEvent.getBody() != null && edgeEvent.getBody().get(EDGE_IS_ROOT_BODY_KEY) != null) { try { isRoot = Boolean.parseBoolean(edgeEvent.getBody().get(EDGE_IS_ROOT_BODY_KEY).asText()); - } catch (Exception ignored) {} + } catch (Exception ignored) { + } } if (!isRoot) { Edge edge = edgeService.findEdgeById(edgeEvent.getTenantId(), edgeEvent.getEdgeId()); @@ -67,15 +66,12 @@ public class RuleChainEdgeProcessor extends BaseEdgeProcessor { .addRuleChainUpdateMsg(ruleChainUpdateMsg) .build(); } - break; - case DELETED: - case UNASSIGNED_FROM_EDGE: - downlinkMsg = DownlinkMsg.newBuilder() - .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) - .addRuleChainUpdateMsg(((RuleChainMsgConstructor) ruleChainMsgConstructorFactory.getMsgConstructorByEdgeVersion(edgeVersion)) - .constructRuleChainDeleteMsg(ruleChainId)) - .build(); - break; + } + case DELETED, UNASSIGNED_FROM_EDGE -> downlinkMsg = DownlinkMsg.newBuilder() + .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) + .addRuleChainUpdateMsg(((RuleChainMsgConstructor) ruleChainMsgConstructorFactory.getMsgConstructorByEdgeVersion(edgeVersion)) + .constructRuleChainDeleteMsg(ruleChainId)) + .build(); } return downlinkMsg; } @@ -99,4 +95,5 @@ public class RuleChainEdgeProcessor extends BaseEdgeProcessor { } return downlinkMsg; } + } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/settings/AdminSettingsEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/settings/AdminSettingsEdgeProcessor.java index 1f2c767010..d9b0a85056 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/settings/AdminSettingsEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/settings/AdminSettingsEdgeProcessor.java @@ -45,4 +45,5 @@ public class AdminSettingsEdgeProcessor extends BaseEdgeProcessor { .addAdminSettingsUpdateMsg(adminSettingsUpdateMsg) .build(); } + } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/telemetry/BaseTelemetryProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/telemetry/BaseTelemetryProcessor.java index ae8c851a85..2656ef622f 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/telemetry/BaseTelemetryProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/telemetry/BaseTelemetryProcessor.java @@ -138,41 +138,39 @@ public abstract class BaseTelemetryProcessor extends BaseEdgeProcessor { TbMsgMetaData metaData = new TbMsgMetaData(); CustomerId customerId = null; switch (entityId.getEntityType()) { - case DEVICE: + case DEVICE -> { Device device = deviceService.findDeviceById(tenantId, new DeviceId(entityId.getId())); if (device != null) { customerId = device.getCustomerId(); metaData.putValue("deviceName", device.getName()); metaData.putValue("deviceType", device.getType()); } - break; - case ASSET: + } + case ASSET -> { Asset asset = assetService.findAssetById(tenantId, new AssetId(entityId.getId())); if (asset != null) { customerId = asset.getCustomerId(); metaData.putValue("assetName", asset.getName()); metaData.putValue("assetType", asset.getType()); } - break; - case ENTITY_VIEW: + } + case ENTITY_VIEW -> { EntityView entityView = entityViewService.findEntityViewById(tenantId, new EntityViewId(entityId.getId())); if (entityView != null) { customerId = entityView.getCustomerId(); metaData.putValue("entityViewName", entityView.getName()); metaData.putValue("entityViewType", entityView.getType()); } - break; - case EDGE: + } + case EDGE -> { Edge edge = edgeService.findEdgeById(tenantId, new EdgeId(entityId.getId())); if (edge != null) { customerId = edge.getCustomerId(); metaData.putValue("edgeName", edge.getName()); metaData.putValue("edgeType", edge.getType()); } - break; - default: - log.debug("[{}] Using empty metadata for entityId [{}]", tenantId, entityId); - break; + } + default -> log.debug("[{}] Using empty metadata for entityId [{}]", tenantId, entityId); } return new ImmutablePair<>(metaData, customerId != null ? customerId : new CustomerId(ModelConstants.NULL_UUID)); } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/tenant/TenantProfileEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/tenant/TenantProfileEdgeProcessor.java index 6b97fbcb37..6c3bc1733f 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/tenant/TenantProfileEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/tenant/TenantProfileEdgeProcessor.java @@ -53,4 +53,5 @@ public class TenantProfileEdgeProcessor extends BaseEdgeProcessor { } return downlinkMsg; } + } 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 be3d8308a6..607c5e665c 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 @@ -39,8 +39,7 @@ public class UserEdgeProcessor extends BaseEdgeProcessor { UserId userId = new UserId(edgeEvent.getEntityId()); DownlinkMsg downlinkMsg = null; switch (edgeEvent.getAction()) { - case ADDED: - case UPDATED: + case ADDED, UPDATED -> { User user = userService.findUserById(edgeEvent.getTenantId(), userId); if (user != null) { UpdateMsgType msgType = getUpdateMsgType(edgeEvent.getAction()); @@ -49,14 +48,12 @@ public class UserEdgeProcessor extends BaseEdgeProcessor { .addUserUpdateMsg(((UserMsgConstructor) userMsgConstructorFactory.getMsgConstructorByEdgeVersion(edgeVersion)).constructUserUpdatedMsg(msgType, user)) .build(); } - break; - case DELETED: - downlinkMsg = DownlinkMsg.newBuilder() - .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) - .addUserUpdateMsg(((UserMsgConstructor) userMsgConstructorFactory.getMsgConstructorByEdgeVersion(edgeVersion)).constructUserDeleteMsg(userId)) - .build(); - break; - case CREDENTIALS_UPDATED: + } + case DELETED -> downlinkMsg = DownlinkMsg.newBuilder() + .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) + .addUserUpdateMsg(((UserMsgConstructor) userMsgConstructorFactory.getMsgConstructorByEdgeVersion(edgeVersion)).constructUserDeleteMsg(userId)) + .build(); + case CREDENTIALS_UPDATED -> { UserCredentials userCredentialsByUserId = userService.findUserCredentialsByUserId(edgeEvent.getTenantId(), userId); if (userCredentialsByUserId != null && userCredentialsByUserId.isEnabled()) { UserCredentialsUpdateMsg userCredentialsUpdateMsg = @@ -66,8 +63,9 @@ public class UserEdgeProcessor extends BaseEdgeProcessor { .addUserCredentialsUpdateMsg(userCredentialsUpdateMsg) .build(); } - break; + } } return downlinkMsg; } + } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/widget/WidgetBundleEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/widget/WidgetBundleEdgeProcessor.java index 4f1aa9afcc..662b3677cc 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/widget/WidgetBundleEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/widget/WidgetBundleEdgeProcessor.java @@ -40,8 +40,7 @@ public class WidgetBundleEdgeProcessor extends BaseEdgeProcessor { WidgetsBundleId widgetsBundleId = new WidgetsBundleId(edgeEvent.getEntityId()); DownlinkMsg downlinkMsg = null; switch (edgeEvent.getAction()) { - case ADDED: - case UPDATED: + case ADDED, UPDATED -> { WidgetsBundle widgetsBundle = widgetsBundleService.findWidgetsBundleById(edgeEvent.getTenantId(), widgetsBundleId); if (widgetsBundle != null) { List widgets = widgetTypeService.findWidgetFqnsByWidgetsBundleId(edgeEvent.getTenantId(), widgetsBundleId); @@ -53,16 +52,17 @@ public class WidgetBundleEdgeProcessor extends BaseEdgeProcessor { .addWidgetsBundleUpdateMsg(widgetsBundleUpdateMsg) .build(); } - break; - case DELETED: + } + case DELETED -> { WidgetsBundleUpdateMsg widgetsBundleUpdateMsg = ((WidgetMsgConstructor) widgetMsgConstructorFactory.getMsgConstructorByEdgeVersion(edgeVersion)).constructWidgetsBundleDeleteMsg(widgetsBundleId); downlinkMsg = DownlinkMsg.newBuilder() .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) .addWidgetsBundleUpdateMsg(widgetsBundleUpdateMsg) .build(); - break; + } } return downlinkMsg; } + } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/widget/WidgetTypeEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/widget/WidgetTypeEdgeProcessor.java index 552ae47f43..c1891b8fdb 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/widget/WidgetTypeEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/widget/WidgetTypeEdgeProcessor.java @@ -38,8 +38,7 @@ public class WidgetTypeEdgeProcessor extends BaseEdgeProcessor { WidgetTypeId widgetTypeId = new WidgetTypeId(edgeEvent.getEntityId()); DownlinkMsg downlinkMsg = null; switch (edgeEvent.getAction()) { - case ADDED: - case UPDATED: + case ADDED, UPDATED -> { WidgetTypeDetails widgetTypeDetails = widgetTypeService.findWidgetTypeDetailsById(edgeEvent.getTenantId(), widgetTypeId); if (widgetTypeDetails != null) { UpdateMsgType msgType = getUpdateMsgType(edgeEvent.getAction()); @@ -50,16 +49,17 @@ public class WidgetTypeEdgeProcessor extends BaseEdgeProcessor { .addWidgetTypeUpdateMsg(widgetTypeUpdateMsg) .build(); } - break; - case DELETED: + } + case DELETED -> { WidgetTypeUpdateMsg widgetTypeUpdateMsg = ((WidgetMsgConstructor) widgetMsgConstructorFactory.getMsgConstructorByEdgeVersion(edgeVersion)).constructWidgetTypeDeleteMsg(widgetTypeId); downlinkMsg = DownlinkMsg.newBuilder() .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) .addWidgetTypeUpdateMsg(widgetTypeUpdateMsg) .build(); - break; + } } return downlinkMsg; } + } 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 f76b9ec828..3f61734801 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 @@ -27,7 +27,6 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Service; import org.thingsboard.common.util.JacksonUtil; -import org.thingsboard.server.cluster.TbClusterService; import org.thingsboard.server.common.data.AttributeScope; import org.thingsboard.server.common.data.EdgeUtils; import org.thingsboard.server.common.data.EntityType; @@ -110,9 +109,6 @@ public class DefaultEdgeRequestsService implements EdgeRequestsService { @Autowired private DbCallbackExecutorService dbCallbackExecutorService; - @Autowired - private TbClusterService tbClusterService; - @Override public ListenableFuture processRuleChainMetadataRequestMsg(TenantId tenantId, Edge edge, RuleChainMetadataRequestMsg ruleChainMetadataRequestMsg) { log.trace("[{}] processRuleChainMetadataRequestMsg [{}][{}]", tenantId, edge.getName(), ruleChainMetadataRequestMsg); @@ -292,8 +288,7 @@ public class DefaultEdgeRequestsService implements EdgeRequestsService { return futureToSet; } - private ListenableFuture> findRelationByQuery(TenantId tenantId, Edge edge, - EntityId entityId, EntitySearchDirection direction) { + private ListenableFuture> findRelationByQuery(TenantId tenantId, Edge edge, EntityId entityId, EntitySearchDirection direction) { EntityRelationsQuery query = new EntityRelationsQuery(); query.setParameters(new RelationsSearchParameters(entityId, direction, 1, false)); return relationService.findByQuery(tenantId, query); diff --git a/application/src/main/java/org/thingsboard/server/service/notification/DefaultNotificationCenter.java b/application/src/main/java/org/thingsboard/server/service/notification/DefaultNotificationCenter.java index 362d0d971e..2f0ca02c9b 100644 --- a/application/src/main/java/org/thingsboard/server/service/notification/DefaultNotificationCenter.java +++ b/application/src/main/java/org/thingsboard/server/service/notification/DefaultNotificationCenter.java @@ -247,7 +247,7 @@ public class DefaultNotificationCenter extends AbstractSubscriptionService imple private void processForTarget(NotificationTarget target, NotificationProcessingContext ctx) { Iterable recipients; switch (target.getConfiguration().getType()) { - case PLATFORM_USERS: { + case PLATFORM_USERS -> { PlatformUsersNotificationTargetConfig targetConfig = (PlatformUsersNotificationTargetConfig) target.getConfiguration(); if (targetConfig.getUsersFilter().getType().isForRules() && ctx.getRequest().getInfo() instanceof RuleOriginatedNotificationInfo) { recipients = new PageDataIterable<>(pageLink -> { @@ -258,21 +258,16 @@ public class DefaultNotificationCenter extends AbstractSubscriptionService imple return notificationTargetService.findRecipientsForNotificationTargetConfig(ctx.getTenantId(), targetConfig, pageLink); }, 256); } - break; } - case SLACK: { + case SLACK -> { SlackNotificationTargetConfig targetConfig = (SlackNotificationTargetConfig) target.getConfiguration(); recipients = List.of(targetConfig.getConversation()); - break; } - case MICROSOFT_TEAMS: { + case MICROSOFT_TEAMS -> { MicrosoftTeamsNotificationTargetConfig targetConfig = (MicrosoftTeamsNotificationTargetConfig) target.getConfiguration(); recipients = List.of(targetConfig); - break; - } - default: { - recipients = Collections.emptyList(); } + default -> recipients = Collections.emptyList(); } Set deliveryMethods = new HashSet<>(ctx.getDeliveryMethods()); diff --git a/application/src/test/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessorTest.java b/application/src/test/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessorTest.java index f3ed8e12e6..9e4fd65ab4 100644 --- a/application/src/test/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessorTest.java +++ b/application/src/test/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessorTest.java @@ -46,6 +46,9 @@ import org.thingsboard.server.dao.edge.EdgeEventService; import org.thingsboard.server.dao.edge.EdgeService; import org.thingsboard.server.dao.edge.EdgeSynchronizationManager; import org.thingsboard.server.dao.entityview.EntityViewService; +import org.thingsboard.server.dao.notification.NotificationRuleService; +import org.thingsboard.server.dao.notification.NotificationTargetService; +import org.thingsboard.server.dao.notification.NotificationTemplateService; import org.thingsboard.server.dao.oauth2.OAuth2Service; import org.thingsboard.server.dao.ota.OtaPackageService; import org.thingsboard.server.dao.queue.QueueService; @@ -82,6 +85,7 @@ import org.thingsboard.server.service.edge.rpc.constructor.edge.EdgeMsgConstruct import org.thingsboard.server.service.edge.rpc.constructor.entityview.EntityViewMsgConstructorFactory; import org.thingsboard.server.service.edge.rpc.constructor.entityview.EntityViewMsgConstructorV1; import org.thingsboard.server.service.edge.rpc.constructor.entityview.EntityViewMsgConstructorV2; +import org.thingsboard.server.service.edge.rpc.constructor.notification.NotificationMsgConstructor; import org.thingsboard.server.service.edge.rpc.constructor.oauth2.OAuth2MsgConstructor; import org.thingsboard.server.service.edge.rpc.constructor.ota.OtaPackageMsgConstructorFactory; import org.thingsboard.server.service.edge.rpc.constructor.ota.OtaPackageMsgConstructorV1; @@ -130,6 +134,7 @@ import org.thingsboard.server.service.edge.rpc.processor.device.profile.DevicePr import org.thingsboard.server.service.edge.rpc.processor.entityview.EntityViewProcessorFactory; import org.thingsboard.server.service.edge.rpc.processor.entityview.EntityViewProcessorV1; import org.thingsboard.server.service.edge.rpc.processor.entityview.EntityViewProcessorV2; +import org.thingsboard.server.service.edge.rpc.processor.notification.NotificationEdgeProcessor; import org.thingsboard.server.service.edge.rpc.processor.oauth2.OAuth2EdgeProcessor; import org.thingsboard.server.service.edge.rpc.processor.relation.RelationEdgeProcessorFactory; import org.thingsboard.server.service.edge.rpc.processor.relation.RelationEdgeProcessorV1; @@ -197,6 +202,15 @@ public abstract class BaseEdgeProcessorTest { @MockBean protected UserService userService; + @MockBean + protected NotificationRuleService notificationRuleService; + + @MockBean + protected NotificationTargetService notificationTargetService; + + @MockBean + protected NotificationTemplateService notificationTemplateService; + @MockBean protected DeviceProfileService deviceProfileService; @@ -366,6 +380,9 @@ public abstract class BaseEdgeProcessorTest { @MockBean protected WidgetMsgConstructorV2 widgetMsgConstructorV2; + @MockBean + protected NotificationMsgConstructor notificationMsgConstructor; + @MockBean protected OAuth2MsgConstructor oAuth2MsgConstructor; @@ -429,6 +446,9 @@ public abstract class BaseEdgeProcessorTest { @MockBean protected OAuth2EdgeProcessor oAuth2EdgeProcessor; + @MockBean + protected NotificationEdgeProcessor notificationEdgeProcessor; + @SpyBean protected RuleChainMsgConstructorFactory ruleChainMsgConstructorFactory; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/edge/EdgeEventType.java b/common/data/src/main/java/org/thingsboard/server/common/data/edge/EdgeEventType.java index 783bb3ffd5..44a3ed8efe 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/edge/EdgeEventType.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/edge/EdgeEventType.java @@ -41,6 +41,9 @@ public enum EdgeEventType { ADMIN_SETTINGS(true, null), OTA_PACKAGE(true, EntityType.OTA_PACKAGE), QUEUE(true, EntityType.QUEUE), + NOTIFICATION_RULE (true, EntityType.NOTIFICATION_RULE), + NOTIFICATION_TARGET (true, EntityType.NOTIFICATION_TARGET), + NOTIFICATION_TEMPLATE (true, EntityType.NOTIFICATION_TEMPLATE), TB_RESOURCE(true, EntityType.TB_RESOURCE), OAUTH2(true, null); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityIdFactory.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityIdFactory.java index 7a9e4388f7..ad2df69349 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityIdFactory.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityIdFactory.java @@ -147,6 +147,12 @@ public class EntityIdFactory { return new QueueId(uuid); case TB_RESOURCE: return new TbResourceId(uuid); + case NOTIFICATION_RULE: + return new NotificationRuleId(uuid); + case NOTIFICATION_TARGET: + return new NotificationTargetId(uuid); + case NOTIFICATION_TEMPLATE: + return new NotificationTemplateId(uuid); } throw new IllegalArgumentException("EdgeEventType " + edgeEventType + " is not supported!"); } diff --git a/common/edge-api/src/main/proto/edge.proto b/common/edge-api/src/main/proto/edge.proto index 58959e4d19..c3b37846fb 100644 --- a/common/edge-api/src/main/proto/edge.proto +++ b/common/edge-api/src/main/proto/edge.proto @@ -472,6 +472,27 @@ message OAuth2UpdateMsg { string entity = 1; } +message NotificationRuleUpdateMsg { + UpdateMsgType msgType = 1; + int64 idMSB = 2; + int64 idLSB = 3; + optional string entity = 4; +} + +message NotificationTargetUpdateMsg { + UpdateMsgType msgType = 1; + int64 idMSB = 2; + int64 idLSB = 3; + optional string entity = 4; +} + +message NotificationTemplateUpdateMsg { + UpdateMsgType msgType = 1; + int64 idMSB = 2; + int64 idLSB = 3; + optional string entity = 4; +} + message RuleChainMetadataRequestMsg { int64 ruleChainIdMSB = 1; int64 ruleChainIdLSB = 2; @@ -674,5 +695,8 @@ message DownlinkMsg { repeated ResourceUpdateMsg resourceUpdateMsg = 28; repeated AlarmCommentUpdateMsg alarmCommentUpdateMsg = 29; repeated OAuth2UpdateMsg oAuth2UpdateMsg = 30; + repeated NotificationRuleUpdateMsg notificationRuleUpdateMsg = 31; + repeated NotificationTargetUpdateMsg notificationTargetUpdateMsg = 32; + repeated NotificationTemplateUpdateMsg notificationTemplateUpdateMsg = 33; } diff --git a/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationTargetService.java b/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationTargetService.java index a92e4f6a4e..90f54a316a 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationTargetService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationTargetService.java @@ -42,6 +42,8 @@ import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.dao.entity.AbstractEntityService; import org.thingsboard.server.dao.entity.EntityDaoService; +import org.thingsboard.server.dao.eventsourcing.DeleteEntityEvent; +import org.thingsboard.server.dao.eventsourcing.SaveEntityEvent; import org.thingsboard.server.dao.user.UserService; import java.util.List; @@ -65,7 +67,10 @@ public class DefaultNotificationTargetService extends AbstractEntityService impl @Override public NotificationTarget saveNotificationTarget(TenantId tenantId, NotificationTarget notificationTarget) { try { - return notificationTargetDao.saveAndFlush(tenantId, notificationTarget); + NotificationTarget savedTarget = notificationTargetDao.saveAndFlush(tenantId, notificationTarget); + eventPublisher.publishEvent(SaveEntityEvent.builder().tenantId(tenantId).entityId(savedTarget.getId()) + .created(notificationTarget.getId() == null).build()); + return savedTarget; } catch (Exception e) { checkConstraintViolation(e, Map.of( "uq_notification_target_name", "Recipients group with such name already exists" @@ -158,19 +163,21 @@ public class DefaultNotificationTargetService extends AbstractEntityService impl @Override public PageData findRecipientsForRuleNotificationTargetConfig(TenantId tenantId, PlatformUsersNotificationTargetConfig targetConfig, RuleOriginatedNotificationInfo info, PageLink pageLink) { switch (targetConfig.getUsersFilter().getType()) { - case ORIGINATOR_ENTITY_OWNER_USERS: + case ORIGINATOR_ENTITY_OWNER_USERS -> { CustomerId customerId = info.getAffectedCustomerId(); if (customerId != null && !customerId.isNullUid()) { return userService.findCustomerUsers(tenantId, customerId, pageLink); } else { return userService.findTenantAdmins(tenantId, pageLink); } - case AFFECTED_USER: + } + case AFFECTED_USER -> { UserId userId = info.getAffectedUserId(); if (userId != null) { return new PageData<>(List.of(userService.findUserById(tenantId, userId)), 1, 1, false); } - case AFFECTED_TENANT_ADMINISTRATORS: + } + case AFFECTED_TENANT_ADMINISTRATORS -> { TenantId affectedTenantId = info.getAffectedTenantId(); if (affectedTenantId == null) { affectedTenantId = tenantId; @@ -178,9 +185,8 @@ public class DefaultNotificationTargetService extends AbstractEntityService impl if (!affectedTenantId.isNullUid()) { return userService.findTenantAdmins(affectedTenantId, pageLink); } - break; - default: - throw new IllegalArgumentException("Recipient type not supported"); + } + default -> throw new IllegalArgumentException("Recipient type not supported"); } return new PageData<>(); } @@ -194,6 +200,7 @@ public class DefaultNotificationTargetService extends AbstractEntityService impl throw new IllegalArgumentException("Recipients group is being used in notification rule"); } notificationTargetDao.removeById(tenantId, id.getId()); + eventPublisher.publishEvent(DeleteEntityEvent.builder().tenantId(tenantId).entityId(id).build()); } @Override diff --git a/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationTemplateService.java b/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationTemplateService.java index 17b166c3ec..58ec572b7f 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationTemplateService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationTemplateService.java @@ -29,6 +29,8 @@ import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.dao.entity.AbstractEntityService; import org.thingsboard.server.dao.entity.EntityDaoService; +import org.thingsboard.server.dao.eventsourcing.DeleteEntityEvent; +import org.thingsboard.server.dao.eventsourcing.SaveEntityEvent; import java.util.List; import java.util.Map; @@ -55,7 +57,10 @@ public class DefaultNotificationTemplateService extends AbstractEntityService im } } try { - return notificationTemplateDao.saveAndFlush(tenantId, notificationTemplate); + NotificationTemplate savedTemplate = notificationTemplateDao.saveAndFlush(tenantId, notificationTemplate); + eventPublisher.publishEvent(SaveEntityEvent.builder().tenantId(tenantId).entityId(savedTemplate.getId()) + .created(notificationTemplate.getId() == null).build()); + return savedTemplate; } catch (Exception e) { checkConstraintViolation(e, Map.of( "uq_notification_template_name", "Notification template with such name already exists" @@ -82,6 +87,7 @@ public class DefaultNotificationTemplateService extends AbstractEntityService im )); throw e; } + eventPublisher.publishEvent(DeleteEntityEvent.builder().tenantId(tenantId).entityId(id).build()); } @Override From 6c592a249e0c06aaf9b3bba4facb546cfe3b6b38 Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Thu, 11 Apr 2024 15:57:06 +0300 Subject: [PATCH 51/80] 3.6.3 to 3.6.4 upgrade script --- .../thingsboard/server/install/ThingsboardInstallService.java | 3 +++ .../server/service/install/SqlDatabaseUpgradeService.java | 3 +++ dao/src/main/resources/sql/schema-entities.sql | 2 +- 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java b/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java index 44d81902a8..400d1eda27 100644 --- a/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java +++ b/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java @@ -127,6 +127,9 @@ public class ThingsboardInstallService { log.info("Upgrading ThingsBoard from version 3.6.2 to 3.6.3 ..."); databaseEntitiesUpgradeService.upgradeDatabase("3.6.2"); systemDataLoaderService.updateDefaultNotificationConfigs(); + case "3.6.3": + log.info("Upgrading ThingsBoard from version 3.6.3 to 3.6.4 ..."); + databaseEntitiesUpgradeService.upgradeDatabase("3.6.3"); //TODO DON'T FORGET to update switch statement in the CacheCleanupService if you need to clear the cache break; default: diff --git a/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java b/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java index 436c2fcc84..bf281b8571 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java +++ b/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java @@ -115,6 +115,9 @@ public class SqlDatabaseUpgradeService implements DatabaseEntitiesUpgradeService case "3.6.2": updateSchema("3.6.2", 3006002, "3.6.3", 3006003, null); break; + case "3.6.3": + updateSchema("3.6.3", 3006003, "3.6.4", 3006004, null); + break; default: throw new RuntimeException("Unable to upgrade SQL database, unsupported fromVersion: " + fromVersion); } diff --git a/dao/src/main/resources/sql/schema-entities.sql b/dao/src/main/resources/sql/schema-entities.sql index 34320ccd9e..f5f6dce638 100644 --- a/dao/src/main/resources/sql/schema-entities.sql +++ b/dao/src/main/resources/sql/schema-entities.sql @@ -25,7 +25,7 @@ CREATE OR REPLACE PROCEDURE insert_tb_schema_settings() $$ BEGIN IF (SELECT COUNT(*) FROM tb_schema_settings) = 0 THEN - INSERT INTO tb_schema_settings (schema_version) VALUES (3006000); + INSERT INTO tb_schema_settings (schema_version) VALUES (3006004); END IF; END; $$; From 0f259cf3570d72ff2aa0917af8423848a3b46b8f Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Thu, 11 Apr 2024 16:13:00 +0300 Subject: [PATCH 52/80] Update pkg.updateVersion to 3.6.4 --- msa/tb/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/msa/tb/pom.xml b/msa/tb/pom.xml index 52e4978303..0fde0e9889 100644 --- a/msa/tb/pom.xml +++ b/msa/tb/pom.xml @@ -38,7 +38,7 @@ tb-postgres tb-cassandra /usr/share/${pkg.name} - 3.6.3 + 3.6.4 From 0a16214af5b8ec8c568e34f0e0155cbbbc524ebc Mon Sep 17 00:00:00 2001 From: nick Date: Thu, 11 Apr 2024 16:34:23 +0300 Subject: [PATCH 53/80] fix bug: lwm2m tests dif Port Comments 2 --- application/src/test/resources/logback-test.xml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/application/src/test/resources/logback-test.xml b/application/src/test/resources/logback-test.xml index 96386c00ff..8b20a2817a 100644 --- a/application/src/test/resources/logback-test.xml +++ b/application/src/test/resources/logback-test.xml @@ -37,9 +37,9 @@ - - - + + + From e8c238e5a61adc44a559c42d4a774f5dd8f1b3b3 Mon Sep 17 00:00:00 2001 From: Andrii Landiak Date: Thu, 11 Apr 2024 17:30:05 +0300 Subject: [PATCH 54/80] Add NotificationEdgeTest --- .../edge/DefaultEdgeNotificationService.java | 13 +- .../edge/rpc/processor/BaseEdgeProcessor.java | 5 +- .../processor/asset/AssetEdgeProcessor.java | 16 +- .../profile/AssetProfileEdgeProcessor.java | 12 +- .../dashboard/DashboardEdgeProcessor.java | 16 +- .../processor/device/DeviceEdgeProcessor.java | 2 +- .../profile/DeviceProfileEdgeProcessor.java | 12 +- .../entityview/BaseEntityViewProcessor.java | 1 + .../entityview/EntityViewEdgeProcessor.java | 16 +- .../entityview/EntityViewProcessor.java | 1 + .../server/edge/NotificationEdgeTest.java | 293 ++++++++++++++++++ .../server/edge/imitator/EdgeImitator.java | 28 +- 12 files changed, 360 insertions(+), 55 deletions(-) create mode 100644 application/src/test/java/org/thingsboard/server/edge/NotificationEdgeTest.java diff --git a/application/src/main/java/org/thingsboard/server/service/edge/DefaultEdgeNotificationService.java b/application/src/main/java/org/thingsboard/server/service/edge/DefaultEdgeNotificationService.java index 1573fbb69d..f29255e414 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/DefaultEdgeNotificationService.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/DefaultEdgeNotificationService.java @@ -16,6 +16,8 @@ package org.thingsboard.server.service.edge; import com.fasterxml.jackson.databind.node.ObjectNode; +import jakarta.annotation.PostConstruct; +import jakarta.annotation.PreDestroy; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; @@ -42,6 +44,7 @@ import org.thingsboard.server.service.edge.rpc.processor.device.DeviceEdgeProces import org.thingsboard.server.service.edge.rpc.processor.device.profile.DeviceProfileEdgeProcessor; import org.thingsboard.server.service.edge.rpc.processor.edge.EdgeProcessor; import org.thingsboard.server.service.edge.rpc.processor.entityview.EntityViewEdgeProcessor; +import org.thingsboard.server.service.edge.rpc.processor.notification.NotificationEdgeProcessor; import org.thingsboard.server.service.edge.rpc.processor.oauth2.OAuth2EdgeProcessor; import org.thingsboard.server.service.edge.rpc.processor.ota.OtaPackageEdgeProcessor; import org.thingsboard.server.service.edge.rpc.processor.queue.QueueEdgeProcessor; @@ -54,8 +57,6 @@ import org.thingsboard.server.service.edge.rpc.processor.user.UserEdgeProcessor; import org.thingsboard.server.service.edge.rpc.processor.widget.WidgetBundleEdgeProcessor; import org.thingsboard.server.service.edge.rpc.processor.widget.WidgetTypeEdgeProcessor; -import jakarta.annotation.PostConstruct; -import jakarta.annotation.PreDestroy; import java.util.UUID; import java.util.concurrent.ExecutorService; import java.util.concurrent.TimeUnit; @@ -127,6 +128,9 @@ public class DefaultEdgeNotificationService implements EdgeNotificationService { @Autowired private ResourceEdgeProcessor resourceEdgeProcessor; + @Autowired + private NotificationEdgeProcessor notificationEdgeProcessor; + @Autowired private OAuth2EdgeProcessor oAuth2EdgeProcessor; @@ -194,9 +198,8 @@ public class DefaultEdgeNotificationService implements EdgeNotificationService { case RELATION -> relationProcessor.processRelationNotification(tenantId, edgeNotificationMsg); case TENANT -> tenantEdgeProcessor.processEntityNotification(tenantId, edgeNotificationMsg); case TENANT_PROFILE -> tenantProfileEdgeProcessor.processEntityNotification(tenantId, edgeNotificationMsg); - case NOTIFICATION_RULE -> System.out.println(); - case NOTIFICATION_TARGET -> System.out.println(); - case NOTIFICATION_TEMPLATE -> System.out.println(); + case NOTIFICATION_RULE, NOTIFICATION_TARGET, NOTIFICATION_TEMPLATE -> + notificationEdgeProcessor.processEntityNotification(tenantId, edgeNotificationMsg); case TB_RESOURCE -> resourceEdgeProcessor.processEntityNotification(tenantId, edgeNotificationMsg); case OAUTH2 -> oAuth2EdgeProcessor.processOAuth2Notification(tenantId, edgeNotificationMsg); default -> log.warn("[{}] Edge event type [{}] is not designed to be pushed to edge", tenantId, type); 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 1abf6c025f..3275676fd0 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 @@ -606,8 +606,9 @@ public abstract class BaseEdgeProcessor { return metaData; } - protected void pushEntityEventToRuleEngine(TenantId tenantId, EntityId entityId, CustomerId customerId, String msgData, TbMsgMetaData metaData) { - TbMsg tbMsg = TbMsg.newMsg(TbMsgType.ENTITY_CREATED, entityId, customerId, metaData, TbMsgDataType.JSON, msgData); + protected void pushEntityEventToRuleEngine(TenantId tenantId, EntityId entityId, CustomerId customerId, + TbMsgType msgType, String msgData, TbMsgMetaData metaData) { + TbMsg tbMsg = TbMsg.newMsg(msgType, entityId, customerId, metaData, TbMsgDataType.JSON, msgData); tbClusterService.pushMsgToRuleEngine(tenantId, entityId, tbMsg, new TbQueueCallback() { @Override public void onSuccess(TbQueueMsgMetadata metadata) { diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/asset/AssetEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/asset/AssetEdgeProcessor.java index deac6fbc1f..5d264cd83d 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/asset/AssetEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/asset/AssetEdgeProcessor.java @@ -97,7 +97,7 @@ public abstract class AssetEdgeProcessor extends BaseAssetProcessor implements A Asset asset = assetService.findAssetById(tenantId, assetId); String assetAsString = JacksonUtil.toString(asset); TbMsgMetaData msgMetaData = getEdgeActionTbMsgMetaData(edge, asset.getCustomerId()); - pushEntityEventToRuleEngine(tenantId, assetId, asset.getCustomerId(), assetAsString, msgMetaData); + pushEntityEventToRuleEngine(tenantId, assetId, asset.getCustomerId(), TbMsgType.ENTITY_CREATED, assetAsString, msgMetaData); } catch (Exception e) { log.warn("[{}][{}] Failed to push asset action to rule engine: {}", tenantId, assetId, TbMsgType.ENTITY_CREATED.name(), e); } @@ -108,11 +108,7 @@ public abstract class AssetEdgeProcessor extends BaseAssetProcessor implements A AssetId assetId = new AssetId(edgeEvent.getEntityId()); DownlinkMsg downlinkMsg = null; switch (edgeEvent.getAction()) { - case ADDED: - case UPDATED: - case ASSIGNED_TO_EDGE: - case ASSIGNED_TO_CUSTOMER: - case UNASSIGNED_FROM_CUSTOMER: + case ADDED, UPDATED, ASSIGNED_TO_EDGE, ASSIGNED_TO_CUSTOMER, UNASSIGNED_FROM_CUSTOMER -> { Asset asset = assetService.findAssetById(edgeEvent.getTenantId(), assetId); if (asset != null) { UpdateMsgType msgType = getUpdateMsgType(edgeEvent.getAction()); @@ -129,17 +125,17 @@ public abstract class AssetEdgeProcessor extends BaseAssetProcessor implements A } downlinkMsg = builder.build(); } - break; - case DELETED: - case UNASSIGNED_FROM_EDGE: + } + case DELETED, UNASSIGNED_FROM_EDGE -> { AssetUpdateMsg assetUpdateMsg = ((AssetMsgConstructor) assetMsgConstructorFactory.getMsgConstructorByEdgeVersion(edgeVersion)).constructAssetDeleteMsg(assetId); downlinkMsg = DownlinkMsg.newBuilder() .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) .addAssetUpdateMsg(assetUpdateMsg) .build(); - break; + } } return downlinkMsg; } + } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/asset/profile/AssetProfileEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/asset/profile/AssetProfileEdgeProcessor.java index bcafaf740e..b4d56ed20f 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/asset/profile/AssetProfileEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/asset/profile/AssetProfileEdgeProcessor.java @@ -86,7 +86,7 @@ public abstract class AssetProfileEdgeProcessor extends BaseAssetProfileProcesso AssetProfile assetProfile = assetProfileService.findAssetProfileById(tenantId, assetProfileId); String assetProfileAsString = JacksonUtil.toString(assetProfile); TbMsgMetaData msgMetaData = getEdgeActionTbMsgMetaData(edge, null); - pushEntityEventToRuleEngine(tenantId, assetProfileId, null, assetProfileAsString, msgMetaData); + pushEntityEventToRuleEngine(tenantId, assetProfileId, null, TbMsgType.ENTITY_CREATED, assetProfileAsString, msgMetaData); } catch (Exception e) { log.warn("[{}][{}] Failed to push asset profile action to rule engine: {}", tenantId, assetProfileId, TbMsgType.ENTITY_CREATED.name(), e); } @@ -97,8 +97,7 @@ public abstract class AssetProfileEdgeProcessor extends BaseAssetProfileProcesso AssetProfileId assetProfileId = new AssetProfileId(edgeEvent.getEntityId()); DownlinkMsg downlinkMsg = null; switch (edgeEvent.getAction()) { - case ADDED: - case UPDATED: + case ADDED, UPDATED -> { AssetProfile assetProfile = assetProfileService.findAssetProfileById(edgeEvent.getTenantId(), assetProfileId); if (assetProfile != null) { UpdateMsgType msgType = getUpdateMsgType(edgeEvent.getAction()); @@ -110,16 +109,17 @@ public abstract class AssetProfileEdgeProcessor extends BaseAssetProfileProcesso .addAssetProfileUpdateMsg(assetProfileUpdateMsg) .build(); } - break; - case DELETED: + } + case DELETED -> { AssetProfileUpdateMsg assetProfileUpdateMsg = ((AssetMsgConstructor) assetMsgConstructorFactory.getMsgConstructorByEdgeVersion(edgeVersion)).constructAssetProfileDeleteMsg(assetProfileId); downlinkMsg = DownlinkMsg.newBuilder() .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) .addAssetProfileUpdateMsg(assetProfileUpdateMsg) .build(); - break; + } } return downlinkMsg; } + } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/dashboard/DashboardEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/dashboard/DashboardEdgeProcessor.java index acf312504b..aab35f89d8 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/dashboard/DashboardEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/dashboard/DashboardEdgeProcessor.java @@ -90,7 +90,7 @@ public abstract class DashboardEdgeProcessor extends BaseDashboardProcessor impl Dashboard dashboard = dashboardService.findDashboardById(tenantId, dashboardId); String dashboardAsString = JacksonUtil.toString(dashboard); TbMsgMetaData msgMetaData = getEdgeActionTbMsgMetaData(edge, null); - pushEntityEventToRuleEngine(tenantId, dashboardId, null, dashboardAsString, msgMetaData); + pushEntityEventToRuleEngine(tenantId, dashboardId, null, TbMsgType.ENTITY_CREATED, dashboardAsString, msgMetaData); } catch (Exception e) { log.warn("[{}][{}] Failed to push dashboard action to rule engine: {}", tenantId, dashboardId, TbMsgType.ENTITY_CREATED.name(), e); } @@ -101,11 +101,7 @@ public abstract class DashboardEdgeProcessor extends BaseDashboardProcessor impl DashboardId dashboardId = new DashboardId(edgeEvent.getEntityId()); DownlinkMsg downlinkMsg = null; switch (edgeEvent.getAction()) { - case ADDED: - case UPDATED: - case ASSIGNED_TO_EDGE: - case ASSIGNED_TO_CUSTOMER: - case UNASSIGNED_FROM_CUSTOMER: + case ADDED, UPDATED, ASSIGNED_TO_EDGE, ASSIGNED_TO_CUSTOMER, UNASSIGNED_FROM_CUSTOMER -> { Dashboard dashboard = dashboardService.findDashboardById(edgeEvent.getTenantId(), dashboardId); if (dashboard != null) { UpdateMsgType msgType = getUpdateMsgType(edgeEvent.getAction()); @@ -116,16 +112,15 @@ public abstract class DashboardEdgeProcessor extends BaseDashboardProcessor impl .addDashboardUpdateMsg(dashboardUpdateMsg) .build(); } - break; - case DELETED: - case UNASSIGNED_FROM_EDGE: + } + case DELETED, UNASSIGNED_FROM_EDGE -> { DashboardUpdateMsg dashboardUpdateMsg = ((DashboardMsgConstructor) dashboardMsgConstructorFactory.getMsgConstructorByEdgeVersion(edgeVersion)).constructDashboardDeleteMsg(dashboardId); downlinkMsg = DownlinkMsg.newBuilder() .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) .addDashboardUpdateMsg(dashboardUpdateMsg) .build(); - break; + } } return downlinkMsg; } @@ -135,4 +130,5 @@ public abstract class DashboardEdgeProcessor extends BaseDashboardProcessor impl // do nothing on cloud return assignedCustomers; } + } 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 27f86a34b9..5e73034b1c 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 @@ -125,7 +125,7 @@ public abstract class DeviceEdgeProcessor extends BaseDeviceProcessor implements Device device = deviceService.findDeviceById(tenantId, deviceId); String deviceAsString = JacksonUtil.toString(device); TbMsgMetaData msgMetaData = getEdgeActionTbMsgMetaData(edge, device.getCustomerId()); - pushEntityEventToRuleEngine(tenantId, deviceId, device.getCustomerId(), deviceAsString, msgMetaData); + pushEntityEventToRuleEngine(tenantId, deviceId, device.getCustomerId(), TbMsgType.ENTITY_CREATED, deviceAsString, msgMetaData); } catch (Exception e) { log.warn("[{}][{}] Failed to push device action to rule engine: {}", tenantId, deviceId, TbMsgType.ENTITY_CREATED.name(), e); } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/profile/DeviceProfileEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/profile/DeviceProfileEdgeProcessor.java index 2f1a60be1a..464fa3ba54 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/profile/DeviceProfileEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/profile/DeviceProfileEdgeProcessor.java @@ -86,7 +86,7 @@ public abstract class DeviceProfileEdgeProcessor extends BaseDeviceProfileProces DeviceProfile deviceProfile = deviceProfileService.findDeviceProfileById(tenantId, deviceProfileId); String deviceProfileAsString = JacksonUtil.toString(deviceProfile); TbMsgMetaData msgMetaData = getEdgeActionTbMsgMetaData(edge, null); - pushEntityEventToRuleEngine(tenantId, deviceProfileId, null, deviceProfileAsString, msgMetaData); + pushEntityEventToRuleEngine(tenantId, deviceProfileId, null, TbMsgType.ENTITY_CREATED, deviceProfileAsString, msgMetaData); } catch (Exception e) { log.warn("[{}][{}] Failed to push device profile action to rule engine: {}", tenantId, deviceProfileId, TbMsgType.ENTITY_CREATED.name(), e); } @@ -97,8 +97,7 @@ public abstract class DeviceProfileEdgeProcessor extends BaseDeviceProfileProces DeviceProfileId deviceProfileId = new DeviceProfileId(edgeEvent.getEntityId()); DownlinkMsg downlinkMsg = null; switch (edgeEvent.getAction()) { - case ADDED: - case UPDATED: + case ADDED, UPDATED -> { DeviceProfile deviceProfile = deviceProfileService.findDeviceProfileById(edgeEvent.getTenantId(), deviceProfileId); if (deviceProfile != null) { UpdateMsgType msgType = getUpdateMsgType(edgeEvent.getAction()); @@ -110,16 +109,17 @@ public abstract class DeviceProfileEdgeProcessor extends BaseDeviceProfileProces .addDeviceProfileUpdateMsg(deviceProfileUpdateMsg) .build(); } - break; - case DELETED: + } + case DELETED -> { DeviceProfileUpdateMsg deviceProfileUpdateMsg = ((DeviceMsgConstructor) deviceMsgConstructorFactory.getMsgConstructorByEdgeVersion(edgeVersion)).constructDeviceProfileDeleteMsg(deviceProfileId); downlinkMsg = DownlinkMsg.newBuilder() .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) .addDeviceProfileUpdateMsg(deviceProfileUpdateMsg) .build(); - break; + } } return downlinkMsg; } + } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/entityview/BaseEntityViewProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/entityview/BaseEntityViewProcessor.java index b221b1576f..9c2b9a37bd 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/entityview/BaseEntityViewProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/entityview/BaseEntityViewProcessor.java @@ -64,4 +64,5 @@ public abstract class BaseEntityViewProcessor extends BaseEdgeProcessor { protected abstract EntityView constructEntityViewFromUpdateMsg(TenantId tenantId, EntityViewId entityViewId, EntityViewUpdateMsg entityViewUpdateMsg); protected abstract void setCustomerId(TenantId tenantId, CustomerId customerId, EntityView entityView, EntityViewUpdateMsg entityViewUpdateMsg); + } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/entityview/EntityViewEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/entityview/EntityViewEdgeProcessor.java index 125b50f60c..70d9d9686f 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/entityview/EntityViewEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/entityview/EntityViewEdgeProcessor.java @@ -95,7 +95,7 @@ public abstract class EntityViewEdgeProcessor extends BaseEntityViewProcessor im EntityView entityView = entityViewService.findEntityViewById(tenantId, entityViewId); String entityViewAsString = JacksonUtil.toString(entityView); TbMsgMetaData msgMetaData = getEdgeActionTbMsgMetaData(edge, entityView.getCustomerId()); - pushEntityEventToRuleEngine(tenantId, entityViewId, entityView.getCustomerId(), entityViewAsString, msgMetaData); + pushEntityEventToRuleEngine(tenantId, entityViewId, entityView.getCustomerId(), TbMsgType.ENTITY_CREATED, entityViewAsString, msgMetaData); } catch (Exception e) { log.warn("[{}][{}] Failed to push entity view action to rule engine: {}", tenantId, entityViewId, TbMsgType.ENTITY_CREATED.name(), e); } @@ -105,11 +105,7 @@ public abstract class EntityViewEdgeProcessor extends BaseEntityViewProcessor im EntityViewId entityViewId = new EntityViewId(edgeEvent.getEntityId()); DownlinkMsg downlinkMsg = null; switch (edgeEvent.getAction()) { - case ADDED: - case UPDATED: - case ASSIGNED_TO_EDGE: - case ASSIGNED_TO_CUSTOMER: - case UNASSIGNED_FROM_CUSTOMER: + case ADDED, UPDATED, ASSIGNED_TO_EDGE, ASSIGNED_TO_CUSTOMER, UNASSIGNED_FROM_CUSTOMER -> { EntityView entityView = entityViewService.findEntityViewById(edgeEvent.getTenantId(), entityViewId); if (entityView != null) { UpdateMsgType msgType = getUpdateMsgType(edgeEvent.getAction()); @@ -120,17 +116,17 @@ public abstract class EntityViewEdgeProcessor extends BaseEntityViewProcessor im .addEntityViewUpdateMsg(entityViewUpdateMsg) .build(); } - break; - case DELETED: - case UNASSIGNED_FROM_EDGE: + } + case DELETED, UNASSIGNED_FROM_EDGE -> { EntityViewUpdateMsg entityViewUpdateMsg = ((EntityViewMsgConstructor) entityViewMsgConstructorFactory.getMsgConstructorByEdgeVersion(edgeVersion)).constructEntityViewDeleteMsg(entityViewId); downlinkMsg = DownlinkMsg.newBuilder() .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) .addEntityViewUpdateMsg(entityViewUpdateMsg) .build(); - break; + } } return downlinkMsg; } + } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/entityview/EntityViewProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/entityview/EntityViewProcessor.java index 80803a2cd4..061cd89bf0 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/entityview/EntityViewProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/entityview/EntityViewProcessor.java @@ -29,4 +29,5 @@ public interface EntityViewProcessor extends EdgeProcessor { ListenableFuture processEntityViewMsgFromEdge(TenantId tenantId, Edge edge, EntityViewUpdateMsg entityViewUpdateMsg); DownlinkMsg convertEntityViewEventToDownlink(EdgeEvent edgeEvent, EdgeVersion edgeVersion); + } diff --git a/application/src/test/java/org/thingsboard/server/edge/NotificationEdgeTest.java b/application/src/test/java/org/thingsboard/server/edge/NotificationEdgeTest.java new file mode 100644 index 0000000000..37069baa44 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/edge/NotificationEdgeTest.java @@ -0,0 +1,293 @@ +/** + * 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.edge; + +import com.google.protobuf.AbstractMessage; +import org.junit.Assert; +import org.junit.Test; +import org.springframework.test.context.TestPropertySource; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.common.data.StringUtils; +import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod; +import org.thingsboard.server.common.data.notification.NotificationType; +import org.thingsboard.server.common.data.notification.rule.EscalatedNotificationRuleRecipientsConfig; +import org.thingsboard.server.common.data.notification.rule.NotificationRule; +import org.thingsboard.server.common.data.notification.rule.trigger.config.AlarmNotificationRuleTriggerConfig; +import org.thingsboard.server.common.data.notification.rule.trigger.config.NotificationRuleTriggerType; +import org.thingsboard.server.common.data.notification.targets.NotificationTarget; +import org.thingsboard.server.common.data.notification.targets.platform.PlatformUsersNotificationTargetConfig; +import org.thingsboard.server.common.data.notification.targets.platform.TenantAdministratorsFilter; +import org.thingsboard.server.common.data.notification.template.DeliveryMethodNotificationTemplate; +import org.thingsboard.server.common.data.notification.template.EmailDeliveryMethodNotificationTemplate; +import org.thingsboard.server.common.data.notification.template.HasSubject; +import org.thingsboard.server.common.data.notification.template.MobileAppDeliveryMethodNotificationTemplate; +import org.thingsboard.server.common.data.notification.template.NotificationTemplate; +import org.thingsboard.server.common.data.notification.template.NotificationTemplateConfig; +import org.thingsboard.server.common.data.notification.template.SmsDeliveryMethodNotificationTemplate; +import org.thingsboard.server.common.data.notification.template.WebDeliveryMethodNotificationTemplate; +import org.thingsboard.server.dao.service.DaoSqlTest; +import org.thingsboard.server.gen.edge.v1.NotificationRuleUpdateMsg; +import org.thingsboard.server.gen.edge.v1.NotificationTargetUpdateMsg; +import org.thingsboard.server.gen.edge.v1.NotificationTemplateUpdateMsg; +import org.thingsboard.server.gen.edge.v1.UpdateMsgType; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; + +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@TestPropertySource(properties = { + "transport.mqtt.enabled=true" +}) +@DaoSqlTest +public class NotificationEdgeTest extends AbstractEdgeTest { + + @Test + public void testNotificationTemplate() throws Exception { + // create notification template + edgeImitator.expectMessageAmount(1); + NotificationDeliveryMethod[] deliveryMethods = new NotificationDeliveryMethod[]{ + NotificationDeliveryMethod.WEB + }; + NotificationTemplate template = createNotificationTemplate(NotificationType.GENERAL, deliveryMethods); + Assert.assertTrue(edgeImitator.waitForMessages()); + AbstractMessage latestMessage = edgeImitator.getLatestMessage(); + Assert.assertTrue(latestMessage instanceof NotificationTemplateUpdateMsg); + NotificationTemplateUpdateMsg notificationTemplateUpdateMsg = (NotificationTemplateUpdateMsg) latestMessage; + Assert.assertEquals(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, notificationTemplateUpdateMsg.getMsgType()); + NotificationTemplate notificationTemplate = JacksonUtil.fromString(notificationTemplateUpdateMsg.getEntity(), NotificationTemplate.class, true); + Assert.assertNotNull(notificationTemplate); + Assert.assertEquals(template.getId(), notificationTemplate.getId()); + Assert.assertEquals(template.getName(), notificationTemplate.getName()); + Assert.assertEquals(template.getNotificationType(), notificationTemplate.getNotificationType()); + + // update notification template + edgeImitator.expectMessageAmount(1); + template.setName(StringUtils.randomAlphanumeric(15)); + saveNotificationTemplate(template); + Assert.assertTrue(edgeImitator.waitForMessages()); + latestMessage = edgeImitator.getLatestMessage(); + Assert.assertTrue(latestMessage instanceof NotificationTemplateUpdateMsg); + notificationTemplateUpdateMsg = (NotificationTemplateUpdateMsg) latestMessage; + Assert.assertEquals(UpdateMsgType.ENTITY_UPDATED_RPC_MESSAGE, notificationTemplateUpdateMsg.getMsgType()); + notificationTemplate = JacksonUtil.fromString(notificationTemplateUpdateMsg.getEntity(), NotificationTemplate.class, true); + Assert.assertNotNull(notificationTemplate); + Assert.assertEquals(template.getId(), notificationTemplate.getId()); + Assert.assertEquals(template.getName(), notificationTemplate.getName()); + Assert.assertEquals(template.getNotificationType(), notificationTemplate.getNotificationType()); + + // delete notification template + edgeImitator.expectMessageAmount(1); + doDelete("/api/notification/template/" + notificationTemplate.getUuidId()) + .andExpect(status().isOk()); + Assert.assertTrue(edgeImitator.waitForMessages()); + latestMessage = edgeImitator.getLatestMessage(); + Assert.assertTrue(latestMessage instanceof NotificationTemplateUpdateMsg); + notificationTemplateUpdateMsg = (NotificationTemplateUpdateMsg) latestMessage; + Assert.assertEquals(UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE, notificationTemplateUpdateMsg.getMsgType()); + Assert.assertEquals(notificationTemplate.getUuidId().getMostSignificantBits(), notificationTemplateUpdateMsg.getIdMSB()); + Assert.assertEquals(notificationTemplate.getUuidId().getLeastSignificantBits(), notificationTemplateUpdateMsg.getIdLSB()); + } + + @Test + public void testNotificationTarget() throws Exception { + // create notification target + edgeImitator.expectMessageAmount(1); + NotificationTarget target = createNotificationTarget(); + Assert.assertTrue(edgeImitator.waitForMessages()); + AbstractMessage latestMessage = edgeImitator.getLatestMessage(); + Assert.assertTrue(latestMessage instanceof NotificationTargetUpdateMsg); + NotificationTargetUpdateMsg notificationTargetUpdateMsg = (NotificationTargetUpdateMsg) latestMessage; + Assert.assertEquals(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, notificationTargetUpdateMsg.getMsgType()); + NotificationTarget notificationTarget = JacksonUtil.fromString(notificationTargetUpdateMsg.getEntity(), NotificationTarget.class, true); + Assert.assertNotNull(notificationTarget); + Assert.assertEquals(target, notificationTarget); + + // update notification target + edgeImitator.expectMessageAmount(1); + target.setName(StringUtils.randomAlphanumeric(15)); + target = saveNotificationTarget(target); + Assert.assertTrue(edgeImitator.waitForMessages()); + latestMessage = edgeImitator.getLatestMessage(); + Assert.assertTrue(latestMessage instanceof NotificationTargetUpdateMsg); + notificationTargetUpdateMsg = (NotificationTargetUpdateMsg) latestMessage; + Assert.assertEquals(UpdateMsgType.ENTITY_UPDATED_RPC_MESSAGE, notificationTargetUpdateMsg.getMsgType()); + notificationTarget = JacksonUtil.fromString(notificationTargetUpdateMsg.getEntity(), NotificationTarget.class, true); + Assert.assertNotNull(notificationTarget); + Assert.assertEquals(target, notificationTarget); + + // delete notification target + edgeImitator.expectMessageAmount(1); + doDelete("/api/notification/target/" + notificationTarget.getUuidId()) + .andExpect(status().isOk()); + Assert.assertTrue(edgeImitator.waitForMessages()); + latestMessage = edgeImitator.getLatestMessage(); + Assert.assertTrue(latestMessage instanceof NotificationTargetUpdateMsg); + notificationTargetUpdateMsg = (NotificationTargetUpdateMsg) latestMessage; + Assert.assertEquals(UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE, notificationTargetUpdateMsg.getMsgType()); + Assert.assertEquals(notificationTarget.getUuidId().getMostSignificantBits(), notificationTargetUpdateMsg.getIdMSB()); + Assert.assertEquals(notificationTarget.getUuidId().getLeastSignificantBits(), notificationTargetUpdateMsg.getIdLSB()); + } + + @Test + public void testNotificationRule() throws Exception { + // create notification template for notification rule + edgeImitator.expectMessageAmount(1); + NotificationDeliveryMethod[] deliveryMethods = new NotificationDeliveryMethod[]{ + NotificationDeliveryMethod.WEB, NotificationDeliveryMethod.EMAIL + }; + NotificationTemplate template = createNotificationTemplate(NotificationType.EDGE_CONNECTION, deliveryMethods); + Assert.assertTrue(edgeImitator.waitForMessages()); + AbstractMessage latestMessage = edgeImitator.getLatestMessage(); + Assert.assertTrue(latestMessage instanceof NotificationTemplateUpdateMsg); + NotificationTemplateUpdateMsg notificationTemplateUpdateMsg = (NotificationTemplateUpdateMsg) latestMessage; + Assert.assertEquals(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, notificationTemplateUpdateMsg.getMsgType()); + NotificationTemplate notificationTemplate = JacksonUtil.fromString(notificationTemplateUpdateMsg.getEntity(), NotificationTemplate.class, true); + Assert.assertNotNull(notificationTemplate); + Assert.assertEquals(template.getId(), notificationTemplate.getId()); + Assert.assertEquals(template.getName(), notificationTemplate.getName()); + Assert.assertEquals(template.getNotificationType(), notificationTemplate.getNotificationType()); + + // create notification rule + edgeImitator.expectMessageAmount(1); + NotificationRule rule = createNotificationRule(template); + Assert.assertTrue(edgeImitator.waitForMessages()); + latestMessage = edgeImitator.getLatestMessage(); + Assert.assertTrue(latestMessage instanceof NotificationRuleUpdateMsg); + NotificationRuleUpdateMsg notificationRuleUpdateMsg = (NotificationRuleUpdateMsg) latestMessage; + Assert.assertEquals(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, notificationRuleUpdateMsg.getMsgType()); + NotificationRule notificationRule = JacksonUtil.fromString(notificationRuleUpdateMsg.getEntity(), NotificationRule.class, true); + Assert.assertNotNull(notificationRule); + Assert.assertEquals(rule, notificationRule); + + // update notification rule + edgeImitator.expectMessageAmount(1); + rule.setEnabled(false); + rule = saveNotificationRule(rule); + Assert.assertTrue(edgeImitator.waitForMessages()); + latestMessage = edgeImitator.getLatestMessage(); + Assert.assertTrue(latestMessage instanceof NotificationRuleUpdateMsg); + notificationRuleUpdateMsg = (NotificationRuleUpdateMsg) latestMessage; + Assert.assertEquals(UpdateMsgType.ENTITY_UPDATED_RPC_MESSAGE, notificationRuleUpdateMsg.getMsgType()); + notificationRule = JacksonUtil.fromString(notificationRuleUpdateMsg.getEntity(), NotificationRule.class, true); + Assert.assertNotNull(notificationRule); + Assert.assertEquals(rule, notificationRule); + + // delete notification rule + edgeImitator.expectMessageAmount(1); + doDelete("/api/notification/rule/" + notificationRule.getUuidId()) + .andExpect(status().isOk()); + Assert.assertTrue(edgeImitator.waitForMessages()); + latestMessage = edgeImitator.getLatestMessage(); + Assert.assertTrue(latestMessage instanceof NotificationRuleUpdateMsg); + notificationRuleUpdateMsg = (NotificationRuleUpdateMsg) latestMessage; + Assert.assertEquals(UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE, notificationRuleUpdateMsg.getMsgType()); + Assert.assertEquals(notificationRule.getUuidId().getMostSignificantBits(), notificationRuleUpdateMsg.getIdMSB()); + Assert.assertEquals(notificationRule.getUuidId().getLeastSignificantBits(), notificationRuleUpdateMsg.getIdLSB()); + + // delete notification template + edgeImitator.expectMessageAmount(1); + doDelete("/api/notification/template/" + notificationTemplate.getUuidId()) + .andExpect(status().isOk()); + Assert.assertTrue(edgeImitator.waitForMessages()); + latestMessage = edgeImitator.getLatestMessage(); + Assert.assertTrue(latestMessage instanceof NotificationTemplateUpdateMsg); + notificationTemplateUpdateMsg = (NotificationTemplateUpdateMsg) latestMessage; + Assert.assertEquals(UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE, notificationTemplateUpdateMsg.getMsgType()); + Assert.assertEquals(notificationTemplate.getUuidId().getMostSignificantBits(), notificationTemplateUpdateMsg.getIdMSB()); + Assert.assertEquals(notificationTemplate.getUuidId().getLeastSignificantBits(), notificationTemplateUpdateMsg.getIdLSB()); + } + + private NotificationTemplate createNotificationTemplate(NotificationType notificationType, NotificationDeliveryMethod... deliveryMethods) { + NotificationTemplate notificationTemplate = new NotificationTemplate(); + notificationTemplate.setTenantId(tenantId); + notificationTemplate.setName(StringUtils.randomAlphanumeric(15)); + notificationTemplate.setNotificationType(notificationType); + NotificationTemplateConfig config = new NotificationTemplateConfig(); + config.setDeliveryMethodsTemplates(new HashMap<>()); + for (NotificationDeliveryMethod deliveryMethod : deliveryMethods) { + DeliveryMethodNotificationTemplate deliveryMethodNotificationTemplate; + switch (deliveryMethod) { + case WEB -> deliveryMethodNotificationTemplate = new WebDeliveryMethodNotificationTemplate(); + case EMAIL -> deliveryMethodNotificationTemplate = new EmailDeliveryMethodNotificationTemplate(); + case SMS -> deliveryMethodNotificationTemplate = new SmsDeliveryMethodNotificationTemplate(); + case MOBILE_APP -> deliveryMethodNotificationTemplate = new MobileAppDeliveryMethodNotificationTemplate(); + default -> throw new IllegalArgumentException("Unsupported delivery method " + deliveryMethod); + } + deliveryMethodNotificationTemplate.setEnabled(true); + deliveryMethodNotificationTemplate.setBody("Test text"); + if (deliveryMethodNotificationTemplate instanceof HasSubject) { + ((HasSubject) deliveryMethodNotificationTemplate).setSubject("Test subject"); + } + config.getDeliveryMethodsTemplates().put(deliveryMethod, deliveryMethodNotificationTemplate); + } + notificationTemplate.setConfiguration(config); + return saveNotificationTemplate(notificationTemplate); + } + + private NotificationTarget createNotificationTarget() { + NotificationTarget notificationTarget = new NotificationTarget(); + notificationTarget.setTenantId(tenantId); + notificationTarget.setName("Test target"); + + PlatformUsersNotificationTargetConfig targetConfig = new PlatformUsersNotificationTargetConfig(); + TenantAdministratorsFilter tenantAdministratorsFilter = new TenantAdministratorsFilter(); + tenantAdministratorsFilter.setTenantsIds(Set.of()); + tenantAdministratorsFilter.setTenantProfilesIds(Set.of()); + targetConfig.setUsersFilter(tenantAdministratorsFilter); + notificationTarget.setConfiguration(targetConfig); + return saveNotificationTarget(notificationTarget); + } + + private NotificationRule createNotificationRule(NotificationTemplate notificationTemplate) { + NotificationRule notificationRule = new NotificationRule(); + notificationRule.setName("Web notification on any alarm"); + notificationRule.setEnabled(true); + notificationRule.setTemplateId(notificationTemplate.getId()); + notificationRule.setTriggerType(NotificationRuleTriggerType.ALARM); + + AlarmNotificationRuleTriggerConfig triggerConfig = new AlarmNotificationRuleTriggerConfig(); + triggerConfig.setAlarmTypes(null); + triggerConfig.setAlarmSeverities(null); + triggerConfig.setNotifyOn(Set.of(AlarmNotificationRuleTriggerConfig.AlarmAction.CREATED, AlarmNotificationRuleTriggerConfig.AlarmAction.SEVERITY_CHANGED, AlarmNotificationRuleTriggerConfig.AlarmAction.ACKNOWLEDGED, AlarmNotificationRuleTriggerConfig.AlarmAction.CLEARED)); + notificationRule.setTriggerConfig(triggerConfig); + + EscalatedNotificationRuleRecipientsConfig recipientsConfig = new EscalatedNotificationRuleRecipientsConfig(); + recipientsConfig.setTriggerType(NotificationRuleTriggerType.ALARM); + Map> escalationTable = new HashMap<>(); + escalationTable.put(Integer.valueOf("1"), new ArrayList<>()); + recipientsConfig.setEscalationTable(escalationTable); + notificationRule.setRecipientsConfig(recipientsConfig); + return saveNotificationRule(notificationRule); + } + + private NotificationTemplate saveNotificationTemplate(NotificationTemplate notificationTemplate) { + return doPost("/api/notification/template", notificationTemplate, NotificationTemplate.class); + } + + private NotificationTarget saveNotificationTarget(NotificationTarget notificationTarget) { + return doPost("/api/notification/target", notificationTarget, NotificationTarget.class); + } + + private NotificationRule saveNotificationRule(NotificationRule notificationRule) { + return doPost("/api/notification/rule", notificationRule, NotificationRule.class); + } + +} diff --git a/application/src/test/java/org/thingsboard/server/edge/imitator/EdgeImitator.java b/application/src/test/java/org/thingsboard/server/edge/imitator/EdgeImitator.java index 12405a7d5f..d470ee686f 100644 --- a/application/src/test/java/org/thingsboard/server/edge/imitator/EdgeImitator.java +++ b/application/src/test/java/org/thingsboard/server/edge/imitator/EdgeImitator.java @@ -44,6 +44,9 @@ import org.thingsboard.server.gen.edge.v1.DownlinkResponseMsg; import org.thingsboard.server.gen.edge.v1.EdgeConfiguration; import org.thingsboard.server.gen.edge.v1.EntityDataProto; import org.thingsboard.server.gen.edge.v1.EntityViewUpdateMsg; +import org.thingsboard.server.gen.edge.v1.NotificationRuleUpdateMsg; +import org.thingsboard.server.gen.edge.v1.NotificationTargetUpdateMsg; +import org.thingsboard.server.gen.edge.v1.NotificationTemplateUpdateMsg; import org.thingsboard.server.gen.edge.v1.OAuth2UpdateMsg; import org.thingsboard.server.gen.edge.v1.OtaPackageUpdateMsg; import org.thingsboard.server.gen.edge.v1.QueueUpdateMsg; @@ -74,16 +77,16 @@ import java.util.stream.Collectors; @Slf4j public class EdgeImitator { - private String routingKey; - private String routingSecret; + private final String routingKey; + private final String routingSecret; - private EdgeRpcClient edgeRpcClient; + private final EdgeRpcClient edgeRpcClient; private final Lock lock = new ReentrantLock(); private CountDownLatch messagesLatch; private CountDownLatch responsesLatch; - private List> ignoredTypes; + private final List> ignoredTypes; @Setter private boolean randomFailuresOnTimeseriesDownlink = false; @@ -93,7 +96,7 @@ public class EdgeImitator { @Getter private EdgeConfiguration configuration; @Getter - private List downlinkMsgs; + private final List downlinkMsgs; @Getter private UplinkResponseMsg latestResponseMsg; @@ -320,6 +323,21 @@ public class EdgeImitator { result.add(saveDownlinkMsg(oAuth2UpdateMsg)); } } + if (downlinkMsg.getNotificationTemplateUpdateMsgCount() > 0) { + for (NotificationTemplateUpdateMsg notificationTemplateUpdateMsg : downlinkMsg.getNotificationTemplateUpdateMsgList()) { + result.add(saveDownlinkMsg(notificationTemplateUpdateMsg)); + } + } + if (downlinkMsg.getNotificationRuleUpdateMsgCount() > 0) { + for (NotificationRuleUpdateMsg notificationRuleUpdateMsg : downlinkMsg.getNotificationRuleUpdateMsgList()) { + result.add(saveDownlinkMsg(notificationRuleUpdateMsg)); + } + } + if (downlinkMsg.getNotificationTargetUpdateMsgCount() > 0) { + for (NotificationTargetUpdateMsg notificationTargetUpdateMsg : downlinkMsg.getNotificationTargetUpdateMsgList()) { + result.add(saveDownlinkMsg(notificationTargetUpdateMsg)); + } + } if (downlinkMsg.hasEdgeConfiguration()) { result.add(saveDownlinkMsg(downlinkMsg.getEdgeConfiguration())); } From fed295017d1a5895bc55ba65c4afb460b3f1c7e2 Mon Sep 17 00:00:00 2001 From: Andrii Landiak Date: Thu, 11 Apr 2024 17:31:51 +0300 Subject: [PATCH 55/80] Improve NotificationMsgConstructorImpl --- .../notification/NotificationMsgConstructorImpl.java | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/notification/NotificationMsgConstructorImpl.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/notification/NotificationMsgConstructorImpl.java index d7ec8d5c92..8340bee693 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/notification/NotificationMsgConstructorImpl.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/notification/NotificationMsgConstructorImpl.java @@ -35,9 +35,7 @@ public class NotificationMsgConstructorImpl implements NotificationMsgConstructo @Override public NotificationRuleUpdateMsg constructNotificationRuleUpdateMsg(UpdateMsgType msgType, NotificationRule notificationRule) { - return NotificationRuleUpdateMsg.newBuilder().setMsgType(msgType).setEntity(JacksonUtil.toString(notificationRule)) - .setIdMSB(notificationRule.getId().getId().getMostSignificantBits()) - .setIdLSB(notificationRule.getId().getId().getLeastSignificantBits()).build(); + return NotificationRuleUpdateMsg.newBuilder().setMsgType(msgType).setEntity(JacksonUtil.toString(notificationRule)).build(); } @Override @@ -50,9 +48,7 @@ public class NotificationMsgConstructorImpl implements NotificationMsgConstructo @Override public NotificationTargetUpdateMsg constructNotificationTargetUpdateMsg(UpdateMsgType msgType, NotificationTarget notificationTarget) { - return NotificationTargetUpdateMsg.newBuilder().setMsgType(msgType).setEntity(JacksonUtil.toString(notificationTarget)) - .setIdMSB(notificationTarget.getId().getId().getMostSignificantBits()) - .setIdLSB(notificationTarget.getId().getId().getLeastSignificantBits()).build(); + return NotificationTargetUpdateMsg.newBuilder().setMsgType(msgType).setEntity(JacksonUtil.toString(notificationTarget)).build(); } @Override @@ -65,9 +61,7 @@ public class NotificationMsgConstructorImpl implements NotificationMsgConstructo @Override public NotificationTemplateUpdateMsg constructNotificationTemplateUpdateMsg(UpdateMsgType msgType, NotificationTemplate notificationTemplate) { - return NotificationTemplateUpdateMsg.newBuilder().setMsgType(msgType).setEntity(JacksonUtil.toString(notificationTemplate)) - .setIdMSB(notificationTemplate.getId().getId().getMostSignificantBits()) - .setIdLSB(notificationTemplate.getId().getId().getLeastSignificantBits()).build(); + return NotificationTemplateUpdateMsg.newBuilder().setMsgType(msgType).setEntity(JacksonUtil.toString(notificationTemplate)).build(); } @Override From c4ba6411c834a9ae042700ae8c4078e18e0392ca Mon Sep 17 00:00:00 2001 From: Andrii Landiak Date: Thu, 11 Apr 2024 17:33:03 +0300 Subject: [PATCH 56/80] Add optional to int64 idMsg, idLsb for NotificationMsg --- common/edge-api/src/main/proto/edge.proto | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/common/edge-api/src/main/proto/edge.proto b/common/edge-api/src/main/proto/edge.proto index c3b37846fb..d2ff7a1a9f 100644 --- a/common/edge-api/src/main/proto/edge.proto +++ b/common/edge-api/src/main/proto/edge.proto @@ -474,22 +474,22 @@ message OAuth2UpdateMsg { message NotificationRuleUpdateMsg { UpdateMsgType msgType = 1; - int64 idMSB = 2; - int64 idLSB = 3; + optional int64 idMSB = 2; + optional int64 idLSB = 3; optional string entity = 4; } message NotificationTargetUpdateMsg { UpdateMsgType msgType = 1; - int64 idMSB = 2; - int64 idLSB = 3; + optional int64 idMSB = 2; + optional int64 idLSB = 3; optional string entity = 4; } message NotificationTemplateUpdateMsg { UpdateMsgType msgType = 1; - int64 idMSB = 2; - int64 idLSB = 3; + optional int64 idMSB = 2; + optional int64 idLSB = 3; optional string entity = 4; } From 69e38fe372db425cd5d34c82a679b2ec060f4df9 Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Thu, 11 Apr 2024 18:41:10 +0300 Subject: [PATCH 57/80] Fix API annotations --- .../server/controller/NotificationController.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/controller/NotificationController.java b/application/src/main/java/org/thingsboard/server/controller/NotificationController.java index 0d78348cbb..96f38daf35 100644 --- a/application/src/main/java/org/thingsboard/server/controller/NotificationController.java +++ b/application/src/main/java/org/thingsboard/server/controller/NotificationController.java @@ -16,6 +16,7 @@ package org.thingsboard.server.controller; import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -174,7 +175,7 @@ public class NotificationController extends BaseController { @RequestParam(required = false) String sortOrder, @Parameter(description = "To search for unread notifications only") @RequestParam(defaultValue = "false") boolean unreadOnly, - @ApiParam(value = "Delivery method", allowableValues = DELIVERY_METHOD_ALLOWABLE_VALUES) + @Parameter(description = "Delivery method", schema = @Schema(allowableValues = {DELIVERY_METHOD_ALLOWABLE_VALUES})) @RequestParam(defaultValue = "WEB") NotificationDeliveryMethod deliveryMethod, @AuthenticationPrincipal SecurityUser user) throws ThingsboardException { // no permissions @@ -187,7 +188,7 @@ public class NotificationController extends BaseController { AVAILABLE_FOR_ANY_AUTHORIZED_USER) @GetMapping("/notifications/unread/count") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") - public Integer getUnreadNotificationsCount(@ApiParam(value = "Delivery method", allowableValues = DELIVERY_METHOD_ALLOWABLE_VALUES) + public Integer getUnreadNotificationsCount(@Parameter(description = "Delivery method", schema = @Schema(allowableValues = {DELIVERY_METHOD_ALLOWABLE_VALUES})) @RequestParam(defaultValue = "MOBILE_APP") NotificationDeliveryMethod deliveryMethod, @AuthenticationPrincipal SecurityUser user) { return notificationService.countUnreadNotificationsByRecipientId(user.getTenantId(), deliveryMethod, user.getId()); @@ -210,7 +211,7 @@ public class NotificationController extends BaseController { AVAILABLE_FOR_ANY_AUTHORIZED_USER) @PutMapping("/notifications/read") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") - public void markAllNotificationsAsRead(@ApiParam(value = "Delivery method", allowableValues = DELIVERY_METHOD_ALLOWABLE_VALUES) + public void markAllNotificationsAsRead(@Parameter(description = "Delivery method", schema = @Schema(allowableValues = {DELIVERY_METHOD_ALLOWABLE_VALUES})) @RequestParam(defaultValue = "WEB") NotificationDeliveryMethod deliveryMethod, @AuthenticationPrincipal SecurityUser user) { // no permissions From d15f9da1f4b1f7df6739deff7dd1bb6a6c593f3c Mon Sep 17 00:00:00 2001 From: d2eight Date: Fri, 12 Apr 2024 17:25:49 +0300 Subject: [PATCH 58/80] Deleting an extra in in the Docker Commands/Launch Command pop-overs --- 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 1e21f28cda..89f5aebeab 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -2735,7 +2735,7 @@ "rpc-command-result": "Response", "rpc-command-edit-params": "Edit parameters", "gateway-configuration": "General Configuration", - "docker-label": "Use the following instruction to run IoT Gateway in in Docker compose with credentials for selected device", + "docker-label": "Use the following instruction to run IoT Gateway in Docker compose with credentials for selected device", "install-docker-compose": "Use the instructions to download, install and setup docker compose", "download-configuration-file": "Download configuration file", "download-docker-compose": "Download docker-compose.yml for your gateway", From 82ab52889bb6d1ee5fd600558f880020d1beaeed Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Fri, 12 Apr 2024 17:34:43 +0300 Subject: [PATCH 59/80] UI: Implement time series comparison widget settings. --- .../basic/basic-widget-config.module.ts | 8 +- .../chart/comparison-key-row.component.html | 37 +++++ .../chart/comparison-key-row.component.scss | 54 ++++++++ .../chart/comparison-key-row.component.ts | 131 ++++++++++++++++++ .../comparison-keys-table.component.html | 38 +++++ .../comparison-keys-table.component.scss | 53 +++++++ .../chart/comparison-keys-table.component.ts | 110 +++++++++++++++ ...e-series-chart-basic-config.component.html | 87 +++++++++--- ...ime-series-chart-basic-config.component.ts | 39 +++++- .../common/data-keys-panel.component.html | 6 +- .../basic/common/data-keys-panel.component.ts | 4 + .../widget/dynamic-widget.component.ts | 2 + .../lib/chart/time-series-chart.models.ts | 7 +- .../widget/lib/chart/time-series-chart.ts | 8 +- ...e-series-chart-key-settings.component.html | 29 ++++ ...ime-series-chart-key-settings.component.ts | 19 ++- ...eries-chart-widget-settings.component.html | 40 ++++++ ...-series-chart-widget-settings.component.ts | 20 ++- ...-chart-axis-settings-button.component.html | 28 ++++ ...es-chart-axis-settings-button.component.ts | 108 +++++++++++++++ ...-chart-axis-settings-panel.component.html} | 16 +-- ...-chart-axis-settings-panel.component.scss} | 8 +- ...es-chart-axis-settings-panel.component.ts} | 37 +++-- .../time-series-chart-y-axis-row.component.ts | 14 +- .../common/data-key-input.component.html | 4 +- .../common/data-key-input.component.scss | 7 + .../common/data-key-input.component.ts | 4 + .../common/widget-settings-common.module.ts | 13 +- .../home/models/widget-component.models.ts | 2 + .../assets/locale/locale.constant-en_US.json | 10 ++ ui-ngx/src/form.scss | 12 ++ 31 files changed, 890 insertions(+), 65 deletions(-) create mode 100644 ui-ngx/src/app/modules/home/components/widget/config/basic/chart/comparison-key-row.component.html create mode 100644 ui-ngx/src/app/modules/home/components/widget/config/basic/chart/comparison-key-row.component.scss create mode 100644 ui-ngx/src/app/modules/home/components/widget/config/basic/chart/comparison-key-row.component.ts create mode 100644 ui-ngx/src/app/modules/home/components/widget/config/basic/chart/comparison-keys-table.component.html create mode 100644 ui-ngx/src/app/modules/home/components/widget/config/basic/chart/comparison-keys-table.component.scss create mode 100644 ui-ngx/src/app/modules/home/components/widget/config/basic/chart/comparison-keys-table.component.ts create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-axis-settings-button.component.html create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-axis-settings-button.component.ts rename ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/{time-series-chart-y-axis-settings-panel.component.html => time-series-chart-axis-settings-panel.component.html} (69%) rename ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/{time-series-chart-y-axis-settings-panel.component.scss => time-series-chart-axis-settings-panel.component.scss} (89%) rename ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/{time-series-chart-y-axis-settings-panel.component.ts => time-series-chart-axis-settings-panel.component.ts} (56%) diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/basic-widget-config.module.ts b/ui-ngx/src/app/modules/home/components/widget/config/basic/basic-widget-config.module.ts index 2ff5142b9c..79ec4b03c7 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/basic-widget-config.module.ts +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/basic-widget-config.module.ts @@ -110,6 +110,10 @@ import { import { TimeSeriesChartBasicConfigComponent } from '@home/components/widget/config/basic/chart/time-series-chart-basic-config.component'; +import { ComparisonKeyRowComponent } from '@home/components/widget/config/basic/chart/comparison-key-row.component'; +import { + ComparisonKeysTableComponent +} from '@home/components/widget/config/basic/chart/comparison-keys-table.component'; @NgModule({ declarations: [ @@ -145,7 +149,9 @@ import { PowerButtonBasicConfigComponent, SliderBasicConfigComponent, ToggleButtonBasicConfigComponent, - TimeSeriesChartBasicConfigComponent + TimeSeriesChartBasicConfigComponent, + ComparisonKeyRowComponent, + ComparisonKeysTableComponent ], imports: [ CommonModule, diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/comparison-key-row.component.html b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/comparison-key-row.component.html new file mode 100644 index 0000000000..79b9ecad15 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/comparison-key-row.component.html @@ -0,0 +1,37 @@ + +
+ + + +
+ + + +
+
+ + +
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/comparison-key-row.component.scss b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/comparison-key-row.component.scss new file mode 100644 index 0000000000..a71d07371e --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/comparison-key-row.component.scss @@ -0,0 +1,54 @@ +/** + * 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 '../../../../../../../../scss/constants'; + +.tb-comparison-key-row { + .tb-show-field { + width: 40px; + min-width: 40px; + } + + .tb-data-key-input { + flex: 1; + @media #{$mat-gt-xs} { + min-width: 100px; + flex: 1 1 40%; + } + } + + .tb-label-field, .tb-color-field { + display: flex; + flex-direction: row; + place-content: center; + align-items: center; + .tb-inline-field { + flex: 1; + } + } + + .tb-label-field { + flex: 1; + @media #{$mat-gt-xs} { + min-width: 150px; + flex: 1 1 60%; + } + } + + .tb-color-field { + width: 40px; + min-width: 40px; + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/comparison-key-row.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/comparison-key-row.component.ts new file mode 100644 index 0000000000..8836a31f8b --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/comparison-key-row.component.ts @@ -0,0 +1,131 @@ +/// +/// 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 { ChangeDetectorRef, Component, forwardRef, Input, OnInit, ViewEncapsulation } from '@angular/core'; +import { + ControlValueAccessor, + NG_VALUE_ACCESSOR, + UntypedFormBuilder, + UntypedFormControl, + UntypedFormGroup +} from '@angular/forms'; +import { + DataKey, + DataKeyComparisonSettings, + DataKeySettingsWithComparison, + DatasourceType +} from '@shared/models/widget.models'; +import { deepClone } from '@core/utils'; + +@Component({ + selector: 'tb-comparison-key-row', + templateUrl: './comparison-key-row.component.html', + styleUrls: ['./comparison-key-row.component.scss', '../../data-keys.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => ComparisonKeyRowComponent), + multi: true + } + ], + encapsulation: ViewEncapsulation.None +}) +export class ComparisonKeyRowComponent implements ControlValueAccessor, OnInit { + + @Input() + disabled: boolean; + + @Input() + datasourceType: DatasourceType; + + keyFormControl: UntypedFormControl; + + keyRowFormGroup: UntypedFormGroup; + + modelValue: DataKey; + + private propagateChange = (_val: any) => {}; + + constructor(private fb: UntypedFormBuilder, + private cd: ChangeDetectorRef) { + } + + ngOnInit() { + this.keyFormControl = this.fb.control(null, []); + this.keyRowFormGroup = this.fb.group({ + showValuesForComparison: [null, []], + comparisonValuesLabel: [null, []], + color: [null, []] + }); + this.keyRowFormGroup.valueChanges.subscribe( + () => this.updateModel() + ); + this.keyRowFormGroup.get('showValuesForComparison').valueChanges.subscribe(() => this.updateValidators()); + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(_fn: any): void { + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (isDisabled) { + this.keyFormControl.disable({emitEvent: false}); + this.keyRowFormGroup.disable({emitEvent: false}); + } else { + this.keyFormControl.enable({emitEvent: false}); + this.keyRowFormGroup.enable({emitEvent: false}); + this.updateValidators(); + } + } + + writeValue(value: DataKey): void { + this.modelValue = value; + const comparisonSettings = (value?.settings as DataKeySettingsWithComparison)?.comparisonSettings; + this.keyRowFormGroup.patchValue( + comparisonSettings, {emitEvent: false} + ); + this.keyFormControl.patchValue(deepClone(this.modelValue), {emitEvent: false}); + this.updateValidators(); + this.cd.markForCheck(); + } + + private updateValidators() { + const showValuesForComparison: boolean = this.keyRowFormGroup.get('showValuesForComparison').value; + if (showValuesForComparison) { + this.keyFormControl.enable({emitEvent: false}); + this.keyRowFormGroup.get('comparisonValuesLabel').enable({emitEvent: false}); + this.keyRowFormGroup.get('color').enable({emitEvent: false}); + } else { + this.keyFormControl.disable({emitEvent: false}); + this.keyRowFormGroup.get('comparisonValuesLabel').disable({emitEvent: false}); + this.keyRowFormGroup.get('color').disable({emitEvent: false}); + } + } + + private updateModel() { + const comparisonSettings: DataKeyComparisonSettings = this.keyRowFormGroup.value; + if (!this.modelValue.settings) { + this.modelValue.settings = {}; + } + this.modelValue.settings.comparisonSettings = comparisonSettings; + this.propagateChange(this.modelValue); + } + +} diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/comparison-keys-table.component.html b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/comparison-keys-table.component.html new file mode 100644 index 0000000000..a202a81cf6 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/comparison-keys-table.component.html @@ -0,0 +1,38 @@ + +
+
+
widgets.time-series-chart.comparison.show
+
datakey.key
+
datakey.label
+
datakey.color
+
+
+
+ + +
+
+
+ + {{ 'widgets.chart.no-series' | translate }} + diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/comparison-keys-table.component.scss b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/comparison-keys-table.component.scss new file mode 100644 index 0000000000..1d9689a333 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/comparison-keys-table.component.scss @@ -0,0 +1,53 @@ +/** + * 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 '../../../../../../../../scss/constants'; + +.tb-comparison-keys-table { + .tb-form-table-header-cell { + + &.tb-show-header { + width: 40px; + min-width: 40px; + } + + &.tb-key-header { + flex: 1; + @media #{$mat-gt-xs} { + min-width: 100px; + flex: 1 1 40%; + } + } + + &.tb-label-header { + flex: 1; + @media #{$mat-gt-xs} { + min-width: 150px; + flex: 1 1 60%; + } + } + + &.tb-color-header { + width: 40px; + min-width: 40px; + } + } + + .tb-form-table-body { + tb-comparison-key-row { + overflow: hidden; + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/comparison-keys-table.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/comparison-keys-table.component.ts new file mode 100644 index 0000000000..b8241531b3 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/comparison-keys-table.component.ts @@ -0,0 +1,110 @@ +/// +/// 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 { Component, forwardRef, Input, OnInit, ViewEncapsulation } from '@angular/core'; +import { + AbstractControl, + ControlValueAccessor, + NG_VALUE_ACCESSOR, + UntypedFormArray, + UntypedFormBuilder, + UntypedFormGroup +} from '@angular/forms'; +import { DataKey, DatasourceType } from '@shared/models/widget.models'; + +@Component({ + selector: 'tb-comparison-keys-table', + templateUrl: './comparison-keys-table.component.html', + styleUrls: ['./comparison-keys-table.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => ComparisonKeysTableComponent), + multi: true + } + ], + encapsulation: ViewEncapsulation.None +}) +export class ComparisonKeysTableComponent implements ControlValueAccessor, OnInit { + + @Input() + disabled: boolean; + + @Input() + datasourceType: DatasourceType; + + keysListFormGroup: UntypedFormGroup; + + get noKeys(): boolean { + const keys: DataKey[] = this.keysListFormGroup.get('keys').value; + return keys.length === 0; + } + + private propagateChange = (_val: any) => {}; + + constructor(private fb: UntypedFormBuilder) { + } + + ngOnInit() { + this.keysListFormGroup = this.fb.group({ + keys: [this.fb.array([]), []] + }); + this.keysListFormGroup.valueChanges.subscribe( + () => { + const keys: DataKey[] = this.keysListFormGroup.get('keys').value; + this.propagateChange(keys); + } + ); + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(_fn: any): void { + } + + setDisabledState(isDisabled: boolean): void { + if (isDisabled) { + this.keysListFormGroup.disable({emitEvent: false}); + } else { + this.keysListFormGroup.enable({emitEvent: false}); + } + } + + writeValue(value: DataKey[] | undefined): void { + this.keysListFormGroup.setControl('keys', this.prepareKeysFormArray(value), {emitEvent: false}); + } + + keysFormArray(): UntypedFormArray { + return this.keysListFormGroup.get('keys') as UntypedFormArray; + } + + trackByKey(_index: number, keyControl: AbstractControl): any { + return keyControl; + } + + private prepareKeysFormArray(keys: DataKey[] | undefined): UntypedFormArray { + const keysControls: Array = []; + if (keys) { + keys.forEach((key) => { + keysControls.push(this.fb.control(key, [])); + }); + } + return this.fb.array(keysControls); + } + +} diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/time-series-chart-basic-config.component.html b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/time-series-chart-basic-config.component.html index 80e13a96a2..bf88f77b2f 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/time-series-chart-basic-config.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/time-series-chart-basic-config.component.html @@ -25,22 +25,77 @@ forceSingleDatasource formControlName="datasources"> - - +
+
+
{{ 'widgets.chart.series' | translate }}
+ + {{ 'widgets.chart.series' | translate }} + {{ 'widgets.time-series-chart.comparison.comparison' | translate }} + +
+ + +
+ + {{ 'widgets.time-series-chart.comparison.comparison' | translate }} + +
+ + + + {{ 'widgets.chart.time-for-comparison-previous-interval' | translate }} + + + {{ 'widgets.chart.time-for-comparison-days' | translate }} + + + {{ 'widgets.chart.time-for-comparison-weeks' | translate }} + + + {{ 'widgets.chart.time-for-comparison-months' | translate }} + + + {{ 'widgets.chart.time-for-comparison-years' | translate }} + + + {{ 'widgets.chart.time-for-comparison-custom-interval' | translate }} + + + + + + + + +
+
+ + +
diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/time-series-chart-basic-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/time-series-chart-basic-config.component.ts index 508709a3f9..789f3e9c33 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/time-series-chart-basic-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/time-series-chart-basic-config.component.ts @@ -23,6 +23,7 @@ import { WidgetConfigComponentData } from '@home/models/widget-component.models' import { DataKey, Datasource, + DatasourceType, legendPositions, legendPositionTranslationMap, WidgetConfig, @@ -89,6 +90,8 @@ export class TimeSeriesChartBasicConfigComponent extends BasicWidgetConfigCompon chartType: TimeSeriesChartType = TimeSeriesChartType.default; + seriesMode = 'series'; + constructor(protected store: Store, protected widgetConfigComponent: WidgetConfigComponent, private $injector: Injector, @@ -105,6 +108,11 @@ export class TimeSeriesChartBasicConfigComponent extends BasicWidgetConfigCompon } } + seriesModeChange(seriesMode: string) { + this.seriesMode = seriesMode; + this.updateSeriesState(); + } + protected configForm(): UntypedFormGroup { return this.timeSeriesChartWidgetConfigForm; } @@ -135,6 +143,7 @@ export class TimeSeriesChartBasicConfigComponent extends BasicWidgetConfigCompon comparisonEnabled: [settings.comparisonEnabled, []], timeForComparison: [settings.timeForComparison, []], comparisonCustomIntervalValue: [settings.comparisonCustomIntervalValue, [Validators.min(0)]], + comparisonXAxis: [settings.comparisonXAxis, []], thresholds: [settings.thresholds, []], @@ -187,6 +196,7 @@ export class TimeSeriesChartBasicConfigComponent extends BasicWidgetConfigCompon if (this.chartType === TimeSeriesChartType.state) { this.timeSeriesChartWidgetConfigForm.addControl('states', this.fb.control(settings.states, [])); } + this.timeSeriesChartWidgetConfigForm.get('comparisonEnabled').valueChanges.subscribe(() => this.updateSeriesState()); } protected prepareOutputConfig(config: any): WidgetConfigComponentData { @@ -209,6 +219,7 @@ export class TimeSeriesChartBasicConfigComponent extends BasicWidgetConfigCompon this.widgetConfig.config.settings.comparisonEnabled = config.comparisonEnabled; this.widgetConfig.config.settings.timeForComparison = config.timeForComparison; this.widgetConfig.config.settings.comparisonCustomIntervalValue = config.comparisonCustomIntervalValue; + this.widgetConfig.config.settings.comparisonXAxis = config.comparisonXAxis; this.widgetConfig.config.settings.thresholds = config.thresholds; @@ -254,16 +265,27 @@ export class TimeSeriesChartBasicConfigComponent extends BasicWidgetConfigCompon } protected validatorTriggers(): string[] { - return ['showTitle', 'showIcon', 'showLegend', 'showTooltip', 'tooltipShowDate']; + return ['comparisonEnabled', 'showTitle', 'showIcon', 'showLegend', 'showTooltip', 'tooltipShowDate']; } protected updateValidators(emitEvent: boolean, trigger?: string) { + const comparisonEnabled: boolean = this.timeSeriesChartWidgetConfigForm.get('comparisonEnabled').value; const showTitle: boolean = this.timeSeriesChartWidgetConfigForm.get('showTitle').value; const showIcon: boolean = this.timeSeriesChartWidgetConfigForm.get('showIcon').value; const showLegend: boolean = this.timeSeriesChartWidgetConfigForm.get('showLegend').value; const showTooltip: boolean = this.timeSeriesChartWidgetConfigForm.get('showTooltip').value; const tooltipShowDate: boolean = this.timeSeriesChartWidgetConfigForm.get('tooltipShowDate').value; + if (comparisonEnabled) { + this.timeSeriesChartWidgetConfigForm.get('timeForComparison').enable(); + this.timeSeriesChartWidgetConfigForm.get('comparisonCustomIntervalValue').enable(); + this.timeSeriesChartWidgetConfigForm.get('comparisonXAxis').enable(); + } else { + this.timeSeriesChartWidgetConfigForm.get('timeForComparison').disable(); + this.timeSeriesChartWidgetConfigForm.get('comparisonCustomIntervalValue').disable(); + this.timeSeriesChartWidgetConfigForm.get('comparisonXAxis').disable(); + } + if (showTitle) { this.timeSeriesChartWidgetConfigForm.get('title').enable(); this.timeSeriesChartWidgetConfigForm.get('titleFont').enable(); @@ -345,6 +367,19 @@ export class TimeSeriesChartBasicConfigComponent extends BasicWidgetConfigCompon } } + private updateSeriesState() { + if (this.seriesMode === 'series') { + this.timeSeriesChartWidgetConfigForm.get('series').enable({emitEvent: false}); + } else { + const comparisonEnabled = this.timeSeriesChartWidgetConfigForm.get('comparisonEnabled').value; + if (comparisonEnabled) { + this.timeSeriesChartWidgetConfigForm.get('series').enable({emitEvent: false}); + } else { + this.timeSeriesChartWidgetConfigForm.get('series').disable({emitEvent: false}); + } + } + } + private removeYaxisId(series: DataKey[], yAxisId: TimeSeriesChartYAxisId): boolean { let changed = false; if (series) { @@ -381,4 +416,6 @@ export class TimeSeriesChartBasicConfigComponent extends BasicWidgetConfigCompon processor.update(Date.now()); return processor.formatted; } + + protected readonly DatasourceType = DatasourceType; } diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-keys-panel.component.html b/ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-keys-panel.component.html index f140bd7445..59a3bcb3cb 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-keys-panel.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-keys-panel.component.html @@ -15,8 +15,10 @@ limitations under the License. --> -
-
{{ panelTitle }}
+
+
{{ panelTitle }}
datakey.source
diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-keys-panel.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-keys-panel.component.ts index 86367771de..67ed19b2d2 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-keys-panel.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-keys-panel.component.ts @@ -96,6 +96,10 @@ export class DataKeysPanelComponent implements ControlValueAccessor, OnInit, OnC @Input() deviceId: string; + @Input() + @coerceBoolean() + hidePanel = false; + @Input() @coerceBoolean() hideDataKeyColor = false; diff --git a/ui-ngx/src/app/modules/home/components/widget/dynamic-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/dynamic-widget.component.ts index 6cef868e66..d047abdec5 100644 --- a/ui-ngx/src/app/modules/home/components/widget/dynamic-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/dynamic-widget.component.ts @@ -49,6 +49,7 @@ import { TbInject } from '@shared/decorators/tb-inject'; import { MillisecondsToTimeStringPipe } from '@shared/pipe/milliseconds-to-time-string.pipe'; import { UserSettingsService } from '@core/http/user-settings.service'; import { ImagePipe } from '@shared/pipe/image.pipe'; +import { UtilsService } from '@core/services/utils.service'; @Directive() // eslint-disable-next-line @angular-eslint/directive-class-suffix @@ -86,6 +87,7 @@ export class DynamicWidgetComponent extends PageComponent implements IDynamicWid this.ctx.customDialog = $injector.get(CustomDialogService); this.ctx.resourceService = $injector.get(ResourceService); this.ctx.userSettingsService = $injector.get(UserSettingsService); + this.ctx.utilsService = $injector.get(UtilsService); this.ctx.telemetryWsService = $injector.get(TelemetryWebsocketService); this.ctx.date = $injector.get(DatePipe); this.ctx.imagePipe = $injector.get(ImagePipe); diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.models.ts index db3831f6a3..6131aa0f1d 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.models.ts @@ -71,6 +71,7 @@ import { DatePipe } from '@angular/common'; import { BuiltinTextPosition } from 'zrender/src/core/types'; import { CartesianAxisOption } from 'echarts/types/src/coord/cartesian/AxisModel'; import { WidgetTimewindow } from '@shared/models/time/time.models'; +import { UtilsService } from '@core/services/utils.service'; export enum TimeSeriesChartType { default = 'default', @@ -932,6 +933,7 @@ export interface TimeSeriesChartXAxis extends TimeSeriesChartAxis { export const createTimeSeriesYAxis = (units: string, decimals: number, settings: TimeSeriesChartYAxisSettings, + utils: UtilsService, darkMode: boolean): TimeSeriesChartYAxis => { const yAxisTickLabelStyle = createChartTextStyle(settings.tickLabelFont, settings.tickLabelColor, darkMode, 'axis.tickLabel'); @@ -986,7 +988,7 @@ export const createTimeSeriesYAxis = (units: string, splitNumber, interval, ticksGenerator, - name: settings.label, + name: utils.customTranslation(settings.label, settings.label), nameLocation: 'middle', nameRotate: settings.position === AxisPosition.left ? 90 : -90, nameTextStyle: { @@ -1044,6 +1046,7 @@ export const createTimeSeriesXAxis = (id: string, settings: TimeSeriesChartXAxisSettings, min: number, max: number, datePipe: DatePipe, + utils: UtilsService, darkMode: boolean): TimeSeriesChartXAxis => { const xAxisTickLabelStyle = createChartTextStyle(settings.tickLabelFont, settings.tickLabelColor, darkMode, 'axis.tickLabel'); @@ -1060,7 +1063,7 @@ export const createTimeSeriesXAxis = (id: string, scale: true, position: settings.position, id, - name: settings.label, + name: utils.customTranslation(settings.label, settings.label), nameLocation: 'middle', nameTextStyle: { color: xAxisNameStyle.color, diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.ts b/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.ts index 89aa599afb..6fd6da0199 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.ts @@ -170,7 +170,7 @@ export class TbTimeSeriesChart { this.comparisonEnabled = !!this.ctx.defaultSubscription.comparisonEnabled; this.stackMode = !this.comparisonEnabled && this.settings.stack; if (this.settings.states && this.settings.states.length) { - this.stateValueConverter = new TimeSeriesChartStateValueConverter(this.ctx.dashboard.utils, this.settings.states); + this.stateValueConverter = new TimeSeriesChartStateValueConverter(this.ctx.utilsService, this.settings.states); this.tooltipValueFormatFunction = this.stateValueConverter.tooltipFormatter; } const $dashboardPageElement = this.ctx.$containerParent.parents('.tb-dashboard-page'); @@ -503,12 +503,12 @@ export class TbTimeSeriesChart { private setupXAxes(): void { const mainXAxis = createTimeSeriesXAxis('main', this.settings.xAxis, this.ctx.defaultSubscription.timeWindow.minTime, - this.ctx.defaultSubscription.timeWindow.maxTime, this.ctx.date, this.darkMode); + this.ctx.defaultSubscription.timeWindow.maxTime, this.ctx.date, this.ctx.utilsService, this.darkMode); this.xAxisList.push(mainXAxis); if (this.comparisonEnabled) { const comparisonXAxis = createTimeSeriesXAxis('comparison', this.settings.comparisonXAxis, this.ctx.defaultSubscription.comparisonTimeWindow.minTime, this.ctx.defaultSubscription.comparisonTimeWindow.maxTime, - this.ctx.date, this.darkMode); + this.ctx.date, this.ctx.utilsService, this.darkMode); this.xAxisList.push(comparisonXAxis); } } @@ -526,7 +526,7 @@ export class TbTimeSeriesChart { axisSettings.ticksGenerator = this.stateValueConverter.ticksGenerator; axisSettings.ticksFormatter = this.stateValueConverter.ticksFormatter; } - const yAxis = createTimeSeriesYAxis(units, decimals, axisSettings, this.darkMode); + const yAxis = createTimeSeriesYAxis(units, decimals, axisSettings, this.ctx.utilsService, this.darkMode); this.yAxisList.push(yAxis); } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-key-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-key-settings.component.html index 6d015e04fa..a265a1b431 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-key-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-key-settings.component.html @@ -66,6 +66,35 @@ helpId="widget/lib/flot/tooltip_value_format_fn">
+
+
widgets.time-series-chart.comparison.settings
+ + + + + {{ 'widgets.time-series-chart.comparison.show-values-for-comparison' | translate }} + + + + +
+
widgets.time-series-chart.comparison.comparison-values-label
+ + + +
+
+
{{ 'widgets.time-series-chart.comparison.comparison-data-color' | translate }}
+ + +
+
+
+
{{ timeSeriesChartTypeTranslations.get(chartType) | translate }}
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-key-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-key-settings.component.ts index b2495f5dda..229607d7f3 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-key-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-key-settings.component.ts @@ -54,6 +54,8 @@ export class TimeSeriesChartKeySettingsComponent extends WidgetSettingsComponent yAxisIds: TimeSeriesChartYAxisId[]; + comparisonEnabled: boolean; + functionScopeVariables = this.widgetService.getWidgetScopeVariables(); constructor(protected store: Store, @@ -73,6 +75,7 @@ export class TimeSeriesChartKeySettingsComponent extends WidgetSettingsComponent } const widgetSettings = (widgetConfig.config?.settings || {}) as TimeSeriesChartWidgetSettings; this.yAxisIds = widgetSettings.yAxes ? Object.keys(widgetSettings.yAxes) : ['default']; + this.comparisonEnabled = !!widgetSettings.comparisonEnabled; } protected defaultSettings(): WidgetSettings { @@ -103,12 +106,14 @@ export class TimeSeriesChartKeySettingsComponent extends WidgetSettingsComponent } protected validatorTriggers(): string[] { - return ['showInLegend', 'type']; + return ['showInLegend', 'type', 'comparisonSettings.showValuesForComparison']; } protected updateValidators(_emitEvent: boolean) { const showInLegend: boolean = this.timeSeriesChartKeySettingsForm.get('showInLegend').value; const type: TimeSeriesChartSeriesType = this.timeSeriesChartKeySettingsForm.get('type').value; + const showValuesForComparison: boolean = + this.timeSeriesChartKeySettingsForm.get('comparisonSettings').get('showValuesForComparison').value; if (showInLegend) { this.timeSeriesChartKeySettingsForm.get('dataHiddenByDefault').enable(); } else { @@ -122,5 +127,17 @@ export class TimeSeriesChartKeySettingsComponent extends WidgetSettingsComponent this.timeSeriesChartKeySettingsForm.get('lineSettings').disable(); this.timeSeriesChartKeySettingsForm.get('barSettings').enable(); } + if (this.comparisonEnabled) { + this.timeSeriesChartKeySettingsForm.get('comparisonSettings').enable({emitEvent: false}); + if (showValuesForComparison) { + this.timeSeriesChartKeySettingsForm.get('comparisonSettings').get('comparisonValuesLabel').enable(); + this.timeSeriesChartKeySettingsForm.get('comparisonSettings').get('color').enable(); + } else { + this.timeSeriesChartKeySettingsForm.get('comparisonSettings').get('comparisonValuesLabel').disable(); + this.timeSeriesChartKeySettingsForm.get('comparisonSettings').get('color').disable(); + } + } else { + this.timeSeriesChartKeySettingsForm.get('comparisonSettings').disable({emitEvent: false}); + } } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-widget-settings.component.html index d98f9ee319..5afcd9c860 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-widget-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-widget-settings.component.html @@ -16,6 +16,46 @@ --> +
+
+ + {{ 'widgets.time-series-chart.comparison.comparison' | translate }} + +
+ + + + {{ 'widgets.chart.time-for-comparison-previous-interval' | translate }} + + + {{ 'widgets.chart.time-for-comparison-days' | translate }} + + + {{ 'widgets.chart.time-for-comparison-weeks' | translate }} + + + {{ 'widgets.chart.time-for-comparison-months' | translate }} + + + {{ 'widgets.chart.time-for-comparison-years' | translate }} + + + {{ 'widgets.chart.time-for-comparison-custom-interval' | translate }} + + + + + + + + +
+
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-widget-settings.component.ts index 01ecca2944..6dff6e1496 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-widget-settings.component.ts @@ -23,7 +23,7 @@ import { WidgetSettings, WidgetSettingsComponent } from '@shared/models/widget.models'; -import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; +import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; import { formatValue, isDefinedAndNotNull, mergeDeep } from '@core/utils'; @@ -114,6 +114,11 @@ export class TimeSeriesChartWidgetSettingsComponent extends WidgetSettingsCompon protected onSettingsSet(settings: WidgetSettings) { this.timeSeriesChartWidgetSettingsForm = this.fb.group({ + comparisonEnabled: [settings.comparisonEnabled, []], + timeForComparison: [settings.timeForComparison, []], + comparisonCustomIntervalValue: [settings.comparisonCustomIntervalValue, [Validators.min(0)]], + comparisonXAxis: [settings.comparisonXAxis, []], + yAxes: [settings.yAxes, []], thresholds: [settings.thresholds, []], @@ -154,14 +159,25 @@ export class TimeSeriesChartWidgetSettingsComponent extends WidgetSettingsCompon } protected validatorTriggers(): string[] { - return ['showLegend', 'showTooltip', 'tooltipShowDate']; + return ['comparisonEnabled', 'showLegend', 'showTooltip', 'tooltipShowDate']; } protected updateValidators(emitEvent: boolean) { + const comparisonEnabled: boolean = this.timeSeriesChartWidgetSettingsForm.get('comparisonEnabled').value; const showLegend: boolean = this.timeSeriesChartWidgetSettingsForm.get('showLegend').value; const showTooltip: boolean = this.timeSeriesChartWidgetSettingsForm.get('showTooltip').value; const tooltipShowDate: boolean = this.timeSeriesChartWidgetSettingsForm.get('tooltipShowDate').value; + if (comparisonEnabled) { + this.timeSeriesChartWidgetSettingsForm.get('timeForComparison').enable(); + this.timeSeriesChartWidgetSettingsForm.get('comparisonCustomIntervalValue').enable(); + this.timeSeriesChartWidgetSettingsForm.get('comparisonXAxis').enable(); + } else { + this.timeSeriesChartWidgetSettingsForm.get('timeForComparison').disable(); + this.timeSeriesChartWidgetSettingsForm.get('comparisonCustomIntervalValue').disable(); + this.timeSeriesChartWidgetSettingsForm.get('comparisonXAxis').disable(); + } + if (showLegend) { this.timeSeriesChartWidgetSettingsForm.get('legendLabelFont').enable(); this.timeSeriesChartWidgetSettingsForm.get('legendLabelColor').enable(); diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-axis-settings-button.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-axis-settings-button.component.html new file mode 100644 index 0000000000..cd7a26441a --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-axis-settings-button.component.html @@ -0,0 +1,28 @@ + + diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-axis-settings-button.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-axis-settings-button.component.ts new file mode 100644 index 0000000000..7072d7051f --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-axis-settings-button.component.ts @@ -0,0 +1,108 @@ +/// +/// 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 { Component, forwardRef, Input, OnInit, Renderer2, ViewContainerRef } from '@angular/core'; +import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; +import { MatButton } from '@angular/material/button'; +import { TbPopoverService } from '@shared/components/popover.service'; +import { coerceBoolean } from '@shared/decorators/coercion'; +import { TimeSeriesChartAxisSettings } from '@home/components/widget/lib/chart/time-series-chart.models'; +import { + TimeSeriesChartAxisSettingsPanelComponent +} from '@home/components/widget/lib/settings/common/chart/time-series-chart-axis-settings-panel.component'; + +@Component({ + selector: 'tb-time-series-chart-axis-settings-button', + templateUrl: './time-series-chart-axis-settings-button.component.html', + styleUrls: [], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => TimeSeriesChartAxisSettingsButtonComponent), + multi: true + } + ] +}) +export class TimeSeriesChartAxisSettingsButtonComponent implements OnInit, ControlValueAccessor { + + @Input() + disabled: boolean; + + @Input() + axisType: 'xAxis' | 'yAxis' = 'xAxis'; + + @Input() + panelTitle: string; + + @Input() + @coerceBoolean() + advanced = false; + + private modelValue: TimeSeriesChartAxisSettings; + + private propagateChange = null; + + constructor(private popoverService: TbPopoverService, + private renderer: Renderer2, + private viewContainerRef: ViewContainerRef) {} + + ngOnInit(): void { + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(_fn: any): void { + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + } + + writeValue(value: TimeSeriesChartAxisSettings): void { + this.modelValue = value; + } + + openAxisSettingsPopup($event: Event, matButton: MatButton) { + if ($event) { + $event.stopPropagation(); + } + const trigger = matButton._elementRef.nativeElement; + if (this.popoverService.hasPopover(trigger)) { + this.popoverService.hidePopover(trigger); + } else { + const ctx: any = { + axisSettings: this.modelValue, + axisType: this.axisType, + panelTitle: this.panelTitle, + advanced: this.advanced + }; + const axisSettingsPanelPopover = this.popoverService.displayPopover(trigger, this.renderer, + this.viewContainerRef, TimeSeriesChartAxisSettingsPanelComponent, ['leftOnly', 'leftTopOnly', 'leftBottomOnly'], true, null, + ctx, + {}, + {}, {}, true); + axisSettingsPanelPopover.tbComponentRef.instance.popover = axisSettingsPanelPopover; + axisSettingsPanelPopover.tbComponentRef.instance.axisSettingsApplied.subscribe((axisSettings) => { + axisSettingsPanelPopover.hide(); + this.modelValue = axisSettings; + this.propagateChange(this.modelValue); + }); + } + } + +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axis-settings-panel.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-axis-settings-panel.component.html similarity index 69% rename from ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axis-settings-panel.component.html rename to ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-axis-settings-panel.component.html index 759074b4be..dbc74d9ed1 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axis-settings-panel.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-axis-settings-panel.component.html @@ -15,17 +15,17 @@ limitations under the License. --> -
-
{{ 'widgets.time-series-chart.axis.y-axis-settings' | translate }}
-
+
+
{{ panelTitle }}
+
+ [axisType]="axisType">
-
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axis-settings-panel.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-axis-settings-panel.component.scss similarity index 89% rename from ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axis-settings-panel.component.scss rename to ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-axis-settings-panel.component.scss index 095c38b7f7..853057c24e 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axis-settings-panel.component.scss +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-axis-settings-panel.component.scss @@ -15,7 +15,7 @@ */ @import '../../../../../../../../../scss/constants'; -.tb-y-axis-settings-panel { +.tb-axis-settings-panel { width: 530px; display: flex; flex-direction: column; @@ -23,7 +23,7 @@ @media #{$mat-lt-md} { width: 90vw; } - .tb-y-axis-settings-panel-content { + .tb-axis-settings-panel-content { display: flex; flex-direction: column; gap: 16px; @@ -31,14 +31,14 @@ margin: -10px; padding: 10px; } - .tb-y-axis-settings-title { + .tb-axis-settings-title { font-size: 16px; font-weight: 500; line-height: 24px; letter-spacing: 0.25px; color: rgba(0, 0, 0, 0.87); } - .tb-y-axis-settings-panel-buttons { + .tb-axis-settings-panel-buttons { height: 40px; display: flex; flex-direction: row; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axis-settings-panel.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-axis-settings-panel.component.ts similarity index 56% rename from ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axis-settings-panel.component.ts rename to ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-axis-settings-panel.component.ts index 8046aafd29..ebfbcf84a1 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axis-settings-panel.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-axis-settings-panel.component.ts @@ -17,40 +17,49 @@ import { Component, EventEmitter, Input, OnInit, Output, ViewEncapsulation } from '@angular/core'; import { TbPopoverComponent } from '@shared/components/popover.component'; import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; -import { TimeSeriesChartYAxisSettings } from '@home/components/widget/lib/chart/time-series-chart.models'; +import { + TimeSeriesChartAxisSettings, + TimeSeriesChartYAxisSettings +} from '@home/components/widget/lib/chart/time-series-chart.models'; import { coerceBoolean } from '@shared/decorators/coercion'; @Component({ - selector: 'tb-time-series-chart-y-axis-settings-panel', - templateUrl: './time-series-chart-y-axis-settings-panel.component.html', + selector: 'tb-time-series-chart-axis-settings-panel', + templateUrl: './time-series-chart-axis-settings-panel.component.html', providers: [], - styleUrls: ['./time-series-chart-y-axis-settings-panel.component.scss'], + styleUrls: ['./time-series-chart-axis-settings-panel.component.scss'], encapsulation: ViewEncapsulation.None }) -export class TimeSeriesChartYAxisSettingsPanelComponent implements OnInit { +export class TimeSeriesChartAxisSettingsPanelComponent implements OnInit { @Input() - yAxisSettings: TimeSeriesChartYAxisSettings; + axisType: 'xAxis' | 'yAxis' = 'xAxis'; + + @Input() + panelTitle: string; + + @Input() + axisSettings: TimeSeriesChartAxisSettings; @Input() @coerceBoolean() advanced = false; @Input() - popover: TbPopoverComponent; + popover: TbPopoverComponent; @Output() - yAxisSettingsApplied = new EventEmitter(); + axisSettingsApplied = new EventEmitter(); - yAxisSettingsFormGroup: UntypedFormGroup; + axisSettingsFormGroup: UntypedFormGroup; constructor(private fb: UntypedFormBuilder) { } ngOnInit(): void { - this.yAxisSettingsFormGroup = this.fb.group( + this.axisSettingsFormGroup = this.fb.group( { - yAxis: [this.yAxisSettings, []] + axis: [this.axisSettings, []] } ); } @@ -59,8 +68,8 @@ export class TimeSeriesChartYAxisSettingsPanelComponent implements OnInit { this.popover?.hide(); } - applyYAxisSettings() { - const yAxisSettings = this.yAxisSettingsFormGroup.get('yAxis').getRawValue(); - this.yAxisSettingsApplied.emit(yAxisSettings); + applyAxisSettings() { + const axisSettings = this.axisSettingsFormGroup.get('axis').getRawValue(); + this.axisSettingsApplied.emit(axisSettings); } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axis-row.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axis-row.component.ts index a4824fd51e..3fcde90cef 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axis-row.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axis-row.component.ts @@ -36,9 +36,10 @@ import { MatButton } from '@angular/material/button'; import { TbPopoverService } from '@shared/components/popover.service'; import { coerceBoolean } from '@shared/decorators/coercion'; import { - TimeSeriesChartYAxisSettingsPanelComponent -} from '@home/components/widget/lib/settings/common/chart/time-series-chart-y-axis-settings-panel.component'; + TimeSeriesChartAxisSettingsPanelComponent +} from '@home/components/widget/lib/settings/common/chart/time-series-chart-axis-settings-panel.component'; import { deepClone } from '@core/utils'; +import { TranslateService } from '@ngx-translate/core'; @Component({ selector: 'tb-time-series-chart-y-axis-row', @@ -76,6 +77,7 @@ export class TimeSeriesChartYAxisRowComponent implements ControlValueAccessor, O private propagateChange = (_val: any) => {}; constructor(private fb: UntypedFormBuilder, + private translate: TranslateService, private popoverService: TbPopoverService, private renderer: Renderer2, private viewContainerRef: ViewContainerRef, @@ -143,16 +145,18 @@ export class TimeSeriesChartYAxisRowComponent implements ControlValueAccessor, O this.popoverService.hidePopover(trigger); } else { const ctx: any = { - yAxisSettings: deepClone(this.modelValue), + axisType: 'yAxis', + panelTitle: this.translate.instant('widgets.time-series-chart.axis.y-axis-settings'), + axisSettings: deepClone(this.modelValue), advanced: this.advanced }; const yAxisSettingsPanelPopover = this.popoverService.displayPopover(trigger, this.renderer, - this.viewContainerRef, TimeSeriesChartYAxisSettingsPanelComponent, ['leftOnly', 'leftTopOnly', 'leftBottomOnly'], true, null, + this.viewContainerRef, TimeSeriesChartAxisSettingsPanelComponent, ['leftOnly', 'leftTopOnly', 'leftBottomOnly'], true, null, ctx, {}, {}, {}, true); yAxisSettingsPanelPopover.tbComponentRef.instance.popover = yAxisSettingsPanelPopover; - yAxisSettingsPanelPopover.tbComponentRef.instance.yAxisSettingsApplied.subscribe((yAxisSettings) => { + yAxisSettingsPanelPopover.tbComponentRef.instance.axisSettingsApplied.subscribe((yAxisSettings) => { yAxisSettingsPanelPopover.hide(); this.modelValue = {...this.modelValue, ...yAxisSettings}; this.axisFormGroup.patchValue( diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/data-key-input.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/data-key-input.component.html index 94831b530f..d5cc9a183e 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/data-key-input.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/data-key-input.component.html @@ -19,6 +19,7 @@ [class.tb-suffix-absolute]="!keysFormControl.value?.length">
@@ -49,7 +50,8 @@ (click)="editKey()" mat-icon-button class="tb-mat-24"> edit -
+ +
widgets.bar-chart.bar-appearance
diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/bar-chart-with-labels-basic-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/bar-chart-with-labels-basic-config.component.ts index ab5fc0a867..49c98aa0aa 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/bar-chart-with-labels-basic-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/bar-chart-with-labels-basic-config.component.ts @@ -33,7 +33,7 @@ import { getTimewindowConfig, setTimewindowConfig } from '@home/components/widget/config/timewindow-config-panel.component'; -import { formatValue, isUndefined } from '@core/utils'; +import { formatValue, isUndefined, mergeDeep } from '@core/utils'; import { cssSizeToStrSize, DateFormatProcessor, @@ -88,7 +88,8 @@ export class BarChartWithLabelsBasicConfigComponent extends BasicWidgetConfigCom } protected onConfigSet(configData: WidgetConfigComponentData) { - const settings: BarChartWithLabelsWidgetSettings = {...barChartWithLabelsDefaultSettings, ...(configData.config.settings || {})}; + const settings: BarChartWithLabelsWidgetSettings = mergeDeep({} as BarChartWithLabelsWidgetSettings, + barChartWithLabelsDefaultSettings, configData.config.settings as BarChartWithLabelsWidgetSettings); const iconSize = resolveCssSize(configData.config.iconSize); this.barChartWidgetConfigForm = this.fb.group({ timewindowConfig: [getTimewindowConfig(configData.config), []], @@ -123,6 +124,8 @@ export class BarChartWithLabelsBasicConfigComponent extends BasicWidgetConfigCom units: [configData.config.units, []], decimals: [configData.config.decimals, []], + grid: [settings.grid, []], + yAxis: [settings.yAxis, []], xAxis: [settings.xAxis, []], @@ -192,6 +195,8 @@ export class BarChartWithLabelsBasicConfigComponent extends BasicWidgetConfigCom this.widgetConfig.config.units = config.units; this.widgetConfig.config.decimals = config.decimals; + this.widgetConfig.config.settings.grid = config.grid; + this.widgetConfig.config.settings.yAxis = config.yAxis; this.widgetConfig.config.settings.xAxis = config.xAxis; diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/range-chart-basic-config.component.html b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/range-chart-basic-config.component.html index eb94f9c977..4045d1d928 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/range-chart-basic-config.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/range-chart-basic-config.component.html @@ -75,6 +75,9 @@ {{ 'widgets.range-chart.data-zoom' | translate }}
+ +
widgets.range-chart.range-chart-appearance
diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/range-chart-basic-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/range-chart-basic-config.component.ts index 40c9bb9d12..9c39ef8f27 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/range-chart-basic-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/range-chart-basic-config.component.ts @@ -33,7 +33,7 @@ import { getTimewindowConfig, setTimewindowConfig } from '@home/components/widget/config/timewindow-config-panel.component'; -import { formatValue, isUndefined } from '@core/utils'; +import { formatValue, isUndefined, mergeDeep } from '@core/utils'; import { cssSizeToStrSize, DateFormatProcessor, @@ -114,7 +114,8 @@ export class RangeChartBasicConfigComponent extends BasicWidgetConfigComponent { } protected onConfigSet(configData: WidgetConfigComponentData) { - const settings: RangeChartWidgetSettings = {...rangeChartDefaultSettings, ...(configData.config.settings || {})}; + const settings: RangeChartWidgetSettings = mergeDeep({} as RangeChartWidgetSettings, + rangeChartDefaultSettings, configData.config.settings as RangeChartWidgetSettings); const iconSize = resolveCssSize(configData.config.iconSize); this.rangeChartWidgetConfigForm = this.fb.group({ timewindowConfig: [getTimewindowConfig(configData.config), []], @@ -159,6 +160,8 @@ export class RangeChartBasicConfigComponent extends BasicWidgetConfigComponent { pointShape: [settings.pointShape, []], pointSize: [settings.pointSize, [Validators.min(0)]], + grid: [settings.grid, []], + yAxis: [settings.yAxis, []], xAxis: [settings.xAxis, []], @@ -239,6 +242,8 @@ export class RangeChartBasicConfigComponent extends BasicWidgetConfigComponent { this.widgetConfig.config.settings.pointShape = config.pointShape; this.widgetConfig.config.settings.pointSize = config.pointSize; + this.widgetConfig.config.settings.grid = config.grid; + this.widgetConfig.config.settings.yAxis = config.yAxis; this.widgetConfig.config.settings.xAxis = config.xAxis; diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/time-series-chart-basic-config.component.html b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/time-series-chart-basic-config.component.html index bf88f77b2f..126f9b6260 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/time-series-chart-basic-config.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/time-series-chart-basic-config.component.html @@ -161,6 +161,9 @@ {{ 'widgets.time-series-chart.data-zoom' | translate }}
+ +
diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/time-series-chart-basic-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/time-series-chart-basic-config.component.ts index 789f3e9c33..044e29614a 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/time-series-chart-basic-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/time-series-chart-basic-config.component.ts @@ -161,6 +161,8 @@ export class TimeSeriesChartBasicConfigComponent extends BasicWidgetConfigCompon dataZoom: [settings.dataZoom, []], stack: [settings.stack, []], + grid: [settings.grid, []], + xAxis: [settings.xAxis, []], noAggregationBarWidthSettings: [settings.noAggregationBarWidthSettings, []], @@ -226,6 +228,8 @@ export class TimeSeriesChartBasicConfigComponent extends BasicWidgetConfigCompon this.widgetConfig.config.settings.dataZoom = config.dataZoom; this.widgetConfig.config.settings.stack = config.stack; + this.widgetConfig.config.settings.grid = config.grid; + this.widgetConfig.config.settings.yAxes = config.yAxes; this.widgetConfig.config.settings.xAxis = config.xAxis; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/bar-chart-with-labels-widget.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/chart/bar-chart-with-labels-widget.models.ts index 73ecad5264..53e4dc8527 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/chart/bar-chart-with-labels-widget.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/bar-chart-with-labels-widget.models.ts @@ -32,6 +32,8 @@ import { SeriesFillType, timeSeriesChartAnimationDefaultSettings, TimeSeriesChartAnimationSettings, + timeSeriesChartGridDefaultSettings, + TimeSeriesChartGridSettings, TimeSeriesChartKeySettings, timeSeriesChartNoAggregationBarWidthDefaultSettings, TimeSeriesChartNoAggregationBarWidthSettings, @@ -58,6 +60,7 @@ export interface BarChartWithLabelsWidgetSettings extends EChartsTooltipWidgetSe barBorderRadius: number; barBackgroundSettings: SeriesFillSettings; noAggregationBarWidthSettings: TimeSeriesChartNoAggregationBarWidthSettings; + grid: TimeSeriesChartGridSettings; yAxis: TimeSeriesChartYAxisSettings; xAxis: TimeSeriesChartXAxisSettings; animation: TimeSeriesChartAnimationSettings; @@ -105,6 +108,8 @@ export const barChartWithLabelsDefaultSettings: BarChartWithLabelsWidgetSettings }, noAggregationBarWidthSettings: mergeDeep({} as TimeSeriesChartNoAggregationBarWidthSettings, timeSeriesChartNoAggregationBarWidthDefaultSettings), + grid: mergeDeep({} as TimeSeriesChartGridSettings, + timeSeriesChartGridDefaultSettings), yAxis: mergeDeep({} as TimeSeriesChartYAxisSettings, defaultTimeSeriesChartYAxisSettings, { id: 'default', order: 0, showLine: false, showTicks: false } as TimeSeriesChartYAxisSettings), @@ -163,6 +168,7 @@ export const barChartWithLabelsDefaultSettings: BarChartWithLabelsWidgetSettings export const barChartWithLabelsTimeSeriesSettings = (settings: BarChartWithLabelsWidgetSettings): DeepPartial => ({ dataZoom: settings.dataZoom, + grid: settings.grid, yAxes: { default: settings.yAxis }, diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/range-chart-widget.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/chart/range-chart-widget.models.ts index e04dccf74d..aa9c290231 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/chart/range-chart-widget.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/range-chart-widget.models.ts @@ -33,7 +33,7 @@ import { SeriesFillType, SeriesLabelPosition, ThresholdLabelPosition, timeSeriesChartAnimationDefaultSettings, TimeSeriesChartAnimationSettings, - timeSeriesChartColorScheme, + timeSeriesChartColorScheme, timeSeriesChartGridDefaultSettings, TimeSeriesChartGridSettings, TimeSeriesChartKeySettings, TimeSeriesChartLineType, TimeSeriesChartSeriesType, @@ -81,6 +81,7 @@ export interface RangeChartWidgetSettings extends EChartsTooltipWidgetSettings { pointLabelBackground: string; pointShape: EChartsShape; pointSize: number; + grid: TimeSeriesChartGridSettings; yAxis: TimeSeriesChartYAxisSettings; xAxis: TimeSeriesChartXAxisSettings; animation: TimeSeriesChartAnimationSettings; @@ -141,6 +142,8 @@ export const rangeChartDefaultSettings: RangeChartWidgetSettings = { pointLabelBackground: 'rgba(255,255,255,0.56)', pointShape: EChartsShape.emptyCircle, pointSize: 4, + grid: mergeDeep({} as TimeSeriesChartGridSettings, + timeSeriesChartGridDefaultSettings), yAxis: mergeDeep({} as TimeSeriesChartYAxisSettings, defaultTimeSeriesChartYAxisSettings, { id: 'default', order: 0, showLine: false, showTicks: false } as TimeSeriesChartYAxisSettings), @@ -213,6 +216,7 @@ export const rangeChartTimeSeriesSettings = (settings: RangeChartWidgetSettings, return { dataZoom: settings.dataZoom, thresholds, + grid: settings.grid, yAxes: { default: { ...settings.yAxis, diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.models.ts index 147d694b30..bfe65a0d7c 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.models.ts @@ -699,11 +699,26 @@ export interface TimeSeriesChartComparisonSettings extends WidgetComparisonSetti comparisonXAxis?: TimeSeriesChartXAxisSettings; } +export interface TimeSeriesChartGridSettings { + show: boolean; + backgroundColor: string; + borderWidth: number; + borderColor: string; +} + +export const timeSeriesChartGridDefaultSettings: TimeSeriesChartGridSettings = { + show: false, + backgroundColor: null, + borderWidth: 1, + borderColor: '#ccc' +}; + export interface TimeSeriesChartSettings extends EChartsTooltipWidgetSettings, TimeSeriesChartComparisonSettings { thresholds: TimeSeriesChartThreshold[]; darkMode: boolean; dataZoom: boolean; stack: boolean; + grid: TimeSeriesChartGridSettings; yAxes: TimeSeriesChartYAxes; xAxis: TimeSeriesChartXAxisSettings; animation: TimeSeriesChartAnimationSettings; @@ -718,6 +733,8 @@ export const timeSeriesChartDefaultSettings: TimeSeriesChartSettings = { darkMode: false, dataZoom: true, stack: false, + grid: mergeDeep({} as TimeSeriesChartGridSettings, + timeSeriesChartGridDefaultSettings), yAxes: { default: mergeDeep({} as TimeSeriesChartYAxisSettings, defaultTimeSeriesChartYAxisSettings, diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.ts b/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.ts index b352720101..2f987f14fe 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.ts @@ -612,6 +612,10 @@ export class TbTimeSeriesChart { extraCssText: `line-height: 1; backdrop-filter: blur(${this.settings.tooltipBackgroundBlur}px);` }], grid: [{ + show: this.settings.grid.show, + backgroundColor: this.settings.grid.backgroundColor, + borderWidth: this.settings.grid.borderWidth, + borderColor: this.settings.grid.borderColor, top: this.minTopOffset(), left: this.settings.dataZoom ? 5 : 0, right: this.settings.dataZoom ? 5 : 0, diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/bar-chart-with-labels-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/bar-chart-with-labels-widget-settings.component.html index 7db5a92746..4123df9c9f 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/bar-chart-with-labels-widget-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/bar-chart-with-labels-widget-settings.component.html @@ -23,6 +23,9 @@ {{ 'widgets.time-series-chart.data-zoom' | translate }}
+ +
widgets.bar-chart.bar-appearance
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/bar-chart-with-labels-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/bar-chart-with-labels-widget-settings.component.ts index b221d09cf9..3c3703642c 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/bar-chart-with-labels-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/bar-chart-with-labels-widget-settings.component.ts @@ -25,10 +25,10 @@ import { import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; -import { formatValue } from '@core/utils'; +import { formatValue, mergeDeep } from '@core/utils'; import { DateFormatProcessor, DateFormatSettings } from '@shared/models/widget-settings.models'; import { - barChartWithLabelsDefaultSettings + barChartWithLabelsDefaultSettings, BarChartWithLabelsWidgetSettings } from '@home/components/widget/lib/chart/bar-chart-with-labels-widget.models'; @Component({ @@ -68,7 +68,7 @@ export class BarChartWithLabelsWidgetSettingsComponent extends WidgetSettingsCom } protected defaultSettings(): WidgetSettings { - return {...barChartWithLabelsDefaultSettings}; + return mergeDeep({} as BarChartWithLabelsWidgetSettings, barChartWithLabelsDefaultSettings); } protected onSettingsSet(settings: WidgetSettings) { @@ -88,6 +88,8 @@ export class BarChartWithLabelsWidgetSettingsComponent extends WidgetSettingsCom barBackgroundSettings: [settings.barBackgroundSettings, []], noAggregationBarWidthSettings: [settings.noAggregationBarWidthSettings, []], + grid: [settings.grid, []], + yAxis: [settings.yAxis, []], xAxis: [settings.xAxis, []], diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/range-chart-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/range-chart-widget-settings.component.html index a19b8eb845..c5de2a86a7 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/range-chart-widget-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/range-chart-widget-settings.component.html @@ -23,6 +23,9 @@ {{ 'widgets.range-chart.data-zoom' | translate }}
+ +
widgets.range-chart.range-chart-appearance
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/range-chart-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/range-chart-widget-settings.component.ts index b9a058af24..7e568ca7ab 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/range-chart-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/range-chart-widget-settings.component.ts @@ -25,13 +25,19 @@ import { import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; -import { formatValue } from '@core/utils'; -import { rangeChartDefaultSettings } from '@home/components/widget/lib/chart/range-chart-widget.models'; +import { formatValue, mergeDeep } from '@core/utils'; +import { + rangeChartDefaultSettings, + RangeChartWidgetSettings +} from '@home/components/widget/lib/chart/range-chart-widget.models'; import { DateFormatProcessor, DateFormatSettings } from '@shared/models/widget-settings.models'; import { - lineSeriesStepTypes, lineSeriesStepTypeTranslations, - seriesLabelPositions, seriesLabelPositionTranslations, - timeSeriesLineTypes, timeSeriesLineTypeTranslations + lineSeriesStepTypes, + lineSeriesStepTypeTranslations, + seriesLabelPositions, + seriesLabelPositionTranslations, + timeSeriesLineTypes, + timeSeriesLineTypeTranslations } from '@home/components/widget/lib/chart/time-series-chart.models'; import { echartsShapes, echartsShapeTranslations } from '@home/components/widget/lib/chart/echarts-widget.models'; @@ -90,7 +96,7 @@ export class RangeChartWidgetSettingsComponent extends WidgetSettingsComponent { } protected defaultSettings(): WidgetSettings { - return {...rangeChartDefaultSettings}; + return mergeDeep({} as RangeChartWidgetSettings, rangeChartDefaultSettings); } protected onSettingsSet(settings: WidgetSettings) { @@ -120,6 +126,8 @@ export class RangeChartWidgetSettingsComponent extends WidgetSettingsComponent { pointShape: [settings.pointShape, []], pointSize: [settings.pointSize, [Validators.min(0)]], + grid: [settings.grid, []], + yAxis: [settings.yAxis, []], xAxis: [settings.xAxis, []], diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-widget-settings.component.html index 5afcd9c860..6134f3dcae 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-widget-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-widget-settings.component.html @@ -80,6 +80,9 @@ {{ 'widgets.time-series-chart.data-zoom' | translate }}
+ +
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-widget-settings.component.ts index 6dff6e1496..2b95d57b7d 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-widget-settings.component.ts @@ -35,7 +35,8 @@ import { } from '@home/components/widget/lib/chart/time-series-chart-widget.models'; import { TimeSeriesChartKeySettings, - TimeSeriesChartType, TimeSeriesChartYAxes, + TimeSeriesChartType, + TimeSeriesChartYAxes, TimeSeriesChartYAxisId } from '@home/components/widget/lib/chart/time-series-chart.models'; import { WidgetConfigComponentData } from '@home/models/widget-component.models'; @@ -108,7 +109,7 @@ export class TimeSeriesChartWidgetSettingsComponent extends WidgetSettingsCompon } protected defaultSettings(): WidgetSettings { - return mergeDeep({} as TimeSeriesChartWidgetSettings, timeSeriesChartWidgetDefaultSettings); + return mergeDeep({} as TimeSeriesChartWidgetSettings, timeSeriesChartWidgetDefaultSettings); } protected onSettingsSet(settings: WidgetSettings) { @@ -125,6 +126,8 @@ export class TimeSeriesChartWidgetSettingsComponent extends WidgetSettingsCompon dataZoom: [settings.dataZoom, []], stack: [settings.stack, []], + grid: [settings.grid, []], + xAxis: [settings.xAxis, []], noAggregationBarWidthSettings: [settings.noAggregationBarWidthSettings, []], diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-grid-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-grid-settings.component.html new file mode 100644 index 0000000000..1b439e5bea --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-grid-settings.component.html @@ -0,0 +1,54 @@ + + +
+ + + + + {{ 'widgets.time-series-chart.grid.grid' | translate }} + + + + +
+
{{ 'widgets.time-series-chart.grid.background-color' | translate }}
+ + +
+
+
{{ 'widgets.time-series-chart.grid.border' | translate }}
+
+ + + px + + + +
+
+
+
+
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-grid-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-grid-settings.component.ts new file mode 100644 index 0000000000..fd88ce2a8f --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-grid-settings.component.ts @@ -0,0 +1,115 @@ +/// +/// 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 { Component, forwardRef, Input, OnInit } from '@angular/core'; +import { + ControlValueAccessor, + NG_VALUE_ACCESSOR, + UntypedFormBuilder, + UntypedFormGroup, + Validators +} from '@angular/forms'; +import { TimeSeriesChartGridSettings } from '@home/components/widget/lib/chart/time-series-chart.models'; +import { WidgetService } from '@core/http/widget.service'; + +@Component({ + selector: 'tb-time-series-chart-grid-settings', + templateUrl: './time-series-chart-grid-settings.component.html', + styleUrls: ['./../../widget-settings.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => TimeSeriesChartGridSettingsComponent), + multi: true + } + ] +}) +export class TimeSeriesChartGridSettingsComponent implements OnInit, ControlValueAccessor { + + settingsExpanded = false; + + @Input() + disabled: boolean; + + private modelValue: TimeSeriesChartGridSettings; + + private propagateChange = null; + + public gridSettingsFormGroup: UntypedFormGroup; + + constructor(private fb: UntypedFormBuilder, + private widgetService: WidgetService,) { + } + + ngOnInit(): void { + this.gridSettingsFormGroup = this.fb.group({ + show: [null, []], + backgroundColor: [null, []], + borderWidth: [null, [Validators.min(0)]], + borderColor: [null, []], + }); + this.gridSettingsFormGroup.valueChanges.subscribe(() => { + this.updateModel(); + }); + this.gridSettingsFormGroup.get('show').valueChanges + .subscribe(() => { + this.updateValidators(); + }); + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(_fn: any): void { + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (isDisabled) { + this.gridSettingsFormGroup.disable({emitEvent: false}); + } else { + this.gridSettingsFormGroup.enable({emitEvent: false}); + this.updateValidators(); + } + } + + writeValue(value: TimeSeriesChartGridSettings): void { + this.modelValue = value; + this.gridSettingsFormGroup.patchValue( + value, {emitEvent: false} + ); + this.updateValidators(); + this.gridSettingsFormGroup.get('show').valueChanges.subscribe((show) => { + this.settingsExpanded = show; + }); + } + + private updateValidators() { + const show: boolean = this.gridSettingsFormGroup.get('show').value; + if (show) { + this.gridSettingsFormGroup.enable({emitEvent: false}); + } else { + this.gridSettingsFormGroup.disable({emitEvent: false}); + this.gridSettingsFormGroup.get('show').enable({emitEvent: false}); + } + } + + private updateModel() { + this.modelValue = this.gridSettingsFormGroup.getRawValue(); + this.propagateChange(this.modelValue); + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/widget-settings-common.module.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/widget-settings-common.module.ts index d0e1238f02..3742708e1a 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/widget-settings-common.module.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/widget-settings-common.module.ts @@ -142,6 +142,9 @@ import { import { TimeSeriesChartAxisSettingsButtonComponent } from '@home/components/widget/lib/settings/common/chart/time-series-chart-axis-settings-button.component'; +import { + TimeSeriesChartGridSettingsComponent +} from '@home/components/widget/lib/settings/common/chart/time-series-chart-grid-settings.component'; @NgModule({ declarations: [ @@ -194,6 +197,7 @@ import { TimeSeriesChartThresholdSettingsComponent, TimeSeriesChartStatesPanelComponent, TimeSeriesChartStateRowComponent, + TimeSeriesChartGridSettingsComponent, DataKeyInputComponent, EntityAliasInputComponent ], @@ -252,6 +256,7 @@ import { TimeSeriesChartThresholdSettingsComponent, TimeSeriesChartStatesPanelComponent, TimeSeriesChartStateRowComponent, + TimeSeriesChartGridSettingsComponent, DataKeyInputComponent, EntityAliasInputComponent ], 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 2701736bbd..b62aac95c2 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -6806,6 +6806,11 @@ "to": "To", "remove-state": "Remove state" }, + "grid": { + "grid": "Grid", + "background-color": "Background color", + "border": "Border" + }, "axis": { "axes": "Axes", "x-axis": "X axis", From 5f7f57cb636cba85acf5a73a89057ef43684dc61 Mon Sep 17 00:00:00 2001 From: nick Date: Tue, 16 Apr 2024 17:33:12 +0300 Subject: [PATCH 66/80] tbel: encodeURI() decodeURI() == mdn --- .../thingsboard/script/api/tbel/TbUtils.java | 63 ++++++++++++++++--- .../script/api/tbel/TbUtilsTest.java | 13 +++- 2 files changed, 63 insertions(+), 13 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 bbcdd1bf00..1cf88ce3f9 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 @@ -16,6 +16,7 @@ package org.thingsboard.script.api.tbel; import com.google.common.primitives.Bytes; +import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.ArrayUtils; import org.mvel2.ExecutionContext; import org.mvel2.ParserConfiguration; @@ -29,32 +30,50 @@ import java.io.UnsupportedEncodingException; import java.math.BigDecimal; import java.math.BigInteger; import java.math.RoundingMode; +import java.net.URLDecoder; +import java.net.URLEncoder; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.nio.charset.StandardCharsets; -import java.text.DateFormat; -import java.text.SimpleDateFormat; -import java.time.Instant; -import java.time.LocalDate; -import java.time.LocalDateTime; -import java.time.ZoneId; -import java.time.ZonedDateTime; -import java.time.format.DateTimeFormatter; -import java.time.temporal.ChronoUnit; -import java.time.temporal.TemporalAccessor; import java.util.ArrayList; import java.util.Arrays; import java.util.Base64; import java.util.Collection; import java.util.Collections; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.Set; +import java.util.regex.Matcher; +@Slf4j public class TbUtils { private static final byte[] HEX_ARRAY = "0123456789ABCDEF".getBytes(StandardCharsets.US_ASCII); + private static final LinkedHashMap enCodeMdn = new LinkedHashMap<>(); + + static { + enCodeMdn.put("\\+", "%20"); + enCodeMdn.put("%21", "!"); + enCodeMdn.put("%27", "'"); + enCodeMdn.put("%28", "\\("); + enCodeMdn.put("%29", "\\)"); + enCodeMdn.put("%7E", "~"); + enCodeMdn.put("%3B", ";"); + enCodeMdn.put("%2C", ","); + enCodeMdn.put("%2F", "/"); + enCodeMdn.put("%3F", "\\?"); + enCodeMdn.put("%3A", ":"); + enCodeMdn.put("%40", "@"); + enCodeMdn.put("%26", "&"); + enCodeMdn.put("%3D", "="); + enCodeMdn.put("%2B", "\\+"); + enCodeMdn.put("%24", Matcher.quoteReplacement("$")); + enCodeMdn.put("%23", "#"); + } + public static void register(ParserConfiguration parserConfig) throws Exception { parserConfig.addImport("btoa", new MethodStub(TbUtils.class.getMethod("btoa", String.class))); @@ -175,6 +194,10 @@ public class TbUtils { ExecutionContext.class, Map.class, List.class))); parserConfig.addImport("toFlatMap", new MethodStub(TbUtils.class.getMethod("toFlatMap", ExecutionContext.class, Map.class, List.class, boolean.class))); + parserConfig.addImport("encodeURI", new MethodStub(TbUtils.class.getMethod("encodeURI", + String.class))); + parserConfig.addImport("decodeURI", new MethodStub(TbUtils.class.getMethod("decodeURI", + String.class))); } public static String btoa(String input) { @@ -623,6 +646,26 @@ public class TbUtils { return map; } + public static String encodeURI(String uri) { + String encoded = URLEncoder.encode(uri, StandardCharsets.UTF_8); + Optional encodedMdnOpt = Optional.of(encoded); + for (var entry : enCodeMdn.entrySet()) { + encodedMdnOpt = Optional.of(encodedMdnOpt.get().replaceAll(entry.getKey(), entry.getValue())); + } + return encodedMdnOpt.orElse(null); + } + + public static String decodeURI(String uri) { + ArrayList alKeys = new ArrayList<>(enCodeMdn.keySet()); + Collections.reverse(alKeys); + Optional encodedMdnOpt = Optional.of(uri); + for (String strKey : alKeys) { + encodedMdnOpt = Optional.of(encodedMdnOpt.get().replaceAll(enCodeMdn.get(strKey), strKey)); + enCodeMdn.get(strKey); + } + return encodedMdnOpt.orElse(null) == null ? null : URLDecoder.decode(encodedMdnOpt.orElse(null), StandardCharsets.UTF_8); + } + 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; 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 c140559a0f..8325e69f66 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 @@ -31,13 +31,11 @@ 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.List; import java.util.Random; @Slf4j - public class TbUtilsTest { private ExecutionContext ctx; @@ -474,7 +472,16 @@ public class TbUtilsTest { } } - + @Test + public void encodeDecodeUri_Test() { + String uriOriginal = "-_.!~*'();/?:@&=+$,#ht://example.ж д a/path with spaces/?param1=Київ 1¶m2=Україна2"; + String uriEncodeExpected = "-_.!~*'();/?:@&=+$,#ht://example.%D0%B6%20%D0%B4%20a/path%20with%20spaces/?param1=%D0%9A%D0%B8%D1%97%D0%B2%201¶m2=%D0%A3%D0%BA%D1%80%D0%B0%D1%97%D0%BD%D0%B02"; + String uriEncodeActual = TbUtils.encodeURI(uriOriginal); + Assert.assertEquals(uriEncodeExpected, uriEncodeActual); + + String uriDecodeActual = TbUtils.decodeURI(uriEncodeActual); + Assert.assertEquals(uriOriginal, uriDecodeActual); + } private static List toList(byte[] data) { List result = new ArrayList<>(data.length); for (Byte b : data) { From e488f71444698c6cd8d2f35317fc02de64f65f10 Mon Sep 17 00:00:00 2001 From: nick Date: Tue, 16 Apr 2024 17:56:18 +0300 Subject: [PATCH 67/80] tbel: encodeURI() decodeURI() == mdn 1 --- .../src/main/java/org/thingsboard/script/api/tbel/TbUtils.java | 1 - 1 file changed, 1 deletion(-) 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 1cf88ce3f9..63019c6da1 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 @@ -661,7 +661,6 @@ public class TbUtils { Optional encodedMdnOpt = Optional.of(uri); for (String strKey : alKeys) { encodedMdnOpt = Optional.of(encodedMdnOpt.get().replaceAll(enCodeMdn.get(strKey), strKey)); - enCodeMdn.get(strKey); } return encodedMdnOpt.orElse(null) == null ? null : URLDecoder.decode(encodedMdnOpt.orElse(null), StandardCharsets.UTF_8); } From 2923c9dbd39121f1b79bcb85cbf72dd20f56f4c5 Mon Sep 17 00:00:00 2001 From: nick Date: Tue, 16 Apr 2024 18:17:30 +0300 Subject: [PATCH 68/80] tbel: encodeURI() decodeURI() == mdn 2 --- .../src/main/java/org/thingsboard/script/api/tbel/TbUtils.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 63019c6da1..8d3bae3e09 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 @@ -662,7 +662,7 @@ public class TbUtils { for (String strKey : alKeys) { encodedMdnOpt = Optional.of(encodedMdnOpt.get().replaceAll(enCodeMdn.get(strKey), strKey)); } - return encodedMdnOpt.orElse(null) == null ? null : URLDecoder.decode(encodedMdnOpt.orElse(null), StandardCharsets.UTF_8); + return encodedMdnOpt.orElse(null) == null ? null : URLDecoder.decode(encodedMdnOpt.get(), StandardCharsets.UTF_8); } private static void parseRecursive(Object json, Map map, List excludeList, String path, boolean pathInKey) { From 10aa7ae795eaa9644b633c9d240627ac2a2b74c1 Mon Sep 17 00:00:00 2001 From: nick Date: Wed, 17 Apr 2024 11:12:43 +0300 Subject: [PATCH 69/80] tbel: encodeURI() decodeURI() == mdn 3 --- .../src/main/java/org/thingsboard/script/api/tbel/TbUtils.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 8d3bae3e09..1ec62aacd6 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 @@ -662,7 +662,7 @@ public class TbUtils { for (String strKey : alKeys) { encodedMdnOpt = Optional.of(encodedMdnOpt.get().replaceAll(enCodeMdn.get(strKey), strKey)); } - return encodedMdnOpt.orElse(null) == null ? null : URLDecoder.decode(encodedMdnOpt.get(), StandardCharsets.UTF_8); + return encodedMdnOpt.map(s -> URLDecoder.decode(s, StandardCharsets.UTF_8)).orElse(null); } private static void parseRecursive(Object json, Map map, List excludeList, String path, boolean pathInKey) { From 332c67631767414fccdff68b4a57e69c7a102b22 Mon Sep 17 00:00:00 2001 From: nick Date: Wed, 17 Apr 2024 11:50:51 +0300 Subject: [PATCH 70/80] tbel: encodeURI() decodeURI() comments 1 --- .../thingsboard/script/api/tbel/TbUtils.java | 46 +++++++++---------- 1 file changed, 23 insertions(+), 23 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 1ec62aacd6..209df886b5 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 @@ -52,26 +52,26 @@ public class TbUtils { private static final byte[] HEX_ARRAY = "0123456789ABCDEF".getBytes(StandardCharsets.US_ASCII); - private static final LinkedHashMap enCodeMdn = new LinkedHashMap<>(); + private static final LinkedHashMap mdnEncodingReplacements = new LinkedHashMap<>(); static { - enCodeMdn.put("\\+", "%20"); - enCodeMdn.put("%21", "!"); - enCodeMdn.put("%27", "'"); - enCodeMdn.put("%28", "\\("); - enCodeMdn.put("%29", "\\)"); - enCodeMdn.put("%7E", "~"); - enCodeMdn.put("%3B", ";"); - enCodeMdn.put("%2C", ","); - enCodeMdn.put("%2F", "/"); - enCodeMdn.put("%3F", "\\?"); - enCodeMdn.put("%3A", ":"); - enCodeMdn.put("%40", "@"); - enCodeMdn.put("%26", "&"); - enCodeMdn.put("%3D", "="); - enCodeMdn.put("%2B", "\\+"); - enCodeMdn.put("%24", Matcher.quoteReplacement("$")); - enCodeMdn.put("%23", "#"); + mdnEncodingReplacements.put("\\+", "%20"); + mdnEncodingReplacements.put("%21", "!"); + mdnEncodingReplacements.put("%27", "'"); + mdnEncodingReplacements.put("%28", "\\("); + mdnEncodingReplacements.put("%29", "\\)"); + mdnEncodingReplacements.put("%7E", "~"); + mdnEncodingReplacements.put("%3B", ";"); + mdnEncodingReplacements.put("%2C", ","); + mdnEncodingReplacements.put("%2F", "/"); + mdnEncodingReplacements.put("%3F", "\\?"); + mdnEncodingReplacements.put("%3A", ":"); + mdnEncodingReplacements.put("%40", "@"); + mdnEncodingReplacements.put("%26", "&"); + mdnEncodingReplacements.put("%3D", "="); + mdnEncodingReplacements.put("%2B", "\\+"); + mdnEncodingReplacements.put("%24", Matcher.quoteReplacement("$")); + mdnEncodingReplacements.put("%23", "#"); } public static void register(ParserConfiguration parserConfig) throws Exception { @@ -649,18 +649,18 @@ public class TbUtils { public static String encodeURI(String uri) { String encoded = URLEncoder.encode(uri, StandardCharsets.UTF_8); Optional encodedMdnOpt = Optional.of(encoded); - for (var entry : enCodeMdn.entrySet()) { + for (var entry : mdnEncodingReplacements.entrySet()) { encodedMdnOpt = Optional.of(encodedMdnOpt.get().replaceAll(entry.getKey(), entry.getValue())); } return encodedMdnOpt.orElse(null); } public static String decodeURI(String uri) { - ArrayList alKeys = new ArrayList<>(enCodeMdn.keySet()); - Collections.reverse(alKeys); + ArrayList allKeys = new ArrayList<>(mdnEncodingReplacements.keySet()); + Collections.reverse(allKeys); Optional encodedMdnOpt = Optional.of(uri); - for (String strKey : alKeys) { - encodedMdnOpt = Optional.of(encodedMdnOpt.get().replaceAll(enCodeMdn.get(strKey), strKey)); + for (String strKey : allKeys) { + encodedMdnOpt = Optional.of(encodedMdnOpt.get().replaceAll(mdnEncodingReplacements.get(strKey), strKey)); } return encodedMdnOpt.map(s -> URLDecoder.decode(s, StandardCharsets.UTF_8)).orElse(null); } From 4e9d03fce4d5b40480b6a4d56dff25f15a2eacb5 Mon Sep 17 00:00:00 2001 From: nick Date: Wed, 17 Apr 2024 13:09:54 +0300 Subject: [PATCH 71/80] tbel: encodeURI() decodeURI() comments 2 --- .../thingsboard/script/api/tbel/TbUtils.java | 44 +++++++++---------- 1 file changed, 21 insertions(+), 23 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 209df886b5..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 @@ -43,7 +43,6 @@ import java.util.Collections; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; -import java.util.Optional; import java.util.Set; import java.util.regex.Matcher; @@ -612,22 +611,6 @@ public class TbUtils { return BigDecimal.valueOf(value).setScale(precision, RoundingMode.HALF_UP).floatValue(); } - 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(",", "."); - } - return value; - } - public static ExecutionHashMap toFlatMap(ExecutionContext ctx, Map json) { return toFlatMap(ctx, json, new ArrayList<>(), true); } @@ -646,23 +629,22 @@ public class TbUtils { return map; } + public static String encodeURI(String uri) { String encoded = URLEncoder.encode(uri, StandardCharsets.UTF_8); - Optional encodedMdnOpt = Optional.of(encoded); for (var entry : mdnEncodingReplacements.entrySet()) { - encodedMdnOpt = Optional.of(encodedMdnOpt.get().replaceAll(entry.getKey(), entry.getValue())); + encoded = encoded.replaceAll(entry.getKey(), entry.getValue()); } - return encodedMdnOpt.orElse(null); + return encoded; } public static String decodeURI(String uri) { ArrayList allKeys = new ArrayList<>(mdnEncodingReplacements.keySet()); Collections.reverse(allKeys); - Optional encodedMdnOpt = Optional.of(uri); for (String strKey : allKeys) { - encodedMdnOpt = Optional.of(encodedMdnOpt.get().replaceAll(mdnEncodingReplacements.get(strKey), strKey)); + uri = uri.replaceAll(mdnEncodingReplacements.get(strKey), strKey); } - return encodedMdnOpt.map(s -> URLDecoder.decode(s, StandardCharsets.UTF_8)).orElse(null); + return URLDecoder.decode(uri, StandardCharsets.UTF_8); } private static void parseRecursive(Object json, Map map, List excludeList, String path, boolean pathInKey) { @@ -705,6 +687,22 @@ 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(",", "."); + } + return value; + } + private static boolean isValidRadix(String value, int radix) { for (int i = 0; i < value.length(); i++) { if (i == 0 && value.charAt(i) == '-') { From 63b827e9536b711e25a7bbe68948640c81878b5f Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Wed, 17 Apr 2024 13:58:59 +0300 Subject: [PATCH 72/80] UI: Improve time series charts labels rendering and size calculation. --- .../home/components/widget/lib/chart/time-series-chart.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.ts b/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.ts index 2f987f14fe..f1ea248d80 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.ts @@ -588,6 +588,7 @@ export class TbTimeSeriesChart { private drawChart() { echartsModule.init(); + this.renderer.setStyle(this.chartElement, 'letterSpacing', 'normal'); this.timeSeriesChart = echarts.init(this.chartElement, null, { renderer: 'canvas' }); From 02648dd4a53211f17a4357a1cbaab6f7799b0263 Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Wed, 17 Apr 2024 16:28:16 +0300 Subject: [PATCH 73/80] UI: Time series charts improvements --- .../widget/lib/chart/echarts-widget.models.ts | 4 ++-- .../widget/lib/chart/time-series-chart-bar.models.ts | 5 +++-- .../components/widget/lib/chart/time-series-chart.ts | 10 +++++++--- 3 files changed, 12 insertions(+), 7 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/echarts-widget.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/chart/echarts-widget.models.ts index 35b929319b..8fc248502e 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/chart/echarts-widget.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/echarts-widget.models.ts @@ -417,8 +417,8 @@ export const adjustTimeAxisExtentToData = (timeAxisOption: TimeAxisBaseOption, } } } - timeAxisOption.min = (typeof min !== 'undefined') ? min : defaultMin; - timeAxisOption.max = (typeof max !== 'undefined') ? max : defaultMax; + timeAxisOption.min = (typeof min !== 'undefined' && Math.abs(min - defaultMin) < 1000) ? min : defaultMin; + timeAxisOption.max = (typeof max !== 'undefined' && Math.abs(max - defaultMax) < 1000) ? max : defaultMax; }; const toEChartsDataItem = (entry: DataEntry, valueConverter?: (value: any) => any): EChartsDataItem => { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart-bar.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart-bar.models.ts index ee3c703a17..f54a803d12 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart-bar.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart-bar.models.ts @@ -128,7 +128,8 @@ export const renderTimeSeriesBar = (params: CustomSeriesRenderItemParams, api: C height: coordSys.height }); - const zeroPos = api.coord([0, offset]); + const zeroCoord = api.coord([0, offset]); + const zeroPos = Math.min(zeroCoord[1], coordSys.y + coordSys.height); let style: any = { fill: renderCtx.visualSettings.color, @@ -176,7 +177,7 @@ export const renderTimeSeriesBar = (params: CustomSeriesRenderItemParams, api: C transition: 'all', enterFrom: { style: { opacity: 0 }, - shape: { height: 0, y: zeroPos[1] } + shape: { height: 0, y: zeroPos } } }; }; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.ts b/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.ts index f1ea248d80..ad5aafd4eb 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.ts @@ -803,9 +803,13 @@ export class TbTimeSeriesChart { } private scaleYAxis(yAxis: TimeSeriesChartYAxis): boolean { - const axisBarDataItems = this.dataItems.filter(d => d.yAxisId === yAxis.id && d.enabled && - d.data.length && d.dataKey.settings.type === TimeSeriesChartSeriesType.bar); - return !axisBarDataItems.length; + if (!this.stateData) { + const axisBarDataItems = this.dataItems.filter(d => d.yAxisId === yAxis.id && d.enabled && + d.data.length && d.dataKey.settings.type === TimeSeriesChartSeriesType.bar); + return !axisBarDataItems.length; + } else { + return false; + } } private minTopOffset(): number { From 94da3da2ffbbc57a679d541653efcf7f58b43ace Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Wed, 17 Apr 2024 17:49:25 +0300 Subject: [PATCH 74/80] UI: Improve time series chart legend configuration - added font/color settings for legend value and column title. --- ...e-series-chart-basic-config.component.html | 27 +++++++++++++++++++ ...ime-series-chart-basic-config.component.ts | 16 +++++++++++ .../time-series-chart-widget.component.html | 24 ++++++++--------- .../time-series-chart-widget.component.scss | 10 ------- .../time-series-chart-widget.component.ts | 6 +++++ .../chart/time-series-chart-widget.models.ts | 22 +++++++++++++++ ...eries-chart-widget-settings.component.html | 27 +++++++++++++++++++ ...-series-chart-widget-settings.component.ts | 12 +++++++++ .../assets/locale/locale.constant-en_US.json | 1 + 9 files changed, 123 insertions(+), 22 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/time-series-chart-basic-config.component.html b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/time-series-chart-basic-config.component.html index 126f9b6260..da30c4d163 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/time-series-chart-basic-config.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/time-series-chart-basic-config.component.html @@ -199,6 +199,7 @@
{{ 'legend.label' | translate }}
+
+
{{ 'legend.value' | translate }}
+
+ + + + +
+
+
+
{{ 'legend.column-title' | translate }}
+
+ + + + +
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/time-series-chart-basic-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/time-series-chart-basic-config.component.ts index 044e29614a..305b22d11f 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/time-series-chart-basic-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/time-series-chart-basic-config.component.ts @@ -168,8 +168,12 @@ export class TimeSeriesChartBasicConfigComponent extends BasicWidgetConfigCompon noAggregationBarWidthSettings: [settings.noAggregationBarWidthSettings, []], showLegend: [settings.showLegend, []], + legendColumnTitleFont: [settings.legendColumnTitleFont, []], + legendColumnTitleColor: [settings.legendColumnTitleColor, []], legendLabelFont: [settings.legendLabelFont, []], legendLabelColor: [settings.legendLabelColor, []], + legendValueFont: [settings.legendValueFont, []], + legendValueColor: [settings.legendValueColor, []], legendConfig: [settings.legendConfig, []], showTooltip: [settings.showTooltip, []], @@ -236,8 +240,12 @@ export class TimeSeriesChartBasicConfigComponent extends BasicWidgetConfigCompon this.widgetConfig.config.settings.noAggregationBarWidthSettings = config.noAggregationBarWidthSettings; this.widgetConfig.config.settings.showLegend = config.showLegend; + this.widgetConfig.config.settings.legendColumnTitleFont = config.legendColumnTitleFont; + this.widgetConfig.config.settings.legendColumnTitleColor = config.legendColumnTitleColor; this.widgetConfig.config.settings.legendLabelFont = config.legendLabelFont; this.widgetConfig.config.settings.legendLabelColor = config.legendLabelColor; + this.widgetConfig.config.settings.legendValueFont = config.legendValueFont; + this.widgetConfig.config.settings.legendValueColor = config.legendValueColor; this.widgetConfig.config.settings.legendConfig = config.legendConfig; this.widgetConfig.config.settings.showTooltip = config.showTooltip; @@ -318,12 +326,20 @@ export class TimeSeriesChartBasicConfigComponent extends BasicWidgetConfigCompon } if (showLegend) { + this.timeSeriesChartWidgetConfigForm.get('legendColumnTitleFont').enable(); + this.timeSeriesChartWidgetConfigForm.get('legendColumnTitleColor').enable(); this.timeSeriesChartWidgetConfigForm.get('legendLabelFont').enable(); this.timeSeriesChartWidgetConfigForm.get('legendLabelColor').enable(); + this.timeSeriesChartWidgetConfigForm.get('legendValueFont').enable(); + this.timeSeriesChartWidgetConfigForm.get('legendValueColor').enable(); this.timeSeriesChartWidgetConfigForm.get('legendConfig').enable(); } else { + this.timeSeriesChartWidgetConfigForm.get('legendColumnTitleFont').disable(); + this.timeSeriesChartWidgetConfigForm.get('legendColumnTitleColor').disable(); this.timeSeriesChartWidgetConfigForm.get('legendLabelFont').disable(); this.timeSeriesChartWidgetConfigForm.get('legendLabelColor').disable(); + this.timeSeriesChartWidgetConfigForm.get('legendValueFont').disable(); + this.timeSeriesChartWidgetConfigForm.get('legendValueColor').disable(); this.timeSeriesChartWidgetConfigForm.get('legendConfig').disable(); } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart-widget.component.html index 4430579899..147214c355 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart-widget.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart-widget.component.html @@ -63,11 +63,11 @@ - {{ 'legend.Min' | translate }} - {{ 'legend.Max' | translate }} - {{ 'legend.Avg' | translate }} - {{ 'legend.Total' | translate }} - {{ 'legend.Latest' | translate }} + {{ 'legend.Min' | translate }} + {{ 'legend.Max' | translate }} + {{ 'legend.Avg' | translate }} + {{ 'legend.Total' | translate }} + {{ 'legend.Latest' | translate }} @@ -75,19 +75,19 @@ - + {{ legendData.data[legendKey.dataIndex].min }} - + {{ legendData.data[legendKey.dataIndex].max }} - + {{ legendData.data[legendKey.dataIndex].avg }} - + {{ legendData.data[legendKey.dataIndex].total }} - + {{ legendData.data[legendKey.dataIndex].latest }} @@ -113,8 +113,8 @@ - {{ label | translate }} - + {{ label | translate }} + {{ legendData.data[legendKey.dataIndex][type] }} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart-widget.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart-widget.component.scss index ee0400d96f..79a162a7ae 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart-widget.component.scss +++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart-widget.component.scss @@ -147,11 +147,6 @@ $maxLegendHeight: 35%; } } .tb-time-series-chart-legend-type-label { - font-size: 12px; - font-style: normal; - font-weight: 400; - line-height: 16px; - color: rgba(0, 0, 0, 0.38); white-space: nowrap; text-align: left; &.right { @@ -159,11 +154,6 @@ $maxLegendHeight: 35%; } } .tb-time-series-chart-legend-value { - font-size: 12px; - font-style: normal; - font-weight: 500; - line-height: 16px; - color: rgba(0, 0, 0, 0.87); white-space: nowrap; text-align: right; } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart-widget.component.ts index fbb23c6c84..81ea480531 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart-widget.component.ts @@ -73,8 +73,10 @@ export class TimeSeriesChartWidgetComponent implements OnInit, OnDestroy, AfterV overlayEnabled: boolean; padding: string; + legendColumnTitleStyle: ComponentStyle; legendLabelStyle: ComponentStyle; disabledLegendLabelStyle: ComponentStyle; + legendValueStyle: ComponentStyle; displayLegendValues = false; @@ -117,9 +119,13 @@ export class TimeSeriesChartWidgetComponent implements OnInit, OnDestroy, AfterV if (this.showLegend) { this.horizontalLegendPosition = [LegendPosition.left, LegendPosition.right].includes(this.legendConfig.position); this.legendClass = `legend-${this.legendConfig.position}`; + this.legendColumnTitleStyle = textStyle(this.settings.legendColumnTitleFont); + this.legendColumnTitleStyle.color = this.settings.legendColumnTitleColor; this.legendLabelStyle = textStyle(this.settings.legendLabelFont); this.disabledLegendLabelStyle = textStyle(this.settings.legendLabelFont); this.legendLabelStyle.color = this.settings.legendLabelColor; + this.legendValueStyle = textStyle(this.settings.legendValueFont); + this.legendValueStyle.color = this.settings.legendValueColor; this.displayLegendValues = this.legendConfig.showMin || this.legendConfig.showMax || this.legendConfig.showAvg || this.legendConfig.showTotal || this.legendConfig.showLatest; } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart-widget.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart-widget.models.ts index 6edf87251c..00abd27c33 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart-widget.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart-widget.models.ts @@ -21,8 +21,12 @@ import { mergeDeep } from '@core/utils'; export interface TimeSeriesChartWidgetSettings extends TimeSeriesChartSettings { showLegend: boolean; + legendColumnTitleFont: Font; + legendColumnTitleColor: string; legendLabelFont: Font; legendLabelColor: string; + legendValueFont: Font; + legendValueColor: string; legendConfig: LegendConfig; background: BackgroundSettings; padding: string; @@ -31,6 +35,15 @@ export interface TimeSeriesChartWidgetSettings extends TimeSeriesChartSettings { export const timeSeriesChartWidgetDefaultSettings: TimeSeriesChartWidgetSettings = mergeDeep({} as TimeSeriesChartWidgetSettings, timeSeriesChartDefaultSettings as TimeSeriesChartWidgetSettings, { showLegend: true, + legendColumnTitleFont: { + family: 'Roboto', + size: 12, + sizeUnit: 'px', + style: 'normal', + weight: '400', + lineHeight: '16px' + }, + legendColumnTitleColor: 'rgba(0, 0, 0, 0.38)', legendLabelFont: { family: 'Roboto', size: 12, @@ -40,6 +53,15 @@ export const timeSeriesChartWidgetDefaultSettings: TimeSeriesChartWidgetSettings lineHeight: '16px' }, legendLabelColor: 'rgba(0, 0, 0, 0.76)', + legendValueFont: { + family: 'Roboto', + size: 12, + sizeUnit: 'px', + style: 'normal', + weight: '500', + lineHeight: '16px' + }, + legendValueColor: 'rgba(0, 0, 0, 0.87)', legendConfig: {...defaultLegendConfig(widgetType.timeseries), position: LegendPosition.top}, background: { type: BackgroundType.color, diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-widget-settings.component.html index 6134f3dcae..08e59eef13 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-widget-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-widget-settings.component.html @@ -118,6 +118,7 @@
{{ 'legend.label' | translate }}
+
+
{{ 'legend.value' | translate }}
+
+ + + + +
+
+
+
{{ 'legend.column-title' | translate }}
+
+ + + + +
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-widget-settings.component.ts index 2b95d57b7d..cafa8c84b1 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-widget-settings.component.ts @@ -133,8 +133,12 @@ export class TimeSeriesChartWidgetSettingsComponent extends WidgetSettingsCompon noAggregationBarWidthSettings: [settings.noAggregationBarWidthSettings, []], showLegend: [settings.showLegend, []], + legendColumnTitleFont: [settings.legendColumnTitleFont, []], + legendColumnTitleColor: [settings.legendColumnTitleColor, []], legendLabelFont: [settings.legendLabelFont, []], legendLabelColor: [settings.legendLabelColor, []], + legendValueFont: [settings.legendValueFont, []], + legendValueColor: [settings.legendValueColor, []], legendConfig: [settings.legendConfig, []], showTooltip: [settings.showTooltip, []], @@ -182,12 +186,20 @@ export class TimeSeriesChartWidgetSettingsComponent extends WidgetSettingsCompon } if (showLegend) { + this.timeSeriesChartWidgetSettingsForm.get('legendColumnTitleFont').enable(); + this.timeSeriesChartWidgetSettingsForm.get('legendColumnTitleColor').enable(); this.timeSeriesChartWidgetSettingsForm.get('legendLabelFont').enable(); this.timeSeriesChartWidgetSettingsForm.get('legendLabelColor').enable(); + this.timeSeriesChartWidgetSettingsForm.get('legendValueFont').enable(); + this.timeSeriesChartWidgetSettingsForm.get('legendValueColor').enable(); this.timeSeriesChartWidgetSettingsForm.get('legendConfig').enable(); } else { + this.timeSeriesChartWidgetSettingsForm.get('legendColumnTitleFont').disable(); + this.timeSeriesChartWidgetSettingsForm.get('legendColumnTitleColor').disable(); this.timeSeriesChartWidgetSettingsForm.get('legendLabelFont').disable(); this.timeSeriesChartWidgetSettingsForm.get('legendLabelColor').disable(); + this.timeSeriesChartWidgetSettingsForm.get('legendValueFont').disable(); + this.timeSeriesChartWidgetSettingsForm.get('legendValueColor').disable(); this.timeSeriesChartWidgetSettingsForm.get('legendConfig').disable(); } 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 b62aac95c2..f4f8b4fced 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -3292,6 +3292,7 @@ "months": "(month ago)", "years": "(year ago)" }, + "column-title": "Column title", "label": "Label", "value": "Value" }, From fd7c7e9b519443b2fac6cd9d73ae0e9145d437c9 Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Wed, 17 Apr 2024 18:30:49 +0300 Subject: [PATCH 75/80] UI: Improve time series chart tooltip configuration - added font/color settings for tooltip label. --- ...art-with-labels-basic-config.component.html | 15 +++++++++++++++ ...chart-with-labels-basic-config.component.ts | 8 ++++++++ .../range-chart-basic-config.component.html | 18 ++++++++++++++++-- .../range-chart-basic-config.component.ts | 8 ++++++++ ...me-series-chart-basic-config.component.html | 15 +++++++++++++++ ...time-series-chart-basic-config.component.ts | 9 +++++++++ .../bar-chart-with-labels-widget.models.ts | 11 +++++++++++ .../widget/lib/chart/echarts-widget.models.ts | 15 ++++++++------- .../lib/chart/range-chart-widget.models.ts | 11 +++++++++++ .../lib/chart/time-series-chart.models.ts | 9 +++++++++ ...-with-labels-widget-settings.component.html | 15 +++++++++++++++ ...rt-with-labels-widget-settings.component.ts | 6 ++++++ .../range-chart-widget-settings.component.html | 15 +++++++++++++++ .../range-chart-widget-settings.component.ts | 6 ++++++ ...series-chart-widget-settings.component.html | 15 +++++++++++++++ ...e-series-chart-widget-settings.component.ts | 7 +++++++ .../assets/locale/locale.constant-en_US.json | 1 + 17 files changed, 175 insertions(+), 9 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/bar-chart-with-labels-basic-config.component.html b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/bar-chart-with-labels-basic-config.component.html index fa0932175c..6f263f05d3 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/bar-chart-with-labels-basic-config.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/bar-chart-with-labels-basic-config.component.html @@ -239,10 +239,24 @@ +
+
{{ 'tooltip.label' | translate }}
+
+ + + + +
+
{{ 'tooltip.value' | translate }}
+
+
{{ 'tooltip.label' | translate }}
+
+ + + + +
+
{{ 'tooltip.value' | translate }}
-
-
+
{{ 'tooltip.date' | translate }} @@ -321,6 +334,7 @@ {{ 'tooltip.trigger-axis' | translate }}
+
+
{{ 'tooltip.label' | translate }}
+
+ + + + +
+
{{ 'tooltip.value' | translate }}
item.piece) }, showTooltip: settings.showTooltip, + tooltipLabelFont: settings.tooltipLabelFont, + tooltipLabelColor: settings.tooltipLabelColor, tooltipValueFont: settings.tooltipValueFont, tooltipValueColor: settings.tooltipValueColor, tooltipShowDate: settings.tooltipShowDate, diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.models.ts index bfe65a0d7c..4fdc8063aa 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.models.ts @@ -752,6 +752,15 @@ export const timeSeriesChartDefaultSettings: TimeSeriesChartSettings = { timeSeriesChartNoAggregationBarWidthDefaultSettings), showTooltip: true, tooltipTrigger: EChartsTooltipTrigger.axis, + tooltipLabelFont: { + family: 'Roboto', + size: 12, + sizeUnit: 'px', + style: 'normal', + weight: '400', + lineHeight: '16px' + }, + tooltipLabelColor: 'rgba(0, 0, 0, 0.76)', tooltipValueFont: { family: 'Roboto', size: 12, diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/bar-chart-with-labels-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/bar-chart-with-labels-widget-settings.component.html index 4123df9c9f..04b2bff29c 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/bar-chart-with-labels-widget-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/bar-chart-with-labels-widget-settings.component.html @@ -160,10 +160,24 @@ +
+
{{ 'tooltip.label' | translate }}
+
+ + + + +
+
{{ 'tooltip.value' | translate }}
+
+
{{ 'tooltip.label' | translate }}
+
+ + + + +
+
{{ 'tooltip.value' | translate }}
{{ 'tooltip.trigger-axis' | translate }}
+
+
{{ 'tooltip.label' | translate }}
+
+ + + + +
+
{{ 'tooltip.value' | translate }}
Date: Thu, 18 Apr 2024 10:05:13 +0300 Subject: [PATCH 76/80] UI: Improve time series charts ticks generator function. --- .../widget/lib/chart/time-series-chart.models.ts | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.models.ts index 4fdc8063aa..e03c2a7158 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.models.ts @@ -974,18 +974,19 @@ export const createTimeSeriesYAxis = (units: string, ticksFormatter = parseFunction(settings.ticksFormatter, ['value']); } } + let configuredTicksGenerator: TimeSeriesChartTicksGenerator; let ticksGenerator: TimeSeriesChartTicksGenerator; let minInterval: number; let interval: number; let splitNumber: number; if (settings.ticksGenerator) { if (isFunction(settings.ticksGenerator)) { - ticksGenerator = settings.ticksGenerator as TimeSeriesChartTicksGenerator; + configuredTicksGenerator = settings.ticksGenerator as TimeSeriesChartTicksGenerator; } else if (settings.ticksGenerator.length) { - ticksGenerator = parseFunction(settings.ticksGenerator, ['extent', 'interval', 'niceTickExtent', 'intervalPrecision']); + configuredTicksGenerator = parseFunction(settings.ticksGenerator, ['extent', 'interval', 'niceTickExtent', 'intervalPrecision']); } } - if (!ticksGenerator) { + if (!configuredTicksGenerator) { interval = settings.interval; if (isUndefinedOrNull(interval)) { if (isDefinedAndNotNull(settings.splitNumber)) { @@ -994,6 +995,11 @@ export const createTimeSeriesYAxis = (units: string, minInterval = (1 / Math.pow(10, decimals)); } } + } else { + ticksGenerator = (extent, ticksInterval, niceTickExtent, intervalPrecision) => { + const ticks = configuredTicksGenerator(extent, ticksInterval, niceTickExtent, intervalPrecision); + return ticks?.filter(tick => tick.value >= extent[0] && tick.value <= extent[1]); + }; } return { id: settings.id, From 558d2421e6c08ea9be813884a87d36cce6e04a3b Mon Sep 17 00:00:00 2001 From: Volodymyr Babak Date: Thu, 18 Apr 2024 12:12:12 +0300 Subject: [PATCH 77/80] Added details description of EDGES_RPC_CLIENT_MAX_KEEP_ALIVE_TIME_SEC, EDGES_RPC_KEEP_ALIVE_TIME_SEC, EDGES_RPC_KEEP_ALIVE_TIMEOUT_SEC --- application/src/main/resources/thingsboard.yml | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index 33146b1b82..3623d93291 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -1291,11 +1291,17 @@ edges: rpc: # RPC port bind port: "${EDGES_RPC_PORT:7070}" - # Maximum time of keeping alive client RPC connection. Time in seconds + # Specifies the minimum amount of time that should elapse between keepalive pings sent by the client + # This prevents clients from sending pings too frequently, which can be a nuisance to the server (potentially even a denial-of-service attack vector if abused) + # If a client sends pings more frequently than this interval, the server may terminate the connection. client_max_keep_alive_time_sec: "${EDGES_RPC_CLIENT_MAX_KEEP_ALIVE_TIME_SEC:1}" - # Maximum time of keep-alive connection. Time in seconds + # Sets the time of inactivity (no read operations on the connection) after which the server will send a keepalive ping to the client. + # This is used to ensure that the connection is still alive and to prevent network intermediaries from dropping connections due to inactivity. + # It's a way for the server to proactively check if the client is still responsive. keep_alive_time_sec: "${EDGES_RPC_KEEP_ALIVE_TIME_SEC:10}" - # Timeout of keep alive RPC connection. Time in seconds + # Specifies the maximum amount of time the server waits for a response to its keepalive ping. + # If the ping is not acknowledged within this time frame, the server considers the connection dead and may close it. + # This timeout helps detect unresponsive clients. keep_alive_timeout_sec: "${EDGES_RPC_KEEP_ALIVE_TIMEOUT_SEC:5}" ssl: # Enable/disable SSL support From 6f2907355634c0a90f95a10eac14f99d2f35e735 Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Thu, 18 Apr 2024 13:20:04 +0300 Subject: [PATCH 78/80] OAuth2 fix: Detect client registration method according to new spring security API. --- .../server/dao/oauth2/HybridClientRegistrationRepository.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/dao/src/main/java/org/thingsboard/server/dao/oauth2/HybridClientRegistrationRepository.java b/dao/src/main/java/org/thingsboard/server/dao/oauth2/HybridClientRegistrationRepository.java index 4c90583e19..004c89bd54 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/oauth2/HybridClientRegistrationRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/oauth2/HybridClientRegistrationRepository.java @@ -52,7 +52,8 @@ public class HybridClientRegistrationRepository implements ClientRegistrationRep .userInfoUri(registration.getUserInfoUri()) .userNameAttributeName(registration.getUserNameAttributeName()) .jwkSetUri(registration.getJwkSetUri()) - .clientAuthenticationMethod(new ClientAuthenticationMethod(registration.getClientAuthenticationMethod())) + .clientAuthenticationMethod(registration.getClientAuthenticationMethod().equals("POST") ? + ClientAuthenticationMethod.CLIENT_SECRET_POST : ClientAuthenticationMethod.CLIENT_SECRET_BASIC) .redirectUri(defaultRedirectUriTemplate) .build(); } From f0cd43db5c2d91c2aed6594a314146264a1cf9a9 Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Thu, 18 Apr 2024 13:34:47 +0300 Subject: [PATCH 79/80] LWM2M: Catch client serialization error. --- .../lwm2m/server/store/TbRedisLwM2MClientStore.java | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbRedisLwM2MClientStore.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbRedisLwM2MClientStore.java index 542327cccc..17f9b4a978 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbRedisLwM2MClientStore.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbRedisLwM2MClientStore.java @@ -94,9 +94,13 @@ public class TbRedisLwM2MClientStore implements TbLwM2MClientStore { if (client.getState().equals(LwM2MClientState.UNREGISTERED)) { log.error("[{}] Client is in invalid state: {}!", client.getEndpoint(), client.getState(), new Exception()); } else { - byte[] clientSerialized = serialize(client); - try (var connection = connectionFactory.getConnection()) { - connection.getSet(getKey(client.getEndpoint()), clientSerialized); + try { + byte[] clientSerialized = serialize(client); + try (var connection = connectionFactory.getConnection()) { + connection.getSet(getKey(client.getEndpoint()), clientSerialized); + } + } catch (Exception e) { + log.warn("Failed to serialize client: {}", client, e); } } } From c07f432b7dc1b96c5714a7f9f8c0f0769c3e7c6e Mon Sep 17 00:00:00 2001 From: nick Date: Fri, 19 Apr 2024 12:21:41 +0300 Subject: [PATCH 80/80] lwm2m: fix bug API getLwm2mObjects --- ui-ngx/src/app/core/http/device-profile.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui-ngx/src/app/core/http/device-profile.service.ts b/ui-ngx/src/app/core/http/device-profile.service.ts index 91ca903c7a..7a9dfd13ba 100644 --- a/ui-ngx/src/app/core/http/device-profile.service.ts +++ b/ui-ngx/src/app/core/http/device-profile.service.ts @@ -60,7 +60,7 @@ export class DeviceProfileService { public getLwm2mObjects(sortOrder: SortOrder, objectIds?: string[], searchText?: string, config?: RequestConfig): Observable> { - let url = `/api/resource/lwm2m/?sortProperty=${sortOrder.property}&sortOrder=${sortOrder.direction}`; + let url = `/api/resource/lwm2m?sortProperty=${sortOrder.property}&sortOrder=${sortOrder.direction}`; if (isDefinedAndNotNull(objectIds) && objectIds.length > 0) { url += `&objectIds=${objectIds}`; }