diff --git a/bundles/org.openhab.transform.basicprofiles/README.md b/bundles/org.openhab.transform.basicprofiles/README.md index 0efbef60229..65629f1ef23 100644 --- a/bundles/org.openhab.transform.basicprofiles/README.md +++ b/bundles/org.openhab.transform.basicprofiles/README.md @@ -213,6 +213,12 @@ The `LHS_OPERAND` and the `RHS_OPERAND` can be either one of these: - A number with a unit to represent a `QuantityType`, for example `1.2 kW`, or `24 °C`. - One of the special functions supported by State Filter: - `$DELTA` to represent the absolute difference between the incoming value and the previously accepted value. + The calculated delta value is absolute, i.e. it is always positive. + For example, with an initial data of `10`, a new data of `12` or `8` would both result in a $DELTA of `2`. + - `$DELTA_PERCENT` to represent the difference in percentage. + It is calculated as `($DELTA / current_data) * 100`. + Note that this can also be done by omitting the `LHS_OPERAND` and using a number followed with a percent sign `%` as the `RHS_OPERAND`. + See the example below. - `$AVERAGE`, or `$AVG` to represent the average of the previous unfiltered incoming values. - `$STDDEV` to represent the _population_ standard deviation of the previous unfiltered incoming values. - `$MEDIAN` to represent the median value of the previous unfiltered incoming values. @@ -271,6 +277,19 @@ Number:Power PowerUsage { } ``` +Accept new data only if it's 10% higher or 10% lower than the previously accepted data: + +```java +Number:Temperature BoilerTemperature { + channel="mybinding:mything:mychannel" [ profile="basic-profiles:state-filter", conditions="$DELTA_PERCENT > 10" ] +} + +// Or more succinctly: +Number:Temperature BoilerTemperature { + channel="mybinding:mything:mychannel" [ profile="basic-profiles:state-filter", conditions="> 10%" ] +} +``` + The incoming state can be compared against other items: ```java diff --git a/bundles/org.openhab.transform.basicprofiles/src/main/java/org/openhab/transform/basicprofiles/internal/profiles/StateFilterProfile.java b/bundles/org.openhab.transform.basicprofiles/src/main/java/org/openhab/transform/basicprofiles/internal/profiles/StateFilterProfile.java index 30a0ddf8956..9340a9d0cea 100644 --- a/bundles/org.openhab.transform.basicprofiles/src/main/java/org/openhab/transform/basicprofiles/internal/profiles/StateFilterProfile.java +++ b/bundles/org.openhab.transform.basicprofiles/src/main/java/org/openhab/transform/basicprofiles/internal/profiles/StateFilterProfile.java @@ -103,7 +103,7 @@ public class StateFilterProfile implements StateProfile { private @Nullable Item linkedItem = null; private State newState = UnDefType.UNDEF; - private State deltaState = UnDefType.UNDEF; + private State acceptedState = UnDefType.UNDEF; private LinkedList previousStates = new LinkedList<>(); private final int windowSize; @@ -300,8 +300,16 @@ public class StateFilterProfile implements StateProfile { public StateCondition(String lhs, ComparisonType comparisonType, String rhs) { this.comparisonType = comparisonType; - this.lhsString = lhs; - this.rhsString = rhs; + + if (lhs.isEmpty() && rhs.endsWith("%")) { + // Allow comparing percentages without a left hand side, + // e.g. `> 50%` -> translate this to `$DELTA_PERCENT > 50` + lhsString = "$DELTA_PERCENT"; + rhsString = rhs.substring(0, rhs.length() - 1).trim(); + } else { + lhsString = lhs; + rhsString = rhs; + } // Convert quoted strings to StringType, and UnDefTypes to UnDefType // UnDefType gets special treatment because we don't want `UNDEF` to be parsed as a string // Anything else, defer parsing until we're checking the condition @@ -422,7 +430,7 @@ public class StateFilterProfile implements StateProfile { }; if (result) { - deltaState = input; + acceptedState = input; } return result; @@ -494,6 +502,7 @@ public class StateFilterProfile implements StateProfile { class FunctionType implements State { enum Function { DELTA, + DELTA_PERCENT, AVERAGE, AVG, MEDIAN, @@ -517,6 +526,7 @@ public class StateFilterProfile implements StateProfile { List states = start <= 0 ? previousStates : previousStates.subList(start, size); return switch (type) { case DELTA -> calculateDelta(); + case DELTA_PERCENT -> calculateDeltaPercent(); case AVG, AVERAGE -> calculateAverage(states); case MEDIAN -> calculateMedian(states); case STDDEV -> calculateStdDev(states); @@ -534,9 +544,9 @@ public class StateFilterProfile implements StateProfile { } public int getWindowSize() { - if (type == Function.DELTA) { + if (type == Function.DELTA || type == Function.DELTA_PERCENT) { // We don't need to keep previous states list to calculate the delta, - // the previous state is kept in the deltaState variable + // the previous state is kept in the acceptedState variable return 0; } return windowSize.orElse(DEFAULT_WINDOW_SIZE); @@ -650,19 +660,39 @@ public class StateFilterProfile implements StateProfile { } private @Nullable State calculateDelta() { - if (deltaState == UnDefType.UNDEF) { + if (acceptedState == UnDefType.UNDEF) { logger.debug("No previous data to calculate delta"); - deltaState = newState; + acceptedState = newState; return null; } if (newState instanceof QuantityType newStateQuantity) { - QuantityType result = newStateQuantity.subtract((QuantityType) deltaState); + QuantityType result = newStateQuantity.subtract((QuantityType) acceptedState); return result.toBigDecimal().compareTo(BigDecimal.ZERO) < 0 ? result.negate() : result; } BigDecimal result = ((DecimalType) newState).toBigDecimal() - .subtract(((DecimalType) deltaState).toBigDecimal()); + .subtract(((DecimalType) acceptedState).toBigDecimal()); return result.compareTo(BigDecimal.ZERO) < 0 ? new DecimalType(result.negate()) : new DecimalType(result); } + + private @Nullable State calculateDeltaPercent() { + State calculatedDelta = calculateDelta(); + if (calculatedDelta == null) { + return null; + } + + BigDecimal bdDelta; + BigDecimal bdBase; + if (acceptedState instanceof QuantityType acceptedStateQuantity) { + // Assume that delta and base are in the same unit + bdDelta = ((QuantityType) calculatedDelta).toBigDecimal(); + bdBase = acceptedStateQuantity.toBigDecimal(); + } else { + bdDelta = ((DecimalType) calculatedDelta).toBigDecimal(); + bdBase = ((DecimalType) acceptedState).toBigDecimal(); + } + BigDecimal percent = bdDelta.multiply(BigDecimal.valueOf(100)).divide(bdBase, 2, RoundingMode.HALF_EVEN); + return new DecimalType(percent); + } } } diff --git a/bundles/org.openhab.transform.basicprofiles/src/test/java/org/openhab/transform/basicprofiles/internal/profiles/StateFilterProfileTest.java b/bundles/org.openhab.transform.basicprofiles/src/test/java/org/openhab/transform/basicprofiles/internal/profiles/StateFilterProfileTest.java index 7f4e57686f6..a97cd4724e1 100644 --- a/bundles/org.openhab.transform.basicprofiles/src/test/java/org/openhab/transform/basicprofiles/internal/profiles/StateFilterProfileTest.java +++ b/bundles/org.openhab.transform.basicprofiles/src/test/java/org/openhab/transform/basicprofiles/internal/profiles/StateFilterProfileTest.java @@ -674,6 +674,32 @@ public class StateFilterProfileTest { Arguments.of(decimalItem, "$DELTA >= 1", decimals, DecimalType.valueOf("10"), true), // Arguments.of(decimalItem, "$DELTA >= 1", decimals, DecimalType.valueOf("5.5"), false), // + Arguments.of(decimalItem, "$DELTA_PERCENT >= 10", decimals, DecimalType.valueOf("4.6"), false), // + Arguments.of(decimalItem, "$DELTA_PERCENT >= 10", decimals, DecimalType.valueOf("4.5"), true), // + Arguments.of(decimalItem, "$DELTA_PERCENT >= 10", decimals, DecimalType.valueOf("5.4"), false), // + Arguments.of(decimalItem, "$DELTA_PERCENT >= 10", decimals, DecimalType.valueOf("5.5"), true), // + Arguments.of(decimalItem, "$DELTA_PERCENT >= 10", decimals, DecimalType.valueOf("6"), true), // + + Arguments.of(decimalItem, ">= 10 %", decimals, DecimalType.valueOf("4.6"), false), // + Arguments.of(decimalItem, ">= 10%", decimals, DecimalType.valueOf("4.6"), false), // + Arguments.of(decimalItem, ">= 10%", decimals, DecimalType.valueOf("4.5"), true), // + Arguments.of(decimalItem, ">= 10%", decimals, DecimalType.valueOf("5.4"), false), // + Arguments.of(decimalItem, ">= 10%", decimals, DecimalType.valueOf("5.5"), true), // + Arguments.of(decimalItem, ">= 10%", decimals, DecimalType.valueOf("6"), true), // + + // The following will only accept new data if it is within 10% of the previously accepted data. + // so the second and subsequent initial data (i.e.: 2, 3, 4, 5) will be rejected. + // The new data is compared against the first (1) + Arguments.of(decimalItem, "$DELTA_PERCENT < 10", decimals, DecimalType.valueOf("1.09"), true), // + Arguments.of(decimalItem, "$DELTA_PERCENT < 10", decimals, DecimalType.valueOf("1.11"), false), // + Arguments.of(decimalItem, "$DELTA_PERCENT < 10", decimals, DecimalType.valueOf("0.91"), true), // + Arguments.of(decimalItem, "$DELTA_PERCENT < 10", decimals, DecimalType.valueOf("0.89"), false), // + + Arguments.of(decimalItem, "< 10%", decimals, DecimalType.valueOf("1.09"), true), // + Arguments.of(decimalItem, "< 10%", decimals, DecimalType.valueOf("1.11"), false), // + Arguments.of(decimalItem, "< 10%", decimals, DecimalType.valueOf("0.91"), true), // + Arguments.of(decimalItem, "< 10%", decimals, DecimalType.valueOf("0.89"), false), // + Arguments.of(decimalItem, "1 == $MIN", decimals, DecimalType.valueOf("20"), true), // Arguments.of(decimalItem, "0 < $MIN", decimals, DecimalType.valueOf("20"), true), // Arguments.of(decimalItem, "$MIN > 0", decimals, DecimalType.valueOf("20"), true), //