Skip to content

Commit

Permalink
convert units for bounds when compiling full state descriptions
Browse files Browse the repository at this point in the history
Signed-off-by: Cody Cutrer <cody@cutrer.us>
  • Loading branch information
ccutrer committed Oct 25, 2022
1 parent e86f388 commit 815b0d6
Show file tree
Hide file tree
Showing 4 changed files with 156 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -30,6 +36,8 @@
*/
@NonNullByDefault
public class StateDescriptionFragmentImpl implements StateDescriptionFragment {
private static final Pattern PATTERN_PRECISION_PATTERN = Pattern.compile("%[-#+ ,\\(<0-9$]*.(\\d+)[eEf]");
private static final String INTEGER_FORMAT_TYPE = "d";

private class StateDescriptionImpl extends StateDescription {
StateDescriptionImpl(@Nullable BigDecimal minimum, @Nullable BigDecimal maximum, @Nullable BigDecimal step,
Expand Down Expand Up @@ -182,13 +190,90 @@ public void setOptions(List<StateOption> 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()) {
String precision = m.group(1);
if (precision != null) {
scale = Integer.valueOf(precision);
} else if (m.group(2).equals(INTEGER_FORMAT_TYPE)) {
scale = 0;
}
}

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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<T> toUnitRelative(Unit<T> targetUnit) {
if (targetUnit.equals(getUnit())) {
return this;
}
if (!quantity.getUnit().isCompatible(targetUnit)) {
return null;
}
Quantity<?> result = quantity.to(targetUnit);

return new QuantityType<T>(result.getValue(), (Unit<T>) targetUnit);
}

public BigDecimal toBigDecimal() {
return new BigDecimal(quantity.getValue().toString());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -483,4 +484,11 @@ public void testMireds() {
QuantityType<?> andBack = mireds.toInvertibleUnit(Units.KELVIN);
assertEquals(2700, andBack.intValue());
}

@Test
public void testRelativeConversion() {
QuantityType<Temperature> c = new QuantityType("1 °C");
QuantityType<Temperature> f = c.toUnitRelative(ImperialUnits.FAHRENHEIT);
assertEquals(1.8, f.doubleValue());
}
}

0 comments on commit 815b0d6

Please sign in to comment.