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`.
|
- 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
|
||||||
|
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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), //
|
||||||
|
Loading…
Reference in New Issue
Block a user