From da2e997e1bb571ca3dd5ebdeeb2f031328e6aa63 Mon Sep 17 00:00:00 2001 From: "Jan N. Klug" Date: Mon, 27 Mar 2023 19:54:48 +0200 Subject: [PATCH 1/4] Add currency as unit Signed-off-by: Jan N. Klug --- .../core/internal/i18n/I18nProviderImpl.java | 41 ++- .../library/unit/CurrencyConverter.java | 85 +++++ .../library/unit/CurrencyService.java | 86 +++++ .../library/unit/UnitInitializer.java | 2 + .../core/library/dimension/Currency.java | 26 ++ .../core/library/dimension/EnergyPrice.java | 26 ++ .../core/library/unit/CurrencyProvider.java | 66 ++++ .../core/library/unit/CurrencyUnit.java | 303 ++++++++++++++++++ .../core/library/unit/CurrencyUnits.java | 75 +++++ .../openhab/core/types/util/UnitUtils.java | 3 +- .../src/main/resources/OH-INF/config/i18n.xml | 4 + .../internal/i18n/I18nProviderImplTest.java | 8 +- .../core/library/unit/CurrencyUnitTest.java | 111 +++++++ 13 files changed, 832 insertions(+), 4 deletions(-) create mode 100644 bundles/org.openhab.core/src/main/java/org/openhab/core/internal/library/unit/CurrencyConverter.java create mode 100644 bundles/org.openhab.core/src/main/java/org/openhab/core/internal/library/unit/CurrencyService.java create mode 100644 bundles/org.openhab.core/src/main/java/org/openhab/core/library/dimension/Currency.java create mode 100644 bundles/org.openhab.core/src/main/java/org/openhab/core/library/dimension/EnergyPrice.java create mode 100644 bundles/org.openhab.core/src/main/java/org/openhab/core/library/unit/CurrencyProvider.java create mode 100644 bundles/org.openhab.core/src/main/java/org/openhab/core/library/unit/CurrencyUnit.java create mode 100644 bundles/org.openhab.core/src/main/java/org/openhab/core/library/unit/CurrencyUnits.java create mode 100644 bundles/org.openhab.core/src/test/java/org/openhab/core/library/unit/CurrencyUnitTest.java diff --git a/bundles/org.openhab.core/src/main/java/org/openhab/core/internal/i18n/I18nProviderImpl.java b/bundles/org.openhab.core/src/main/java/org/openhab/core/internal/i18n/I18nProviderImpl.java index abe76195a22..126a04e1cfa 100644 --- a/bundles/org.openhab.core/src/main/java/org/openhab/core/internal/i18n/I18nProviderImpl.java +++ b/bundles/org.openhab.core/src/main/java/org/openhab/core/internal/i18n/I18nProviderImpl.java @@ -14,6 +14,7 @@ import static org.openhab.core.library.unit.MetricPrefix.HECTO; +import java.math.BigDecimal; import java.text.MessageFormat; import java.time.DateTimeException; import java.time.ZoneId; @@ -24,6 +25,7 @@ import java.util.Objects; import java.util.ResourceBundle; import java.util.Set; +import java.util.function.Function; import javax.measure.Quantity; import javax.measure.Unit; @@ -70,14 +72,19 @@ 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.Currency; 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.EnergyPrice; import org.openhab.core.library.dimension.Intensity; import org.openhab.core.library.dimension.RadiationSpecificActivity; import org.openhab.core.library.dimension.VolumetricFlowRate; import org.openhab.core.library.types.PointType; +import org.openhab.core.library.unit.CurrencyProvider; +import org.openhab.core.library.unit.CurrencyUnit; +import org.openhab.core.library.unit.CurrencyUnits; import org.openhab.core.library.unit.ImperialUnits; import org.openhab.core.library.unit.SIUnits; import org.openhab.core.library.unit.Units; @@ -117,8 +124,8 @@ "service.config.category=system", // "service.config.description.uri=system:i18n" }) @NonNullByDefault -public class I18nProviderImpl - implements TranslationProvider, LocaleProvider, LocationProvider, TimeZoneProvider, UnitProvider { +public class I18nProviderImpl implements TranslationProvider, LocaleProvider, LocationProvider, TimeZoneProvider, + UnitProvider, CurrencyProvider { private final Logger logger = LoggerFactory.getLogger(I18nProviderImpl.class); @@ -126,10 +133,12 @@ public class I18nProviderImpl // LocaleProvider public static final String LANGUAGE = "language"; + public static final String BASE_CURRENCY = "baseCurrency"; public static final String SCRIPT = "script"; public static final String REGION = "region"; public static final String VARIANT = "variant"; private @Nullable Locale locale; + private @Nullable String currencyCode; // TranslationProvider private final ResourceBundleTracker resourceBundleTracker; @@ -165,6 +174,7 @@ protected void deactivate() { @Modified protected synchronized void modified(Map config) { final String language = toStringOrNull(config.get(LANGUAGE)); + this.currencyCode = toStringOrNull(config.get(BASE_CURRENCY)); final String script = toStringOrNull(config.get(SCRIPT)); final String region = toStringOrNull(config.get(REGION)); final String variant = toStringOrNull(config.get(VARIANT)); @@ -395,6 +405,7 @@ public static Map>, Map>, Map> void addDefaultUnit( Class dimension, Unit unit) { dimensionMap.put(dimension, Map.of(SIUnits.getInstance(), unit, ImperialUnits.getInstance(), unit)); } + + @Override + public Unit getBaseCurrency() { + String currencyCode = this.currencyCode; + if (currencyCode == null && locale != null) { + currencyCode = java.util.Currency.getInstance(locale).getCurrencyCode(); + } + if (currencyCode != null) { + // either the currency was set or determined from the locale + String symbol = java.util.Currency.getInstance(currencyCode).getSymbol(); + return new CurrencyUnit(currencyCode, symbol); + } else { + return new CurrencyUnit("DEF", null); + } + } + + @Override + public Collection> getCurrencies() { + return Set.of(); + } + + @Override + public Function, @Nullable BigDecimal> getExchangeRateFunction() { + return unit -> null; + } } diff --git a/bundles/org.openhab.core/src/main/java/org/openhab/core/internal/library/unit/CurrencyConverter.java b/bundles/org.openhab.core/src/main/java/org/openhab/core/internal/library/unit/CurrencyConverter.java new file mode 100644 index 00000000000..8564ae3b16a --- /dev/null +++ b/bundles/org.openhab.core/src/main/java/org/openhab/core/internal/library/unit/CurrencyConverter.java @@ -0,0 +1,85 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.core.internal.library.unit; + +import java.math.BigDecimal; +import java.math.MathContext; +import java.util.Objects; + +import javax.measure.UnitConverter; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +import tech.units.indriya.function.AbstractConverter; + +/** + * The {@link CurrencyConverter} implements an {@link UnitConverter} for + * {@link org.openhab.core.library.unit.CurrencyUnit} + * + * @author Jan N. Klug - Initial contribution + */ +@NonNullByDefault +public class CurrencyConverter extends AbstractConverter { + + private final BigDecimal factor; + + public CurrencyConverter(BigDecimal factor) { + this.factor = factor; + } + + @Override + public boolean equals(@Nullable Object cvtr) { + return cvtr instanceof CurrencyConverter currencyConverter && factor.equals(currencyConverter.factor); + } + + @Override + public int hashCode() { + return Objects.hashCode(factor); + } + + @Override + protected @Nullable String transformationLiteral() { + return null; + } + + @Override + protected AbstractConverter inverseWhenNotIdentity() { + return new CurrencyConverter(BigDecimal.ONE.divide(factor, MathContext.DECIMAL128)); + } + + @Override + protected boolean canReduceWith(@Nullable AbstractConverter that) { + return false; + } + + @Override + protected Number convertWhenNotIdentity(@NonNullByDefault({}) Number value) { + return new BigDecimal(value.toString()).multiply(factor, MathContext.DECIMAL128); + } + + @Override + public int compareTo(@Nullable UnitConverter o) { + return o instanceof CurrencyConverter currencyConverter ? factor.compareTo(currencyConverter.factor) : -1; + } + + @Override + public boolean isIdentity() { + return false; + } + + @Override + public boolean isLinear() { + return true; + } +} diff --git a/bundles/org.openhab.core/src/main/java/org/openhab/core/internal/library/unit/CurrencyService.java b/bundles/org.openhab.core/src/main/java/org/openhab/core/internal/library/unit/CurrencyService.java new file mode 100644 index 00000000000..2e777fb8d71 --- /dev/null +++ b/bundles/org.openhab.core/src/main/java/org/openhab/core/internal/library/unit/CurrencyService.java @@ -0,0 +1,86 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.core.internal.library.unit; + +import static org.openhab.core.library.unit.CurrencyUnits.BASE_CURRENCY; + +import java.math.BigDecimal; +import java.util.Set; +import java.util.concurrent.CopyOnWriteArraySet; +import java.util.function.Function; + +import javax.measure.Unit; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.core.library.dimension.Currency; +import org.openhab.core.library.unit.CurrencyProvider; +import org.openhab.core.library.unit.CurrencyUnit; +import org.openhab.core.library.unit.CurrencyUnits; +import org.osgi.framework.Constants; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Reference; +import org.osgi.service.component.annotations.ReferenceCardinality; +import org.osgi.service.component.annotations.ReferencePolicy; + +import tech.units.indriya.format.SimpleUnitFormat; + +/** + * The {@link CurrencyService} is allows to register and switch {@link CurrencyProvider}s and provides exchange rates + * for currencies + * + * @author Jan N. Klug - Initial contribution + */ +@Component +@NonNullByDefault +public class CurrencyService { + + public static Function, @Nullable BigDecimal> FACTOR_FCN = unit -> null; + + private final Set currencyProviders = new CopyOnWriteArraySet<>(); + + @Activate + public CurrencyService( + @Reference(target = "(" + Constants.SERVICE_PID + "=org.openhab.i18n)") CurrencyProvider currencyProvider) { + currencyProviders.add(currencyProvider); + enableProvider(currencyProvider); + } + + @Reference(cardinality = ReferenceCardinality.MULTIPLE, policy = ReferencePolicy.DYNAMIC) + public void addCurrencyProvider(CurrencyProvider currencyProvider) { + currencyProviders.add(currencyProvider); + } + + public void removeCurrencyProvider(CurrencyProvider currencyProvider) { + currencyProviders.remove(currencyProvider); + } + + private synchronized void enableProvider(CurrencyProvider currencyProvider) { + FACTOR_FCN = currencyProvider.getExchangeRateFunction(); + ((CurrencyUnit) BASE_CURRENCY).setSymbol(currencyProvider.getBaseCurrency().getSymbol()); + ((CurrencyUnit) BASE_CURRENCY).setName(currencyProvider.getBaseCurrency().getName()); + SimpleUnitFormat.getInstance().label(BASE_CURRENCY, currencyProvider.getBaseCurrency().getSymbol()); + currencyProvider.getCurrencies().forEach(CurrencyUnits::addUnit); + } + + /** + * Get the exchange rate for a given currency to the system's base unit + * + * @param currency the currency + * @return the exchange rate + */ + public static @Nullable BigDecimal getExchangeRate(Unit currency) { + return FACTOR_FCN.apply(currency); + } +} diff --git a/bundles/org.openhab.core/src/main/java/org/openhab/core/internal/library/unit/UnitInitializer.java b/bundles/org.openhab.core/src/main/java/org/openhab/core/internal/library/unit/UnitInitializer.java index af1ec55eaba..27faca5dba8 100644 --- a/bundles/org.openhab.core/src/main/java/org/openhab/core/internal/library/unit/UnitInitializer.java +++ b/bundles/org.openhab.core/src/main/java/org/openhab/core/internal/library/unit/UnitInitializer.java @@ -13,6 +13,7 @@ package org.openhab.core.internal.library.unit; import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.core.library.unit.CurrencyUnits; import org.openhab.core.library.unit.ImperialUnits; import org.openhab.core.library.unit.SIUnits; import org.openhab.core.library.unit.Units; @@ -29,6 +30,7 @@ public class UnitInitializer { Units.getInstance(); SIUnits.getInstance(); ImperialUnits.getInstance(); + CurrencyUnits.getInstance(); } public static void init() { diff --git a/bundles/org.openhab.core/src/main/java/org/openhab/core/library/dimension/Currency.java b/bundles/org.openhab.core/src/main/java/org/openhab/core/library/dimension/Currency.java new file mode 100644 index 00000000000..3b46a1119e2 --- /dev/null +++ b/bundles/org.openhab.core/src/main/java/org/openhab/core/library/dimension/Currency.java @@ -0,0 +1,26 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.core.library.dimension; + +import javax.measure.Quantity; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link Currency} defines the dimension for currencies + * + * @author Jan N. Klug - Initial contribution + */ +@NonNullByDefault +public interface Currency extends Quantity { +} diff --git a/bundles/org.openhab.core/src/main/java/org/openhab/core/library/dimension/EnergyPrice.java b/bundles/org.openhab.core/src/main/java/org/openhab/core/library/dimension/EnergyPrice.java new file mode 100644 index 00000000000..e4b5ca15ec6 --- /dev/null +++ b/bundles/org.openhab.core/src/main/java/org/openhab/core/library/dimension/EnergyPrice.java @@ -0,0 +1,26 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.core.library.dimension; + +import javax.measure.Quantity; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link EnergyPrice} defines the dimension for prices + * + * @author Jan N. Klug - Initial contribution + */ +@NonNullByDefault +public interface EnergyPrice extends Quantity { +} diff --git a/bundles/org.openhab.core/src/main/java/org/openhab/core/library/unit/CurrencyProvider.java b/bundles/org.openhab.core/src/main/java/org/openhab/core/library/unit/CurrencyProvider.java new file mode 100644 index 00000000000..5674c91d165 --- /dev/null +++ b/bundles/org.openhab.core/src/main/java/org/openhab/core/library/unit/CurrencyProvider.java @@ -0,0 +1,66 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.core.library.unit; + +import java.math.BigDecimal; +import java.util.Collection; +import java.util.function.Function; + +import javax.measure.Unit; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.core.library.dimension.Currency; + +/** + * The {@link CurrencyProvider} can be implemented by services that supply currencies and their + * + * @author Jan N. Klug - Initial contribution + */ +@NonNullByDefault +public interface CurrencyProvider { + + /** + * Get the name of this {@link CurrencyProvider} + * + * @return the name, defaults to the class name + */ + default String getName() { + return getClass().getName(); + } + + /** + * Get the base currency from this provider + *

