Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add default units for all dimensions #3143

Merged
merged 3 commits into from
Nov 19, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,39 @@

import javax.measure.Quantity;
import javax.measure.Unit;
import javax.measure.quantity.Acceleration;
import javax.measure.quantity.AmountOfSubstance;
import javax.measure.quantity.Angle;
import javax.measure.quantity.Area;
import javax.measure.quantity.CatalyticActivity;
import javax.measure.quantity.Dimensionless;
import javax.measure.quantity.ElectricCapacitance;
import javax.measure.quantity.ElectricCharge;
import javax.measure.quantity.ElectricConductance;
import javax.measure.quantity.ElectricCurrent;
import javax.measure.quantity.ElectricInductance;
import javax.measure.quantity.ElectricPotential;
import javax.measure.quantity.ElectricResistance;
import javax.measure.quantity.Energy;
import javax.measure.quantity.Force;
import javax.measure.quantity.Frequency;
import javax.measure.quantity.Illuminance;
import javax.measure.quantity.Length;
import javax.measure.quantity.LuminousFlux;
import javax.measure.quantity.LuminousIntensity;
import javax.measure.quantity.MagneticFlux;
import javax.measure.quantity.MagneticFluxDensity;
import javax.measure.quantity.Mass;
import javax.measure.quantity.Power;
import javax.measure.quantity.Pressure;
import javax.measure.quantity.RadiationDoseAbsorbed;
import javax.measure.quantity.RadiationDoseEffective;
import javax.measure.quantity.Radioactivity;
import javax.measure.quantity.SolidAngle;
import javax.measure.quantity.Speed;
import javax.measure.quantity.Temperature;
import javax.measure.quantity.Time;
import javax.measure.quantity.Volume;
import javax.measure.spi.SystemOfUnits;

import org.eclipse.jdt.annotation.NonNullByDefault;
Expand All @@ -40,7 +67,13 @@
import org.openhab.core.i18n.TimeZoneProvider;
import org.openhab.core.i18n.TranslationProvider;
import org.openhab.core.i18n.UnitProvider;
import org.openhab.core.library.dimension.ArealDensity;
import org.openhab.core.library.dimension.DataAmount;
import org.openhab.core.library.dimension.DataTransferRate;
import org.openhab.core.library.dimension.Density;
import org.openhab.core.library.dimension.ElectricConductivity;
import org.openhab.core.library.dimension.Intensity;
import org.openhab.core.library.dimension.VolumetricFlowRate;
import org.openhab.core.library.types.PointType;
import org.openhab.core.library.unit.ImperialUnits;
import org.openhab.core.library.unit.SIUnits;
Expand Down Expand Up @@ -107,7 +140,7 @@ public class I18nProviderImpl
private @Nullable ZoneId timeZone;

// UnitProvider
private static final String MEASUREMENT_SYSTEM = "measurementSystem";
static final String MEASUREMENT_SYSTEM = "measurementSystem";
private @Nullable SystemOfUnits measurementSystem;
private final Map<Class<? extends Quantity<?>>, Map<SystemOfUnits, Unit<? extends Quantity<?>>>> dimensionMap = new HashMap<>();

Expand All @@ -122,7 +155,7 @@ public I18nProviderImpl(ComponentContext componentContext) {
}

