Support mired units (#3108)

* Support mired units

Mired are fairly common to describe the color temperature of
lightbulbs (slightly less common than Kelvin), but are very
useful for various calculations when adjusting the color
temperature, as well as being necessary for various integerations
that require mired units.

This commit makes them a well-known unit (previously they were
still usable, using "MK^-1"), as well as making them easier to
work with on QuantityType. The hiccup is that Mireds aren't
technically a Temperature dimension, because they're a reciprocal.
So add a `inverse` method that delegates to javax.measure's
same method, and then use it as necessary when doing unit
conversions and comparisons. Unfortunately, because the
dimension changes, the return value of a conversion won't
necessarily be the same type, an additional method is added
for callers that are willing to handle the change in
dimension. This is implemented for all callers that can use
it in core.

Signed-off-by: Cody Cutrer <cody@cutrer.us>
This commit is contained in:
Cody Cutrer 2022-10-16 05:50:46 -06:00 committed by GitHub
parent 7f38d419c6
commit 3659542bae
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 91 additions and 17 deletions

View File

@ -135,7 +135,7 @@ public class SseItemStatesEventBuilder {
// state description will display the new unit:
Unit<?> patternUnit = UnitUtils.parseUnit(pattern);
if (patternUnit != null && !quantityState.getUnit().equals(patternUnit)) {
quantityState = quantityState.toUnit(patternUnit);
quantityState = quantityState.toInvertibleUnit(patternUnit);
}
if (quantityState != null) {

View File

@ -374,7 +374,7 @@ public class NumberExtensions {
public static BigDecimal numberToBigDecimal(Number number) {
if (number instanceof QuantityType) {
QuantityType<?> state = ((QuantityType<?>) number)
.toUnit(((QuantityType<?>) number).getUnit().getSystemUnit());
.toInvertibleUnit(((QuantityType<?>) number).getUnit().getSystemUnit());
if (state != null) {
return state.toBigDecimal();
}

View File

@ -66,7 +66,8 @@ public class SystemHysteresisStateProfile implements StateProfile {
}
this.lower = lowerParam;
final QuantityType<?> upperParam = getParam(context, UPPER_PARAM);
final QuantityType<?> convertedUpperParam = upperParam == null ? lower : upperParam.toUnit(lower.getUnit());
final QuantityType<?> convertedUpperParam = upperParam == null ? lower
: upperParam.toInvertibleUnit(lower.getUnit());
if (convertedUpperParam == null) {
throw new IllegalArgumentException(
String.format("Units of parameters '%s' and '%s' are not compatible: %s != %s", LOWER_PARAM,
@ -145,8 +146,8 @@ public class SystemHysteresisStateProfile implements StateProfile {
finalLower = new QuantityType<>(lower.toBigDecimal(), qtState.getUnit());
finalUpper = new QuantityType<>(upper.toBigDecimal(), qtState.getUnit());
} else {
finalLower = lower.toUnit(qtState.getUnit());
finalUpper = upper.toUnit(qtState.getUnit());
finalLower = lower.toInvertibleUnit(qtState.getUnit());
finalUpper = upper.toInvertibleUnit(qtState.getUnit());
if (finalLower == null || finalUpper == null) {
logger.warn(
"Cannot compare state '{}' to boundaries because units (lower={}, upper={}) do not match.",

View File

@ -69,7 +69,7 @@ public class SystemRangeStateProfile implements StateProfile {
if (upperParam == null) {
throw new IllegalArgumentException(String.format("Parameter '%s' is not a Number value.", UPPER_PARAM));
}
final QuantityType<?> convertedUpperParam = upperParam.toUnit(lower.getUnit());
final QuantityType<?> convertedUpperParam = upperParam.toInvertibleUnit(lower.getUnit());
if (convertedUpperParam == null) {
throw new IllegalArgumentException(
String.format("Units of parameters '%s' and '%s' are not compatible: %s != %s", LOWER_PARAM,
@ -153,8 +153,8 @@ public class SystemRangeStateProfile implements StateProfile {
finalLower = new QuantityType<>(lower.toBigDecimal(), qtState.getUnit());
finalUpper = new QuantityType<>(upper.toBigDecimal(), qtState.getUnit());
} else {
finalLower = lower.toUnit(qtState.getUnit());
finalUpper = upper.toUnit(qtState.getUnit());
finalLower = lower.toInvertibleUnit(qtState.getUnit());
finalUpper = upper.toInvertibleUnit(qtState.getUnit());
if (finalLower == null || finalUpper == null) {
logger.warn(
"Cannot compare state '{}' to boundaries because units (lower={}, upper={}) do not match.",

View File

@ -413,7 +413,7 @@ public class ItemUIRegistryImpl implements ItemUIRegistry {
// display the new unit:
Unit<?> patternUnit = UnitUtils.parseUnit(formatPattern);
if (patternUnit != null && !quantityState.getUnit().equals(patternUnit)) {
quantityState = quantityState.toUnit(patternUnit);
quantityState = quantityState.toInvertibleUnit(patternUnit);
}
// The widget may define its own unit in the widget label. Convert to this unit:
@ -462,7 +462,7 @@ public class ItemUIRegistryImpl implements ItemUIRegistry {
private QuantityType<?> convertStateToWidgetUnit(QuantityType<?> quantityState, Widget w) {
Unit<?> widgetUnit = UnitUtils.parseUnit(getFormatPattern(w.getLabel()));
if (widgetUnit != null && !widgetUnit.equals(quantityState.getUnit())) {
return Objects.requireNonNullElse(quantityState.toUnit(widgetUnit), quantityState);
return Objects.requireNonNullElse(quantityState.toInvertibleUnit(widgetUnit), quantityState);
}
return quantityState;

View File

@ -124,7 +124,7 @@ public class NumberItem extends GenericItem {
Unit<?> stateUnit = ((QuantityType<?>) state).getUnit();
if (itemUnit != null && (!stateUnit.getSystemUnit().equals(itemUnit.getSystemUnit())
|| UnitUtils.isDifferentMeasurementSystem(itemUnit, stateUnit))) {
QuantityType<?> convertedState = ((QuantityType<?>) state).toUnit(itemUnit);
QuantityType<?> convertedState = ((QuantityType<?>) state).toInvertibleUnit(itemUnit);
if (convertedState != null) {
super.setState(convertedState);
return;

View File

@ -219,9 +219,10 @@ public class QuantityType<T extends Quantity<T>> extends Number
return false;
}
QuantityType<?> other = (QuantityType<?>) obj;
if (!quantity.getUnit().isCompatible(other.quantity.getUnit())) {
if (!quantity.getUnit().isCompatible(other.quantity.getUnit())
&& !quantity.getUnit().inverse().isCompatible(other.quantity.getUnit())) {
return false;
} else if (compareTo((QuantityType<T>) other) != 0) {
} else if (internalCompareTo(other) != 0) {
return false;
}
@ -230,6 +231,10 @@ public class QuantityType<T extends Quantity<T>> extends Number
@Override
public int compareTo(QuantityType<T> o) {
return internalCompareTo((QuantityType<?>) o);
}
private int internalCompareTo(QuantityType<?> o) {
if (quantity.getUnit().isCompatible(o.quantity.getUnit())) {
QuantityType<T> v1 = this.toUnit(getUnit().getSystemUnit());
QuantityType<?> v2 = o.toUnit(o.getUnit().getSystemUnit());
@ -238,6 +243,8 @@ public class QuantityType<T extends Quantity<T>> extends Number
} else {
throw new IllegalArgumentException("Unable to convert to system unit during compare.");
}
} else if (quantity.getUnit().inverse().isCompatible(o.quantity.getUnit())) {
return inverse().internalCompareTo(o);
} else {
throw new IllegalArgumentException("Can not compare incompatible units.");
}
@ -255,7 +262,7 @@ public class QuantityType<T extends Quantity<T>> extends Number
* Convert this QuantityType to a new {@link QuantityType} using the given target unit.
*
* @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 a
* @return the new {@link QuantityType} in the given {@link Unit} or {@code null} in case of an error.
*/
@SuppressWarnings("unchecked")
public @Nullable QuantityType<T> toUnit(Unit<?> targetUnit) {
@ -283,6 +290,22 @@ public class QuantityType<T extends Quantity<T>> extends Number
return null;
}
/**
* Convert this QuantityType to a new {@link QuantityType} using the given target unit.
*
* Implicit conversions using inverse units are allowed (i.e. mired <=> Kelvin). This may
* change the dimension.
*
* @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 erro.
*/
public @Nullable QuantityType<?> toInvertibleUnit(Unit<?> targetUnit) {
if (!targetUnit.equals(getUnit()) && getUnit().inverse().isCompatible(targetUnit)) {
return inverse().toUnit(targetUnit);
}
return toUnit(targetUnit);
}
public BigDecimal toBigDecimal() {
return new BigDecimal(quantity.getValue().toString());
}
@ -490,4 +513,13 @@ public class QuantityType<T extends Quantity<T>> extends Number
.get();
return new QuantityType<T>(sum);
}
/**
* Return the reciprocal of this QuantityType.
*
* @return a QuantityType with both the value and unit reciprocated
*/
public QuantityType<?> inverse() {
return new QuantityType<>(this.quantity.inverse());
}
}

View File

@ -92,7 +92,7 @@ public interface QuantityTypeArithmeticGroupFunction extends GroupFunction {
sum = itemState; // initialise the sum from the first item
count++;
} else {
itemState = itemState.toUnit(sum.getUnit());
itemState = itemState.toInvertibleUnit(sum.getUnit());
if (itemState != null) {
sum = sum.add(itemState);
count++;

View File

@ -171,6 +171,7 @@ public final class Units extends CustomUnits {
MultiplyConverter.ofRational(BigInteger.valueOf(1852), BigInteger.valueOf(1000))));
public static final Unit<SolidAngle> STERADIAN = addUnit(tech.units.indriya.unit.Units.STERADIAN);
public static final Unit<Temperature> KELVIN = addUnit(tech.units.indriya.unit.Units.KELVIN);
public static final Unit<?> MIRED = addUnit(MetricPrefix.MEGA(tech.units.indriya.unit.Units.KELVIN).inverse());
public static final Unit<Time> SECOND = addUnit(tech.units.indriya.unit.Units.SECOND);
public static final Unit<Time> MINUTE = addUnit(tech.units.indriya.unit.Units.MINUTE);
public static final Unit<Time> HOUR = addUnit(tech.units.indriya.unit.Units.HOUR);
@ -266,6 +267,7 @@ public final class Units extends CustomUnits {
SimpleUnitFormat.getInstance().label(MILLIAMPERE_HOUR, "mAh");
SimpleUnitFormat.getInstance().label(MILLIBAR, "mbar");
SimpleUnitFormat.getInstance().label(MILLIMETRE_OF_MERCURY, MILLIMETRE_OF_MERCURY.getSymbol());
SimpleUnitFormat.getInstance().label(MIRED, "mired");
SimpleUnitFormat.getInstance().label(PARTS_PER_BILLION, "ppb");
SimpleUnitFormat.getInstance().label(PARTS_PER_MILLION, "ppm");
SimpleUnitFormat.getInstance().label(PETABYTE, "PB");

View File

@ -16,6 +16,7 @@ import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.lang.reflect.WildcardType;
import java.util.Arrays;
import java.util.Collection;
import java.util.Set;
@ -116,8 +117,11 @@ public class UnitUtils {
if (field.getType().isAssignableFrom(Unit.class) && Modifier.isStatic(field.getModifiers())) {
Type genericType = field.getGenericType();
if (genericType instanceof ParameterizedType) {
String dimension = ((Class<?>) ((ParameterizedType) genericType).getActualTypeArguments()[0])
.getSimpleName();
Type typeParam = ((ParameterizedType) genericType).getActualTypeArguments()[0];
if (typeParam instanceof WildcardType) {
continue;
}
String dimension = ((Class<?>) typeParam).getSimpleName();
try {
Unit<?> systemUnit = (Unit<?>) field.get(null);
if (systemUnit == null) {

View File

@ -140,4 +140,28 @@ public class NumberItemTest {
assertThat(item.getStateDescription().getPattern(), is("%.1f " + UnitUtils.UNIT_PLACEHOLDER));
}
@SuppressWarnings("null")
@Test
public void testMiredToKelvin() {
NumberItem item = new NumberItem("Number:Temperature", ITEM_NAME);
when(stateDescriptionServiceMock.getStateDescription(ITEM_NAME, null)).thenReturn(
StateDescriptionFragmentBuilder.create().withPattern("%.0f K").build().toStateDescription());
item.setStateDescriptionService(stateDescriptionServiceMock);
item.setState(new QuantityType<>("370 mired"));
assertThat(item.getState().format("%.0f K"), is("2703 K"));
}
@SuppressWarnings("null")
@Test
public void testKelvinToMired() {
NumberItem item = new NumberItem("Number:Temperature", ITEM_NAME);
when(stateDescriptionServiceMock.getStateDescription(ITEM_NAME, null)).thenReturn(
StateDescriptionFragmentBuilder.create().withPattern("%.0f mired").build().toStateDescription());
item.setStateDescriptionService(stateDescriptionServiceMock);
item.setState(new QuantityType<>("2700 K"));
assertThat(item.getState().format("%.0f mired"), is("370 mired"));
}
}

View File

@ -472,4 +472,15 @@ public class QuantityTypeTest {
QuantityType<DataTransferRate> octets = gsm2G.toUnit(MetricPrefix.KILO(Units.OCTET).divide(Units.SECOND));
assertEquals(14375, octets.intValue());
}
@Test
public void testMireds() {
QuantityType<Temperature> colorTemp = new QuantityType<>("2700 K");
QuantityType<?> mireds = colorTemp.toInvertibleUnit(Units.MIRED);
assertEquals(370, mireds.intValue());
assertThat(colorTemp.equals(mireds), is(true));
assertThat(mireds.equals(colorTemp), is(true));
QuantityType<?> andBack = mireds.toInvertibleUnit(Units.KELVIN);
assertEquals(2700, andBack.intValue());
}
}