+ * This currency is used as base for calculating exchange rates. + * + * @return the base currency of this provider + */ + Unit getBaseCurrency(); + + /** + * Get all units that are supported by this provider + * + * @return a {@link Collection} of {@link Unit}s + */ + Collection> getCurrencies(); + + /** + * Get a {@link Function} that supplies exchanges rates for currencies supported by this provider + *

+ * This needs to be dynamic because in most cases exchange rates are not constant over time. + * + * @return the function + */ + Function, @Nullable BigDecimal> getExchangeRateFunction(); +} diff --git a/bundles/org.openhab.core/src/main/java/org/openhab/core/library/unit/CurrencyUnit.java b/bundles/org.openhab.core/src/main/java/org/openhab/core/library/unit/CurrencyUnit.java new file mode 100644 index 00000000000..29248d58a00 --- /dev/null +++ b/bundles/org.openhab.core/src/main/java/org/openhab/core/library/unit/CurrencyUnit.java @@ -0,0 +1,303 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.core.library.unit; + +import static org.eclipse.jdt.annotation.DefaultLocation.FIELD; +import static org.eclipse.jdt.annotation.DefaultLocation.PARAMETER; +import static org.eclipse.jdt.annotation.DefaultLocation.RETURN_TYPE; +import static org.eclipse.jdt.annotation.DefaultLocation.TYPE_BOUND; +import static org.openhab.core.library.unit.CurrencyUnits.BASE_CURRENCY; +import static tech.units.indriya.AbstractUnit.ONE; + +import java.io.Serializable; +import java.math.BigDecimal; +import java.math.MathContext; +import java.util.Map; +import java.util.Objects; + +import javax.measure.Dimension; +import javax.measure.IncommensurableException; +import javax.measure.Prefix; +import javax.measure.Quantity; +import javax.measure.UnconvertibleException; +import javax.measure.Unit; +import javax.measure.UnitConverter; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.core.internal.library.unit.CurrencyConverter; +import org.openhab.core.internal.library.unit.CurrencyService; +import org.openhab.core.library.dimension.Currency; + +import tech.units.indriya.format.SimpleUnitFormat; +import tech.units.indriya.function.AbstractConverter; +import tech.units.indriya.function.AddConverter; +import tech.units.indriya.function.Calculus; +import tech.units.indriya.function.MultiplyConverter; +import tech.units.indriya.function.RationalNumber; +import tech.units.indriya.unit.AlternateUnit; +import tech.units.indriya.unit.ProductUnit; +import tech.units.indriya.unit.TransformedUnit; +import tech.units.indriya.unit.UnitDimension; +import tech.uom.lib.common.function.Nameable; +import tech.uom.lib.common.function.PrefixOperator; +import tech.uom.lib.common.function.SymbolSupplier; + +/** + * The {@link CurrencyUnit} is a UoM compatible unit for currencies. + * + * @author Jan N. Klug - Initial contribution + */ +@NonNullByDefault({ PARAMETER, RETURN_TYPE, FIELD, TYPE_BOUND }) +public final class CurrencyUnit implements Unit, Comparable>, PrefixOperator, + Nameable, Serializable, SymbolSupplier { + + private static final long serialVersionUID = -1L; + private static final Dimension DIMENSION = UnitDimension.parse('$'); + private String name; + private @Nullable String symbol; + + /** + * Create a new Currency + * + * @param name 3-letter ISO-Code + * @param symbol an (optional) symbol + * @throws IllegalArgumentException if name is not valid + */ + public CurrencyUnit(String name, @Nullable String symbol) throws IllegalArgumentException { + if (name.length() != 3) { + throw new IllegalArgumentException("Only three characters allowed for currency name"); + } + this.symbol = symbol; + this.name = name; + } + + public UnitConverter getSystemConverter() { + return AbstractConverter.IDENTITY; + } + + @Override + public String toString() { + return SimpleUnitFormat.getInstance().format(this); + } + + @Override + public final Unit getSystemUnit() { + return this; + } + + @Override + public final boolean isCompatible(@NonNullByDefault({}) Unit that) { + return DIMENSION.equals(that.getDimension()); + } + + @SuppressWarnings("unchecked") + @Override + public @NonNullByDefault({}) > Unit asType(@NonNullByDefault({}) Class type) { + Dimension typeDimension = UnitDimension.of(type); + if (typeDimension != null && !typeDimension.equals(this.getDimension())) { + throw new ClassCastException("The unit: " + this + " is not compatible with quantities of type " + type); + } + return (Unit) this; + } + + @Override + public @NonNullByDefault({}) Map, Integer> getBaseUnits() { + return Map.of(); + } + + @Override + public Dimension getDimension() { + return DIMENSION; + } + + public void setName(String name) { + this.name = name; + } + + public String getName() { + return name; + } + + public @Nullable String getSymbol() { + return symbol; + } + + public void setSymbol(@Nullable String s) { + this.symbol = s; + } + + @Override + public final UnitConverter getConverterTo(@NonNullByDefault({}) Unit that) throws UnconvertibleException { + return internalGetConverterTo(that); + } + + @SuppressWarnings("unchecked") + @Override + public final @NonNullByDefault({}) UnitConverter getConverterToAny(@NonNullByDefault({}) Unit that) + throws IncommensurableException, UnconvertibleException { + if (!isCompatible(that)) { + throw new IncommensurableException(this + " is not compatible with " + that); + } + return internalGetConverterTo((Unit) that); + } + + @Override + public final Unit alternate(@NonNullByDefault({}) String newSymbol) { + return new AlternateUnit<>(this, newSymbol); + } + + @Override + public final Unit transform(@NonNullByDefault({}) UnitConverter operation) { + return operation.isIdentity() ? this : new TransformedUnit<>(null, this, this, operation); + } + + @Override + public Unit shift(@NonNullByDefault({}) Number offset) { + return Calculus.currentNumberSystem().isZero(offset) ? this : transform(new AddConverter(offset)); + } + + @Override + public Unit multiply(@NonNullByDefault({}) Number factor) { + return Calculus.currentNumberSystem().isOne(factor) ? this : transform(MultiplyConverter.of(factor)); + } + + @Override + public Unit shift(double offset) { + return shift(RationalNumber.of(offset)); + } + + @Override + public Unit multiply(double multiplier) { + return multiply(RationalNumber.of(multiplier)); + } + + @Override + public Unit divide(double divisor) { + return divide(RationalNumber.of(divisor)); + } + + private UnitConverter internalGetConverterTo(Unit that) throws UnconvertibleException { + if (this.equals(that)) { + return AbstractConverter.IDENTITY; + } + if (BASE_CURRENCY.equals(this)) { + BigDecimal factor = CurrencyService.FACTOR_FCN.apply(that); + if (factor != null) { + return new CurrencyConverter(factor); + } + } else if (BASE_CURRENCY.equals(that)) { + BigDecimal factor = CurrencyService.FACTOR_FCN.apply(this); + if (factor != null) { + return new CurrencyConverter(factor).inverse(); + } + } else { + BigDecimal f1 = CurrencyService.FACTOR_FCN.apply(this); + BigDecimal f2 = CurrencyService.FACTOR_FCN.apply(that); + + if (f1 != null && f2 != null) { + return new CurrencyConverter(f2.divide(f1, MathContext.DECIMAL128)); + } + } + throw new UnconvertibleException( + "Could not get factor for converting " + this.getName() + " to " + that.getName()); + } + + public final Unit multiply(@NonNullByDefault({}) Unit that) { + return that.equals(ONE) ? this : ProductUnit.ofProduct(this, that); + } + + @Override + public final Unit inverse() { + return ProductUnit.ofQuotient(ONE, this); + } + + @Override + public final Unit divide(@NonNullByDefault({}) Number divisor) { + if (Calculus.currentNumberSystem().isOne(divisor)) { + return this; + } + BigDecimal factor = BigDecimal.ONE.divide(new BigDecimal(divisor.toString()), MathContext.DECIMAL128); + return transform(MultiplyConverter.of(factor)); + } + + @Override + public final Unit divide(@NonNullByDefault({}) Unit that) { + return this.multiply(that.inverse()); + } + + @Override + public final Unit root(int n) { + if (n > 0) { + return ProductUnit.ofRoot(this, n); + } else if (n == 0) { + throw new ArithmeticException("Root's order of zero"); + } else { + // n < 0 + return ONE.divide(this.root(-n)); + } + } + + @Override + public Unit pow(int n) { + if (n > 0) { + return this.multiply(this.pow(n - 1)); + } else if (n == 0) { + return ONE; + } else { + // n < 0 + return ONE.divide(this.pow(-n)); + } + } + + @Override + public Unit prefix(@NonNullByDefault({}) Prefix prefix) { + return this.transform(MultiplyConverter.ofPrefix(prefix)); + } + + @Override + public int compareTo(Unit that) { + int nameCompare = getName().compareTo(that.getName()); + if (nameCompare != 0) { + return nameCompare; + } + String thatSymbol = that.getSymbol(); + if (symbol != null && thatSymbol != null) { + return symbol.compareTo(thatSymbol); + } else if (symbol != null) { + return 1; + } else if (thatSymbol != null) { + return -1; + } + + return 0; + } + + @Override + public boolean isEquivalentTo(@NonNullByDefault({}) Unit that) { + return this.getConverterTo(that).isIdentity(); + } + + @Override + public boolean equals(@Nullable Object obj) { + if (obj instanceof CurrencyUnit that) { + return (name.equals(that.name) && Objects.equals(symbol, that.symbol)); + } + return false; + } + + @Override + public int hashCode() { + return Objects.hash(name, symbol); + } +} diff --git a/bundles/org.openhab.core/src/main/java/org/openhab/core/library/unit/CurrencyUnits.java b/bundles/org.openhab.core/src/main/java/org/openhab/core/library/unit/CurrencyUnits.java new file mode 100644 index 00000000000..7738dfbb182 --- /dev/null +++ b/bundles/org.openhab.core/src/main/java/org/openhab/core/library/unit/CurrencyUnits.java @@ -0,0 +1,75 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.core.library.unit; + +import java.util.Objects; + +import javax.measure.Unit; +import javax.measure.spi.SystemOfUnits; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.core.library.dimension.Currency; +import org.openhab.core.library.dimension.EnergyPrice; + +import tech.units.indriya.AbstractSystemOfUnits; +import tech.units.indriya.format.SimpleUnitFormat; +import tech.units.indriya.unit.ProductUnit; + +/** + * The {@link CurrencyUnits} defines the UoM system for handling currencies + * + * @author Jan N. Klug - Initial contribution + */ +@NonNullByDefault +public final class CurrencyUnits extends AbstractSystemOfUnits { + + private static final CurrencyUnits INSTANCE = new CurrencyUnits(); + + public static final Unit BASE_CURRENCY = new CurrencyUnit("DEF", null); + public static Unit BASE_ENERGY_PRICE = new ProductUnit<>(BASE_CURRENCY.divide(Units.KILOWATT_HOUR)); + + static { + addUnit(BASE_CURRENCY); + INSTANCE.units.add(BASE_ENERGY_PRICE); + } + + @Override + public String getName() { + return CurrencyUnits.class.getSimpleName(); + } + + public static void addUnit(Unit unit) { + if (!(unit instanceof CurrencyUnit)) { + throw new IllegalArgumentException("Not an instance of CurrencyUnit"); + } + INSTANCE.units.add(unit); + SimpleUnitFormat.getInstance().label(unit, unit.getName()); + String symbol = unit.getSymbol(); + if (symbol != null && !symbol.isBlank()) { + SimpleUnitFormat.getInstance().alias(unit, symbol); + } + } + + public void removeUnit(Unit unit) { + INSTANCE.units.remove(unit); + // TODO: remove labels/aliases, depends on unreleased indriya feature + } + + public static SystemOfUnits getInstance() { + return Objects.requireNonNull(INSTANCE); + } + + public static Unit createCurrency(String symbol, String name) { + return new CurrencyUnit(symbol, name); + } +} diff --git a/bundles/org.openhab.core/src/main/java/org/openhab/core/types/util/UnitUtils.java b/bundles/org.openhab.core/src/main/java/org/openhab/core/types/util/UnitUtils.java index 64bde36dd02..a9d3db958cb 100644 --- a/bundles/org.openhab.core/src/main/java/org/openhab/core/types/util/UnitUtils.java +++ b/bundles/org.openhab.core/src/main/java/org/openhab/core/types/util/UnitUtils.java @@ -32,6 +32,7 @@ import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.openhab.core.internal.library.unit.UnitInitializer; +import org.openhab.core.library.unit.CurrencyUnits; import org.openhab.core.library.unit.ImperialUnits; import org.openhab.core.library.unit.SIUnits; import org.openhab.core.library.unit.Units; @@ -60,7 +61,7 @@ public class UnitUtils { private static final String FRAMEWORK_DIMENSION_PREFIX = "org.openhab.core.library.dimension."; private static final Collection> ALL_SYSTEM_OF_UNITS = List.of(SIUnits.class, - ImperialUnits.class, Units.class, tech.units.indriya.unit.Units.class); + ImperialUnits.class, Units.class, tech.units.indriya.unit.Units.class, CurrencyUnits.class); static { UnitInitializer.init(); diff --git a/bundles/org.openhab.core/src/main/resources/OH-INF/config/i18n.xml b/bundles/org.openhab.core/src/main/resources/OH-INF/config/i18n.xml index 06f73e0d4c1..257efd0e7cd 100644 --- a/bundles/org.openhab.core/src/main/resources/OH-INF/config/i18n.xml +++ b/bundles/org.openhab.core/src/main/resources/OH-INF/config/i18n.xml @@ -33,6 +33,10 @@ Coordinates as <latitude>,<longitude>[<altitude>]
Example: "52.5200066,13.4049540" (Berlin)]]>
+ + + The base currency of this system (determined by locale if not set) + The measurement system is used for unit conversion. diff --git a/bundles/org.openhab.core/src/test/java/org/openhab/core/internal/i18n/I18nProviderImplTest.java b/bundles/org.openhab.core/src/test/java/org/openhab/core/internal/i18n/I18nProviderImplTest.java index 8efc084e296..9eaaca56c17 100644 --- a/bundles/org.openhab.core/src/test/java/org/openhab/core/internal/i18n/I18nProviderImplTest.java +++ b/bundles/org.openhab.core/src/test/java/org/openhab/core/internal/i18n/I18nProviderImplTest.java @@ -41,6 +41,8 @@ import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.openhab.core.library.types.PointType; +import org.openhab.core.library.unit.CurrencyUnit; +import org.openhab.core.library.unit.CurrencyUnits; import org.openhab.core.library.unit.ImperialUnits; import org.openhab.core.library.unit.SIUnits; import org.openhab.core.library.unit.Units; @@ -108,6 +110,7 @@ public void assertThatConfigurationWasSet() { assertThat(setLocale.getCountry(), is(initialConfig.get(REGION))); assertThat(setLocale.getVariant(), is(initialConfig.get(VARIANT))); assertThat(i18nProviderImpl.getTimeZone(), is(ZoneId.of(TIMEZONE_GMT9))); + assertThat(i18nProviderImpl.getBaseCurrency(), is(new CurrencyUnit("EUR", "€"))); } @Test @@ -170,6 +173,7 @@ public void assertThatConfigurationChangeWorks() { assertThat(setLocale.getScript(), is(SCRIPT_RU)); assertThat(setLocale.getCountry(), is(REGION_RU)); assertThat(setLocale.getVariant(), is(VARIANT_RU)); + assertThat(i18nProviderImpl.getBaseCurrency(), is(new CurrencyUnit("RUB", "RUB"))); } @ParameterizedTest @@ -183,7 +187,9 @@ public > void assertThatUnitProviderIsComplete(String dime } private static Stream getAllDimensions() { - return Stream.of(SIUnits.getInstance(), Units.getInstance(), ImperialUnits.getInstance()) + return Stream + .of(SIUnits.getInstance(), Units.getInstance(), ImperialUnits.getInstance(), + CurrencyUnits.getInstance()) .map(SystemOfUnits::getUnits).flatMap(Collection::stream) // .map(UnitUtils::getDimensionName).filter(Objects::nonNull).map(Objects::requireNonNull).distinct(); } diff --git a/bundles/org.openhab.core/src/test/java/org/openhab/core/library/unit/CurrencyUnitTest.java b/bundles/org.openhab.core/src/test/java/org/openhab/core/library/unit/CurrencyUnitTest.java new file mode 100644 index 00000000000..2bd40530908 --- /dev/null +++ b/bundles/org.openhab.core/src/test/java/org/openhab/core/library/unit/CurrencyUnitTest.java @@ -0,0 +1,111 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.core.library.unit; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.*; + +import java.math.BigDecimal; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.function.Function; + +import javax.measure.Unit; +import javax.measure.quantity.Energy; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.junit.jupiter.api.Test; +import org.openhab.core.internal.library.unit.CurrencyService; +import org.openhab.core.library.dimension.Currency; +import org.openhab.core.library.dimension.EnergyPrice; +import org.openhab.core.library.types.QuantityType; + +/** + * The {@link CurrencyUnitTest} contains tests for the currency units + * + * @author Jan N. Klug - Initial contribution + */ +@NonNullByDefault +public class CurrencyUnitTest { + + @SuppressWarnings("unused") + private final CurrencyService currencyService = new CurrencyService(new TestCurrencyProvider()); + + @Test + public void testSimpleConversion() { + QuantityType amount = new QuantityType<>("7.45 DKK"); + QuantityType convertedAmount = amount.toUnit("€"); + + assertThat(convertedAmount, is(notNullValue())); + assertThat(convertedAmount.getUnit(), is(TestCurrencyProvider.EUR)); + assertThat(convertedAmount.doubleValue(), closeTo(1, 1e-4)); + } + + @Test + public void testInverseConversion() { + QuantityType amount = new QuantityType<>("1.00 €"); + QuantityType convertedAmount = amount.toUnit("DKK"); + + assertThat(convertedAmount, is(notNullValue())); + assertThat(convertedAmount.getUnit(), is(TestCurrencyProvider.DKK)); + assertThat(convertedAmount.doubleValue(), closeTo(7.45, 1e-4)); + } + + @Test + public void testComplexConversion() { + QuantityType amount = new QuantityType<>("13 DKK"); + QuantityType convertedAmount = amount.toUnit("$"); + + assertThat(convertedAmount, is(notNullValue())); + assertThat(convertedAmount.getUnit(), is(TestCurrencyProvider.USD)); + assertThat(convertedAmount.doubleValue(), closeTo(1.8845, 1e-4)); + } + + @Test + public void testPriceCalculation() { + QuantityType unitPrice = new QuantityType<>("0.25 €/kWh"); + QuantityType amount = new QuantityType<>("5 kWh"); + QuantityType price = amount.multiply(unitPrice); + + assertThat(price, is(notNullValue())); + assertThat(price.getUnit(), is(TestCurrencyProvider.EUR)); + assertThat(price.doubleValue(), closeTo(1.25, 1E-4)); + } + + private static class TestCurrencyProvider implements CurrencyProvider { + public static final Unit EUR = new CurrencyUnit("EUR", "€"); + public static final Unit DKK = new CurrencyUnit("DKK", null); + public static final Unit USD = new CurrencyUnit("USD", "$"); + + private static final Map, BigDecimal> FACTORS = Map.of( // + DKK, new BigDecimal("7.45"), // + USD, new BigDecimal("1.08")); + + @Override + public Unit getBaseCurrency() { + return EUR; + } + + @Override + public Collection> getCurrencies() { + return List.of(DKK, USD); + } + + @Override + public Function, @Nullable BigDecimal> getExchangeRateFunction() { + return FACTORS::get; + } + } +} From 0238ea3f5262b99c0fec4885ce90c9118538ebf1 Mon Sep 17 00:00:00 2001 From: "Jan N. Klug" Date: Mon, 28 Aug 2023 20:54:17 +0200 Subject: [PATCH 2/4] further work Signed-off-by: Jan N. Klug --- .../CurrencyServiceConfigOptionProvider.java | 65 ++++++++++++ .../i18n/I18nConfigOptionsProvider.java | 2 +- .../core/internal/i18n/I18nProviderImpl.java | 35 +------ .../library/unit/CurrencyService.java | 98 +++++++++++++++---- .../library/unit/FixedCurrencyProvider.java | 76 ++++++++++++++ .../unit/LocaleBasedCurrencyProvider.java | 71 ++++++++++++++ .../core/library/unit/CurrencyProvider.java | 6 +- .../core/library/unit/CurrencyUnit.java | 7 +- .../core/library/unit/CurrencyUnits.java | 21 +++- .../src/main/resources/OH-INF/config/i18n.xml | 4 - .../main/resources/OH-INF/config/units.xml | 20 ++++ .../internal/i18n/I18nProviderImplTest.java | 3 - .../core/library/unit/CurrencyUnitTest.java | 17 +++- 13 files changed, 352 insertions(+), 73 deletions(-) create mode 100644 bundles/org.openhab.core.config.core/src/main/java/org/openhab/core/config/core/CurrencyServiceConfigOptionProvider.java create mode 100644 bundles/org.openhab.core/src/main/java/org/openhab/core/internal/library/unit/FixedCurrencyProvider.java create mode 100644 bundles/org.openhab.core/src/main/java/org/openhab/core/internal/library/unit/LocaleBasedCurrencyProvider.java create mode 100644 bundles/org.openhab.core/src/main/resources/OH-INF/config/units.xml diff --git a/bundles/org.openhab.core.config.core/src/main/java/org/openhab/core/config/core/CurrencyServiceConfigOptionProvider.java b/bundles/org.openhab.core.config.core/src/main/java/org/openhab/core/config/core/CurrencyServiceConfigOptionProvider.java new file mode 100644 index 00000000000..45cd7a2fcf7 --- /dev/null +++ b/bundles/org.openhab.core.config.core/src/main/java/org/openhab/core/config/core/CurrencyServiceConfigOptionProvider.java @@ -0,0 +1,65 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.core.config.core; + +import java.net.URI; +import java.util.Collection; +import java.util.List; +import java.util.Locale; +import java.util.concurrent.CopyOnWriteArrayList; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.core.library.unit.CurrencyProvider; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Reference; +import org.osgi.service.component.annotations.ReferenceCardinality; +import org.osgi.service.component.annotations.ReferencePolicy; + +/** + * The {@link CurrencyServiceConfigOptionProvider} is an implementation of {@link ConfigOptionProvider} for the + * available currency providers. + * + * @author Jan N. Klug - Initial contribution + */ +@Component(service = ConfigOptionProvider.class) +@NonNullByDefault +public class CurrencyServiceConfigOptionProvider implements ConfigOptionProvider { + + private final List currencyProviders = new CopyOnWriteArrayList<>(); + + @Reference(cardinality = ReferenceCardinality.MULTIPLE, policy = ReferencePolicy.DYNAMIC) + public void addCurrencyProvider(CurrencyProvider currencyProvider) { + currencyProviders.add(currencyProvider); + } + + public void removeCurrencyProvider(CurrencyProvider currencyProvider) { + currencyProviders.remove(currencyProvider); + } + + @Override + public @Nullable Collection getParameterOptions(URI uri, String param, @Nullable String context, + @Nullable Locale locale) { + if ("system:units".equals(uri.toString()) && param.equals("currencyProvider")) { + return currencyProviders.stream().map(this::mapProvider).toList(); + } + return null; + } + + private ParameterOption mapProvider(CurrencyProvider currencyProvider) { + String providerName = currencyProvider.getName(); + int lastDot = providerName.lastIndexOf("."); + String providerDescription = lastDot > -1 ? providerName.substring(lastDot + 1) : providerName; + return new ParameterOption(providerName, providerDescription); + } +} diff --git a/bundles/org.openhab.core.config.core/src/main/java/org/openhab/core/config/core/internal/i18n/I18nConfigOptionsProvider.java b/bundles/org.openhab.core.config.core/src/main/java/org/openhab/core/config/core/internal/i18n/I18nConfigOptionsProvider.java index 13df696aa4f..53b6dc4d6e7 100644 --- a/bundles/org.openhab.core.config.core/src/main/java/org/openhab/core/config/core/internal/i18n/I18nConfigOptionsProvider.java +++ b/bundles/org.openhab.core.config.core/src/main/java/org/openhab/core/config/core/internal/i18n/I18nConfigOptionsProvider.java @@ -29,7 +29,7 @@ import org.osgi.service.component.annotations.Component; /** - * {@link ConfigOptionProvider} that provides a list of + * A {@link ConfigOptionProvider} that provides a list of config options for the i18n service * * @author Simon Kaufmann - Initial contribution * @author Erdoan Hadzhiyusein - Added time zone diff --git a/bundles/org.openhab.core/src/main/java/org/openhab/core/internal/i18n/I18nProviderImpl.java b/bundles/org.openhab.core/src/main/java/org/openhab/core/internal/i18n/I18nProviderImpl.java index 126a04e1cfa..524dc889a94 100644 --- a/bundles/org.openhab.core/src/main/java/org/openhab/core/internal/i18n/I18nProviderImpl.java +++ b/bundles/org.openhab.core/src/main/java/org/openhab/core/internal/i18n/I18nProviderImpl.java @@ -14,7 +14,6 @@ import static org.openhab.core.library.unit.MetricPrefix.HECTO; -import java.math.BigDecimal; import java.text.MessageFormat; import java.time.DateTimeException; import java.time.ZoneId; @@ -25,7 +24,6 @@ import java.util.Objects; import java.util.ResourceBundle; import java.util.Set; -import java.util.function.Function; import javax.measure.Quantity; import javax.measure.Unit; @@ -82,8 +80,6 @@ import org.openhab.core.library.dimension.RadiationSpecificActivity; import org.openhab.core.library.dimension.VolumetricFlowRate; import org.openhab.core.library.types.PointType; -import org.openhab.core.library.unit.CurrencyProvider; -import org.openhab.core.library.unit.CurrencyUnit; import org.openhab.core.library.unit.CurrencyUnits; import org.openhab.core.library.unit.ImperialUnits; import org.openhab.core.library.unit.SIUnits; @@ -124,8 +120,8 @@ "service.config.category=system", // "service.config.description.uri=system:i18n" }) @NonNullByDefault -public class I18nProviderImpl implements TranslationProvider, LocaleProvider, LocationProvider, TimeZoneProvider, - UnitProvider, CurrencyProvider { +public class I18nProviderImpl + implements TranslationProvider, LocaleProvider, LocationProvider, TimeZoneProvider, UnitProvider { private final Logger logger = LoggerFactory.getLogger(I18nProviderImpl.class); @@ -133,7 +129,6 @@ public class I18nProviderImpl implements TranslationProvider, LocaleProvider, Lo // LocaleProvider public static final String LANGUAGE = "language"; - public static final String BASE_CURRENCY = "baseCurrency"; public static final String SCRIPT = "script"; public static final String REGION = "region"; public static final String VARIANT = "variant"; @@ -174,7 +169,6 @@ protected void deactivate() { @Modified protected synchronized void modified(Map config) { final String language = toStringOrNull(config.get(LANGUAGE)); - this.currencyCode = toStringOrNull(config.get(BASE_CURRENCY)); final String script = toStringOrNull(config.get(SCRIPT)); final String region = toStringOrNull(config.get(REGION)); final String variant = toStringOrNull(config.get(VARIANT)); @@ -457,29 +451,4 @@ private static > void addDefaultUnit( Class dimension, Unit unit) { dimensionMap.put(dimension, Map.of(SIUnits.getInstance(), unit, ImperialUnits.getInstance(), unit)); } - - @Override - public Unit getBaseCurrency() { - String currencyCode = this.currencyCode; - if (currencyCode == null && locale != null) { - currencyCode = java.util.Currency.getInstance(locale).getCurrencyCode(); - } - if (currencyCode != null) { - // either the currency was set or determined from the locale - String symbol = java.util.Currency.getInstance(currencyCode).getSymbol(); - return new CurrencyUnit(currencyCode, symbol); - } else { - return new CurrencyUnit("DEF", null); - } - } - - @Override - public Collection> getCurrencies() { - return Set.of(); - } - - @Override - public Function, @Nullable BigDecimal> getExchangeRateFunction() { - return unit -> null; - } } diff --git a/bundles/org.openhab.core/src/main/java/org/openhab/core/internal/library/unit/CurrencyService.java b/bundles/org.openhab.core/src/main/java/org/openhab/core/internal/library/unit/CurrencyService.java index 2e777fb8d71..2ebcbdba75e 100644 --- a/bundles/org.openhab.core/src/main/java/org/openhab/core/internal/library/unit/CurrencyService.java +++ b/bundles/org.openhab.core/src/main/java/org/openhab/core/internal/library/unit/CurrencyService.java @@ -15,8 +15,11 @@ import static org.openhab.core.library.unit.CurrencyUnits.BASE_CURRENCY; import java.math.BigDecimal; +import java.util.Collection; +import java.util.Map; +import java.util.Objects; import java.util.Set; -import java.util.concurrent.CopyOnWriteArraySet; +import java.util.concurrent.ConcurrentHashMap; import java.util.function.Function; import javax.measure.Unit; @@ -30,57 +33,110 @@ import org.osgi.framework.Constants; import org.osgi.service.component.annotations.Activate; import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Modified; import org.osgi.service.component.annotations.Reference; import org.osgi.service.component.annotations.ReferenceCardinality; import org.osgi.service.component.annotations.ReferencePolicy; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import tech.units.indriya.format.SimpleUnitFormat; /** - * The {@link CurrencyService} is allows to register and switch {@link CurrencyProvider}s and provides exchange rates + * The {@link CurrencyService} allows to register and switch {@link CurrencyProvider}s and provides exchange rates * for currencies * * @author Jan N. Klug - Initial contribution */ -@Component +@Component(service = CurrencyService.class, immediate = true, configurationPid = CurrencyService.CONFIGURATION_PID, property = { + Constants.SERVICE_PID + "=org.openhab.units", // + "service.config.label=Unit Settings", // + "service.config.category=system", // + "service.config.description.uri=system:units" }) @NonNullByDefault public class CurrencyService { + public static final String CONFIGURATION_PID = "org.openhab.units"; + public static final String CONFIG_OPTION_CURRENCY_PROVIDER = "currencyProvider"; + private final Logger logger = LoggerFactory.getLogger(CurrencyService.class); public static Function, @Nullable BigDecimal> FACTOR_FCN = unit -> null; - private final Set currencyProviders = new CopyOnWriteArraySet<>(); + private final Map currencyProviders = new ConcurrentHashMap<>(); + + private CurrencyProvider enabledCurrencyProvider = DefaultCurrencyProvider.getInstance(); + private String configuredCurrencyProvider = DefaultCurrencyProvider.getInstance().getName(); @Activate - public CurrencyService( - @Reference(target = "(" + Constants.SERVICE_PID + "=org.openhab.i18n)") CurrencyProvider currencyProvider) { - currencyProviders.add(currencyProvider); + public CurrencyService(Map config) { + modified(config); + } + + @Modified + public void modified(Map config) { + String configOption = (String) config.get(CONFIG_OPTION_CURRENCY_PROVIDER); + configuredCurrencyProvider = Objects.requireNonNullElse(configOption, + DefaultCurrencyProvider.getInstance().getName()); + CurrencyProvider currencyProvider = currencyProviders.getOrDefault(configuredCurrencyProvider, + DefaultCurrencyProvider.getInstance()); enableProvider(currencyProvider); } @Reference(cardinality = ReferenceCardinality.MULTIPLE, policy = ReferencePolicy.DYNAMIC) public void addCurrencyProvider(CurrencyProvider currencyProvider) { - currencyProviders.add(currencyProvider); + logger.error("Added {}", currencyProvider.getName()); + currencyProviders.put(currencyProvider.getName(), currencyProvider); + if (configuredCurrencyProvider.equals(currencyProvider.getName())) { + enableProvider(currencyProvider); + } } public void removeCurrencyProvider(CurrencyProvider currencyProvider) { - currencyProviders.remove(currencyProvider); + if (currencyProvider.equals(enabledCurrencyProvider)) { + logger.warn("The currently activated currency provider is being removed. Enabling default."); + enableProvider(DefaultCurrencyProvider.getInstance()); + } + currencyProviders.remove(currencyProvider.getName()); } private synchronized void enableProvider(CurrencyProvider currencyProvider) { + SimpleUnitFormat unitFormatter = SimpleUnitFormat.getInstance(); + // remove units from old provider + enabledCurrencyProvider.getAdditionalCurrencies().forEach(CurrencyUnits::removeUnit); + unitFormatter.removeLabel(enabledCurrencyProvider.getBaseCurrency()); + + // add new units FACTOR_FCN = currencyProvider.getExchangeRateFunction(); - ((CurrencyUnit) BASE_CURRENCY).setSymbol(currencyProvider.getBaseCurrency().getSymbol()); - ((CurrencyUnit) BASE_CURRENCY).setName(currencyProvider.getBaseCurrency().getName()); - SimpleUnitFormat.getInstance().label(BASE_CURRENCY, currencyProvider.getBaseCurrency().getSymbol()); - currencyProvider.getCurrencies().forEach(CurrencyUnits::addUnit); + Unit baseCurrency = currencyProvider.getBaseCurrency(); + ((CurrencyUnit) BASE_CURRENCY).setSymbol(baseCurrency.getSymbol()); + ((CurrencyUnit) BASE_CURRENCY).setName(baseCurrency.getName()); + unitFormatter.label(BASE_CURRENCY, + Objects.requireNonNullElse(baseCurrency.getSymbol(), baseCurrency.getName())); + + currencyProvider.getAdditionalCurrencies().forEach(CurrencyUnits::addUnit); + + this.enabledCurrencyProvider = currencyProvider; } - /** - * Get the exchange rate for a given currency to the system's base unit - * - * @param currency the currency - * @return the exchange rate - */ - public static @Nullable BigDecimal getExchangeRate(Unit currency) { - return FACTOR_FCN.apply(currency); + private static class DefaultCurrencyProvider implements CurrencyProvider { + private static final CurrencyProvider INSTANCE = new DefaultCurrencyProvider(); + + @Override + public Unit getBaseCurrency() { + return new CurrencyUnit("DEF", null); + } + + @Override + public Collection> getAdditionalCurrencies() { + return Set.of(); + } + + @Override + public Function, @Nullable BigDecimal> getExchangeRateFunction() { + return unit -> null; + } + + public static CurrencyProvider getInstance() { + return INSTANCE; + } } } diff --git a/bundles/org.openhab.core/src/main/java/org/openhab/core/internal/library/unit/FixedCurrencyProvider.java b/bundles/org.openhab.core/src/main/java/org/openhab/core/internal/library/unit/FixedCurrencyProvider.java new file mode 100644 index 00000000000..1221c04712b --- /dev/null +++ b/bundles/org.openhab.core/src/main/java/org/openhab/core/internal/library/unit/FixedCurrencyProvider.java @@ -0,0 +1,76 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.core.internal.library.unit; + +import java.math.BigDecimal; +import java.util.Collection; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.function.Function; + +import javax.measure.Unit; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.core.library.dimension.Currency; +import org.openhab.core.library.unit.CurrencyProvider; +import org.openhab.core.library.unit.CurrencyUnit; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Modified; + +/** + * The {@link FixedCurrencyProvider} is an implementation of {@link CurrencyProvider} that provides only a single + * (configurable) currency. + * + * @author Jan N. Klug - Initial contribution + */ +@Component(service = CurrencyProvider.class, configurationPid = CurrencyService.CONFIGURATION_PID) +@NonNullByDefault +public class FixedCurrencyProvider implements CurrencyProvider { + public static final String CONFIG_OPTION_BASE_CURRENCY = "fixedBaseCurrency"; + private String currencyCode = "DEF"; + + @Activate + public FixedCurrencyProvider(Map config) { + modified(config); + } + + @Modified + public void modified(Map config) { + String code = (String) config.get(CONFIG_OPTION_BASE_CURRENCY); + currencyCode = Objects.requireNonNullElse(code, "DEF"); + } + + @Override + public Unit getBaseCurrency() { + String symbol = null; + try { + symbol = java.util.Currency.getInstance(currencyCode).getSymbol(); + } catch (IllegalArgumentException ignored) { + } + + return new CurrencyUnit(currencyCode, symbol); + } + + @Override + public Collection> getAdditionalCurrencies() { + return Set.of(); + } + + @Override + public Function, @Nullable BigDecimal> getExchangeRateFunction() { + return unit -> null; + } +} diff --git a/bundles/org.openhab.core/src/main/java/org/openhab/core/internal/library/unit/LocaleBasedCurrencyProvider.java b/bundles/org.openhab.core/src/main/java/org/openhab/core/internal/library/unit/LocaleBasedCurrencyProvider.java new file mode 100644 index 00000000000..b56e43e28ae --- /dev/null +++ b/bundles/org.openhab.core/src/main/java/org/openhab/core/internal/library/unit/LocaleBasedCurrencyProvider.java @@ -0,0 +1,71 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.core.internal.library.unit; + +import java.math.BigDecimal; +import java.util.Collection; +import java.util.Set; +import java.util.function.Function; + +import javax.measure.Unit; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.core.i18n.LocaleProvider; +import org.openhab.core.library.dimension.Currency; +import org.openhab.core.library.unit.CurrencyProvider; +import org.openhab.core.library.unit.CurrencyUnit; +import org.osgi.framework.Constants; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Reference; + +/** + * The {@link LocaleBasedCurrencyProvider} is an implementation of {@link CurrencyProvider} that provides the base + * currency based on the configured locale + * + * @author Jan N. Klug - Initial contribution + */ +@Component(service = CurrencyProvider.class, property = { Constants.SERVICE_PID + "=org.openhab.localebasedcurrency" }) +@NonNullByDefault +public class LocaleBasedCurrencyProvider implements CurrencyProvider { + + private final LocaleProvider localeProvider; + + @Activate + public LocaleBasedCurrencyProvider(@Reference LocaleProvider localeProvider) { + this.localeProvider = localeProvider; + } + + @Override + public Unit getBaseCurrency() { + String currencyCode = java.util.Currency.getInstance(localeProvider.getLocale()).getCurrencyCode(); + if (currencyCode != null) { + // either the currency was set or determined from the locale + String symbol = java.util.Currency.getInstance(currencyCode).getSymbol(); + return new CurrencyUnit(currencyCode, symbol); + } else { + return new CurrencyUnit("DEF", null); + } + } + + @Override + public Collection> getAdditionalCurrencies() { + return Set.of(); + } + + @Override + public Function, @Nullable BigDecimal> getExchangeRateFunction() { + return unit -> null; + } +} diff --git a/bundles/org.openhab.core/src/main/java/org/openhab/core/library/unit/CurrencyProvider.java b/bundles/org.openhab.core/src/main/java/org/openhab/core/library/unit/CurrencyProvider.java index 5674c91d165..70fc18aa3e0 100644 --- a/bundles/org.openhab.core/src/main/java/org/openhab/core/library/unit/CurrencyProvider.java +++ b/bundles/org.openhab.core/src/main/java/org/openhab/core/library/unit/CurrencyProvider.java @@ -49,11 +49,13 @@ default String getName() { Unit getBaseCurrency(); /** - * Get all units that are supported by this provider + * Get all additional currency that are supported by this provider + *

+ * The collection does NOT include the base currency. * * @return a {@link Collection} of {@link Unit}s */ - Collection> getCurrencies(); + Collection> getAdditionalCurrencies(); /** * Get a {@link Function} that supplies exchanges rates for currencies supported by this provider diff --git a/bundles/org.openhab.core/src/main/java/org/openhab/core/library/unit/CurrencyUnit.java b/bundles/org.openhab.core/src/main/java/org/openhab/core/library/unit/CurrencyUnit.java index 29248d58a00..26585952aca 100644 --- a/bundles/org.openhab.core/src/main/java/org/openhab/core/library/unit/CurrencyUnit.java +++ b/bundles/org.openhab.core/src/main/java/org/openhab/core/library/unit/CurrencyUnit.java @@ -39,7 +39,6 @@ import org.openhab.core.internal.library.unit.CurrencyService; import org.openhab.core.library.dimension.Currency; -import tech.units.indriya.format.SimpleUnitFormat; import tech.units.indriya.function.AbstractConverter; import tech.units.indriya.function.AddConverter; import tech.units.indriya.function.Calculus; @@ -88,16 +87,16 @@ public UnitConverter getSystemConverter() { @Override public String toString() { - return SimpleUnitFormat.getInstance().format(this); + return getName(); } @Override - public final Unit getSystemUnit() { + public Unit getSystemUnit() { return this; } @Override - public final boolean isCompatible(@NonNullByDefault({}) Unit that) { + public boolean isCompatible(@NonNullByDefault({}) Unit that) { return DIMENSION.equals(that.getDimension()); } diff --git a/bundles/org.openhab.core/src/main/java/org/openhab/core/library/unit/CurrencyUnits.java b/bundles/org.openhab.core/src/main/java/org/openhab/core/library/unit/CurrencyUnits.java index 7738dfbb182..d7e79198cf1 100644 --- a/bundles/org.openhab.core/src/main/java/org/openhab/core/library/unit/CurrencyUnits.java +++ b/bundles/org.openhab.core/src/main/java/org/openhab/core/library/unit/CurrencyUnits.java @@ -12,12 +12,15 @@ */ package org.openhab.core.library.unit; +import java.math.BigDecimal; import java.util.Objects; import javax.measure.Unit; import javax.measure.spi.SystemOfUnits; import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.core.internal.library.unit.CurrencyService; import org.openhab.core.library.dimension.Currency; import org.openhab.core.library.dimension.EnergyPrice; @@ -36,7 +39,8 @@ public final class CurrencyUnits extends AbstractSystemOfUnits { private static final CurrencyUnits INSTANCE = new CurrencyUnits(); public static final Unit BASE_CURRENCY = new CurrencyUnit("DEF", null); - public static Unit BASE_ENERGY_PRICE = new ProductUnit<>(BASE_CURRENCY.divide(Units.KILOWATT_HOUR)); + public static final Unit BASE_ENERGY_PRICE = new ProductUnit<>( + BASE_CURRENCY.divide(Units.KILOWATT_HOUR)); static { addUnit(BASE_CURRENCY); @@ -60,9 +64,10 @@ public static void addUnit(Unit unit) { } } - public void removeUnit(Unit unit) { + public static void removeUnit(Unit unit) { + SimpleUnitFormat.getInstance().removeLabel(unit); + SimpleUnitFormat.getInstance().removeAliases(unit); INSTANCE.units.remove(unit); - // TODO: remove labels/aliases, depends on unreleased indriya feature } public static SystemOfUnits getInstance() { @@ -72,4 +77,14 @@ public static SystemOfUnits getInstance() { public static Unit createCurrency(String symbol, String name) { return new CurrencyUnit(symbol, name); } + + /** + * Get the exchange rate for a given currency to the system's base unit + * + * @param currency the currency + * @return the exchange rate + */ + public static @Nullable BigDecimal getExchangeRate(Unit currency) { + return CurrencyService.FACTOR_FCN.apply(currency); + } } diff --git a/bundles/org.openhab.core/src/main/resources/OH-INF/config/i18n.xml b/bundles/org.openhab.core/src/main/resources/OH-INF/config/i18n.xml index 257efd0e7cd..06f73e0d4c1 100644 --- a/bundles/org.openhab.core/src/main/resources/OH-INF/config/i18n.xml +++ b/bundles/org.openhab.core/src/main/resources/OH-INF/config/i18n.xml @@ -33,10 +33,6 @@ Coordinates as <latitude>,<longitude>[<altitude>]
Example: "52.5200066,13.4049540" (Berlin)]]>
- - - The base currency of this system (determined by locale if not set) - The measurement system is used for unit conversion. diff --git a/bundles/org.openhab.core/src/main/resources/OH-INF/config/units.xml b/bundles/org.openhab.core/src/main/resources/OH-INF/config/units.xml new file mode 100644 index 00000000000..58109c21d7b --- /dev/null +++ b/bundles/org.openhab.core/src/main/resources/OH-INF/config/units.xml @@ -0,0 +1,20 @@ + + + + + + + + The service that is used to provide a set of currencies and their exchange rates. + + + + The base currency of this system when the fixed currency provider is used. + + + + diff --git a/bundles/org.openhab.core/src/test/java/org/openhab/core/internal/i18n/I18nProviderImplTest.java b/bundles/org.openhab.core/src/test/java/org/openhab/core/internal/i18n/I18nProviderImplTest.java index 9eaaca56c17..7b6899ca10f 100644 --- a/bundles/org.openhab.core/src/test/java/org/openhab/core/internal/i18n/I18nProviderImplTest.java +++ b/bundles/org.openhab.core/src/test/java/org/openhab/core/internal/i18n/I18nProviderImplTest.java @@ -41,7 +41,6 @@ import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.openhab.core.library.types.PointType; -import org.openhab.core.library.unit.CurrencyUnit; import org.openhab.core.library.unit.CurrencyUnits; import org.openhab.core.library.unit.ImperialUnits; import org.openhab.core.library.unit.SIUnits; @@ -110,7 +109,6 @@ public void assertThatConfigurationWasSet() { assertThat(setLocale.getCountry(), is(initialConfig.get(REGION))); assertThat(setLocale.getVariant(), is(initialConfig.get(VARIANT))); assertThat(i18nProviderImpl.getTimeZone(), is(ZoneId.of(TIMEZONE_GMT9))); - assertThat(i18nProviderImpl.getBaseCurrency(), is(new CurrencyUnit("EUR", "€"))); } @Test @@ -173,7 +171,6 @@ public void assertThatConfigurationChangeWorks() { assertThat(setLocale.getScript(), is(SCRIPT_RU)); assertThat(setLocale.getCountry(), is(REGION_RU)); assertThat(setLocale.getVariant(), is(VARIANT_RU)); - assertThat(i18nProviderImpl.getBaseCurrency(), is(new CurrencyUnit("RUB", "RUB"))); } @ParameterizedTest diff --git a/bundles/org.openhab.core/src/test/java/org/openhab/core/library/unit/CurrencyUnitTest.java b/bundles/org.openhab.core/src/test/java/org/openhab/core/library/unit/CurrencyUnitTest.java index 2bd40530908..0a5f2d10dd9 100644 --- a/bundles/org.openhab.core/src/test/java/org/openhab/core/library/unit/CurrencyUnitTest.java +++ b/bundles/org.openhab.core/src/test/java/org/openhab/core/library/unit/CurrencyUnitTest.java @@ -26,6 +26,8 @@ import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.openhab.core.internal.library.unit.CurrencyService; import org.openhab.core.library.dimension.Currency; @@ -41,7 +43,18 @@ public class CurrencyUnitTest { @SuppressWarnings("unused") - private final CurrencyService currencyService = new CurrencyService(new TestCurrencyProvider()); + private final CurrencyService currencyService = new CurrencyService( + Map.of("currencyProvider", "org.openhab.core.library.unit.CurrencyUnitTest$TestCurrencyProvider")); + + @BeforeEach + public void setup() { + currencyService.addCurrencyProvider(new TestCurrencyProvider()); + } + + @AfterEach + public void tearDown() { + currencyService.removeCurrencyProvider(new TestCurrencyProvider()); + } @Test public void testSimpleConversion() { @@ -99,7 +112,7 @@ public Unit getBaseCurrency() { } @Override - public Collection> getCurrencies() { + public Collection> getAdditionalCurrencies() { return List.of(DKK, USD); } From b1563a833dcbe7040a34b9b8c8bb1239a8f41778 Mon Sep 17 00:00:00 2001 From: "Jan N. Klug" Date: Tue, 29 Aug 2023 19:43:36 +0200 Subject: [PATCH 3/4] fix javadoc Signed-off-by: Jan N. Klug --- .../java/org/openhab/core/library/unit/CurrencyProvider.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bundles/org.openhab.core/src/main/java/org/openhab/core/library/unit/CurrencyProvider.java b/bundles/org.openhab.core/src/main/java/org/openhab/core/library/unit/CurrencyProvider.java index 70fc18aa3e0..7b4f0c1f7ac 100644 --- a/bundles/org.openhab.core/src/main/java/org/openhab/core/library/unit/CurrencyProvider.java +++ b/bundles/org.openhab.core/src/main/java/org/openhab/core/library/unit/CurrencyProvider.java @@ -23,7 +23,7 @@ import org.openhab.core.library.dimension.Currency; /** - * The {@link CurrencyProvider} can be implemented by services that supply currencies and their + * The {@link CurrencyProvider} can be implemented by services that supply currencies and their exchange rates * * @author Jan N. Klug - Initial contribution */ From 1861d50b05890bbcde6a14535ef67dc599bbd323 Mon Sep 17 00:00:00 2001 From: "Jan N. Klug" Date: Tue, 31 Oct 2023 20:49:01 +0100 Subject: [PATCH 4/4] address review comment Signed-off-by: Jan N. Klug --- .../org/openhab/core/internal/library/unit/CurrencyService.java | 1 - 1 file changed, 1 deletion(-) diff --git a/bundles/org.openhab.core/src/main/java/org/openhab/core/internal/library/unit/CurrencyService.java b/bundles/org.openhab.core/src/main/java/org/openhab/core/internal/library/unit/CurrencyService.java index 2ebcbdba75e..510f9a169af 100644 --- a/bundles/org.openhab.core/src/main/java/org/openhab/core/internal/library/unit/CurrencyService.java +++ b/bundles/org.openhab.core/src/main/java/org/openhab/core/internal/library/unit/CurrencyService.java @@ -83,7 +83,6 @@ public void modified(Map config) { @Reference(cardinality = ReferenceCardinality.MULTIPLE, policy = ReferencePolicy.DYNAMIC) public void addCurrencyProvider(CurrencyProvider currencyProvider) { - logger.error("Added {}", currencyProvider.getName()); currencyProviders.put(currencyProvider.getName(), currencyProvider); if (configuredCurrencyProvider.equals(currencyProvider.getName())) { enableProvider(currencyProvider);