mirror of
https://github.com/openhab/openhab-addons.git
synced 2025-01-10 15:11:59 +01:00
[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:
parent
036c1231c4
commit
1190c14f75
@ -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
|
||||
|
@ -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<State> 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<State> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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), //
|
||||
|
Loading…
Reference in New Issue
Block a user