[basicprofiles] Add $DELTA_PERCENT function to State Filter profile (#17843)

* [basicprofiles] Add $DELTA_PERCENT function to State Filter profile

Signed-off-by: Jimmy Tanagra <jcode@tanagra.id.au>
This commit is contained in:
jimtng 2024-12-06 05:52:42 +10:00 committed by GitHub
parent 036c1231c4
commit 1190c14f75
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 85 additions and 10 deletions

View File

@ -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`. - 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: - One of the special functions supported by State Filter:
- `$DELTA` to represent the absolute difference between the incoming value and the previously accepted value. - `$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. - `$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. - `$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. - `$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: The incoming state can be compared against other items:
```java ```java

View File

@ -103,7 +103,7 @@ public class StateFilterProfile implements StateProfile {
private @Nullable Item linkedItem = null; private @Nullable Item linkedItem = null;
private State newState = UnDefType.UNDEF; private State newState = UnDefType.UNDEF;
private State deltaState = UnDefType.UNDEF; private State acceptedState = UnDefType.UNDEF;
private LinkedList<State> previousStates = new LinkedList<>(); private LinkedList<State> previousStates = new LinkedList<>();
private final int windowSize; private final int windowSize;
@ -300,8 +300,16 @@ public class StateFilterProfile implements StateProfile {
public StateCondition(String lhs, ComparisonType comparisonType, String rhs) { public StateCondition(String lhs, ComparisonType comparisonType, String rhs) {
this.comparisonType = comparisonType; 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 // 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 // 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 // Anything else, defer parsing until we're checking the condition
@ -422,7 +430,7 @@ public class StateFilterProfile implements StateProfile {
}; };
if (result) { if (result) {
deltaState = input; acceptedState = input;
} }
return result; return result;
@ -494,6 +502,7 @@ public class StateFilterProfile implements StateProfile {
class FunctionType implements State { class FunctionType implements State {
enum Function { enum Function {
DELTA, DELTA,
DELTA_PERCENT,
AVERAGE, AVERAGE,
AVG, AVG,
MEDIAN, MEDIAN,
@ -517,6 +526,7 @@ public class StateFilterProfile implements StateProfile {
List<State> states = start <= 0 ? previousStates : previousStates.subList(start, size); List<State> states = start <= 0 ? previousStates : previousStates.subList(start, size);
return switch (type) { return switch (type) {
case DELTA -> calculateDelta(); case DELTA -> calculateDelta();
case DELTA_PERCENT -> calculateDeltaPercent();
case AVG, AVERAGE -> calculateAverage(states); case AVG, AVERAGE -> calculateAverage(states);
case MEDIAN -> calculateMedian(states); case MEDIAN -> calculateMedian(states);
case STDDEV -> calculateStdDev(states); case STDDEV -> calculateStdDev(states);
@ -534,9 +544,9 @@ public class StateFilterProfile implements StateProfile {
} }
public int getWindowSize() { 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, // 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 0;
} }
return windowSize.orElse(DEFAULT_WINDOW_SIZE); return windowSize.orElse(DEFAULT_WINDOW_SIZE);
@ -650,19 +660,39 @@ public class StateFilterProfile implements StateProfile {
} }
private @Nullable State calculateDelta() { private @Nullable State calculateDelta() {
if (deltaState == UnDefType.UNDEF) { if (acceptedState == UnDefType.UNDEF) {
logger.debug("No previous data to calculate delta"); logger.debug("No previous data to calculate delta");
deltaState = newState; acceptedState = newState;
return null; return null;
} }
if (newState instanceof QuantityType newStateQuantity) { 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; return result.toBigDecimal().compareTo(BigDecimal.ZERO) < 0 ? result.negate() : result;
} }
BigDecimal result = ((DecimalType) newState).toBigDecimal() 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); 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);
}
} }
} }

View File

@ -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("10"), true), //
Arguments.of(decimalItem, "$DELTA >= 1", decimals, DecimalType.valueOf("5.5"), false), // 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, "1 == $MIN", decimals, DecimalType.valueOf("20"), true), //
Arguments.of(decimalItem, "0 < $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), // Arguments.of(decimalItem, "$MIN > 0", decimals, DecimalType.valueOf("20"), true), //