[basicprofiles] Fix StateFilterProfile to use linked Item system unit (#18144)
* [basicprofiles] Improve StateFilterProfile unit based calculations Signed-off-by: Andrew Fiddian-Green <software@whitebear.ch>pull/18257/head
parent
3b820edcb2
commit
d0bac82f21
|
@ -242,6 +242,10 @@ The `LHS_OPERAND` and the `RHS_OPERAND` can be either one of these:
|
||||||
This can be customized by specifying the "window size" or sample count applicable to the function, e.g. `$MEDIAN(10)` will return the median of the last 10 values.
|
This can be customized by specifying the "window size" or sample count applicable to the function, e.g. `$MEDIAN(10)` will return the median of the last 10 values.
|
||||||
All the functions except `$DELTA` support a custom window size.
|
All the functions except `$DELTA` support a custom window size.
|
||||||
|
|
||||||
|
In the case of comparisons and calculations involving `QuantityType` values, all the values are converted to the Unit of the linked Item before the calculation and/or comparison is done.
|
||||||
|
Note: if the binding sends a value that cannot be converted to the Unit of the linked Item, then that value is excluded.
|
||||||
|
e.g. if the linked item has a Unit of `Units.METRE` and the binding sends a value of `Units.CELSIUS` then the value is ignored.
|
||||||
|
|
||||||
The state of one item can be compared against the state of another item by having item names on both sides of the comparison, e.g.: `Item1 > Item2`.
|
The state of one item can be compared against the state of another item by having item names on both sides of the comparison, e.g.: `Item1 > Item2`.
|
||||||
When `LHS_OPERAND` is omitted, e.g. `> 10, < 100`, the comparisons are applied against the input data from the binding.
|
When `LHS_OPERAND` is omitted, e.g. `> 10, < 100`, the comparisons are applied against the input data from the binding.
|
||||||
The `RHS_OPERAND` can be any of the valid values listed above.
|
The `RHS_OPERAND` can be any of the valid values listed above.
|
||||||
|
|
|
@ -37,6 +37,7 @@ import org.eclipse.jdt.annotation.Nullable;
|
||||||
import org.openhab.core.items.Item;
|
import org.openhab.core.items.Item;
|
||||||
import org.openhab.core.items.ItemNotFoundException;
|
import org.openhab.core.items.ItemNotFoundException;
|
||||||
import org.openhab.core.items.ItemRegistry;
|
import org.openhab.core.items.ItemRegistry;
|
||||||
|
import org.openhab.core.library.items.NumberItem;
|
||||||
import org.openhab.core.library.types.DecimalType;
|
import org.openhab.core.library.types.DecimalType;
|
||||||
import org.openhab.core.library.types.QuantityType;
|
import org.openhab.core.library.types.QuantityType;
|
||||||
import org.openhab.core.library.types.StringType;
|
import org.openhab.core.library.types.StringType;
|
||||||
|
@ -53,6 +54,8 @@ import org.openhab.transform.basicprofiles.internal.config.StateFilterProfileCon
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import tech.units.indriya.AbstractUnit;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Accepts updates to state as long as conditions are met. Support for sending fixed state if conditions are *not*
|
* Accepts updates to state as long as conditions are met. Support for sending fixed state if conditions are *not*
|
||||||
* met.
|
* met.
|
||||||
|
@ -60,6 +63,7 @@ import org.slf4j.LoggerFactory;
|
||||||
* @author Arne Seime - Initial contribution
|
* @author Arne Seime - Initial contribution
|
||||||
* @author Jimmy Tanagra - Expanded the comparison types
|
* @author Jimmy Tanagra - Expanded the comparison types
|
||||||
* @author Jimmy Tanagra - Added support for functions
|
* @author Jimmy Tanagra - Added support for functions
|
||||||
|
* @author Andrew Fiddian-Green - Normalise calculations based on the Unit of the linked Item
|
||||||
*/
|
*/
|
||||||
@NonNullByDefault
|
@NonNullByDefault
|
||||||
public class StateFilterProfile implements StateProfile {
|
public class StateFilterProfile implements StateProfile {
|
||||||
|
@ -103,17 +107,25 @@ 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;
|
||||||
|
|
||||||
|
// single cached numeric state for use in conjunction with DELTA and DELTA_PERCENT functions
|
||||||
private Optional<State> acceptedState = Optional.empty();
|
private Optional<State> acceptedState = Optional.empty();
|
||||||
private LinkedList<State> previousStates = new LinkedList<>();
|
|
||||||
|
// cached list of prior numeric states for use in conjunction with AVG, MEDIAN, STDDEV, MIN, MAX functions
|
||||||
|
private final List<State> previousStates = new LinkedList<>();
|
||||||
|
|
||||||
private final int windowSize;
|
private final int windowSize;
|
||||||
|
|
||||||
|
// reference (zero based) system unit for conversions
|
||||||
|
private @Nullable Unit<?> systemUnit = null;
|
||||||
|
private boolean systemUnitInitialized = false;
|
||||||
|
|
||||||
public StateFilterProfile(ProfileCallback callback, ProfileContext context, ItemRegistry itemRegistry) {
|
public StateFilterProfile(ProfileCallback callback, ProfileContext context, ItemRegistry itemRegistry) {
|
||||||
this.callback = callback;
|
this.callback = callback;
|
||||||
this.itemRegistry = itemRegistry;
|
this.itemRegistry = itemRegistry;
|
||||||
|
|
||||||
StateFilterProfileConfig config = context.getConfiguration().as(StateFilterProfileConfig.class);
|
StateFilterProfileConfig config = context.getConfiguration().as(StateFilterProfileConfig.class);
|
||||||
if (config != null) {
|
|
||||||
conditions = parseConditions(config.conditions, config.separator);
|
conditions = parseConditions(config.conditions, config.separator);
|
||||||
int maxWindowSize = 0;
|
int maxWindowSize = 0;
|
||||||
|
|
||||||
|
@ -138,13 +150,7 @@ public class StateFilterProfile implements StateProfile {
|
||||||
}
|
}
|
||||||
|
|
||||||
windowSize = maxWindowSize;
|
windowSize = maxWindowSize;
|
||||||
|
|
||||||
configMismatchState = parseState(config.mismatchState, context.getAcceptedDataTypes());
|
configMismatchState = parseState(config.mismatchState, context.getAcceptedDataTypes());
|
||||||
} else {
|
|
||||||
conditions = List.of();
|
|
||||||
configMismatchState = null;
|
|
||||||
windowSize = 0;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<StateCondition> parseConditions(List<String> conditions, String separator) {
|
private List<StateCondition> parseConditions(List<String> conditions, String separator) {
|
||||||
|
@ -204,6 +210,10 @@ public class StateFilterProfile implements StateProfile {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onStateUpdateFromHandler(State state) {
|
public void onStateUpdateFromHandler(State state) {
|
||||||
|
if (!isAllowed(state)) {
|
||||||
|
logger.debug("Received non allowed state update from handler: {}, ignored", state);
|
||||||
|
return;
|
||||||
|
}
|
||||||
newState = state;
|
newState = state;
|
||||||
State resultState = checkCondition(state);
|
State resultState = checkCondition(state);
|
||||||
if (resultState != null) {
|
if (resultState != null) {
|
||||||
|
@ -212,7 +222,7 @@ public class StateFilterProfile implements StateProfile {
|
||||||
} else {
|
} else {
|
||||||
logger.debug("Received state update from handler: {}, not forwarded to item", state);
|
logger.debug("Received state update from handler: {}, not forwarded to item", state);
|
||||||
}
|
}
|
||||||
if (windowSize > 0 && (state instanceof DecimalType || state instanceof QuantityType)) {
|
if (windowSize > 0 && isCacheable(state)) {
|
||||||
previousStates.add(state);
|
previousStates.add(state);
|
||||||
if (previousStates.size() > windowSize) {
|
if (previousStates.size() > windowSize) {
|
||||||
previousStates.removeFirst();
|
previousStates.removeFirst();
|
||||||
|
@ -230,7 +240,9 @@ public class StateFilterProfile implements StateProfile {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (conditions.stream().allMatch(c -> c.check(state))) {
|
if (conditions.stream().allMatch(c -> c.check(state))) {
|
||||||
|
if (isCacheable(state)) {
|
||||||
acceptedState = Optional.of(state);
|
acceptedState = Optional.of(state);
|
||||||
|
}
|
||||||
return state;
|
return state;
|
||||||
} else {
|
} else {
|
||||||
return configMismatchState;
|
return configMismatchState;
|
||||||
|
@ -342,6 +354,9 @@ public class StateFilterProfile implements StateProfile {
|
||||||
if (rhsFunction.alwaysAccept()) {
|
if (rhsFunction.alwaysAccept()) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
if (rhsFunction.getType() == FunctionType.Function.DELTA) {
|
||||||
|
isDeltaCheck = true;
|
||||||
|
}
|
||||||
rhsItem = getLinkedItem();
|
rhsItem = getLinkedItem();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -397,10 +412,6 @@ public class StateFilterProfile implements StateProfile {
|
||||||
|
|
||||||
// Don't convert QuantityType to other types, so that 1500 != 1500 W
|
// Don't convert QuantityType to other types, so that 1500 != 1500 W
|
||||||
if (rhsState != null && !(rhsState instanceof QuantityType)) {
|
if (rhsState != null && !(rhsState instanceof QuantityType)) {
|
||||||
if (rhsState instanceof FunctionType rhsFunction
|
|
||||||
&& rhsFunction.getType() == FunctionType.Function.DELTA) {
|
|
||||||
isDeltaCheck = true;
|
|
||||||
}
|
|
||||||
// Try to convert it to the same type as the lhs
|
// Try to convert it to the same type as the lhs
|
||||||
// This allows comparing compatible types, e.g. PercentType vs OnOffType
|
// This allows comparing compatible types, e.g. PercentType vs OnOffType
|
||||||
rhsState = rhsState.as(lhsState.getClass());
|
rhsState = rhsState.as(lhsState.getClass());
|
||||||
|
@ -438,10 +449,15 @@ public class StateFilterProfile implements StateProfile {
|
||||||
|
|
||||||
rhs = Objects.requireNonNull(rhsState instanceof StringType ? rhsState.toString() : rhsState);
|
rhs = Objects.requireNonNull(rhsState instanceof StringType ? rhsState.toString() : rhsState);
|
||||||
|
|
||||||
if (isDeltaCheck && rhs instanceof QuantityType rhsQty && lhs instanceof QuantityType lhsQty) {
|
if ((rhs instanceof QuantityType rhsQty) && (lhs instanceof QuantityType lhsQty)) {
|
||||||
|
if (isDeltaCheck) {
|
||||||
if (rhsQty.toUnitRelative(lhsQty.getUnit()) instanceof QuantityType relativeRhs) {
|
if (rhsQty.toUnitRelative(lhsQty.getUnit()) instanceof QuantityType relativeRhs) {
|
||||||
rhs = relativeRhs;
|
rhs = relativeRhs;
|
||||||
}
|
}
|
||||||
|
} else if (hasSystemUnit()) {
|
||||||
|
lhs = toSystemUnitQuantityType(lhsQty) instanceof QuantityType lhsSU ? lhsSU : lhs;
|
||||||
|
rhs = toSystemUnitQuantityType(rhsQty) instanceof QuantityType rhsSU ? rhsSU : rhs;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (logger.isDebugEnabled()) {
|
if (logger.isDebugEnabled()) {
|
||||||
|
@ -454,13 +470,14 @@ public class StateFilterProfile implements StateProfile {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings({ "rawtypes", "unchecked" })
|
||||||
boolean result = switch (comparisonType) {
|
boolean result = switch (comparisonType) {
|
||||||
case EQ -> lhs.equals(rhs);
|
case EQ -> lhs.equals(rhs);
|
||||||
case NEQ, NEQ_ALT -> !lhs.equals(rhs);
|
case NEQ, NEQ_ALT -> !lhs.equals(rhs);
|
||||||
case GT -> ((Comparable) lhs).compareTo(rhs) > 0;
|
case GT -> ((Comparable<Object>) lhs).compareTo(rhs) > 0;
|
||||||
case GTE -> ((Comparable) lhs).compareTo(rhs) >= 0;
|
case GTE -> ((Comparable<Object>) lhs).compareTo(rhs) >= 0;
|
||||||
case LT -> ((Comparable) lhs).compareTo(rhs) < 0;
|
case LT -> ((Comparable<Object>) lhs).compareTo(rhs) < 0;
|
||||||
case LTE -> ((Comparable) lhs).compareTo(rhs) <= 0;
|
case LTE -> ((Comparable<Object>) lhs).compareTo(rhs) <= 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
|
@ -551,18 +568,31 @@ public class StateFilterProfile implements StateProfile {
|
||||||
|
|
||||||
public @Nullable State calculate() {
|
public @Nullable State calculate() {
|
||||||
logger.debug("Calculating function: {}", this);
|
logger.debug("Calculating function: {}", this);
|
||||||
|
State result;
|
||||||
|
switch (type) {
|
||||||
|
case DELTA -> result = calculateDelta();
|
||||||
|
case DELTA_PERCENT -> result = calculateDeltaPercent();
|
||||||
|
default -> {
|
||||||
int size = previousStates.size();
|
int size = previousStates.size();
|
||||||
int start = windowSize.map(w -> size - w).orElse(0);
|
Integer start = windowSize.map(w -> size - w).orElse(0);
|
||||||
List<State> states = start <= 0 ? previousStates : previousStates.subList(start, size);
|
List<BigDecimal> values = toBigDecimals(
|
||||||
return switch (type) {
|
start == null || start <= 0 ? previousStates : previousStates.subList(start, size));
|
||||||
case DELTA -> calculateDelta();
|
if (values.isEmpty()) {
|
||||||
case DELTA_PERCENT -> calculateDeltaPercent();
|
logger.debug("Not enough states to calculate {}", type);
|
||||||
case AVG, AVERAGE -> calculateAverage(states);
|
result = null;
|
||||||
case MEDIAN -> calculateMedian(states);
|
} else {
|
||||||
case STDDEV -> calculateStdDev(states);
|
switch (type) {
|
||||||
case MIN -> calculateMin(states);
|
case AVG, AVERAGE -> result = calculateAverage(values);
|
||||||
case MAX -> calculateMax(states);
|
case MEDIAN -> result = calculateMedian(values);
|
||||||
};
|
case STDDEV -> result = calculateStdDev(values);
|
||||||
|
case MIN -> result = calculateMin(values);
|
||||||
|
case MAX -> result = calculateMax(values);
|
||||||
|
default -> result = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -579,11 +609,8 @@ public class StateFilterProfile implements StateProfile {
|
||||||
}
|
}
|
||||||
if (type == Function.DELTA_PERCENT) {
|
if (type == Function.DELTA_PERCENT) {
|
||||||
// avoid division by zero
|
// avoid division by zero
|
||||||
if (acceptedState.get() instanceof QuantityType base) {
|
if (toBigDecimal(acceptedState.get()) instanceof BigDecimal base) {
|
||||||
return base.toBigDecimal().compareTo(BigDecimal.ZERO) == 0;
|
return base.compareTo(BigDecimal.ZERO) == 0;
|
||||||
}
|
|
||||||
if (acceptedState.get() instanceof DecimalType base) {
|
|
||||||
return base.toBigDecimal().compareTo(BigDecimal.ZERO) == 0;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
|
@ -591,6 +618,7 @@ public class StateFilterProfile implements StateProfile {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public <T extends State> @Nullable T as(@Nullable Class<T> target) {
|
public <T extends State> @Nullable T as(@Nullable Class<T> target) {
|
||||||
|
// TODO @andrewfg: do we need to change this ??
|
||||||
if (target == DecimalType.class || target == QuantityType.class) {
|
if (target == DecimalType.class || target == QuantityType.class) {
|
||||||
return target.cast(calculate());
|
return target.cast(calculate());
|
||||||
}
|
}
|
||||||
|
@ -603,7 +631,7 @@ public class StateFilterProfile implements StateProfile {
|
||||||
// the previous state is kept in the acceptedState variable
|
// the previous state is kept in the acceptedState variable
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
return windowSize.orElse(DEFAULT_WINDOW_SIZE);
|
return windowSize.isPresent() ? windowSize.get() : DEFAULT_WINDOW_SIZE;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Function getType() {
|
public Function getType() {
|
||||||
|
@ -625,126 +653,179 @@ public class StateFilterProfile implements StateProfile {
|
||||||
return toFullString();
|
return toFullString();
|
||||||
}
|
}
|
||||||
|
|
||||||
private @Nullable State calculateAverage(List<State> states) {
|
private @Nullable State calculateAverage(List<BigDecimal> values) {
|
||||||
if (states.isEmpty()) {
|
return Optional
|
||||||
logger.debug("Not enough states to calculate sum");
|
.ofNullable(values.stream().reduce(BigDecimal.ZERO, BigDecimal::add)
|
||||||
return null;
|
.divide(BigDecimal.valueOf(values.size()), MathContext.DECIMAL32))
|
||||||
}
|
.map(o -> toState(o)).orElse(null);
|
||||||
if (newState instanceof QuantityType newStateQuantity) {
|
|
||||||
QuantityType zero = new QuantityType(0, newStateQuantity.getUnit());
|
|
||||||
QuantityType sum = states.stream().map(s -> (QuantityType) s).reduce(zero, QuantityType::add);
|
|
||||||
return sum.divide(BigDecimal.valueOf(states.size()));
|
|
||||||
}
|
|
||||||
BigDecimal sum = states.stream().map(s -> ((DecimalType) s).toBigDecimal()).reduce(BigDecimal.ZERO,
|
|
||||||
BigDecimal::add);
|
|
||||||
return new DecimalType(sum.divide(BigDecimal.valueOf(states.size()), 2, RoundingMode.HALF_EVEN));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private @Nullable State calculateMedian(List<State> states) {
|
private @Nullable State calculateMedian(List<BigDecimal> values) {
|
||||||
if (states.isEmpty()) {
|
return Optional.ofNullable(Statistics.median(values)).map(o -> toState(o)).orElse(null);
|
||||||
logger.debug("Not enough states to calculate median");
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
if (newState instanceof QuantityType newStateQuantity) {
|
|
||||||
Unit<?> unit = newStateQuantity.getUnit();
|
private @Nullable State calculateStdDev(List<BigDecimal> values) {
|
||||||
List<BigDecimal> bdStates = states.stream()
|
BigDecimal average = values.stream().reduce(BigDecimal.ZERO, BigDecimal::add)
|
||||||
.map(s -> ((QuantityType) s).toInvertibleUnit(unit).toBigDecimal()).toList();
|
.divide(BigDecimal.valueOf(values.size()), 2, RoundingMode.HALF_EVEN);
|
||||||
return Optional.ofNullable(Statistics.median(bdStates)).map(median -> new QuantityType(median, unit))
|
|
||||||
|
BigDecimal variance = values.stream().map(value -> {
|
||||||
|
BigDecimal delta = value.subtract(average);
|
||||||
|
return delta.multiply(delta);
|
||||||
|
}).reduce(BigDecimal.ZERO, BigDecimal::add).divide(BigDecimal.valueOf(values.size()),
|
||||||
|
MathContext.DECIMAL32);
|
||||||
|
|
||||||
|
return toState(variance.sqrt(MathContext.DECIMAL32));
|
||||||
|
}
|
||||||
|
|
||||||
|
private @Nullable State calculateMin(List<BigDecimal> values) {
|
||||||
|
return Optional.ofNullable(values.stream().min(BigDecimal::compareTo).orElse(null)).map(o -> toState(o))
|
||||||
.orElse(null);
|
.orElse(null);
|
||||||
}
|
}
|
||||||
List<BigDecimal> bdStates = states.stream().map(s -> ((DecimalType) s).toBigDecimal()).toList();
|
|
||||||
return Optional.ofNullable(Statistics.median(bdStates)).map(median -> new DecimalType(median)).orElse(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
private @Nullable State calculateStdDev(List<State> states) {
|
private @Nullable State calculateMax(List<BigDecimal> values) {
|
||||||
if (states.isEmpty()) {
|
return Optional.ofNullable(values.stream().max(BigDecimal::compareTo).orElse(null)).map(o -> toState(o))
|
||||||
logger.debug("Not enough states to calculate standard deviation");
|
.orElse(null);
|
||||||
return null;
|
|
||||||
}
|
|
||||||
if (newState instanceof QuantityType newStateQuantity) {
|
|
||||||
QuantityType average = (QuantityType) calculateAverage(states);
|
|
||||||
if (average == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
QuantityType zero = new QuantityType(0, newStateQuantity.getUnit());
|
|
||||||
QuantityType variance = states.stream() //
|
|
||||||
.map(s -> {
|
|
||||||
QuantityType delta = ((QuantityType) s).subtract(average);
|
|
||||||
return (QuantityType) delta.multiply(delta.toBigDecimal()); // don't square the unit
|
|
||||||
}) //
|
|
||||||
.reduce(zero, QuantityType::add) // This reduced into a QuantityType
|
|
||||||
.divide(BigDecimal.valueOf(states.size()));
|
|
||||||
return new QuantityType(variance.toBigDecimal().sqrt(MathContext.DECIMAL32), variance.getUnit());
|
|
||||||
}
|
|
||||||
BigDecimal average = Optional.ofNullable((DecimalType) calculateAverage(states))
|
|
||||||
.map(DecimalType::toBigDecimal).orElse(null);
|
|
||||||
if (average == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
BigDecimal variance = states.stream().map(s -> {
|
|
||||||
BigDecimal delta = ((DecimalType) s).toBigDecimal().subtract(average);
|
|
||||||
return delta.multiply(delta);
|
|
||||||
}).reduce(BigDecimal.ZERO, BigDecimal::add).divide(BigDecimal.valueOf(states.size()),
|
|
||||||
MathContext.DECIMAL32);
|
|
||||||
return new DecimalType(variance.sqrt(MathContext.DECIMAL32));
|
|
||||||
}
|
|
||||||
|
|
||||||
private @Nullable State calculateMin(List<State> states) {
|
|
||||||
if (states.isEmpty()) {
|
|
||||||
logger.debug("Not enough states to calculate min");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
if (newState instanceof QuantityType newStateQuantity) {
|
|
||||||
return states.stream().map(s -> (QuantityType) s).min(QuantityType::compareTo).orElse(null);
|
|
||||||
}
|
|
||||||
return states.stream().map(s -> ((DecimalType) s).toBigDecimal()).min(BigDecimal::compareTo)
|
|
||||||
.map(DecimalType::new).orElse(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
private @Nullable State calculateMax(List<State> states) {
|
|
||||||
if (states.isEmpty()) {
|
|
||||||
logger.debug("Not enough states to calculate max");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
if (newState instanceof QuantityType newStateQuantity) {
|
|
||||||
return states.stream().map(s -> (QuantityType) s).max(QuantityType::compareTo).orElse(null);
|
|
||||||
}
|
|
||||||
return states.stream().map(s -> ((DecimalType) s).toBigDecimal()).max(BigDecimal::compareTo)
|
|
||||||
.map(DecimalType::new).orElse(null);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private @Nullable State calculateDelta() {
|
private @Nullable State calculateDelta() {
|
||||||
if (acceptedState.isEmpty()) {
|
return acceptedState.isPresent() //
|
||||||
return null;
|
&& toBigDecimal(acceptedState.get()) instanceof BigDecimal acceptedValue
|
||||||
}
|
&& toBigDecimal(newState) instanceof BigDecimal newValue //
|
||||||
if (newState instanceof QuantityType newStateQuantity) {
|
? toState(newValue.subtract(acceptedValue).abs())
|
||||||
QuantityType result = newStateQuantity.subtract((QuantityType) acceptedState.get());
|
: null;
|
||||||
return result.toBigDecimal().compareTo(BigDecimal.ZERO) < 0 ? result.negate() : result;
|
|
||||||
}
|
|
||||||
BigDecimal result = ((DecimalType) newState).toBigDecimal()
|
|
||||||
.subtract(((DecimalType) acceptedState.get()).toBigDecimal()) //
|
|
||||||
.abs();
|
|
||||||
return new DecimalType(result);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private @Nullable State calculateDeltaPercent() {
|
private @Nullable State calculateDeltaPercent() {
|
||||||
if (acceptedState.isEmpty()) {
|
return acceptedState.isPresent() //
|
||||||
return null;
|
&& toBigDecimal(acceptedState.get()) instanceof BigDecimal acceptedValue
|
||||||
|
&& toBigDecimal(newState) instanceof BigDecimal newValue
|
||||||
|
// percent is dimension-less; we must return DecimalType
|
||||||
|
? new DecimalType(newValue.subtract(acceptedValue).multiply(BigDecimal.valueOf(100))
|
||||||
|
.divide(acceptedValue, MathContext.DECIMAL32).abs())
|
||||||
|
: null;
|
||||||
}
|
}
|
||||||
State calculatedDelta = calculateDelta();
|
|
||||||
BigDecimal bdDelta;
|
|
||||||
BigDecimal bdBase;
|
|
||||||
if (acceptedState.get() 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.get()).toBigDecimal();
|
|
||||||
}
|
}
|
||||||
bdBase = bdBase.abs();
|
|
||||||
BigDecimal percent = bdDelta.multiply(BigDecimal.valueOf(100)).divide(bdBase, 2, RoundingMode.HALF_EVEN);
|
/**
|
||||||
return new DecimalType(percent);
|
* Return true if 'systemUnit' is defined. The first call to this method initialises 'systemUnit' to its
|
||||||
|
* (effectively) final value, so if this method returns 'true' we can safely use 'Objects.requireNonNull()'
|
||||||
|
* thereafter to assert that 'systemUnit' is indeed non- null. The {@link Unit} is initialized based on the
|
||||||
|
* system unit of the linked {@link Item}. If there is no linked Item, or it is not a {@link NumberItem} or
|
||||||
|
* if the Item does not have a {@link Unit}, then 'systemUnit' is null and this method returns false.
|
||||||
|
*/
|
||||||
|
protected synchronized boolean hasSystemUnit() {
|
||||||
|
if (!systemUnitInitialized) {
|
||||||
|
systemUnitInitialized = true;
|
||||||
|
systemUnit = getLinkedItem() instanceof NumberItem item && item.getUnit() instanceof Unit<?> unit
|
||||||
|
? unit.getSystemUnit()
|
||||||
|
: null;
|
||||||
}
|
}
|
||||||
|
return systemUnit != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert a {@link State} to a {@link BigDecimal}. If it is a {@link QuantityType} and there is a 'systemUnit' its
|
||||||
|
* value is converted (if possible) to the 'systemUnit' before converting it to a {@link BigDecimal}. Returns null
|
||||||
|
* if the {@link State} does not have a numeric value, or if the conversion to 'systemUnit' fails.
|
||||||
|
*
|
||||||
|
* @return a {@link BigDecimal} or null.
|
||||||
|
*/
|
||||||
|
protected @Nullable BigDecimal toBigDecimal(State state) {
|
||||||
|
if (state instanceof DecimalType decimalType) {
|
||||||
|
return decimalType.toBigDecimal();
|
||||||
|
}
|
||||||
|
if (state instanceof QuantityType<?> quantityType) {
|
||||||
|
return hasSystemUnit() //
|
||||||
|
? toSystemUnitQuantityType(state) instanceof QuantityType<?> suQuantityType
|
||||||
|
? suQuantityType.toBigDecimal()
|
||||||
|
: null
|
||||||
|
: quantityType.toBigDecimal();
|
||||||
|
}
|
||||||
|
return state.as(DecimalType.class) instanceof DecimalType decimalType //
|
||||||
|
? decimalType.toBigDecimal()
|
||||||
|
: null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert a list of {@link State} to a list of {@link BigDecimal} values.
|
||||||
|
*
|
||||||
|
* @param states list of {@link State} values.
|
||||||
|
* @return list of {@link BigDecimal} values.
|
||||||
|
*/
|
||||||
|
protected List<BigDecimal> toBigDecimals(List<? extends State> states) {
|
||||||
|
return states.stream().map(s -> toBigDecimal(s)).filter(Objects::nonNull).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new {@link State} from the given {@link BigDecimal} value. If there is a 'systemUnit' it creates a
|
||||||
|
* {@link QuantityType} based on that unit. Otherwise it creates a {@link DecimalType}.
|
||||||
|
*
|
||||||
|
* @return a {@link QuantityType} or a {@link DecimalType}
|
||||||
|
*/
|
||||||
|
protected State toState(BigDecimal value) {
|
||||||
|
return hasSystemUnit() //
|
||||||
|
? new QuantityType<>(value, Objects.requireNonNull(systemUnit))
|
||||||
|
: new DecimalType(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert a {@link State} to a {@link QuantityType} with its value converted to the 'systemUnit'.
|
||||||
|
* Returns null if the state is not a {@link QuantityType} or it does not convert to 'systemUnit'.
|
||||||
|
*
|
||||||
|
* @return a {@link QuantityType} based on 'systemUnit'.
|
||||||
|
*/
|
||||||
|
protected @Nullable QuantityType<?> toSystemUnitQuantityType(State state) {
|
||||||
|
return state instanceof QuantityType<?> quantityType && hasSystemUnit() //
|
||||||
|
? toInvertibleUnit(quantityType, Objects.requireNonNull(systemUnit))
|
||||||
|
: null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert the given {@link QuantityType} to an equivalent based on the target {@link Unit}. The conversion can be
|
||||||
|
* made to both inverted and non-inverted units, so invertible type conversions (e.g. Mirek <=> Kelvin) are
|
||||||
|
* supported.
|
||||||
|
* <p>
|
||||||
|
* Note: we can use {@link QuantityType.toInvertibleUnit()} if OH Core PR #4561 is merged.
|
||||||
|
*
|
||||||
|
* @param source the {@link QuantityType} to be converted.
|
||||||
|
* @param targetUnit the {@link Unit} to convert to.
|
||||||
|
*
|
||||||
|
* @return a new {@link QuantityType} based on 'systemUnit' or null.
|
||||||
|
*/
|
||||||
|
protected @Nullable QuantityType<?> toInvertibleUnit(QuantityType<?> source, Unit<?> targetUnit) {
|
||||||
|
Unit<?> sourceSystemUnit = source.getUnit().getSystemUnit();
|
||||||
|
if (!targetUnit.equals(sourceSystemUnit) && !targetUnit.isCompatible(AbstractUnit.ONE)
|
||||||
|
&& sourceSystemUnit.inverse().isCompatible(targetUnit)) {
|
||||||
|
QuantityType<?> sourceInItsSystemUnit = source.toUnit(sourceSystemUnit);
|
||||||
|
return sourceInItsSystemUnit != null ? sourceInItsSystemUnit.inverse().toUnit(targetUnit) : null;
|
||||||
|
}
|
||||||
|
return source.toUnit(targetUnit);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the given {@link State} is allowed. Non -allowed states are those which are a {@link QuantityType}
|
||||||
|
* and if there is a 'systemUnit' not compatible with that.
|
||||||
|
*
|
||||||
|
* @param state the incoming state.
|
||||||
|
* @return true if allowed.
|
||||||
|
*/
|
||||||
|
protected boolean isAllowed(State state) {
|
||||||
|
return hasSystemUnit() //
|
||||||
|
? toSystemUnitQuantityType(state) != null
|
||||||
|
: true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the given {@link State} is suitable to be cached. This means it is suitable to add to the
|
||||||
|
* 'previousStates' list and/or to set to the 'acceptedState' field. This means that either there is a
|
||||||
|
* 'systemUnit' with which 'state' is compatible, or it can provide a {@link DecimalType} value.
|
||||||
|
*
|
||||||
|
* @param state the {@link State} to be tested.
|
||||||
|
* @return true if the 'state' is suitable to be cached.
|
||||||
|
*/
|
||||||
|
protected boolean isCacheable(State state) {
|
||||||
|
return hasSystemUnit() //
|
||||||
|
? toSystemUnitQuantityType(state) != null
|
||||||
|
: state.as(DecimalType.class) != null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,24 +12,24 @@
|
||||||
*/
|
*/
|
||||||
package org.openhab.transform.basicprofiles.internal.profiles;
|
package org.openhab.transform.basicprofiles.internal.profiles;
|
||||||
|
|
||||||
import static org.hamcrest.Matchers.*;
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
import static org.mockito.ArgumentMatchers.*;
|
import static org.mockito.ArgumentMatchers.*;
|
||||||
import static org.mockito.ArgumentMatchers.any;
|
|
||||||
import static org.mockito.ArgumentMatchers.eq;
|
|
||||||
import static org.mockito.Mockito.*;
|
import static org.mockito.Mockito.*;
|
||||||
import static org.mockito.Mockito.reset;
|
|
||||||
import static org.mockito.Mockito.times;
|
|
||||||
import static org.mockito.Mockito.verify;
|
|
||||||
import static org.mockito.Mockito.when;
|
|
||||||
|
|
||||||
|
import java.util.Collection;
|
||||||
import java.util.Hashtable;
|
import java.util.Hashtable;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
import java.util.stream.Stream;
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
import javax.measure.MetricPrefix;
|
||||||
|
import javax.measure.Quantity;
|
||||||
|
import javax.measure.Unit;
|
||||||
import javax.measure.quantity.Dimensionless;
|
import javax.measure.quantity.Dimensionless;
|
||||||
import javax.measure.quantity.Power;
|
import javax.measure.quantity.Power;
|
||||||
|
import javax.measure.quantity.Time;
|
||||||
|
import javax.measure.spi.SystemOfUnits;
|
||||||
|
|
||||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
@ -63,6 +63,7 @@ import org.openhab.core.library.types.OpenClosedType;
|
||||||
import org.openhab.core.library.types.PercentType;
|
import org.openhab.core.library.types.PercentType;
|
||||||
import org.openhab.core.library.types.QuantityType;
|
import org.openhab.core.library.types.QuantityType;
|
||||||
import org.openhab.core.library.types.StringType;
|
import org.openhab.core.library.types.StringType;
|
||||||
|
import org.openhab.core.library.unit.ImperialUnits;
|
||||||
import org.openhab.core.library.unit.SIUnits;
|
import org.openhab.core.library.unit.SIUnits;
|
||||||
import org.openhab.core.library.unit.Units;
|
import org.openhab.core.library.unit.Units;
|
||||||
import org.openhab.core.thing.link.ItemChannelLink;
|
import org.openhab.core.thing.link.ItemChannelLink;
|
||||||
|
@ -106,7 +107,7 @@ public class StateFilterProfileTest {
|
||||||
reset(mockCallback);
|
reset(mockCallback);
|
||||||
reset(mockItemChannelLink);
|
reset(mockItemChannelLink);
|
||||||
when(mockCallback.getItemChannelLink()).thenReturn(mockItemChannelLink);
|
when(mockCallback.getItemChannelLink()).thenReturn(mockItemChannelLink);
|
||||||
when(mockItemRegistry.getItem("")).thenThrow(ItemNotFoundException.class);
|
// when(mockItemRegistry.getItem("")).thenThrow(ItemNotFoundException.class);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -291,7 +292,7 @@ public class StateFilterProfileTest {
|
||||||
ContactItem contactItem = new ContactItem("contactItem");
|
ContactItem contactItem = new ContactItem("contactItem");
|
||||||
RollershutterItem rollershutterItem = new RollershutterItem("rollershutterItem");
|
RollershutterItem rollershutterItem = new RollershutterItem("rollershutterItem");
|
||||||
|
|
||||||
QuantityType q_1500W = QuantityType.valueOf("1500 W");
|
QuantityType<?> q_1500W = QuantityType.valueOf("1500 W");
|
||||||
DecimalType d_1500 = DecimalType.valueOf("1500");
|
DecimalType d_1500 = DecimalType.valueOf("1500");
|
||||||
StringType s_foo = StringType.valueOf("foo");
|
StringType s_foo = StringType.valueOf("foo");
|
||||||
StringType s_NULL = StringType.valueOf("NULL");
|
StringType s_NULL = StringType.valueOf("NULL");
|
||||||
|
@ -497,9 +498,9 @@ public class StateFilterProfileTest {
|
||||||
ContactItem contactItem = new ContactItem("contactItem");
|
ContactItem contactItem = new ContactItem("contactItem");
|
||||||
ContactItem contactItem2 = new ContactItem("contactItem2");
|
ContactItem contactItem2 = new ContactItem("contactItem2");
|
||||||
|
|
||||||
QuantityType q_1500W = QuantityType.valueOf("1500 W");
|
QuantityType<?> q_1500W = QuantityType.valueOf("1500 W");
|
||||||
QuantityType q_1_5kW = QuantityType.valueOf("1.5 kW");
|
QuantityType<?> q_1_5kW = QuantityType.valueOf("1.5 kW");
|
||||||
QuantityType q_10kW = QuantityType.valueOf("10 kW");
|
QuantityType<?> q_10kW = QuantityType.valueOf("10 kW");
|
||||||
|
|
||||||
DecimalType d_1500 = DecimalType.valueOf("1500");
|
DecimalType d_1500 = DecimalType.valueOf("1500");
|
||||||
DecimalType d_2000 = DecimalType.valueOf("2000");
|
DecimalType d_2000 = DecimalType.valueOf("2000");
|
||||||
|
@ -575,7 +576,7 @@ public class StateFilterProfileTest {
|
||||||
StringItem stringItem = new StringItem("ItemName");
|
StringItem stringItem = new StringItem("ItemName");
|
||||||
DimmerItem dimmerItem = new DimmerItem("ItemName");
|
DimmerItem dimmerItem = new DimmerItem("ItemName");
|
||||||
|
|
||||||
QuantityType q_1500W = QuantityType.valueOf("1500 W");
|
QuantityType<?> q_1500W = QuantityType.valueOf("1500 W");
|
||||||
DecimalType d_1500 = DecimalType.valueOf("1500");
|
DecimalType d_1500 = DecimalType.valueOf("1500");
|
||||||
StringType s_foo = StringType.valueOf("foo");
|
StringType s_foo = StringType.valueOf("foo");
|
||||||
|
|
||||||
|
@ -664,7 +665,6 @@ public class StateFilterProfileTest {
|
||||||
|
|
||||||
profile.onStateUpdateFromHandler(inputState);
|
profile.onStateUpdateFromHandler(inputState);
|
||||||
reset(mockCallback);
|
reset(mockCallback);
|
||||||
when(mockCallback.getItemChannelLink()).thenReturn(mockItemChannelLink);
|
|
||||||
|
|
||||||
item.setState(state);
|
item.setState(state);
|
||||||
profile.onStateUpdateFromHandler(inputState);
|
profile.onStateUpdateFromHandler(inputState);
|
||||||
|
@ -721,8 +721,8 @@ public class StateFilterProfileTest {
|
||||||
Arguments.of(decimalItem, "$DELTA_PERCENT < 10", decimals, DecimalType.valueOf("0.91"), true), //
|
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, "$DELTA_PERCENT < 10", decimals, DecimalType.valueOf("0.89"), false), //
|
||||||
|
|
||||||
Arguments.of(decimalItem, "$DELTA_PERCENT < 10", negativeDecimals, DecimalType.valueOf("0"), false),
|
Arguments.of(decimalItem, "$DELTA_PERCENT < 10", negativeDecimals, DecimalType.valueOf("0"), false), //
|
||||||
Arguments.of(decimalItem, "10 > $DELTA_PERCENT", negativeDecimals, DecimalType.valueOf("0"), false),
|
Arguments.of(decimalItem, "10 > $DELTA_PERCENT", negativeDecimals, DecimalType.valueOf("0"), false), //
|
||||||
|
|
||||||
Arguments.of(decimalItem, "< 10%", decimals, DecimalType.valueOf("1.09"), true), //
|
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("1.11"), false), //
|
||||||
|
@ -868,7 +868,6 @@ public class StateFilterProfileTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
reset(mockCallback);
|
reset(mockCallback);
|
||||||
when(mockCallback.getItemChannelLink()).thenReturn(mockItemChannelLink);
|
|
||||||
|
|
||||||
profile.onStateUpdateFromHandler(input);
|
profile.onStateUpdateFromHandler(input);
|
||||||
verify(mockCallback, times(expected ? 1 : 0)).sendUpdate(input);
|
verify(mockCallback, times(expected ? 1 : 0)).sendUpdate(input);
|
||||||
|
@ -899,4 +898,178 @@ public class StateFilterProfileTest {
|
||||||
profile.onStateUpdateFromHandler(DecimalType.valueOf("1"));
|
profile.onStateUpdateFromHandler(DecimalType.valueOf("1"));
|
||||||
verify(mockCallback, times(1)).sendUpdate(DecimalType.valueOf("1"));
|
verify(mockCallback, times(1)).sendUpdate(DecimalType.valueOf("1"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static Stream<Arguments> testMixedStates() {
|
||||||
|
NumberItem powerItem = new NumberItem("Number:Power", "powerItem", UNIT_PROVIDER);
|
||||||
|
|
||||||
|
List<State> states = List.of( //
|
||||||
|
UnDefType.UNDEF, //
|
||||||
|
QuantityType.valueOf(99, SIUnits.METRE), //
|
||||||
|
QuantityType.valueOf(1, Units.WATT), //
|
||||||
|
DecimalType.valueOf("2"), //
|
||||||
|
QuantityType.valueOf(2000, MetricPrefix.MILLI(Units.WATT)), //
|
||||||
|
QuantityType.valueOf(3, Units.WATT)); //
|
||||||
|
|
||||||
|
return Stream.of(
|
||||||
|
// average function (true)
|
||||||
|
Arguments.of(powerItem, "== $AVG", states, QuantityType.valueOf("2 W"), true),
|
||||||
|
Arguments.of(powerItem, "== $AVG", states, QuantityType.valueOf("2000 mW"), true),
|
||||||
|
Arguments.of(powerItem, "== $AVERAGE", states, QuantityType.valueOf("0.002 kW"), true),
|
||||||
|
Arguments.of(powerItem, "> $AVERAGE", states, QuantityType.valueOf("3 W"), true),
|
||||||
|
|
||||||
|
// average function (false)
|
||||||
|
Arguments.of(powerItem, "> $AVERAGE", states, QuantityType.valueOf("2 W"), false),
|
||||||
|
Arguments.of(powerItem, "== $AVERAGE", states, DecimalType.valueOf("2"), false),
|
||||||
|
|
||||||
|
// min function (true)
|
||||||
|
Arguments.of(powerItem, "== $MIN", states, QuantityType.valueOf("1 W"), true),
|
||||||
|
Arguments.of(powerItem, "== $MIN", states, QuantityType.valueOf("1000 mW"), true),
|
||||||
|
|
||||||
|
// min function (false)
|
||||||
|
Arguments.of(powerItem, "== $MIN", states, DecimalType.valueOf("1"), false),
|
||||||
|
|
||||||
|
// max function (true)
|
||||||
|
Arguments.of(powerItem, "== $MAX", states, QuantityType.valueOf("3 W"), true),
|
||||||
|
Arguments.of(powerItem, "== $MAX", states, QuantityType.valueOf("0.003 kW"), true),
|
||||||
|
|
||||||
|
// max function (false)
|
||||||
|
Arguments.of(powerItem, "== $MAX", states, DecimalType.valueOf("1"), false),
|
||||||
|
|
||||||
|
// delta function (true)
|
||||||
|
Arguments.of(powerItem, "$DELTA <= 1 W", states, QuantityType.valueOf("4 W"), true),
|
||||||
|
Arguments.of(powerItem, "$DELTA > 0.5 W", states, QuantityType.valueOf("4 W"), true),
|
||||||
|
Arguments.of(powerItem, "$DELTA > 0.0005 kW", states, QuantityType.valueOf("4 W"), true),
|
||||||
|
Arguments.of(powerItem, "0.5 W < $DELTA", states, QuantityType.valueOf("4 W"), true),
|
||||||
|
Arguments.of(powerItem, "500 mW < $DELTA", states, QuantityType.valueOf("4 W"), true),
|
||||||
|
|
||||||
|
// delta function (false)
|
||||||
|
Arguments.of(powerItem, "$DELTA > 0.5 W", states, QuantityType.valueOf("3.4 W"), false),
|
||||||
|
Arguments.of(powerItem, "$DELTA > 0.5", states, QuantityType.valueOf("4 W"), false),
|
||||||
|
|
||||||
|
// delta percent function (true)
|
||||||
|
Arguments.of(powerItem, "$DELTA_PERCENT > 30", states, QuantityType.valueOf("4 W"), true),
|
||||||
|
Arguments.of(powerItem, "30 < $DELTA_PERCENT", states, QuantityType.valueOf("4 W"), true),
|
||||||
|
|
||||||
|
// delta percent function (false)
|
||||||
|
Arguments.of(powerItem, "$DELTA_PERCENT > 310", states, QuantityType.valueOf("4 W"), false),
|
||||||
|
Arguments.of(powerItem, "310 < $DELTA_PERCENT", states, QuantityType.valueOf("4 W"), false),
|
||||||
|
|
||||||
|
// unit based comparisons (true)
|
||||||
|
Arguments.of(powerItem, "> 0.5 W", states, QuantityType.valueOf("4 W"), true),
|
||||||
|
Arguments.of(powerItem, "> 500 mW", states, QuantityType.valueOf("4 W"), true),
|
||||||
|
Arguments.of(powerItem, "> 0.0005 kW", states, QuantityType.valueOf("4 W"), true),
|
||||||
|
|
||||||
|
// unit based comparisons (false)
|
||||||
|
Arguments.of(powerItem, "> 0.5 W", states, QuantityType.valueOf("0.4 W"), false),
|
||||||
|
Arguments.of(powerItem, "> 500 mW", states, QuantityType.valueOf("0.4 W"), false),
|
||||||
|
Arguments.of(powerItem, "> 0.0005 kW", states, QuantityType.valueOf("0.4 W"), false),
|
||||||
|
|
||||||
|
// percent comparisons (true)
|
||||||
|
Arguments.of(powerItem, "> 30 %", states, QuantityType.valueOf("4 W"), true),
|
||||||
|
|
||||||
|
// percent comparisons (false)
|
||||||
|
Arguments.of(powerItem, "> 310 %", states, QuantityType.valueOf("4 W"), false) //
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@ParameterizedTest
|
||||||
|
@MethodSource
|
||||||
|
public void testMixedStates(Item item, String condition, List<State> states, State input, boolean expected)
|
||||||
|
throws ItemNotFoundException {
|
||||||
|
internalTestFunctions(item, condition, states, input, expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A {@link UnitProvider} that provides Units.MIRED
|
||||||
|
*/
|
||||||
|
protected static class MirekUnitProvider implements UnitProvider {
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
@Override
|
||||||
|
public <T extends Quantity<T>> Unit<T> getUnit(Class<T> dimension) throws IllegalArgumentException {
|
||||||
|
return (Unit<T>) Units.MIRED;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public SystemOfUnits getMeasurementSystem() {
|
||||||
|
return SIUnits.getInstance();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Collection<Class<? extends Quantity<?>>> getAllDimensions() {
|
||||||
|
return Set.of();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Stream<Arguments> testColorTemperatureValues() {
|
||||||
|
NumberItem kelvinItem = new NumberItem("Number:Temperature", "kelvinItem", UNIT_PROVIDER);
|
||||||
|
NumberItem mirekItem = new NumberItem("Number:Temperature", "mirekItem", new MirekUnitProvider());
|
||||||
|
|
||||||
|
List<State> states = List.of( //
|
||||||
|
QuantityType.valueOf(500, Units.MIRED), //
|
||||||
|
QuantityType.valueOf(2000 + (1 * 100), Units.KELVIN), //
|
||||||
|
QuantityType.valueOf(1726.85 + (2 * 100), SIUnits.CELSIUS), //
|
||||||
|
QuantityType.valueOf(3140.33 + (3 * 180), ImperialUnits.FAHRENHEIT));
|
||||||
|
|
||||||
|
return Stream.of( //
|
||||||
|
// kelvin based item
|
||||||
|
Arguments.of(kelvinItem, "== $MIN", states, QuantityType.valueOf("2000 K"), true),
|
||||||
|
Arguments.of(kelvinItem, "== $MAX", states, QuantityType.valueOf("2300 K"), true),
|
||||||
|
Arguments.of(kelvinItem, "== $MIN", states, QuantityType.valueOf(500, Units.MIRED), true),
|
||||||
|
Arguments.of(kelvinItem, "== $MIN", states, QuantityType.valueOf(1726.85, SIUnits.CELSIUS), true),
|
||||||
|
Arguments.of(kelvinItem, "== $MIN", states, QuantityType.valueOf(3140.33, ImperialUnits.FAHRENHEIT),
|
||||||
|
true),
|
||||||
|
|
||||||
|
// kelvin based item average (note: actual is 2150)
|
||||||
|
Arguments.of(kelvinItem, "<= $AVG", states, QuantityType.valueOf("2149 K"), true),
|
||||||
|
Arguments.of(kelvinItem, ">= $AVG", states, QuantityType.valueOf("2151 K"), true),
|
||||||
|
|
||||||
|
// mirek based item (note: min and max are reversed
|
||||||
|
Arguments.of(mirekItem, "== $MAX", states, QuantityType.valueOf("2000 K"), true),
|
||||||
|
Arguments.of(mirekItem, "== $MIN", states, QuantityType.valueOf("2300 K"), true),
|
||||||
|
Arguments.of(mirekItem, "== $MAX", states, QuantityType.valueOf(500, Units.MIRED), true),
|
||||||
|
Arguments.of(mirekItem, "== $MAX", states, QuantityType.valueOf(1726.85, SIUnits.CELSIUS), true),
|
||||||
|
Arguments.of(mirekItem, "== $MAX", states, QuantityType.valueOf(3140.33, ImperialUnits.FAHRENHEIT),
|
||||||
|
true),
|
||||||
|
|
||||||
|
// mirek based item average (note: actual is 466.37)
|
||||||
|
Arguments.of(mirekItem, "<= $AVG", states, QuantityType.valueOf(466, Units.MIRED), true),
|
||||||
|
Arguments.of(mirekItem, ">= $AVG", states, QuantityType.valueOf(468, Units.MIRED), true) //
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@ParameterizedTest
|
||||||
|
@MethodSource
|
||||||
|
public void testColorTemperatureValues(Item item, String condition, List<State> states, State input,
|
||||||
|
boolean expected) throws ItemNotFoundException {
|
||||||
|
internalTestFunctions(item, condition, states, input, expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Stream<Arguments> testTimeValues() {
|
||||||
|
NumberItem timeItem = new NumberItem("Number:Time", "timeItem", UNIT_PROVIDER);
|
||||||
|
|
||||||
|
QuantityType<Time> microSec = QuantityType.valueOf(1, MetricPrefix.MICRO(Units.SECOND));
|
||||||
|
QuantityType<Time> milliSec = QuantityType.valueOf(1, MetricPrefix.MILLI(Units.SECOND));
|
||||||
|
QuantityType<Time> second = QuantityType.valueOf(1000, MetricPrefix.MILLI(Units.SECOND));
|
||||||
|
QuantityType<Time> minute = QuantityType.valueOf(60000, MetricPrefix.MILLI(Units.SECOND));
|
||||||
|
QuantityType<Time> hour = QuantityType.valueOf(3600000, MetricPrefix.MILLI(Units.SECOND));
|
||||||
|
|
||||||
|
return Stream.of( //
|
||||||
|
Arguments.of(timeItem, "== $MIN", List.of(second, minute), QuantityType.valueOf("1 s"), true),
|
||||||
|
Arguments.of(timeItem, "== $MAX", List.of(second, minute), QuantityType.valueOf("1 min"), true), //
|
||||||
|
|
||||||
|
Arguments.of(timeItem, "== $MIN", List.of(microSec, milliSec), QuantityType.valueOf("1 µs"), true),
|
||||||
|
Arguments.of(timeItem, "== $MAX", List.of(microSec, milliSec), QuantityType.valueOf("1 ms"), true), //
|
||||||
|
|
||||||
|
Arguments.of(timeItem, "== $MIN", List.of(microSec, hour), QuantityType.valueOf("1 µs"), true),
|
||||||
|
Arguments.of(timeItem, "== $MAX", List.of(microSec, hour), QuantityType.valueOf("1 h"), true) //
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@ParameterizedTest
|
||||||
|
@MethodSource
|
||||||
|
public void testTimeValues(Item item, String condition, List<State> states, State input, boolean expected)
|
||||||
|
throws ItemNotFoundException {
|
||||||
|
internalTestFunctions(item, condition, states, input, expected);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue