Fix group calculations to use GroupItem's system unit (#4563)

* QuantityType improve group calculations
* support inverse units, extend tests
* refactoring
* javadoc and code simplification
* use streams; make sums absolute

Signed-off-by: Andrew Fiddian-Green <software@whitebear.ch>
This commit is contained in:
Andrew Fiddian-Green
2025-02-15 17:52:44 +01:00
committed by GitHub
parent 3a30c7a44a
commit 5b28e6feb6
3 changed files with 357 additions and 197 deletions
@@ -15,7 +15,7 @@ package org.openhab.core.internal.items;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import javax.measure.Quantity; import javax.measure.Unit;
import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable; import org.eclipse.jdt.annotation.Nullable;
@@ -48,13 +48,12 @@ public class GroupFunctionHelper {
* arithmetic group function will take unit conversion into account. * arithmetic group function will take unit conversion into account.
* *
* @param function the {@link GroupFunctionDTO} describing the group function. * @param function the {@link GroupFunctionDTO} describing the group function.
* @param baseItem an optional {@link Item} defining the dimension for unit conversion. * @param baseItem an optional {@link Item} defining the dimension/unit for unit conversion.
* @return a {@link GroupFunction} according to the given parameters. * @return a {@link GroupFunction} according to the given parameters.
*/ */
public GroupFunction createGroupFunction(GroupFunctionDTO function, @Nullable Item baseItem) { public GroupFunction createGroupFunction(GroupFunctionDTO function, @Nullable Item baseItem) {
Class<? extends Quantity<?>> dimension = getDimension(baseItem); if (baseItem instanceof NumberItem baseNumberItem && baseNumberItem.getDimension() != null) {
if (dimension != null) { return createDimensionGroupFunction(function, baseNumberItem);
return createDimensionGroupFunction(function, baseItem, dimension);
} }
return createDefaultGroupFunction(function, baseItem); return createDefaultGroupFunction(function, baseItem);
} }
@@ -78,30 +77,25 @@ public class GroupFunctionHelper {
return states; return states;
} }
private @Nullable Class<? extends Quantity<?>> getDimension(@Nullable Item baseItem) { private GroupFunction createDimensionGroupFunction(GroupFunctionDTO function, NumberItem baseItem) {
if (baseItem instanceof NumberItem numberItem) {
return numberItem.getDimension();
}
return null;
}
private GroupFunction createDimensionGroupFunction(GroupFunctionDTO function, @Nullable Item baseItem,
Class<? extends Quantity<?>> dimension) {
final String functionName = function.name; final String functionName = function.name;
switch (functionName.toUpperCase()) { Unit<?> baseItemUnit = baseItem.getUnit();
case "AVG": if (baseItemUnit != null) {
return new QuantityTypeArithmeticGroupFunction.Avg(dimension); switch (functionName.toUpperCase()) {
case "MEDIAN": case "AVG":
return new QuantityTypeArithmeticGroupFunction.Median(dimension, baseItem); return new QuantityTypeArithmeticGroupFunction.Avg(baseItemUnit);
case "SUM": case "MEDIAN":
return new QuantityTypeArithmeticGroupFunction.Sum(dimension); return new QuantityTypeArithmeticGroupFunction.Median(baseItemUnit);
case "MIN": case "SUM":
return new QuantityTypeArithmeticGroupFunction.Min(dimension); return new QuantityTypeArithmeticGroupFunction.Sum(baseItemUnit);
case "MAX": case "MIN":
return new QuantityTypeArithmeticGroupFunction.Max(dimension); return new QuantityTypeArithmeticGroupFunction.Min(baseItemUnit);
default: case "MAX":
return createDefaultGroupFunction(function, baseItem); return new QuantityTypeArithmeticGroupFunction.Max(baseItemUnit);
default:
}
} }
return createDefaultGroupFunction(function, baseItem);
} }
private GroupFunction createDefaultGroupFunction(GroupFunctionDTO function, @Nullable Item baseItem) { private GroupFunction createDefaultGroupFunction(GroupFunctionDTO function, @Nullable Item baseItem) {
@@ -13,12 +13,11 @@
package org.openhab.core.library.types; package org.openhab.core.library.types;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.math.MathContext;
import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.Set; import java.util.Set;
import javax.measure.Quantity;
import javax.measure.Unit; import javax.measure.Unit;
import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.NonNullByDefault;
@@ -26,31 +25,38 @@ import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.items.GroupFunction; import org.openhab.core.items.GroupFunction;
import org.openhab.core.items.GroupItem; import org.openhab.core.items.GroupItem;
import org.openhab.core.items.Item; import org.openhab.core.items.Item;
import org.openhab.core.library.items.NumberItem;
import org.openhab.core.types.State; import org.openhab.core.types.State;
import org.openhab.core.types.UnDefType; import org.openhab.core.types.UnDefType;
import org.openhab.core.util.Statistics; import org.openhab.core.util.Statistics;
import tech.units.indriya.AbstractUnit;
/** /**
* This interface is a container for dimension based functions that require {@link QuantityType}s for its calculations. * This interface is a container for dimension based functions that require {@link QuantityType}s for its calculations.
* *
* @author Henning Treu - Initial contribution * @author Henning Treu - Initial contribution
* @author Andrew Fiddian-Green - Normalise calculations based on the Unit of the GroupItem
*/ */
@NonNullByDefault @NonNullByDefault
public interface QuantityTypeArithmeticGroupFunction extends GroupFunction { public interface QuantityTypeArithmeticGroupFunction extends GroupFunction {
abstract class DimensionalGroupFunction implements GroupFunction { abstract class DimensionalGroupFunction implements GroupFunction {
protected final Class<? extends Quantity<?>> dimension; protected final Unit<?> baseItemUnit; // the actual unit of the owning group item
protected final Unit<?> systemUnit; // the reference unit for group member calculations
public DimensionalGroupFunction(Class<? extends Quantity<?>> dimension) { public DimensionalGroupFunction(Unit<?> baseItemUnit) {
this.dimension = dimension; this.baseItemUnit = baseItemUnit;
this.systemUnit = baseItemUnit.getSystemUnit();
} }
@Override @Override
public @Nullable <T extends State> T getStateAs(@Nullable Set<Item> items, Class<T> stateClass) { public @Nullable <T extends State> T getStateAs(@Nullable Set<Item> items, Class<T> stateClass) {
State state = calculate(items); State state = calculate(items);
if (stateClass.isInstance(state)) { if (stateClass.isInstance(state)) {
if (state instanceof QuantityType<?> quantity) {
state = toInvertibleUnit(quantity, baseItemUnit);
}
return stateClass.cast(state); return stateClass.cast(state);
} else { } else {
return null; return null;
@@ -62,102 +68,76 @@ public interface QuantityTypeArithmeticGroupFunction extends GroupFunction {
return new State[0]; return new State[0];
} }
protected boolean isSameDimension(@Nullable Item item) { /**
if (item instanceof GroupItem groupItem) { * Convert the given {@link QuantityType} to an equivalent based on the given {@link Unit}. The conversion can
return isSameDimension(groupItem.getBaseItem()); * 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 'targetUnit' or null.
*/
private @Nullable QuantityType<?> toInvertibleUnit(QuantityType<?> source, Unit<?> targetUnit) {
if (!targetUnit.equals(source.getUnit()) && !targetUnit.isCompatible(AbstractUnit.ONE)
&& source.getUnit().inverse().isCompatible(targetUnit)) {
QuantityType<?> sourceInSystemUnit = source.toUnit(source.getUnit().getSystemUnit());
return sourceInSystemUnit != null ? sourceInSystemUnit.inverse().toUnit(targetUnit) : null;
} }
return item instanceof NumberItem ni && dimension.equals(ni.getDimension()); return source.toUnit(targetUnit);
}
/**
* Convert the given item {@link State} to a {@link QuantityType} based on the {@link Unit} of the
* {@link GroupItem} i.e. 'referenceUnit'. Returns null if the {@link State} is not a {@link QuantityType} or
* if the {@link QuantityType} could not be converted to 'referenceUnit'.
*
* The conversion can be made to both inverted and non-inverted units, so invertible type conversions (e.g.
* Mirek <=> Kelvin) are supported.
*
* @param state the State of any given group member item
* @return a QuantityType or null
*/
private @Nullable QuantityType<?> toQuantityTypeOfUnit(@Nullable State state, Unit<?> unit) {
return state instanceof QuantityType<?> quantity //
? toInvertibleUnit(quantity, unit)
: null;
}
/**
* Convert a set of {@link Item} to a respective list of {@link QuantityType}. Exclude any {@link Item}s whose
* current {@link State} is not a {@link QuantityType}. Convert any remaining {@link QuantityType} to the
* 'referenceUnit' and exclude any values that did not convert.
*
* @param items a list of {@link Item}
* @return a list of {@link QuantityType} converted to the 'referenceUnit'
*/
@SuppressWarnings({ "rawtypes" })
protected List<QuantityType> toQuantityTypesOfUnit(Set<Item> items, Unit<?> unit) {
return items.stream().map(i -> i.getState()).map(s -> toQuantityTypeOfUnit(s, unit))
.filter(Objects::nonNull).map(s -> (QuantityType) s).toList();
} }
} }
/** /**
* This calculates the numeric average over all item states of {@link QuantityType}. * Calculates the average of a set of item states whose value could be converted to the 'referenceUnit'.
*/ */
class Avg extends DimensionalGroupFunction { class Avg extends DimensionalGroupFunction {
public Avg(Class<? extends Quantity<?>> dimension) { public Avg(Unit<?> baseItemUnit) {
super(dimension); super(baseItemUnit);
}
@Override
@SuppressWarnings({ "unchecked", "rawtypes" })
public State calculate(@Nullable Set<Item> items) {
if (items == null || items.isEmpty()) {
return UnDefType.UNDEF;
}
QuantityType<?> sum = null;
int count = 0;
for (Item item : items) {
if (isSameDimension(item)) {
QuantityType itemState = item.getStateAs(QuantityType.class);
if (itemState != null) {
if (sum == null) {
sum = itemState; // initialise the sum from the first item
count++;
} else {
itemState = itemState.toInvertibleUnit(sum.getUnit());
if (itemState != null) {
sum = sum.add(itemState);
count++;
}
}
}
}
}
if (sum != null && count > 0) {
BigDecimal result = sum.toBigDecimal().divide(BigDecimal.valueOf(count), MathContext.DECIMAL128);
return new QuantityType(result, sum.getUnit());
}
return UnDefType.UNDEF;
}
}
/**
* This calculates the numeric median over all item states of {@link QuantityType}.
*/
class Median extends DimensionalGroupFunction {
private @Nullable Item baseItem;
public Median(Class<? extends Quantity<?>> dimension, @Nullable Item baseItem) {
super(dimension);
this.baseItem = baseItem;
} }
@Override @Override
@SuppressWarnings({ "unchecked", "rawtypes" }) @SuppressWarnings({ "unchecked", "rawtypes" })
public State calculate(@Nullable Set<Item> items) { public State calculate(@Nullable Set<Item> items) {
if (items != null) { if (items != null) {
List<BigDecimal> values = new ArrayList<>(); List<QuantityType> systemUnitQuantities = toQuantityTypesOfUnit(items, systemUnit);
Unit<?> unit = null; if (!systemUnitQuantities.isEmpty()) {
if (baseItem instanceof NumberItem numberItem) { return systemUnitQuantities.stream().reduce(new QuantityType<>(0, systemUnit), QuantityType::add)
unit = numberItem.getUnit(); .divide(BigDecimal.valueOf(systemUnitQuantities.size()));
}
for (Item item : items) {
if (!isSameDimension(item)) {
continue;
}
QuantityType itemState = item.getStateAs(QuantityType.class);
if (itemState == null) {
continue;
}
if (unit == null) {
unit = itemState.getUnit(); // set it to the first item's unit
}
if (itemState.toInvertibleUnit(unit) instanceof QuantityType<?> inverted) {
values.add(inverted.toBigDecimal());
}
}
if (!values.isEmpty()) {
BigDecimal median = Statistics.median(values);
if (median != null && unit != null) {
return new QuantityType<>(median, unit);
}
} }
} }
return UnDefType.UNDEF; return UnDefType.UNDEF;
@@ -165,105 +145,99 @@ public interface QuantityTypeArithmeticGroupFunction extends GroupFunction {
} }
/** /**
* This calculates the numeric sum over all item states of {@link QuantityType}. * Calculates the median of a set of item states whose value could be converted to the 'referenceUnit'.
*/
class Median extends DimensionalGroupFunction {
public Median(Unit<?> baseItemUnit) {
super(baseItemUnit);
}
@Override
public State calculate(@Nullable Set<Item> items) {
if (items != null) {
BigDecimal median = Statistics
.median(toQuantityTypesOfUnit(items, systemUnit).stream().map(q -> q.toBigDecimal()).toList());
if (median != null) {
return new QuantityType<>(median, systemUnit);
}
}
return UnDefType.UNDEF;
}
}
/**
* Calculates the sum of a set of item states whose value could be converted to the 'referenceUnit'.
*
* Uses the {@link QuanitityType.add()} method so the result is an incremental sum based on the 'referenceUnit'. As
* a general rule this class is instantiated with a 'referenceUnit' that is a "system unit" (which are zero based)
* so such incremental sum is in fact also an absolute sum. However the class COULD be instantiated with a "non-
* system unit" (e.g. °C, °F) in which case the result would be an incremental sum based on that unit.
*
*/ */
class Sum extends DimensionalGroupFunction { class Sum extends DimensionalGroupFunction {
public Sum(Class<? extends Quantity<?>> dimension) { public Sum(Unit<?> baseItemUnit) {
super(dimension); super(baseItemUnit);
} }
@Override @Override
@SuppressWarnings({ "unchecked", "rawtypes" }) @SuppressWarnings("unchecked")
public State calculate(@Nullable Set<Item> items) { public State calculate(@Nullable Set<Item> items) {
if (items == null || items.isEmpty()) { if (items != null) {
return UnDefType.UNDEF; @SuppressWarnings("rawtypes")
} List<QuantityType> systemUnitQuantities = toQuantityTypesOfUnit(items, baseItemUnit);
if (!systemUnitQuantities.isEmpty()) {
QuantityType<?> sum = null; return systemUnitQuantities.stream().reduce(new QuantityType<>(0, baseItemUnit), QuantityType::add);
for (Item item : items) {
if (isSameDimension(item)) {
QuantityType itemState = item.getStateAs(QuantityType.class);
if (itemState != null) {
if (sum == null) {
sum = itemState; // initialise the sum from the first item
} else {
itemState = itemState.toUnit(sum.getUnit());
if (itemState != null) {
sum = sum.add(itemState);
}
}
}
} }
} }
return UnDefType.UNDEF;
return sum != null ? sum : UnDefType.UNDEF;
} }
} }
/** /**
* This calculates the minimum value of all item states of {@link QuantityType}. * Calculates the minimum of a set of item states whose value could be converted to the 'referenceUnit'.
*/ */
class Min extends DimensionalGroupFunction { class Min extends DimensionalGroupFunction {
public Min(Class<? extends Quantity<?>> dimension) { public Min(Unit<?> baseItemUnit) {
super(dimension); super(baseItemUnit);
} }
@Override @Override
@SuppressWarnings({ "unchecked", "rawtypes" })
public State calculate(@Nullable Set<Item> items) { public State calculate(@Nullable Set<Item> items) {
if (items == null || items.isEmpty()) { if (items != null) {
return UnDefType.UNDEF; @SuppressWarnings({ "unchecked", "rawtypes" })
} Optional<QuantityType> min = toQuantityTypesOfUnit(items, systemUnit).stream()
.min(QuantityType::compareTo);
QuantityType<?> min = null; if (min.isPresent()) {
for (Item item : items) { return min.get();
if (isSameDimension(item)) {
QuantityType itemState = item.getStateAs(QuantityType.class);
if (itemState != null) {
if (min == null
|| (min.getUnit().isCompatible(itemState.getUnit()) && min.compareTo(itemState) > 0)) {
min = itemState;
}
}
} }
} }
return UnDefType.UNDEF;
return min != null ? min : UnDefType.UNDEF;
} }
} }
/** /**
* This calculates the maximum value of all item states of {@link QuantityType}. * Calculates the maximum of a set of item states whose value could be converted to the 'referenceUnit'.
*/ */
class Max extends DimensionalGroupFunction { class Max extends DimensionalGroupFunction {
public Max(Class<? extends Quantity<?>> dimension) { public Max(Unit<?> targetUnit) {
super(dimension); super(targetUnit);
} }
@Override @Override
@SuppressWarnings({ "unchecked", "rawtypes" })
public State calculate(@Nullable Set<Item> items) { public State calculate(@Nullable Set<Item> items) {
if (items == null || items.isEmpty()) { if (items != null) {
return UnDefType.UNDEF; @SuppressWarnings({ "unchecked", "rawtypes" })
} Optional<QuantityType> max = toQuantityTypesOfUnit(items, systemUnit).stream()
.max(QuantityType::compareTo);
QuantityType<?> max = null; if (max.isPresent()) {
for (Item item : items) { return max.get();
if (isSameDimension(item)) {
QuantityType itemState = item.getStateAs(QuantityType.class);
if (itemState != null) {
if (max == null
|| (max.getUnit().isCompatible(itemState.getUnit()) && max.compareTo(itemState) < 0)) {
max = itemState;
}
}
} }
} }
return UnDefType.UNDEF;
return max != null ? max : UnDefType.UNDEF;
} }
} }
} }
@@ -15,7 +15,7 @@ package org.openhab.core.library.types;
import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.number.IsCloseTo.closeTo; import static org.hamcrest.number.IsCloseTo.closeTo;
import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.*;
import static org.junit.jupiter.params.provider.Arguments.arguments; import static org.junit.jupiter.params.provider.Arguments.arguments;
import java.util.LinkedHashSet; import java.util.LinkedHashSet;
@@ -46,6 +46,9 @@ import org.openhab.core.items.GroupItem;
import org.openhab.core.items.Item; import org.openhab.core.items.Item;
import org.openhab.core.library.CoreItemFactory; import org.openhab.core.library.CoreItemFactory;
import org.openhab.core.library.items.NumberItem; import org.openhab.core.library.items.NumberItem;
import org.openhab.core.library.unit.ImperialUnits;
import org.openhab.core.library.unit.SIUnits;
import org.openhab.core.library.unit.Units;
import org.openhab.core.types.State; import org.openhab.core.types.State;
import org.openhab.core.types.UnDefType; import org.openhab.core.types.UnDefType;
import org.osgi.service.component.ComponentContext; import org.osgi.service.component.ComponentContext;
@@ -85,7 +88,7 @@ public class QuantityTypeArithmeticGroupFunctionTest {
items.add(createNumberItem("TestItem4", Temperature.class, UnDefType.UNDEF)); items.add(createNumberItem("TestItem4", Temperature.class, UnDefType.UNDEF));
items.add(createNumberItem("TestItem5", Temperature.class, new QuantityType<>("122.41 °C"))); items.add(createNumberItem("TestItem5", Temperature.class, new QuantityType<>("122.41 °C")));
GroupFunction function = new QuantityTypeArithmeticGroupFunction.Sum(Temperature.class); GroupFunction function = new QuantityTypeArithmeticGroupFunction.Sum(SIUnits.CELSIUS);
State state = function.calculate(items); State state = function.calculate(items);
assertEquals(new QuantityType<>("234.95 °C"), state); assertEquals(new QuantityType<>("234.95 °C"), state);
@@ -103,7 +106,7 @@ public class QuantityTypeArithmeticGroupFunctionTest {
items.add(createNumberItem("TestItem4", Temperature.class, UnDefType.UNDEF)); items.add(createNumberItem("TestItem4", Temperature.class, UnDefType.UNDEF));
items.add(createNumberItem("TestItem5", Temperature.class, new QuantityType<>("395.56 K"))); items.add(createNumberItem("TestItem5", Temperature.class, new QuantityType<>("395.56 K")));
GroupFunction function = new QuantityTypeArithmeticGroupFunction.Sum(Temperature.class); GroupFunction function = new QuantityTypeArithmeticGroupFunction.Sum(SIUnits.CELSIUS);
State state = function.calculate(items); State state = function.calculate(items);
assertEquals(new QuantityType<>("234.95 °C"), state); assertEquals(new QuantityType<>("234.95 °C"), state);
@@ -119,7 +122,7 @@ public class QuantityTypeArithmeticGroupFunctionTest {
items.add(createNumberItem("TestItem2", Temperature.class, UnDefType.NULL)); items.add(createNumberItem("TestItem2", Temperature.class, UnDefType.NULL));
items.add(createNumberItem("TestItem3", Pressure.class, new QuantityType<>("192.2 hPa"))); items.add(createNumberItem("TestItem3", Pressure.class, new QuantityType<>("192.2 hPa")));
GroupFunction function = new QuantityTypeArithmeticGroupFunction.Sum(Temperature.class); GroupFunction function = new QuantityTypeArithmeticGroupFunction.Sum(SIUnits.CELSIUS);
State state = function.calculate(items); State state = function.calculate(items);
assertEquals(new QuantityType<>("23.54 °C"), state); assertEquals(new QuantityType<>("23.54 °C"), state);
@@ -137,7 +140,7 @@ public class QuantityTypeArithmeticGroupFunctionTest {
items.add(createNumberItem("TestItem4", Temperature.class, UnDefType.UNDEF)); items.add(createNumberItem("TestItem4", Temperature.class, UnDefType.UNDEF));
items.add(createNumberItem("TestItem5", Temperature.class, new QuantityType<>("300 °C"))); items.add(createNumberItem("TestItem5", Temperature.class, new QuantityType<>("300 °C")));
GroupFunction function = new QuantityTypeArithmeticGroupFunction.Avg(Temperature.class); GroupFunction function = new QuantityTypeArithmeticGroupFunction.Avg(Units.KELVIN);
State state = function.calculate(items); State state = function.calculate(items);
assertEquals(new QuantityType<>("200 °C"), state); assertEquals(new QuantityType<>("200 °C"), state);
@@ -163,10 +166,11 @@ public class QuantityTypeArithmeticGroupFunctionTest {
items.add(createNumberItem("TestItem4", Temperature.class, UnDefType.UNDEF)); items.add(createNumberItem("TestItem4", Temperature.class, UnDefType.UNDEF));
items.add(createNumberItem("TestItem5", Temperature.class, new QuantityType<>("294.15 K"))); items.add(createNumberItem("TestItem5", Temperature.class, new QuantityType<>("294.15 K")));
GroupFunction function = new QuantityTypeArithmeticGroupFunction.Avg(Temperature.class); GroupFunction function = new QuantityTypeArithmeticGroupFunction.Avg(Units.KELVIN);
State state = function.calculate(items); State state = function.calculate(items);
assertEquals(new QuantityType<>("55.33333333333333333333333333333334 °C"), state); assertTrue(state instanceof QuantityType<?>);
assertEquals(328.48, ((QuantityType<?>) state).doubleValue(), 0.01);
} }
@ParameterizedTest @ParameterizedTest
@@ -179,12 +183,13 @@ public class QuantityTypeArithmeticGroupFunctionTest {
items.add(createNumberItem("TestItem2", Temperature.class, UnDefType.NULL)); items.add(createNumberItem("TestItem2", Temperature.class, UnDefType.NULL));
items.add(createNumberItem("TestItem3", Pressure.class, new QuantityType<>("192.2 hPa"))); items.add(createNumberItem("TestItem3", Pressure.class, new QuantityType<>("192.2 hPa")));
GroupFunction function = new QuantityTypeArithmeticGroupFunction.Avg(Temperature.class); GroupFunction function = new QuantityTypeArithmeticGroupFunction.Avg(SIUnits.CELSIUS);
State state = function.calculate(items); State state = function.calculate(items);
assertEquals(new QuantityType<>("23.54 °C"), state); assertEquals(new QuantityType<>("23.54 °C"), state);
} }
@SuppressWarnings("rawtypes")
static Stream<Arguments> medianTestSource() { static Stream<Arguments> medianTestSource() {
return Stream.of( // return Stream.of( //
arguments( // arguments( //
@@ -214,13 +219,14 @@ public class QuantityTypeArithmeticGroupFunctionTest {
@ParameterizedTest @ParameterizedTest
@MethodSource("medianTestSource") @MethodSource("medianTestSource")
@SuppressWarnings({ "null", "rawtypes", "unchecked" })
public void testMedianFunctionQuantityType(List<State> states, State expected) { public void testMedianFunctionQuantityType(List<State> states, State expected) {
AtomicInteger index = new AtomicInteger(1); AtomicInteger index = new AtomicInteger(1);
Set<Item> items = states.stream() Set<Item> items = states.stream()
.map(state -> createNumberItem("TestItem" + index.getAndIncrement(), Temperature.class, state)) .map(state -> createNumberItem("TestItem" + index.getAndIncrement(), Temperature.class, state))
.collect(Collectors.toSet()); .collect(Collectors.toSet());
GroupFunction function = new QuantityTypeArithmeticGroupFunction.Median(Temperature.class, null); GroupFunction function = new QuantityTypeArithmeticGroupFunction.Median(SIUnits.CELSIUS);
State state = function.calculate(items); State state = function.calculate(items);
assertEquals(state.getClass(), expected.getClass()); assertEquals(state.getClass(), expected.getClass());
@@ -242,7 +248,7 @@ public class QuantityTypeArithmeticGroupFunctionTest {
items.add(createNumberItem("TestItem4", Temperature.class, UnDefType.UNDEF)); items.add(createNumberItem("TestItem4", Temperature.class, UnDefType.UNDEF));
items.add(createNumberItem("TestItem5", Temperature.class, new QuantityType<>("300 °C"))); items.add(createNumberItem("TestItem5", Temperature.class, new QuantityType<>("300 °C")));
GroupFunction function = new QuantityTypeArithmeticGroupFunction.Max(Temperature.class); GroupFunction function = new QuantityTypeArithmeticGroupFunction.Max(SIUnits.CELSIUS);
State state = function.calculate(items); State state = function.calculate(items);
assertEquals(new QuantityType<>("300 °C"), state); assertEquals(new QuantityType<>("300 °C"), state);
@@ -260,7 +266,7 @@ public class QuantityTypeArithmeticGroupFunctionTest {
items.add(createNumberItem("TestItem4", Temperature.class, UnDefType.UNDEF)); items.add(createNumberItem("TestItem4", Temperature.class, UnDefType.UNDEF));
items.add(createNumberItem("TestItem5", Temperature.class, new QuantityType<>("294.15 K"))); items.add(createNumberItem("TestItem5", Temperature.class, new QuantityType<>("294.15 K")));
GroupFunction function = new QuantityTypeArithmeticGroupFunction.Max(Temperature.class); GroupFunction function = new QuantityTypeArithmeticGroupFunction.Max(SIUnits.CELSIUS);
State state = function.calculate(items); State state = function.calculate(items);
assertEquals(new QuantityType<>("100 °C"), state); assertEquals(new QuantityType<>("100 °C"), state);
@@ -276,7 +282,7 @@ public class QuantityTypeArithmeticGroupFunctionTest {
items.add(createNumberItem("TestItem2", Temperature.class, UnDefType.NULL)); items.add(createNumberItem("TestItem2", Temperature.class, UnDefType.NULL));
items.add(createNumberItem("TestItem3", Pressure.class, new QuantityType<>("192.2 hPa"))); items.add(createNumberItem("TestItem3", Pressure.class, new QuantityType<>("192.2 hPa")));
GroupFunction function = new QuantityTypeArithmeticGroupFunction.Max(Temperature.class); GroupFunction function = new QuantityTypeArithmeticGroupFunction.Max(SIUnits.CELSIUS);
State state = function.calculate(items); State state = function.calculate(items);
assertEquals(new QuantityType<>("23.54 °C"), state); assertEquals(new QuantityType<>("23.54 °C"), state);
@@ -294,7 +300,7 @@ public class QuantityTypeArithmeticGroupFunctionTest {
items.add(createNumberItem("TestItem4", Temperature.class, UnDefType.UNDEF)); items.add(createNumberItem("TestItem4", Temperature.class, UnDefType.UNDEF));
items.add(createNumberItem("TestItem5", Temperature.class, new QuantityType<>("300 °C"))); items.add(createNumberItem("TestItem5", Temperature.class, new QuantityType<>("300 °C")));
GroupFunction function = new QuantityTypeArithmeticGroupFunction.Min(Temperature.class); GroupFunction function = new QuantityTypeArithmeticGroupFunction.Min(SIUnits.CELSIUS);
State state = function.calculate(items); State state = function.calculate(items);
assertEquals(new QuantityType<>("100 °C"), state); assertEquals(new QuantityType<>("100 °C"), state);
@@ -313,7 +319,7 @@ public class QuantityTypeArithmeticGroupFunctionTest {
items.add(createNumberItem("TestItem5", Dimensionless.class, new QuantityType<>("0 %"))); items.add(createNumberItem("TestItem5", Dimensionless.class, new QuantityType<>("0 %")));
items.add(createNumberItem("TestItem6", Dimensionless.class, new QuantityType<>("0 %"))); items.add(createNumberItem("TestItem6", Dimensionless.class, new QuantityType<>("0 %")));
GroupFunction function = new QuantityTypeArithmeticGroupFunction.Max(Dimensionless.class); GroupFunction function = new QuantityTypeArithmeticGroupFunction.Max(Units.ONE);
State state = function.calculate(items); State state = function.calculate(items);
assertEquals(new QuantityType<>("48 %"), state); assertEquals(new QuantityType<>("48 %"), state);
@@ -331,7 +337,7 @@ public class QuantityTypeArithmeticGroupFunctionTest {
items.add(createNumberItem("TestItem4", Temperature.class, UnDefType.UNDEF)); items.add(createNumberItem("TestItem4", Temperature.class, UnDefType.UNDEF));
items.add(createNumberItem("TestItem5", Temperature.class, new QuantityType<>("294.15 K"))); items.add(createNumberItem("TestItem5", Temperature.class, new QuantityType<>("294.15 K")));
GroupFunction function = new QuantityTypeArithmeticGroupFunction.Min(Temperature.class); GroupFunction function = new QuantityTypeArithmeticGroupFunction.Min(SIUnits.CELSIUS);
State state = function.calculate(items); State state = function.calculate(items);
assertEquals(new QuantityType<>("294.15 K"), state); assertEquals(new QuantityType<>("294.15 K"), state);
@@ -347,7 +353,7 @@ public class QuantityTypeArithmeticGroupFunctionTest {
items.add(createNumberItem("TestItem2", Temperature.class, UnDefType.NULL)); items.add(createNumberItem("TestItem2", Temperature.class, UnDefType.NULL));
items.add(createNumberItem("TestItem3", Pressure.class, new QuantityType<>("192.2 hPa"))); items.add(createNumberItem("TestItem3", Pressure.class, new QuantityType<>("192.2 hPa")));
GroupFunction function = new QuantityTypeArithmeticGroupFunction.Min(Temperature.class); GroupFunction function = new QuantityTypeArithmeticGroupFunction.Min(SIUnits.CELSIUS);
State state = function.calculate(items); State state = function.calculate(items);
assertEquals(new QuantityType<>("23.54 °C"), state); assertEquals(new QuantityType<>("23.54 °C"), state);
@@ -362,7 +368,7 @@ public class QuantityTypeArithmeticGroupFunctionTest {
items.add(createNumberItem("TestItem1", Power.class, new QuantityType<>("5 W"))); items.add(createNumberItem("TestItem1", Power.class, new QuantityType<>("5 W")));
items.add(createGroupItem("TestGroup1", Power.class, new QuantityType<>("5 W"))); items.add(createGroupItem("TestGroup1", Power.class, new QuantityType<>("5 W")));
GroupFunction function = new QuantityTypeArithmeticGroupFunction.Sum(Power.class); GroupFunction function = new QuantityTypeArithmeticGroupFunction.Sum(Units.WATT);
State state = function.calculate(items); State state = function.calculate(items);
assertEquals(new QuantityType<>("10 W"), state); assertEquals(new QuantityType<>("10 W"), state);
@@ -380,4 +386,190 @@ public class QuantityTypeArithmeticGroupFunctionTest {
item.setState(state); item.setState(state);
return item; return item;
} }
@ParameterizedTest
@MethodSource("locales")
public void testSumFunctionQuantityTypeDifferentUnitsBaseKelvin(Locale locale) {
Locale.setDefault(locale);
Set<Item> items = new LinkedHashSet<>();
items.add(createNumberItem("TestItem1", Temperature.class, new QuantityType<>("23.54 °C")));
items.add(createNumberItem("TestItem2", Temperature.class, UnDefType.NULL));
items.add(createNumberItem("TestItem3", Temperature.class, new QuantityType<>("192.2 °F")));
items.add(createNumberItem("TestItem4", Temperature.class, UnDefType.UNDEF));
items.add(createNumberItem("TestItem5", Temperature.class, new QuantityType<>("395.56 K")));
GroupFunction function = new QuantityTypeArithmeticGroupFunction.Sum(Units.KELVIN);
State state = function.calculate(items);
assertEquals(new QuantityType<>("1054.40 K"), state);
}
@ParameterizedTest
@MethodSource("locales")
public void testAvgFunctionQuantityTypeDifferentUnitsBaseKelvin(Locale locale) {
Locale.setDefault(locale);
Set<Item> items = new LinkedHashSet<>();
items.add(createNumberItem("TestItem1", Temperature.class, new QuantityType<>("100 °C")));
items.add(createNumberItem("TestItem2", Temperature.class, UnDefType.NULL));
items.add(createNumberItem("TestItem3", Temperature.class, new QuantityType<>("113 °F")));
items.add(createNumberItem("TestItem4", Temperature.class, UnDefType.UNDEF));
items.add(createNumberItem("TestItem5", Temperature.class, new QuantityType<>("294.15 K")));
GroupFunction function = new QuantityTypeArithmeticGroupFunction.Avg(Units.KELVIN);
State state = function.calculate(items);
assertTrue(state instanceof QuantityType<?>);
assertEquals(328.48, ((QuantityType<?>) state).doubleValue(), 0.01);
}
@ParameterizedTest
@MethodSource("locales")
public void testMaxFunctionQuantityTypeDifferentUnitsBaseKelvin(Locale locale) {
Locale.setDefault(locale);
Set<Item> items = new LinkedHashSet<>();
items.add(createNumberItem("TestItem1", Temperature.class, new QuantityType<>("100 °C")));
items.add(createNumberItem("TestItem2", Temperature.class, UnDefType.NULL));
items.add(createNumberItem("TestItem3", Temperature.class, new QuantityType<>("113 °F")));
items.add(createNumberItem("TestItem4", Temperature.class, UnDefType.UNDEF));
items.add(createNumberItem("TestItem5", Temperature.class, new QuantityType<>("294.15 K")));
GroupFunction function = new QuantityTypeArithmeticGroupFunction.Max(Units.KELVIN);
State state = function.calculate(items);
assertEquals(new QuantityType<>("100 °C"), state);
}
@ParameterizedTest
@MethodSource("locales")
public void testMinFunctionQuantityTypeDifferentUnitsBaseKelvin(Locale locale) {
Locale.setDefault(locale);
Set<Item> items = new LinkedHashSet<>();
items.add(createNumberItem("TestItem1", Temperature.class, new QuantityType<>("100 °C")));
items.add(createNumberItem("TestItem2", Temperature.class, UnDefType.NULL));
items.add(createNumberItem("TestItem3", Temperature.class, new QuantityType<>("113 °F")));
items.add(createNumberItem("TestItem4", Temperature.class, UnDefType.UNDEF));
items.add(createNumberItem("TestItem5", Temperature.class, new QuantityType<>("294.15 K")));
GroupFunction function = new QuantityTypeArithmeticGroupFunction.Min(Units.KELVIN);
State state = function.calculate(items);
assertEquals(new QuantityType<>("294.15 K"), state);
}
@ParameterizedTest
@MethodSource("locales")
public void testSumFunctionColorTemperatureDifferentUnitsBaseKelvin(Locale locale) {
Locale.setDefault(locale);
Set<Item> items = new LinkedHashSet<>();
items.add(createNumberItem("TestItem1", Temperature.class, QuantityType.valueOf(2000, Units.KELVIN)));
items.add(createNumberItem("TestItem2", Temperature.class, UnDefType.NULL));
items.add(createNumberItem("TestItem3", Temperature.class, QuantityType.valueOf(1726.85, SIUnits.CELSIUS)));
items.add(createNumberItem("TestItem4", Temperature.class, QuantityType.valueOf(500, Units.MIRED)));
items.add(createNumberItem("TestItem5", Temperature.class,
QuantityType.valueOf(3140.33, ImperialUnits.FAHRENHEIT)));
GroupFunction function = new QuantityTypeArithmeticGroupFunction.Sum(Units.KELVIN);
State state = function.calculate(items);
assertEquals(new QuantityType<>("8000 K"), state);
}
@ParameterizedTest
@MethodSource("locales")
public void testAvgFunctionQuantityTypeColorTempDifferentUnitsBaseKelvin(Locale locale) {
Locale.setDefault(locale);
Set<Item> items = new LinkedHashSet<>();
items.add(createNumberItem("TestItem1", Temperature.class, QuantityType.valueOf(2000, Units.KELVIN)));
items.add(createNumberItem("TestItem2", Temperature.class, UnDefType.NULL));
items.add(createNumberItem("TestItem3", Temperature.class, QuantityType.valueOf(1726.85, SIUnits.CELSIUS)));
items.add(createNumberItem("TestItem4", Temperature.class, QuantityType.valueOf(500, Units.MIRED)));
items.add(createNumberItem("TestItem5", Temperature.class,
QuantityType.valueOf(3140.33, ImperialUnits.FAHRENHEIT)));
GroupFunction function = new QuantityTypeArithmeticGroupFunction.Avg(Units.KELVIN);
State state = function.calculate(items);
assertEquals(new QuantityType<>("2000 K"), state);
}
@ParameterizedTest
@MethodSource("locales")
public void testAvgFunctionQuantityTypeColorTempDifferentUnitsBaseMirek(Locale locale) {
Locale.setDefault(locale);
Set<Item> items = new LinkedHashSet<>();
items.add(createNumberItem("TestItem1", Temperature.class, QuantityType.valueOf(2000, Units.KELVIN)));
items.add(createNumberItem("TestItem2", Temperature.class, UnDefType.NULL));
items.add(createNumberItem("TestItem3", Temperature.class, QuantityType.valueOf(1726.85, SIUnits.CELSIUS)));
items.add(createNumberItem("TestItem4", Temperature.class, QuantityType.valueOf(500, Units.MIRED)));
items.add(createNumberItem("TestItem5", Temperature.class,
QuantityType.valueOf(3140.33, ImperialUnits.FAHRENHEIT)));
GroupFunction function = new QuantityTypeArithmeticGroupFunction.Avg(Units.MIRED);
State state = function.calculate(items);
assertEquals(new QuantityType<>("2000 K"), state);
}
@ParameterizedTest
@MethodSource("locales")
public void testMinFunctionColorTemperatureDifferentUnitsBaseKelvin(Locale locale) {
Locale.setDefault(locale);
Set<Item> items = new LinkedHashSet<>();
items.add(createNumberItem("TestItem1", Temperature.class, QuantityType.valueOf(1999, Units.KELVIN)));
items.add(createNumberItem("TestItem2", Temperature.class, UnDefType.NULL));
items.add(createNumberItem("TestItem3", Temperature.class, QuantityType.valueOf(1726.85, SIUnits.CELSIUS)));
items.add(createNumberItem("TestItem4", Temperature.class, QuantityType.valueOf(500, Units.MIRED)));
items.add(createNumberItem("TestItem5", Temperature.class,
QuantityType.valueOf(3140.33, ImperialUnits.FAHRENHEIT)));
GroupFunction function = new QuantityTypeArithmeticGroupFunction.Min(Units.KELVIN);
State state = function.calculate(items);
assertEquals(new QuantityType<>("1999 K"), state);
}
@ParameterizedTest
@MethodSource("locales")
public void testMaxFunctionColorTemperatureDifferentUnitsBaseKelvin(Locale locale) {
Locale.setDefault(locale);
Set<Item> items = new LinkedHashSet<>();
items.add(createNumberItem("TestItem1", Temperature.class, QuantityType.valueOf(2001, Units.KELVIN)));
items.add(createNumberItem("TestItem2", Temperature.class, UnDefType.NULL));
items.add(createNumberItem("TestItem3", Temperature.class, QuantityType.valueOf(1726.85, SIUnits.CELSIUS)));
items.add(createNumberItem("TestItem4", Temperature.class, QuantityType.valueOf(500, Units.MIRED)));
items.add(createNumberItem("TestItem5", Temperature.class,
QuantityType.valueOf(3140.33, ImperialUnits.FAHRENHEIT)));
GroupFunction function = new QuantityTypeArithmeticGroupFunction.Max(Units.KELVIN);
State state = function.calculate(items);
assertEquals(new QuantityType<>("2001 K"), state);
}
@ParameterizedTest
@MethodSource("locales")
public void testSumFunctionQuantityTypeDifferentUnitsBaseWatt(Locale locale) {
Locale.setDefault(locale);
Set<Item> items = new LinkedHashSet<>();
items.add(createNumberItem("TestItem1", Power.class, new QuantityType<>("1 W")));
items.add(createNumberItem("TestItem2", Temperature.class, UnDefType.NULL));
items.add(createNumberItem("TestItem3", Temperature.class, new QuantityType<>("192.2 °F")));
items.add(createNumberItem("TestItem4", Temperature.class, UnDefType.UNDEF));
items.add(createNumberItem("TestItem5", Power.class, new QuantityType<>("3000 mW")));
GroupFunction function = new QuantityTypeArithmeticGroupFunction.Sum(Units.WATT);
State state = function.calculate(items);
assertEquals(new QuantityType<>("4 W"), state);
}
} }