@Deactivate
protected void deactivate(ComponentContext componentContext) {
protected void deactivate() {
this.resourceBundleTracker.close();
}

Expand Down Expand Up @@ -319,8 +352,8 @@ public Locale getLocale() {
return text;
}

@SuppressWarnings("unchecked")
@Override
@SuppressWarnings("unchecked")
public <T extends Quantity<T>> @Nullable Unit<T> getUnit(@Nullable Class<T> dimension) {
Map<SystemOfUnits, Unit<? extends Quantity<?>>> map = dimensionMap.get(dimension);
if (map == null) {
Expand All @@ -344,39 +377,53 @@ public SystemOfUnits getMeasurementSystem() {
}

private void initDimensionMap() {
Map<SystemOfUnits, Unit<? extends Quantity<?>>> temperatureMap = new HashMap<>();
temperatureMap.put(SIUnits.getInstance(), SIUnits.CELSIUS);
temperatureMap.put(ImperialUnits.getInstance(), ImperialUnits.FAHRENHEIT);
dimensionMap.put(Temperature.class, temperatureMap);

Map<SystemOfUnits, Unit<? extends Quantity<?>>> pressureMap = new HashMap<>();
pressureMap.put(SIUnits.getInstance(), HECTO(SIUnits.PASCAL));
pressureMap.put(ImperialUnits.getInstance(), ImperialUnits.INCH_OF_MERCURY);
dimensionMap.put(Pressure.class, pressureMap);

Map<SystemOfUnits, Unit<? extends Quantity<?>>> speedMap = new HashMap<>();
speedMap.put(SIUnits.getInstance(), SIUnits.KILOMETRE_PER_HOUR);
speedMap.put(ImperialUnits.getInstance(), ImperialUnits.MILES_PER_HOUR);
dimensionMap.put(Speed.class, speedMap);

Map<SystemOfUnits, Unit<? extends Quantity<?>>> lengthMap = new HashMap<>();
lengthMap.put(SIUnits.getInstance(), SIUnits.METRE);
lengthMap.put(ImperialUnits.getInstance(), ImperialUnits.INCH);
dimensionMap.put(Length.class, lengthMap);

Map<SystemOfUnits, Unit<? extends Quantity<?>>> intensityMap = new HashMap<>();
intensityMap.put(SIUnits.getInstance(), Units.IRRADIANCE);
intensityMap.put(ImperialUnits.getInstance(), Units.IRRADIANCE);
dimensionMap.put(Intensity.class, intensityMap);

Map<SystemOfUnits, Unit<? extends Quantity<?>>> percentMap = new HashMap<>();
percentMap.put(SIUnits.getInstance(), Units.ONE);
percentMap.put(ImperialUnits.getInstance(), Units.ONE);
dimensionMap.put(Dimensionless.class, percentMap);

Map<SystemOfUnits, Unit<? extends Quantity<?>>> angleMap = new HashMap<>();
angleMap.put(SIUnits.getInstance(), Units.DEGREE_ANGLE);
angleMap.put(ImperialUnits.getInstance(), Units.DEGREE_ANGLE);
dimensionMap.put(Angle.class, angleMap);
addDefaultUnit(Acceleration.class, Units.METRE_PER_SQUARE_SECOND);
addDefaultUnit(AmountOfSubstance.class, Units.MOLE);
addDefaultUnit(Angle.class, Units.DEGREE_ANGLE, Units.DEGREE_ANGLE);
addDefaultUnit(Area.class, SIUnits.SQUARE_METRE, ImperialUnits.SQUARE_FOOT);
addDefaultUnit(ArealDensity.class, Units.DOBSON_UNIT);
addDefaultUnit(CatalyticActivity.class, Units.KATAL);
addDefaultUnit(DataAmount.class, Units.BYTE);
addDefaultUnit(DataTransferRate.class, Units.MEGABIT_PER_SECOND);
addDefaultUnit(Density.class, Units.KILOGRAM_PER_CUBICMETRE);
addDefaultUnit(Dimensionless.class, Units.ONE);
addDefaultUnit(ElectricCapacitance.class, Units.FARAD);
addDefaultUnit(ElectricCharge.class, Units.COULOMB);
addDefaultUnit(ElectricConductance.class, Units.SIEMENS);
addDefaultUnit(ElectricConductivity.class, Units.SIEMENS_PER_METRE);
addDefaultUnit(ElectricCurrent.class, Units.AMPERE);
addDefaultUnit(ElectricInductance.class, Units.HENRY);
addDefaultUnit(ElectricPotential.class, Units.VOLT);
addDefaultUnit(ElectricResistance.class, Units.OHM);
addDefaultUnit(Energy.class, Units.JOULE);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could it be this change causes all kind of channels that are kWh to display in J, which is not expected. Is there a fix possible?

Copy link
Member

@Hilbrand Hilbrand Dec 5, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could this mean %unit% should not be used in binding xml definitions anymore? Or is it a bug in core where if might be that if specified as %unit% the default takes precedence over the unit in the quantity type? (I haven't looked into the source code yet)

Copy link
Member Author

@J-N-K J-N-K Dec 5, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could be. However, there is no way to avoid that and it was always the case with the dimensions that had a default unit before.

The state description takes precedence over the default (and that works), so we could check if we need to adjust %unit%. I'll have a look.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@J-N-K The difference to other dimensions is that this here now is a breaking change - many of my items now show J instead of KWh and even a couple of my rules are impacted. I assume @Hilbrand (and many others) have a similar situation.
I wonder whether we should maybe define kWh as a default for Energy - after all, this is the unit that is almost always used in a smart home context, while J is rather a very rare exception
Wdyt?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@kaikreuzer for the channels with %unit% my issue was solved in #3201.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While the J might be the standard unit I think in a home automation context kWhshould absolutely be the default.
99% of the time users want to compare/measure energy consumption of devices and that's typically done in kWh

addDefaultUnit(Force.class, Units.NEWTON);
addDefaultUnit(Frequency.class, Units.HERTZ);
addDefaultUnit(Illuminance.class, Units.LUX);
addDefaultUnit(Intensity.class, Units.IRRADIANCE);
addDefaultUnit(Length.class, SIUnits.METRE, ImperialUnits.FOOT);
addDefaultUnit(LuminousFlux.class, Units.LUMEN);
addDefaultUnit(LuminousIntensity.class, Units.CANDELA);
addDefaultUnit(MagneticFlux.class, Units.WEBER);
addDefaultUnit(MagneticFluxDensity.class, Units.TESLA);
addDefaultUnit(Mass.class, SIUnits.KILOGRAM, ImperialUnits.POUND);
addDefaultUnit(Power.class, Units.WATT);
addDefaultUnit(Pressure.class, HECTO(SIUnits.PASCAL), ImperialUnits.INCH_OF_MERCURY);
addDefaultUnit(RadiationDoseAbsorbed.class, Units.GRAY);
addDefaultUnit(RadiationDoseEffective.class, Units.SIEVERT);
addDefaultUnit(Radioactivity.class, Units.BECQUEREL);
addDefaultUnit(SolidAngle.class, Units.STERADIAN);
addDefaultUnit(Speed.class, SIUnits.KILOMETRE_PER_HOUR, ImperialUnits.MILES_PER_HOUR);
addDefaultUnit(Temperature.class, SIUnits.CELSIUS, ImperialUnits.FAHRENHEIT);
addDefaultUnit(Time.class, Units.SECOND);
addDefaultUnit(Volume.class, SIUnits.CUBIC_METRE, ImperialUnits.GALLON_LIQUID_US);
addDefaultUnit(VolumetricFlowRate.class, Units.LITRE_PER_MINUTE, ImperialUnits.GALLON_PER_MINUTE);
}

private <T extends Quantity<T>> void addDefaultUnit(Class<T> dimension, Unit<T> siUnit, Unit<T> imperialUnit) {
dimensionMap.put(dimension, Map.of(SIUnits.getInstance(), siUnit, ImperialUnits.getInstance(), imperialUnit));
}

private <T extends Quantity<T>> void addDefaultUnit(Class<T> dimension, Unit<T> unit) {
dimensionMap.put(dimension, Map.of(SIUnits.getInstance(), unit, ImperialUnits.getInstance(), unit));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import javax.measure.Unit;
import javax.measure.quantity.Area;
import javax.measure.quantity.Length;
import javax.measure.quantity.Mass;
import javax.measure.quantity.Pressure;
import javax.measure.quantity.Speed;
import javax.measure.quantity.Temperature;
Expand Down Expand Up @@ -45,6 +46,8 @@ public final class ImperialUnits extends CustomUnits {

private static final ImperialUnits INSTANCE = new ImperialUnits();

public static final Unit<Mass> POUND = addUnit(new TransformedUnit<>("lb", Units.GRAM,
MultiplyConverter.ofRational(BigInteger.valueOf(45359237), BigInteger.valueOf(100000))));
/** Additionally defined units to be used in openHAB **/
public static final Unit<Pressure> INCH_OF_MERCURY = addUnit(new TransformedUnit<>("inHg", Units.PASCAL,
MultiplyConverter.ofRational(BigInteger.valueOf(3386388), BigInteger.valueOf(1000))));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,23 +14,37 @@

import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.notNullValue;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.mockito.Mockito.when;
import static org.openhab.core.internal.i18n.I18nProviderImpl.*;

import java.time.ZoneId;
import java.util.Collection;
import java.util.Dictionary;
import java.util.Hashtable;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Stream;

import javax.measure.Quantity;
import javax.measure.Unit;
import javax.measure.spi.SystemOfUnits;

import org.eclipse.jdt.annotation.NonNullByDefault;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.openhab.core.library.types.PointType;
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.util.UnitUtils;
import org.osgi.framework.Bundle;
import org.osgi.framework.BundleContext;
import org.osgi.service.component.ComponentContext;
Expand Down Expand Up @@ -158,6 +172,23 @@ public void assertThatConfigurationChangeWorks() {
assertThat(setLocale.getVariant(), is(VARIANT_RU));
}

@ParameterizedTest
@MethodSource("getAllDimensions")
@SuppressWarnings("unchecked")
public <T extends Quantity<T>> void assertThatUnitProviderIsComplete(String dimensionName) {
Class<? extends Quantity<?>> dimension = UnitUtils.parseDimension(dimensionName);
assertThat(dimension, is(notNullValue()));

Unit<?> defaultUnit = i18nProviderImpl.getUnit((Class<T>) dimension);
assertThat(dimensionName + " has no default unit", defaultUnit, notNullValue());
}

private static Stream<String> getAllDimensions() {
return Stream.of(SIUnits.getInstance(), Units.getInstance(), ImperialUnits.getInstance())
.map(SystemOfUnits::getUnits).flatMap(Collection::stream) //
.map(UnitUtils::getDimensionName).filter(Objects::nonNull).map(Objects::requireNonNull).distinct();
}

private Dictionary<String, Object> buildInitialConfig() {
Dictionary<String, Object> conf = new Hashtable<>();
conf.put(LOCATION, LOCATION_ZERO);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import javax.measure.quantity.Dimensionless;
import javax.measure.quantity.Energy;
import javax.measure.quantity.Length;
import javax.measure.quantity.Mass;
import javax.measure.quantity.Power;
import javax.measure.quantity.Pressure;
import javax.measure.quantity.Speed;
Expand Down Expand Up @@ -54,6 +55,14 @@ private static <Q extends Quantity<?>> Matcher<? super Q> isQuantityEquals(Q qua
return new QuantityEquals(quantity);
}

@Test
public void pound2KilogramConversion() {
Quantity<Mass> lb = Quantities.getQuantity(BigDecimal.ONE, ImperialUnits.POUND);

assertThat(lb.to(SIUnits.GRAM),
isQuantityEquals(Quantities.getQuantity(new BigDecimal("453.59237"), SIUnits.GRAM)));
}

@Test
public void testInHg2PascalConversion() {
Quantity<Pressure> inHg = Quantities.getQuantity(BigDecimal.ONE, ImperialUnits.INCH_OF_MERCURY);
Expand Down