Merge pull request #10300 from irynamatveieva/fix-calculate-delta-rule-node
Added property to ignore delta in output messages if it is zero
This commit is contained in:
		
						commit
						19c2c5e955
					
				@ -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",
 | 
			
		||||
@ -101,6 +104,11 @@ public class CalculateDeltaNode implements TbNode {
 | 
			
		||||
                        return;
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    if (config.isExcludeZeroDeltas() && delta.doubleValue() == 0) {
 | 
			
		||||
                        ctx.tellSuccess(msg);
 | 
			
		||||
                        return;
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    if (config.getRound() != null) {
 | 
			
		||||
                        delta = delta.setScale(config.getRound(), RoundingMode.HALF_UP);
 | 
			
		||||
                    }
 | 
			
		||||
@ -128,6 +136,23 @@ public class CalculateDeltaNode implements TbNode {
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public TbPair<Boolean, JsonNode> upgrade(int fromVersion, JsonNode oldConfiguration) throws TbNodeException {
 | 
			
		||||
        boolean hasChanges = false;
 | 
			
		||||
        switch (fromVersion) {
 | 
			
		||||
            case 0:
 | 
			
		||||
                String excludeZeroDeltas = "excludeZeroDeltas";
 | 
			
		||||
                if (!oldConfiguration.has(excludeZeroDeltas)) {
 | 
			
		||||
                    hasChanges = true;
 | 
			
		||||
                    ((ObjectNode) oldConfiguration).put(excludeZeroDeltas, false);
 | 
			
		||||
                }
 | 
			
		||||
                break;
 | 
			
		||||
            default:
 | 
			
		||||
                break;
 | 
			
		||||
        }
 | 
			
		||||
        return new TbPair<>(hasChanges, oldConfiguration);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private ListenableFuture<ValueWithTs> fetchLatestValueAsync(EntityId entityId) {
 | 
			
		||||
        return Futures.transform(timeseriesService.findLatest(ctx.getTenantId(), entityId, Collections.singletonList(config.getInputValueKey())),
 | 
			
		||||
                list -> extractValue(list.get(0))
 | 
			
		||||
 | 
			
		||||
@ -30,6 +30,7 @@ public class CalculateDeltaNodeConfiguration implements NodeConfiguration<Calcul
 | 
			
		||||
    private String periodValueKey;
 | 
			
		||||
    private Integer round;
 | 
			
		||||
    private boolean tellFailureIfDeltaIsNegative;
 | 
			
		||||
    private boolean excludeZeroDeltas;
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public CalculateDeltaNodeConfiguration defaultConfiguration() {
 | 
			
		||||
@ -40,6 +41,7 @@ public class CalculateDeltaNodeConfiguration implements NodeConfiguration<Calcul
 | 
			
		||||
        configuration.setAddPeriodBetweenMsgs(false);
 | 
			
		||||
        configuration.setPeriodValueKey("periodInMs");
 | 
			
		||||
        configuration.setTellFailureIfDeltaIsNegative(true);
 | 
			
		||||
        configuration.setExcludeZeroDeltas(false);
 | 
			
		||||
        return configuration;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -16,19 +16,26 @@
 | 
			
		||||
package org.thingsboard.rule.engine.metadata;
 | 
			
		||||
 | 
			
		||||
import com.google.common.util.concurrent.Futures;
 | 
			
		||||
import lombok.Data;
 | 
			
		||||
import lombok.RequiredArgsConstructor;
 | 
			
		||||
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.junit.jupiter.params.ParameterizedTest;
 | 
			
		||||
import org.junit.jupiter.params.provider.Arguments;
 | 
			
		||||
import org.junit.jupiter.params.provider.MethodSource;
 | 
			
		||||
import org.mockito.ArgumentCaptor;
 | 
			
		||||
import org.mockito.ArgumentMatcher;
 | 
			
		||||
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.AbstractRuleNodeUpgradeTest;
 | 
			
		||||
import org.thingsboard.rule.engine.TestDbCallbackExecutor;
 | 
			
		||||
import org.thingsboard.rule.engine.api.TbContext;
 | 
			
		||||
import org.thingsboard.rule.engine.api.TbNode;
 | 
			
		||||
import org.thingsboard.rule.engine.api.TbNodeConfiguration;
 | 
			
		||||
import org.thingsboard.rule.engine.api.TbNodeException;
 | 
			
		||||
import org.thingsboard.server.common.data.id.DeviceId;
 | 
			
		||||
@ -48,7 +55,10 @@ import org.thingsboard.server.dao.timeseries.TimeseriesService;
 | 
			
		||||
 | 
			
		||||
import java.util.List;
 | 
			
		||||
import java.util.UUID;
 | 
			
		||||
import java.util.function.BiConsumer;
 | 
			
		||||
import java.util.stream.Stream;
 | 
			
		||||
 | 
			
		||||
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.assertInstanceOf;
 | 
			
		||||
@ -63,10 +73,11 @@ 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.verifyNoMoreInteractions;
 | 
			
		||||
import static org.mockito.Mockito.when;
 | 
			
		||||
 | 
			
		||||
@ExtendWith(MockitoExtension.class)
 | 
			
		||||
public class CalculateDeltaNodeTest {
 | 
			
		||||
public class CalculateDeltaNodeTest extends AbstractRuleNodeUpgradeTest {
 | 
			
		||||
 | 
			
		||||
    private static final DeviceId DUMMY_DEVICE_ORIGINATOR = new DeviceId(UUID.randomUUID());
 | 
			
		||||
    private static final TenantId TENANT_ID = new TenantId(UUID.randomUUID());
 | 
			
		||||
@ -75,13 +86,13 @@ public class CalculateDeltaNodeTest {
 | 
			
		||||
    private TbContext ctxMock;
 | 
			
		||||
    @Mock
 | 
			
		||||
    private TimeseriesService timeseriesServiceMock;
 | 
			
		||||
    @Spy
 | 
			
		||||
    private CalculateDeltaNode node;
 | 
			
		||||
    private CalculateDeltaNodeConfiguration config;
 | 
			
		||||
    private TbNodeConfiguration nodeConfiguration;
 | 
			
		||||
 | 
			
		||||
    @BeforeEach
 | 
			
		||||
    public void setUp() throws TbNodeException {
 | 
			
		||||
        node = new CalculateDeltaNode();
 | 
			
		||||
        config = new CalculateDeltaNodeConfiguration().defaultConfiguration();
 | 
			
		||||
        nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config));
 | 
			
		||||
        when(ctxMock.getTimeseriesService()).thenReturn(timeseriesServiceMock);
 | 
			
		||||
@ -424,6 +435,91 @@ public class CalculateDeltaNodeTest {
 | 
			
		||||
                .hasMessage("Calculation failed. JSON values are not supported!");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @ParameterizedTest
 | 
			
		||||
    @MethodSource("CalculateDeltaTestConfig")
 | 
			
		||||
    public void givenCalculateDeltaConfig_whenOnMsg_thenVerify(CalculateDeltaTestConfig testConfig) throws TbNodeException {
 | 
			
		||||
        // GIVEN
 | 
			
		||||
        config.setTellFailureIfDeltaIsNegative(testConfig.isTellFailureIfDeltaIsNegative());
 | 
			
		||||
        config.setExcludeZeroDeltas(testConfig.isExcludeZeroDeltas());
 | 
			
		||||
        config.setInputValueKey("temperature");
 | 
			
		||||
        nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config));
 | 
			
		||||
        node.init(ctxMock, nodeConfiguration);
 | 
			
		||||
 | 
			
		||||
        mockFindLatest(new BasicTsKvEntry(1L, new DoubleDataEntry("temperature", testConfig.getPrevValue())));
 | 
			
		||||
 | 
			
		||||
        var msgData = "{\"temperature\":" + testConfig.getCurrentValue() + ",\"airPressure\":123}";
 | 
			
		||||
        var msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, DUMMY_DEVICE_ORIGINATOR, TbMsgMetaData.EMPTY, msgData);
 | 
			
		||||
 | 
			
		||||
        // WHEN
 | 
			
		||||
 | 
			
		||||
        node.onMsg(ctxMock, msg);
 | 
			
		||||
 | 
			
		||||
        // THEN
 | 
			
		||||
        testConfig.getVerificationMethod().accept(ctxMock, msg);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static Stream<CalculateDeltaTestConfig> 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) -> {
 | 
			
		||||
                    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, 41, 40, (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 is set to true so it should return message with delta if delta is negative is set to false.
 | 
			
		||||
                new CalculateDeltaTestConfig(false, true, 41, 40, (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 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 is set to true so it should return message with delta.
 | 
			
		||||
                new CalculateDeltaTestConfig(false, true, 39, 40, (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 is set to false so it should return message with delta.
 | 
			
		||||
                new CalculateDeltaTestConfig(false, false, 39, 40, (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
 | 
			
		||||
    private static class CalculateDeltaTestConfig {
 | 
			
		||||
        private final boolean tellFailureIfDeltaIsNegative;
 | 
			
		||||
        private final boolean excludeZeroDeltas;
 | 
			
		||||
        private final double prevValue;
 | 
			
		||||
        private final double currentValue;
 | 
			
		||||
        private final BiConsumer<TbContext, TbMsg> verificationMethod;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private void mockFindLatest(TsKvEntry tsKvEntry) {
 | 
			
		||||
        when(ctxMock.getTenantId()).thenReturn(TENANT_ID);
 | 
			
		||||
        when(timeseriesServiceMock.findLatestSync(
 | 
			
		||||
@ -457,4 +553,24 @@ public class CalculateDeltaNodeTest {
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static Stream<Arguments> 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, \"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, \"excludeZeroDeltas\":false}",
 | 
			
		||||
                        false,
 | 
			
		||||
                        "{\"inputValueKey\":\"pulseCounter\",\"outputValueKey\":\"delta\",\"useCache\":true,\"addPeriodBetweenMsgs\":false, \"periodValueKey\":\"periodInMs\", \"round\":null,\"tellFailureIfDeltaIsNegative\":true, \"excludeZeroDeltas\":false}")
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    protected TbNode getTestNode() {
 | 
			
		||||
        return node;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user