diff --git a/bundles/org.openhab.core/src/main/java/org/openhab/core/internal/types/StateDescriptionFragmentImpl.java b/bundles/org.openhab.core/src/main/java/org/openhab/core/internal/types/StateDescriptionFragmentImpl.java index 170a9875c95..5b501b0f006 100644 --- a/bundles/org.openhab.core/src/main/java/org/openhab/core/internal/types/StateDescriptionFragmentImpl.java +++ b/bundles/org.openhab.core/src/main/java/org/openhab/core/internal/types/StateDescriptionFragmentImpl.java @@ -13,15 +13,21 @@ package org.openhab.core.internal.types; import java.math.BigDecimal; +import java.math.RoundingMode; import java.util.Collections; import java.util.List; +import java.util.regex.Pattern; + +import javax.measure.Unit; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; +import org.openhab.core.library.types.QuantityType; import org.openhab.core.types.StateDescription; import org.openhab.core.types.StateDescriptionFragment; import org.openhab.core.types.StateDescriptionFragmentBuilder; import org.openhab.core.types.StateOption; +import org.openhab.core.types.util.UnitUtils; /** * Data holder for StateDescriptionFragment creation. @@ -30,6 +36,7 @@ */ @NonNullByDefault public class StateDescriptionFragmentImpl implements StateDescriptionFragment { + private static final Pattern PATTERN_PRECISION_PATTERN = Pattern.compile("%[-#+ ,\\(<0-9$]*.(\\d+)[eEf]"); private class StateDescriptionImpl extends StateDescription { StateDescriptionImpl(@Nullable BigDecimal minimum, @Nullable BigDecimal maximum, @Nullable BigDecimal step, @@ -182,13 +189,85 @@ public void setOptions(List options) { * @return this instance with the fields merged. */ public StateDescriptionFragment merge(StateDescriptionFragment fragment) { + String newPattern = fragment.getPattern(); + // Do unit conversions if possible. + // Example: + // The GenericItemProvider sets a pattern of ° F, but no min, max, or step. + // The ChannelStateDescriptionProvider sets a pattern of ° C, min of 0, max of 100, step of 0.5 + // The latter is lower priority, so gets merged into the former. + // We want to construct a final description with a pattern of ° F, min of 32, max of 212, and step of 0.9 + // + // In other words, we keep the user's overridden unit, but convert the bounds provided by the + // channel (that is describing the bounds in terms of its unit) to the user's preferred unit. + boolean skipStep = false; + if (pattern != null && newPattern != null) { + Unit oldUnit = UnitUtils.parseUnit(pattern); + Unit newUnit = UnitUtils.parseUnit(newPattern); + if (oldUnit != null && newUnit != null && !oldUnit.equals(newUnit) + && (oldUnit.isCompatible(newUnit) || oldUnit.inverse().isCompatible(newUnit))) { + BigDecimal newValue; + // when inverting, min and max will swap + if (oldUnit.inverse().isCompatible(newUnit)) { + // It's highly likely that an invertible unit conversion will end up with a very long decimal + // So use the format to round min/max to what we're going to display. + Integer scale = null; + var m = PATTERN_PRECISION_PATTERN.matcher(pattern); + if (m.find()) { + scale = Integer.valueOf(m.group(1)); + } + + if (minimum == null && (newValue = fragment.getMaximum()) != null) { + minimum = new QuantityType(newValue, newUnit).toInvertibleUnit(oldUnit).toBigDecimal(); + if (minimum.scale() > 0) { + minimum = minimum.stripTrailingZeros(); + } + if (scale != null && minimum.scale() > scale) { + minimum = minimum.setScale(scale, RoundingMode.FLOOR); + } + } + if (maximum == null && (newValue = fragment.getMinimum()) != null) { + maximum = new QuantityType(newValue, newUnit).toInvertibleUnit(oldUnit).toBigDecimal(); + if (maximum.scale() > 0) { + maximum = maximum.stripTrailingZeros(); + } + if (scale != null && maximum.scale() > scale) { + maximum = maximum.setScale(scale, RoundingMode.CEILING); + } + } + + // Invertible units cannot have a linear relationship, so just leave step blank. + // Make sure it doesn't get overwritten below with a non-sensical value + skipStep = true; + } else { + if (minimum == null && (newValue = fragment.getMinimum()) != null) { + minimum = new QuantityType(newValue, newUnit).toInvertibleUnit(oldUnit).toBigDecimal(); + if (minimum.scale() > 0) { + minimum = minimum.stripTrailingZeros(); + } + } + if (maximum == null && (newValue = fragment.getMaximum()) != null) { + maximum = new QuantityType(newValue, newUnit).toInvertibleUnit(oldUnit).toBigDecimal(); + if (maximum.scale() > 0) { + maximum = maximum.stripTrailingZeros(); + } + } + if (step == null && (newValue = fragment.getStep()) != null) { + step = new QuantityType(newValue, newUnit).toUnitRelative(oldUnit).toBigDecimal(); + if (step.scale() > 0) { + step = step.stripTrailingZeros(); + } + } + } + } + } + if (minimum == null) { minimum = fragment.getMinimum(); } if (maximum == null) { maximum = fragment.getMaximum(); } - if (step == null) { + if (step == null && !skipStep) { step = fragment.getStep(); } if (pattern == null) { diff --git a/bundles/org.openhab.core/src/main/java/org/openhab/core/library/types/QuantityType.java b/bundles/org.openhab.core/src/main/java/org/openhab/core/library/types/QuantityType.java index 22869b661a3..abfe136782d 100644 --- a/bundles/org.openhab.core/src/main/java/org/openhab/core/library/types/QuantityType.java +++ b/bundles/org.openhab.core/src/main/java/org/openhab/core/library/types/QuantityType.java @@ -306,6 +306,31 @@ public Dimension getDimension() { return toUnit(targetUnit); } + /** + * Convert this QuantityType to a new {@link QuantityType} using the given target unit. + * + * Similar to {@link toUnit}, except that it treats the values as relative instead of absolute. + * This means that any offsets in the conversion of absolute values are ignored. + * This is useful when your quantity represents a delta, and not necessarily a measured + * value itself. For example, 32 °F, when converted with toUnit to Celsius, it will become 0 °C. + * But when converted with toUnitRelative, it will become 17.8 °C. + * + * @param targetUnit the unit to which this {@link QuantityType} will be converted to. + * @return the new {@link QuantityType} in the given {@link Unit} or {@code null} in case of an error. + */ + @SuppressWarnings("unchecked") + public @Nullable QuantityType toUnitRelative(Unit targetUnit) { + if (targetUnit.equals(getUnit())) { + return this; + } + if (!quantity.getUnit().isCompatible(targetUnit)) { + return null; + } + Quantity result = quantity.to(targetUnit); + + return new QuantityType(result.getValue(), (Unit) targetUnit); + } + public BigDecimal toBigDecimal() { return new BigDecimal(quantity.getValue().toString()); } diff --git a/bundles/org.openhab.core/src/test/java/org/openhab/core/internal/types/StateDescriptionFragmentImplTest.java b/bundles/org.openhab.core/src/test/java/org/openhab/core/internal/types/StateDescriptionFragmentImplTest.java index 7c9a0a96d65..fbcd6b82f03 100644 --- a/bundles/org.openhab.core/src/test/java/org/openhab/core/internal/types/StateDescriptionFragmentImplTest.java +++ b/bundles/org.openhab.core/src/test/java/org/openhab/core/internal/types/StateDescriptionFragmentImplTest.java @@ -15,6 +15,7 @@ import static org.hamcrest.CoreMatchers.not; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.nullValue; import java.math.BigDecimal; import java.util.List; @@ -74,6 +75,42 @@ public void mergeFragment() { assertThat(fragment.getOptions(), is(not(sourceWithOptions.getOptions()))); } + @Test + public void mergeFragmentWithUnits() { + StateDescriptionFragmentImpl userFragment = new StateDescriptionFragmentImpl(); + userFragment.setPattern("%0.0f °F"); + + StateDescriptionFragmentImpl channelFragment = new StateDescriptionFragmentImpl(); + channelFragment.setPattern("%0.1f °C"); + channelFragment.setMinimum(BigDecimal.ZERO); + channelFragment.setMaximum(new BigDecimal(100)); + channelFragment.setStep(new BigDecimal(0.5)); + + userFragment.merge(channelFragment); + assertThat(userFragment.getPattern(), is("%0.0f °F")); + assertThat(userFragment.getMinimum(), is(new BigDecimal(32))); + assertThat(userFragment.getMaximum(), is(new BigDecimal(212))); + assertThat(userFragment.getStep(), is(new BigDecimal("0.9"))); + } + + @Test + public void mergeFragmentWithInvertibleUnits() { + StateDescriptionFragmentImpl userFragment = new StateDescriptionFragmentImpl(); + userFragment.setPattern("%0.0f K"); + + StateDescriptionFragmentImpl channelFragment = new StateDescriptionFragmentImpl(); + channelFragment.setPattern("%0.0f mired"); + channelFragment.setMinimum(new BigDecimal(153)); + channelFragment.setMaximum(new BigDecimal(400)); + channelFragment.setStep(BigDecimal.ONE); + + userFragment.merge(channelFragment); + assertThat(userFragment.getPattern(), is("%0.0f K")); + assertThat(userFragment.getMinimum(), is(new BigDecimal(2500))); + assertThat(userFragment.getMaximum(), is(new BigDecimal(6536))); + assertThat(userFragment.getStep(), is(nullValue())); + } + @Test @SuppressWarnings("null") public void toStateDescription() { diff --git a/bundles/org.openhab.core/src/test/java/org/openhab/core/library/types/QuantityTypeTest.java b/bundles/org.openhab.core/src/test/java/org/openhab/core/library/types/QuantityTypeTest.java index 756770db4d4..5a34f0a9dba 100644 --- a/bundles/org.openhab.core/src/test/java/org/openhab/core/library/types/QuantityTypeTest.java +++ b/bundles/org.openhab.core/src/test/java/org/openhab/core/library/types/QuantityTypeTest.java @@ -40,6 +40,7 @@ import org.openhab.core.library.dimension.Density; import org.openhab.core.library.dimension.Intensity; import org.openhab.core.library.unit.BinaryPrefix; +import org.openhab.core.library.unit.ImperialUnits; import org.openhab.core.library.unit.MetricPrefix; import org.openhab.core.library.unit.SIUnits; import org.openhab.core.library.unit.Units; @@ -483,4 +484,11 @@ public void testMireds() { QuantityType andBack = mireds.toInvertibleUnit(Units.KELVIN); assertEquals(2700, andBack.intValue()); } + + @Test + public void testRelativeConversion() { + QuantityType c = new QuantityType("1 °C"); + QuantityType f = c.toUnitRelative(ImperialUnits.FAHRENHEIT); + assertEquals(1.8, f.doubleValue()); + } }