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 abe76195a22..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 @@ -70,14 +70,17 @@ 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.CurrencyUnits; import org.openhab.core.library.unit.ImperialUnits; import org.openhab.core.library.unit.SIUnits; import org.openhab.core.library.unit.Units; @@ -130,6 +133,7 @@ public class I18nProviderImpl 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; @@ -395,6 +399,7 @@ public static Map>, Map>, Map, @Nullable BigDecimal> FACTOR_FCN = unit -> null; + + private final Map currencyProviders = new ConcurrentHashMap<>(); + + private CurrencyProvider enabledCurrencyProvider = DefaultCurrencyProvider.getInstance(); + private String configuredCurrencyProvider = DefaultCurrencyProvider.getInstance().getName(); + + @Activate + 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.put(currencyProvider.getName(), currencyProvider); + if (configuredCurrencyProvider.equals(currencyProvider.getName())) { + enableProvider(currencyProvider); + } + } + + public void removeCurrencyProvider(CurrencyProvider 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(); + 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; + } + + 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/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..7b4f0c1f7ac --- /dev/null +++ b/bundles/org.openhab.core/src/main/java/org/openhab/core/library/unit/CurrencyProvider.java @@ -0,0 +1,68 @@ +/** + * 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 exchange rates + * + * @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 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> getAdditionalCurrencies(); + + /** + * 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..26585952aca --- /dev/null +++ b/bundles/org.openhab.core/src/main/java/org/openhab/core/library/unit/CurrencyUnit.java @@ -0,0 +1,302 @@ +/** + * 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.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 getName(); + } + + @Override + public Unit getSystemUnit() { + return this; + } + + @Override + public 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..d7e79198cf1 --- /dev/null +++ b/bundles/org.openhab.core/src/main/java/org/openhab/core/library/unit/CurrencyUnits.java @@ -0,0 +1,90 @@ +/** + * 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.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; + +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 final 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 static void removeUnit(Unit unit) { + SimpleUnitFormat.getInstance().removeLabel(unit); + SimpleUnitFormat.getInstance().removeAliases(unit); + INSTANCE.units.remove(unit); + } + + public static SystemOfUnits getInstance() { + return Objects.requireNonNull(INSTANCE); + } + + 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/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/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 8efc084e296..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,6 +41,7 @@ import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.openhab.core.library.types.PointType; +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; @@ -183,7 +184,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..0a5f2d10dd9 --- /dev/null +++ b/bundles/org.openhab.core/src/test/java/org/openhab/core/library/unit/CurrencyUnitTest.java @@ -0,0 +1,124 @@ +/** + * 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.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; +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( + 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() { + 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> getAdditionalCurrencies() { + return List.of(DKK, USD); + } + + @Override + public Function, @Nullable BigDecimal> getExchangeRateFunction() { + return FACTORS::get; + } + } +}