From 5b28e6feb68075750f0f737c3c01211eae6d7031 Mon Sep 17 00:00:00 2001 From: Andrew Fiddian-Green Date: Sat, 15 Feb 2025 16:52:44 +0000 Subject: [PATCH] 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 --- .../internal/items/GroupFunctionHelper.java | 48 ++- .../QuantityTypeArithmeticGroupFunction.java | 280 ++++++++---------- ...antityTypeArithmeticGroupFunctionTest.java | 226 ++++++++++++-- 3 files changed, 357 insertions(+), 197 deletions(-) diff --git a/bundles/org.openhab.core/src/main/java/org/openhab/core/internal/items/GroupFunctionHelper.java b/bundles/org.openhab.core/src/main/java/org/openhab/core/internal/items/GroupFunctionHelper.java index 47c39bf71..ac38d89b7 100644 --- a/bundles/org.openhab.core/src/main/java/org/openhab/core/internal/items/GroupFunctionHelper.java +++ b/bundles/org.openhab.core/src/main/java/org/openhab/core/internal/items/GroupFunctionHelper.java @@ -15,7 +15,7 @@ package org.openhab.core.internal.items; import java.util.ArrayList; import java.util.List; -import javax.measure.Quantity; +import javax.measure.Unit; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; @@ -48,13 +48,12 @@ public class GroupFunctionHelper { * arithmetic group function will take unit conversion into account. * * @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. */ public GroupFunction createGroupFunction(GroupFunctionDTO function, @Nullable Item baseItem) { - Class> dimension = getDimension(baseItem); - if (dimension != null) { - return createDimensionGroupFunction(function, baseItem, dimension); + if (baseItem instanceof NumberItem baseNumberItem && baseNumberItem.getDimension() != null) { + return createDimensionGroupFunction(function, baseNumberItem); } return createDefaultGroupFunction(function, baseItem); } @@ -78,30 +77,25 @@ public class GroupFunctionHelper { return states; } - private @Nullable Class> getDimension(@Nullable Item baseItem) { - if (baseItem instanceof NumberItem numberItem) { - return numberItem.getDimension(); - } - return null; - } - - private GroupFunction createDimensionGroupFunction(GroupFunctionDTO function, @Nullable Item baseItem, - Class> dimension) { + private GroupFunction createDimensionGroupFunction(GroupFunctionDTO function, NumberItem baseItem) { final String functionName = function.name; - switch (functionName.toUpperCase()) { - case "AVG": - return new QuantityTypeArithmeticGroupFunction.Avg(dimension); - case "MEDIAN": - return new QuantityTypeArithmeticGroupFunction.Median(dimension, baseItem); - case "SUM": - return new QuantityTypeArithmeticGroupFunction.Sum(dimension); - case "MIN": - return new QuantityTypeArithmeticGroupFunction.Min(dimension); - case "MAX": - return new QuantityTypeArithmeticGroupFunction.Max(dimension); - default: - return createDefaultGroupFunction(function, baseItem); + Unit baseItemUnit = baseItem.getUnit(); + if (baseItemUnit != null) { + switch (functionName.toUpperCase()) { + case "AVG": + return new QuantityTypeArithmeticGroupFunction.Avg(baseItemUnit); + case "MEDIAN": + return new QuantityTypeArithmeticGroupFunction.Median(baseItemUnit); + case "SUM": + return new QuantityTypeArithmeticGroupFunction.Sum(baseItemUnit); + case "MIN": + return new QuantityTypeArithmeticGroupFunction.Min(baseItemUnit); + case "MAX": + return new QuantityTypeArithmeticGroupFunction.Max(baseItemUnit); + default: + } } + return createDefaultGroupFunction(function, baseItem); } private GroupFunction createDefaultGroupFunction(GroupFunctionDTO function, @Nullable Item baseItem) { diff --git a/bundles/org.openhab.core/src/main/java/org/openhab/core/library/types/QuantityTypeArithmeticGroupFunction.java b/bundles/org.openhab.core/src/main/java/org/openhab/core/library/types/QuantityTypeArithmeticGroupFunction.java index 69f78dbc2..210e2b4b5 100644 --- a/bundles/org.openhab.core/src/main/java/org/openhab/core/library/types/QuantityTypeArithmeticGroupFunction.java +++ b/bundles/org.openhab.core/src/main/java/org/openhab/core/library/types/QuantityTypeArithmeticGroupFunction.java @@ -13,12 +13,11 @@ package org.openhab.core.library.types; import java.math.BigDecimal; -import java.math.MathContext; -import java.util.ArrayList; import java.util.List; +import java.util.Objects; +import java.util.Optional; import java.util.Set; -import javax.measure.Quantity; import javax.measure.Unit; 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.GroupItem; import org.openhab.core.items.Item; -import org.openhab.core.library.items.NumberItem; import org.openhab.core.types.State; import org.openhab.core.types.UnDefType; 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. * * @author Henning Treu - Initial contribution + * @author Andrew Fiddian-Green - Normalise calculations based on the Unit of the GroupItem */ @NonNullByDefault public interface QuantityTypeArithmeticGroupFunction extends GroupFunction { abstract class DimensionalGroupFunction implements GroupFunction { - protected final Class> 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> dimension) { - this.dimension = dimension; + public DimensionalGroupFunction(Unit baseItemUnit) { + this.baseItemUnit = baseItemUnit; + this.systemUnit = baseItemUnit.getSystemUnit(); } @Override public @Nullable T getStateAs(@Nullable Set items, Class stateClass) { State state = calculate(items); if (stateClass.isInstance(state)) { + if (state instanceof QuantityType quantity) { + state = toInvertibleUnit(quantity, baseItemUnit); + } return stateClass.cast(state); } else { return null; @@ -62,102 +68,76 @@ public interface QuantityTypeArithmeticGroupFunction extends GroupFunction { return new State[0]; } - protected boolean isSameDimension(@Nullable Item item) { - if (item instanceof GroupItem groupItem) { - return isSameDimension(groupItem.getBaseItem()); + /** + * Convert the given {@link QuantityType} to an equivalent based on the given {@link Unit}. The conversion can + * be made to both inverted and non-inverted units, so invertible type conversions (e.g. Mirek <=> Kelvin) are + * supported. + *

+ * 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 toQuantityTypesOfUnit(Set 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 { - public Avg(Class> dimension) { - super(dimension); - } - - @Override - @SuppressWarnings({ "unchecked", "rawtypes" }) - public State calculate(@Nullable Set 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> dimension, @Nullable Item baseItem) { - super(dimension); - this.baseItem = baseItem; + public Avg(Unit baseItemUnit) { + super(baseItemUnit); } @Override @SuppressWarnings({ "unchecked", "rawtypes" }) public State calculate(@Nullable Set items) { if (items != null) { - List values = new ArrayList<>(); - Unit unit = null; - if (baseItem instanceof NumberItem numberItem) { - unit = numberItem.getUnit(); - } - 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); - } - + List systemUnitQuantities = toQuantityTypesOfUnit(items, systemUnit); + if (!systemUnitQuantities.isEmpty()) { + return systemUnitQuantities.stream().reduce(new QuantityType<>(0, systemUnit), QuantityType::add) + .divide(BigDecimal.valueOf(systemUnitQuantities.size())); } } 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 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 { - public Sum(Class> dimension) { - super(dimension); + public Sum(Unit baseItemUnit) { + super(baseItemUnit); } @Override - @SuppressWarnings({ "unchecked", "rawtypes" }) + @SuppressWarnings("unchecked") public State calculate(@Nullable Set items) { - if (items == null || items.isEmpty()) { - return UnDefType.UNDEF; - } - - QuantityType sum = null; - 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); - } - } - } + if (items != null) { + @SuppressWarnings("rawtypes") + List systemUnitQuantities = toQuantityTypesOfUnit(items, baseItemUnit); + if (!systemUnitQuantities.isEmpty()) { + return systemUnitQuantities.stream().reduce(new QuantityType<>(0, baseItemUnit), QuantityType::add); } } - - return sum != null ? sum : UnDefType.UNDEF; + return 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 { - public Min(Class> dimension) { - super(dimension); + public Min(Unit baseItemUnit) { + super(baseItemUnit); } @Override - @SuppressWarnings({ "unchecked", "rawtypes" }) public State calculate(@Nullable Set items) { - if (items == null || items.isEmpty()) { - return UnDefType.UNDEF; - } - - QuantityType min = null; - for (Item item : items) { - 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; - } - } + if (items != null) { + @SuppressWarnings({ "unchecked", "rawtypes" }) + Optional min = toQuantityTypesOfUnit(items, systemUnit).stream() + .min(QuantityType::compareTo); + if (min.isPresent()) { + return min.get(); } } - - return min != null ? min : UnDefType.UNDEF; + return 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 { - public Max(Class> dimension) { - super(dimension); + public Max(Unit targetUnit) { + super(targetUnit); } @Override - @SuppressWarnings({ "unchecked", "rawtypes" }) public State calculate(@Nullable Set items) { - if (items == null || items.isEmpty()) { - return UnDefType.UNDEF; - } - - QuantityType max = null; - for (Item item : items) { - 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; - } - } + if (items != null) { + @SuppressWarnings({ "unchecked", "rawtypes" }) + Optional max = toQuantityTypesOfUnit(items, systemUnit).stream() + .max(QuantityType::compareTo); + if (max.isPresent()) { + return max.get(); } } - - return max != null ? max : UnDefType.UNDEF; + return UnDefType.UNDEF; } } } diff --git a/bundles/org.openhab.core/src/test/java/org/openhab/core/library/types/QuantityTypeArithmeticGroupFunctionTest.java b/bundles/org.openhab.core/src/test/java/org/openhab/core/library/types/QuantityTypeArithmeticGroupFunctionTest.java index 71e2598ad..e67089eff 100644 --- a/bundles/org.openhab.core/src/test/java/org/openhab/core/library/types/QuantityTypeArithmeticGroupFunctionTest.java +++ b/bundles/org.openhab.core/src/test/java/org/openhab/core/library/types/QuantityTypeArithmeticGroupFunctionTest.java @@ -15,7 +15,7 @@ package org.openhab.core.library.types; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; 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 java.util.LinkedHashSet; @@ -46,6 +46,9 @@ import org.openhab.core.items.GroupItem; import org.openhab.core.items.Item; import org.openhab.core.library.CoreItemFactory; 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.UnDefType; import org.osgi.service.component.ComponentContext; @@ -85,7 +88,7 @@ public class QuantityTypeArithmeticGroupFunctionTest { items.add(createNumberItem("TestItem4", Temperature.class, UnDefType.UNDEF)); 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); 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("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); 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("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); 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("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); assertEquals(new QuantityType<>("200 °C"), state); @@ -163,10 +166,11 @@ public class QuantityTypeArithmeticGroupFunctionTest { items.add(createNumberItem("TestItem4", Temperature.class, UnDefType.UNDEF)); 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); - assertEquals(new QuantityType<>("55.33333333333333333333333333333334 °C"), state); + assertTrue(state instanceof QuantityType); + assertEquals(328.48, ((QuantityType) state).doubleValue(), 0.01); } @ParameterizedTest @@ -179,12 +183,13 @@ public class QuantityTypeArithmeticGroupFunctionTest { items.add(createNumberItem("TestItem2", Temperature.class, UnDefType.NULL)); 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); assertEquals(new QuantityType<>("23.54 °C"), state); } + @SuppressWarnings("rawtypes") static Stream medianTestSource() { return Stream.of( // arguments( // @@ -214,13 +219,14 @@ public class QuantityTypeArithmeticGroupFunctionTest { @ParameterizedTest @MethodSource("medianTestSource") + @SuppressWarnings({ "null", "rawtypes", "unchecked" }) public void testMedianFunctionQuantityType(List states, State expected) { AtomicInteger index = new AtomicInteger(1); Set items = states.stream() .map(state -> createNumberItem("TestItem" + index.getAndIncrement(), Temperature.class, state)) .collect(Collectors.toSet()); - GroupFunction function = new QuantityTypeArithmeticGroupFunction.Median(Temperature.class, null); + GroupFunction function = new QuantityTypeArithmeticGroupFunction.Median(SIUnits.CELSIUS); State state = function.calculate(items); assertEquals(state.getClass(), expected.getClass()); @@ -242,7 +248,7 @@ public class QuantityTypeArithmeticGroupFunctionTest { items.add(createNumberItem("TestItem4", Temperature.class, UnDefType.UNDEF)); 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); assertEquals(new QuantityType<>("300 °C"), state); @@ -260,7 +266,7 @@ public class QuantityTypeArithmeticGroupFunctionTest { items.add(createNumberItem("TestItem4", Temperature.class, UnDefType.UNDEF)); 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); assertEquals(new QuantityType<>("100 °C"), state); @@ -276,7 +282,7 @@ public class QuantityTypeArithmeticGroupFunctionTest { items.add(createNumberItem("TestItem2", Temperature.class, UnDefType.NULL)); 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); 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("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); 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("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); assertEquals(new QuantityType<>("48 %"), state); @@ -331,7 +337,7 @@ public class QuantityTypeArithmeticGroupFunctionTest { items.add(createNumberItem("TestItem4", Temperature.class, UnDefType.UNDEF)); 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); 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("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); 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(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); assertEquals(new QuantityType<>("10 W"), state); @@ -380,4 +386,190 @@ public class QuantityTypeArithmeticGroupFunctionTest { item.setState(state); return item; } + + @ParameterizedTest + @MethodSource("locales") + public void testSumFunctionQuantityTypeDifferentUnitsBaseKelvin(Locale locale) { + Locale.setDefault(locale); + + Set 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 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 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 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 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 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 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 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 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 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); + } }