]> git.basschouten.com Git - openhab-addons.git/commitdiff
[freecurrency] Initial contribution (#16194)
authorJ-N-K <github@klug.nrw>
Mon, 15 Jan 2024 21:55:14 +0000 (22:55 +0100)
committerGitHub <noreply@github.com>
Mon, 15 Jan 2024 21:55:14 +0000 (22:55 +0100)
Signed-off-by: Jan N. Klug <github@klug.nrw>
19 files changed:
CODEOWNERS
bom/openhab-addons/pom.xml
bundles/org.openhab.binding.freecurrency/NOTICE [new file with mode: 0644]
bundles/org.openhab.binding.freecurrency/README.md [new file with mode: 0644]
bundles/org.openhab.binding.freecurrency/pom.xml [new file with mode: 0644]
bundles/org.openhab.binding.freecurrency/src/main/feature/feature.xml [new file with mode: 0644]
bundles/org.openhab.binding.freecurrency/src/main/java/org/openhab/binding/freecurrency/internal/ExchangeRateListener.java [new file with mode: 0644]
bundles/org.openhab.binding.freecurrency/src/main/java/org/openhab/binding/freecurrency/internal/FreecurrencyBindingConstants.java [new file with mode: 0644]
bundles/org.openhab.binding.freecurrency/src/main/java/org/openhab/binding/freecurrency/internal/FreecurrencyHandler.java [new file with mode: 0644]
bundles/org.openhab.binding.freecurrency/src/main/java/org/openhab/binding/freecurrency/internal/FreecurrencyHandlerFactory.java [new file with mode: 0644]
bundles/org.openhab.binding.freecurrency/src/main/java/org/openhab/binding/freecurrency/internal/FreecurrencyProvider.java [new file with mode: 0644]
bundles/org.openhab.binding.freecurrency/src/main/java/org/openhab/binding/freecurrency/internal/config/FreecurrencyExhangeRateChannelConfig.java [new file with mode: 0644]
bundles/org.openhab.binding.freecurrency/src/main/java/org/openhab/binding/freecurrency/internal/config/FreecurrencyServiceConfig.java [new file with mode: 0644]
bundles/org.openhab.binding.freecurrency/src/main/java/org/openhab/binding/freecurrency/internal/dto/CurrenciesDTO.java [new file with mode: 0644]
bundles/org.openhab.binding.freecurrency/src/main/java/org/openhab/binding/freecurrency/internal/dto/ExchangeRatesDTO.java [new file with mode: 0644]
bundles/org.openhab.binding.freecurrency/src/main/resources/OH-INF/addon/addon.xml [new file with mode: 0644]
bundles/org.openhab.binding.freecurrency/src/main/resources/OH-INF/i18n/freecurrency.properties [new file with mode: 0644]
bundles/org.openhab.binding.freecurrency/src/main/resources/OH-INF/thing/thing-types.xml [new file with mode: 0644]
bundles/pom.xml

index 79a33f3c15eb1eb29779452c55b1cf611dc75c14..dde5c21974ca36393d8e3e4693007efb9b54436a 100644 (file)
 /bundles/org.openhab.binding.foobot/ @airboxlab @Hilbrand
 /bundles/org.openhab.binding.freebox/ @lolodomo
 /bundles/org.openhab.binding.freeboxos/ @clinique
+/bundles/org.openhab.binding.freecurrency/ @J-N-K
 /bundles/org.openhab.binding.fronius/ @trokohl
 /bundles/org.openhab.binding.fsinternetradio/ @paphko
 /bundles/org.openhab.binding.ftpupload/ @paulianttila
index 44ad3d72de5b15f627cde4de26a296467054a835..761bb476a1df5e111a6448e61aba87ee5b57d18e 100644 (file)
       <artifactId>org.openhab.binding.freeboxos</artifactId>
       <version>${project.version}</version>
     </dependency>
+    <dependency>
+      <groupId>org.openhab.addons.bundles</groupId>
+      <artifactId>org.openhab.binding.freecurrency</artifactId>
+      <version>${project.version}</version>
+    </dependency>
     <dependency>
       <groupId>org.openhab.addons.bundles</groupId>
       <artifactId>org.openhab.binding.fronius</artifactId>
diff --git a/bundles/org.openhab.binding.freecurrency/NOTICE b/bundles/org.openhab.binding.freecurrency/NOTICE
new file mode 100644 (file)
index 0000000..38d625e
--- /dev/null
@@ -0,0 +1,13 @@
+This content is produced and maintained by the openHAB project.
+
+* Project home: https://www.openhab.org
+
+== Declared Project Licenses
+
+This program and the accompanying materials are made available under the terms
+of the Eclipse Public License 2.0 which is available at
+https://www.eclipse.org/legal/epl-2.0/.
+
+== Source Code
+
+https://github.com/openhab/openhab-addons
diff --git a/bundles/org.openhab.binding.freecurrency/README.md b/bundles/org.openhab.binding.freecurrency/README.md
new file mode 100644 (file)
index 0000000..f44e65b
--- /dev/null
@@ -0,0 +1,40 @@
+# Freecurrency Binding
+
+The Freecurrency binding connects [Freecurrency API](https://freecurrencyapi.com) to openHAB.
+It allows to get exchange rates between supported currencies and acts as a currency provider for openHAB's UoM support.
+
+The binding automatically updates the exchange rates at 00:01 UTC.
+There is a limit of 10 (5.000) free request per minute (month), so a daily refresh (and even some restarts per day) will not get you into trouble.
+
+## Supported Things
+
+There is only one thing: `info` which is extensible with exchange rate channels.
+You can add as many of these things as you like, but in general one should be sufficient for most use-cases.
+
+## Binding Configuration
+
+The binding has two configuration parameters: `apiKey` and `baseCurrency`.
+
+The `apiKey` is mandatory and can be retrieved from your dashboard after creating a free account at [Freecurrency API website](https://app.freecurrencyapi.com/login).
+
+The `baseCurrency` defaults to US dollars (`USD`), but can be configured to any other supported currency.
+Available currencies are provided as configuration options.
+Please note that misconfiguration will result in no exchanges rates being provided.
+
+## Thing Configuration
+
+### `info` Thing Configuration
+
+The thing has no configuration options and is automatically attached to the currency provider.
+
+## Channels
+
+| Channel        | Channel Type  | Item Type | Read/Write | Description                                                                                  |
+|----------------|---------------|-----------|------------|----------------------------------------------------------------------------------------------|
+| lastUpdate     | last-update   | DateTime  | R/O        | The timestamp of the last exchange rate refresh                                              |
+| <user defined> | exchange-rate | Number    | R/O        | The exchange rate between the configured currency and the base currency (or second currency) |
+
+The `exchange-rate` channels have two configuration parameters: `currency1` and `currency2`.
+Any currency code can be configured for both parameters, but only `currency1` is mandatory.
+If you omit `currency2`, the configured base-currency will be used as reference.
+Available currencies are provided as configuration options.
diff --git a/bundles/org.openhab.binding.freecurrency/pom.xml b/bundles/org.openhab.binding.freecurrency/pom.xml
new file mode 100644 (file)
index 0000000..f2c8fef
--- /dev/null
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
+  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
+
+  <modelVersion>4.0.0</modelVersion>
+
+  <parent>
+    <groupId>org.openhab.addons.bundles</groupId>
+    <artifactId>org.openhab.addons.reactor.bundles</artifactId>
+    <version>4.2.0-SNAPSHOT</version>
+  </parent>
+
+  <artifactId>org.openhab.binding.freecurrency</artifactId>
+
+  <name>openHAB Add-ons :: Bundles :: Freecurrency Binding</name>
+
+</project>
diff --git a/bundles/org.openhab.binding.freecurrency/src/main/feature/feature.xml b/bundles/org.openhab.binding.freecurrency/src/main/feature/feature.xml
new file mode 100644 (file)
index 0000000..020f20a
--- /dev/null
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<features name="org.openhab.binding.freecurrency-${project.version}" xmlns="http://karaf.apache.org/xmlns/features/v1.4.0">
+       <repository>mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features</repository>
+
+       <feature name="openhab-binding-freecurrency" description="Freecurrency Binding" version="${project.version}">
+               <feature>openhab-runtime-base</feature>
+               <bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.freecurrency/${project.version}</bundle>
+       </feature>
+</features>
diff --git a/bundles/org.openhab.binding.freecurrency/src/main/java/org/openhab/binding/freecurrency/internal/ExchangeRateListener.java b/bundles/org.openhab.binding.freecurrency/src/main/java/org/openhab/binding/freecurrency/internal/ExchangeRateListener.java
new file mode 100644 (file)
index 0000000..b377012
--- /dev/null
@@ -0,0 +1,27 @@
+/**
+ * Copyright (c) 2010-2024 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.binding.freecurrency.internal;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * The {@link ExchangeRateListener} interface can be implemented to receive a notification when exchange rates have been
+ * updated.
+ *
+ * @author Jan N. Klug - Initial contribution
+ */
+@NonNullByDefault
+public interface ExchangeRateListener {
+
+    void onExchangeRatesChanged();
+}
diff --git a/bundles/org.openhab.binding.freecurrency/src/main/java/org/openhab/binding/freecurrency/internal/FreecurrencyBindingConstants.java b/bundles/org.openhab.binding.freecurrency/src/main/java/org/openhab/binding/freecurrency/internal/FreecurrencyBindingConstants.java
new file mode 100644 (file)
index 0000000..4af2bb1
--- /dev/null
@@ -0,0 +1,35 @@
+/**
+ * Copyright (c) 2010-2024 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.binding.freecurrency.internal;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.core.thing.ThingTypeUID;
+import org.openhab.core.thing.type.ChannelTypeUID;
+
+/**
+ * The {@link FreecurrencyBindingConstants} class defines common constants, which are
+ * used across the whole binding.
+ *
+ * @author Jan N. Klug - Initial contribution
+ */
+@NonNullByDefault
+public class FreecurrencyBindingConstants {
+
+    private static final String BINDING_ID = "freecurrency";
+
+    // List of all Thing Type UIDs
+    public static final ThingTypeUID THING_TYPE_INFO = new ThingTypeUID(BINDING_ID, "info");
+
+    public static final ChannelTypeUID CHANNEL_TYPE_EXCHANGE_RATE = new ChannelTypeUID(BINDING_ID, "exchange-rate");
+    public static final ChannelTypeUID CHANNEL_TYPE_LAST_UPDATE = new ChannelTypeUID(BINDING_ID, "last-update");
+}
diff --git a/bundles/org.openhab.binding.freecurrency/src/main/java/org/openhab/binding/freecurrency/internal/FreecurrencyHandler.java b/bundles/org.openhab.binding.freecurrency/src/main/java/org/openhab/binding/freecurrency/internal/FreecurrencyHandler.java
new file mode 100644 (file)
index 0000000..5c07e09
--- /dev/null
@@ -0,0 +1,84 @@
+/**
+ * Copyright (c) 2010-2024 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.binding.freecurrency.internal;
+
+import static org.openhab.binding.freecurrency.internal.FreecurrencyBindingConstants.CHANNEL_TYPE_EXCHANGE_RATE;
+import static org.openhab.binding.freecurrency.internal.FreecurrencyBindingConstants.CHANNEL_TYPE_LAST_UPDATE;
+
+import java.math.BigDecimal;
+import java.time.ZonedDateTime;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.freecurrency.internal.config.FreecurrencyExhangeRateChannelConfig;
+import org.openhab.core.library.types.DateTimeType;
+import org.openhab.core.library.types.DecimalType;
+import org.openhab.core.thing.Channel;
+import org.openhab.core.thing.ChannelUID;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.thing.ThingStatus;
+import org.openhab.core.thing.binding.BaseThingHandler;
+import org.openhab.core.types.Command;
+import org.openhab.core.types.RefreshType;
+import org.openhab.core.types.UnDefType;
+
+/**
+ * The {@link FreecurrencyHandler} is responsible for handling commands, which are
+ * sent to one of the channels.
+ *
+ * @author Jan N. Klug - Initial contribution
+ */
+@NonNullByDefault
+public class FreecurrencyHandler extends BaseThingHandler implements ExchangeRateListener {
+    private final FreecurrencyProvider freecurrencyProvider;
+
+    public FreecurrencyHandler(Thing thing, FreecurrencyProvider freecurrencyProvider) {
+        super(thing);
+        this.freecurrencyProvider = freecurrencyProvider;
+    }
+
+    @Override
+    public void handleCommand(ChannelUID channelUID, Command command) {
+        Channel channel = thing.getChannel(channelUID);
+        if (RefreshType.REFRESH.equals(command) && channel != null) {
+            refreshChannel(channel);
+        }
+    }
+
+    @Override
+    public void initialize() {
+        updateStatus(ThingStatus.ONLINE);
+        freecurrencyProvider.addListener(this);
+    }
+
+    @Override
+    public void dispose() {
+        freecurrencyProvider.removeListener(this);
+    }
+
+    private void refreshChannel(Channel channel) {
+        if (CHANNEL_TYPE_EXCHANGE_RATE.equals(channel.getChannelTypeUID())) {
+            FreecurrencyExhangeRateChannelConfig config = channel.getConfiguration()
+                    .as(FreecurrencyExhangeRateChannelConfig.class);
+            BigDecimal val = freecurrencyProvider.getExchangeRate(config.currency1, config.currency2);
+            updateState(channel.getUID(), val != null ? new DecimalType(val) : UnDefType.UNDEF);
+        } else if (CHANNEL_TYPE_LAST_UPDATE.equals(channel.getChannelTypeUID())) {
+            ZonedDateTime lastUpdated = freecurrencyProvider.getLastUpdated();
+            updateState(channel.getUID(), lastUpdated == null ? UnDefType.UNDEF : new DateTimeType(lastUpdated));
+        }
+    }
+
+    @Override
+    public void onExchangeRatesChanged() {
+        thing.getChannels().forEach(this::refreshChannel);
+    }
+}
diff --git a/bundles/org.openhab.binding.freecurrency/src/main/java/org/openhab/binding/freecurrency/internal/FreecurrencyHandlerFactory.java b/bundles/org.openhab.binding.freecurrency/src/main/java/org/openhab/binding/freecurrency/internal/FreecurrencyHandlerFactory.java
new file mode 100644 (file)
index 0000000..b355389
--- /dev/null
@@ -0,0 +1,63 @@
+/**
+ * Copyright (c) 2010-2024 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.binding.freecurrency.internal;
+
+import static org.openhab.binding.freecurrency.internal.FreecurrencyBindingConstants.*;
+
+import java.util.Set;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.thing.ThingTypeUID;
+import org.openhab.core.thing.binding.BaseThingHandlerFactory;
+import org.openhab.core.thing.binding.ThingHandler;
+import org.openhab.core.thing.binding.ThingHandlerFactory;
+import org.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Reference;
+
+/**
+ * The {@link FreecurrencyHandlerFactory} is responsible for creating things and thing
+ * handlers.
+ *
+ * @author Jan N. Klug - Initial contribution
+ */
+@NonNullByDefault
+@Component(configurationPid = "binding.freecurrency", service = ThingHandlerFactory.class)
+public class FreecurrencyHandlerFactory extends BaseThingHandlerFactory {
+
+    private static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Set.of(THING_TYPE_INFO);
+    private final FreecurrencyProvider freecurrencyProvider;
+
+    @Activate
+    public FreecurrencyHandlerFactory(@Reference FreecurrencyProvider freecurrencyProvider) {
+        this.freecurrencyProvider = freecurrencyProvider;
+    }
+
+    @Override
+    public boolean supportsThingType(ThingTypeUID thingTypeUID) {
+        return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID);
+    }
+
+    @Override
+    protected @Nullable ThingHandler createHandler(Thing thing) {
+        ThingTypeUID thingTypeUID = thing.getThingTypeUID();
+
+        if (THING_TYPE_INFO.equals(thingTypeUID)) {
+            return new FreecurrencyHandler(thing, freecurrencyProvider);
+        }
+
+        return null;
+    }
+}
diff --git a/bundles/org.openhab.binding.freecurrency/src/main/java/org/openhab/binding/freecurrency/internal/FreecurrencyProvider.java b/bundles/org.openhab.binding.freecurrency/src/main/java/org/openhab/binding/freecurrency/internal/FreecurrencyProvider.java
new file mode 100644 (file)
index 0000000..e7c20e4
--- /dev/null
@@ -0,0 +1,231 @@
+/**
+ * Copyright (c) 2010-2024 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.binding.freecurrency.internal;
+
+import java.math.BigDecimal;
+import java.math.MathContext;
+import java.net.URI;
+import java.time.Duration;
+import java.time.Instant;
+import java.time.ZonedDateTime;
+import java.time.temporal.ChronoUnit;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeoutException;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+import javax.measure.Unit;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.eclipse.jetty.client.HttpClient;
+import org.openhab.binding.freecurrency.internal.config.FreecurrencyServiceConfig;
+import org.openhab.binding.freecurrency.internal.dto.CurrenciesDTO;
+import org.openhab.binding.freecurrency.internal.dto.ExchangeRatesDTO;
+import org.openhab.core.config.core.ConfigOptionProvider;
+import org.openhab.core.config.core.Configuration;
+import org.openhab.core.config.core.ParameterOption;
+import org.openhab.core.io.net.http.HttpClientFactory;
+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.scheduler.Scheduler;
+import org.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.ConfigurationPolicy;
+import org.osgi.service.component.annotations.Deactivate;
+import org.osgi.service.component.annotations.Modified;
+import org.osgi.service.component.annotations.Reference;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.gson.Gson;
+
+/**
+ * The {@link FreecurrencyProvider} class implements a {@link CurrencyProvider} based on currencies and dynamic exchange
+ * rates from <a href="https://freecurrencyapi.com">Freecurrency API</a>. It also allows to register
+ * {@link ExchangeRateListener}s for classes that want to be notified about changed exchange rates.
+ * 
+ * @author Jan N. Klug - Initial contribution
+ */
+@Component(immediate = true, configurationPid = "binding.freecurrency", configurationPolicy = ConfigurationPolicy.REQUIRE, service = {
+        CurrencyProvider.class, ConfigOptionProvider.class, FreecurrencyProvider.class })
+@NonNullByDefault
+public class FreecurrencyProvider implements CurrencyProvider, ConfigOptionProvider {
+    private static final CurrencyInformation DEFAULT_CURRENCY_USD = new CurrencyInformation("USD",
+            new CurrencyUnit("USD", null), "US Dollar");
+    private static final Duration REFRESH_OFFSET = Duration.parse("P1DT1M");
+    private final Logger logger = LoggerFactory.getLogger(FreecurrencyProvider.class);
+    private final HttpClient httpClient;
+    private final Scheduler scheduler;
+    private final Gson gson = new Gson();
+    private @NonNullByDefault({}) FreecurrencyServiceConfig config;
+    private @Nullable ScheduledFuture<?> refreshJob;
+    private Map<String, CurrencyInformation> currencies = Map.of();
+    private Map<Unit<Currency>, BigDecimal> exchangeRates = Map.of();
+    private @Nullable ZonedDateTime lastUpdated = null;
+    private Set<ExchangeRateListener> exchangeRateListeners = new HashSet<>();
+
+    @Activate
+    public FreecurrencyProvider(@Reference HttpClientFactory httpClientFactory, @Reference Scheduler scheduler,
+            Map<String, Object> config) {
+        this.httpClient = httpClientFactory.getCommonHttpClient();
+        this.scheduler = scheduler;
+        modified(config);
+    }
+
+    @Modified
+    public void modified(Map<String, Object> config) {
+        stopRefresh();
+        this.config = new Configuration(config).as(FreecurrencyServiceConfig.class);
+
+        if (this.config.apiKey.isBlank()) {
+            logger.warn("Configuration error: API key must not be blank.");
+            return;
+        }
+
+        getCurrencies();
+        getExchangeRates();
+    }
+
+    @Deactivate
+    public void deactivate() {
+        stopRefresh();
+    }
+
+    public void addListener(ExchangeRateListener listener) {
+        exchangeRateListeners.add(listener);
+        listener.onExchangeRatesChanged();
+    }
+
+    public void removeListener(ExchangeRateListener listener) {
+        exchangeRateListeners.remove(listener);
+    }
+
+    public @Nullable BigDecimal getExchangeRate(String currency1, String currency2) {
+        CurrencyInformation info1 = currencies.get(currency1);
+        CurrencyInformation info2 = currencies.get(currency2.isBlank() ? config.baseCurrency : currency2);
+        if (info1 == null || info2 == null) {
+            return null;
+        }
+        BigDecimal rate1 = exchangeRates.get(info1.unit());
+        BigDecimal rate2 = exchangeRates.get(info2.unit());
+        if (rate1 == null || rate2 == null) {
+            return null;
+        }
+        return rate2.divide(rate1, MathContext.DECIMAL128);
+    }
+
+    public @Nullable ZonedDateTime getLastUpdated() {
+        return lastUpdated;
+    }
+
+    private void stopRefresh() {
+        ScheduledFuture<?> localJob = this.refreshJob;
+        if (localJob != null) {
+            localJob.cancel(false);
+            refreshJob = null;
+        }
+    }
+
+    private void getCurrencies() {
+        String uri = "https://api.freecurrencyapi.com/v1/currencies?apikey=" + config.apiKey;
+        try {
+            String currenciesJson = httpClient.GET(uri).getContentAsString();
+            CurrenciesDTO currenciesDTO = gson.fromJson(currenciesJson, CurrenciesDTO.class);
+            currencies = currenciesDTO.data.values().stream()
+                    .map(c -> new CurrencyInformation(c.code, new CurrencyUnit(c.code, null), c.name))
+                    .collect(Collectors.toMap(CurrencyInformation::code, u -> u));
+            logger.debug("Retrieved {} currencies", currencies.size());
+        } catch (InterruptedException | ExecutionException | TimeoutException e) {
+            logger.debug("Failed to request currencies", e);
+        }
+    }
+
+    private void getExchangeRates() {
+        if (!currencies.containsKey(config.baseCurrency)) {
+            logger.warn("Configuration error: Base currency '{}' is not in list of available currencies {}.",
+                    config.baseCurrency, currencies.keySet());
+            return;
+        }
+        String uri = "https://api.freecurrencyapi.com/v1/latest?apikey=" + config.apiKey + "&base_currency="
+                + config.baseCurrency;
+        try {
+            String currenciesJson = httpClient.GET(uri).getContentAsString();
+            ExchangeRatesDTO exchangeRatesDTO = gson.fromJson(currenciesJson, ExchangeRatesDTO.class);
+            Map<Unit<Currency>, BigDecimal> newExchangeRates = new HashMap<>();
+            exchangeRatesDTO.data.forEach((k, v) -> {
+                CurrencyInformation currencyInfo = currencies.get(k);
+                if (currencyInfo == null) {
+                    logger.debug("Not considering exchange rate for '{}' because it is not supported.", k);
+                } else {
+                    newExchangeRates.put(currencyInfo.unit, v);
+                }
+            });
+            exchangeRates = newExchangeRates;
+            logger.debug("Retrieved exchange rates for {} currencies", newExchangeRates.size());
+            lastUpdated = ZonedDateTime.now();
+
+            // exchange rates are refreshed every day at midnight UTC
+            // we refresh one minute later to be sure we are not too early.
+            Instant nextRefresh = Instant.now().truncatedTo(ChronoUnit.DAYS).plus(REFRESH_OFFSET);
+            refreshJob = scheduler.at(this::getExchangeRates, nextRefresh);
+
+            // notify listeners about changed exchange rates
+            exchangeRateListeners.forEach(ExchangeRateListener::onExchangeRatesChanged);
+        } catch (InterruptedException | ExecutionException | TimeoutException e) {
+            logger.debug("Failed to request currencies", e);
+        }
+    }
+
+    @Override
+    public String getName() {
+        return "Freecurrency API";
+    }
+
+    @Override
+    public Unit<Currency> getBaseCurrency() {
+        return currencies.getOrDefault(config.baseCurrency, DEFAULT_CURRENCY_USD).unit();
+    }
+
+    @Override
+    public Collection<Unit<Currency>> getAdditionalCurrencies() {
+        return exchangeRates.keySet().stream().filter(c -> !config.baseCurrency.equals(c.getName())).toList();
+    }
+
+    @Override
+    public Function<Unit<Currency>, @Nullable BigDecimal> getExchangeRateFunction() {
+        return c -> exchangeRates.get(c);
+    }
+
+    @Override
+    public @Nullable Collection<ParameterOption> getParameterOptions(URI uri, String param, @Nullable String context,
+            @Nullable Locale locale) {
+        if (("binding:freecurrency".equals(uri.toString()) && "baseCurrency".equals(param))
+                || ("channel-type:freecurrency:exchange-rate".equals(uri.toString()) && param.startsWith("currency"))) {
+            return currencies.values().stream().map(c -> new ParameterOption(c.code(), c.name() + " (" + c.code + ")"))
+                    .toList();
+        }
+        return null;
+    }
+
+    private record CurrencyInformation(String code, Unit<Currency> unit, String name) {
+    }
+}
diff --git a/bundles/org.openhab.binding.freecurrency/src/main/java/org/openhab/binding/freecurrency/internal/config/FreecurrencyExhangeRateChannelConfig.java b/bundles/org.openhab.binding.freecurrency/src/main/java/org/openhab/binding/freecurrency/internal/config/FreecurrencyExhangeRateChannelConfig.java
new file mode 100644 (file)
index 0000000..4c12784
--- /dev/null
@@ -0,0 +1,26 @@
+/**
+ * Copyright (c) 2010-2024 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.binding.freecurrency.internal.config;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * The {@link FreecurrencyExhangeRateChannelConfig} class defines
+ *
+ * @author Jan N. Klug - Initial contribution
+ */
+@NonNullByDefault
+public class FreecurrencyExhangeRateChannelConfig {
+    public String currency1 = "";
+    public String currency2 = "";
+}
diff --git a/bundles/org.openhab.binding.freecurrency/src/main/java/org/openhab/binding/freecurrency/internal/config/FreecurrencyServiceConfig.java b/bundles/org.openhab.binding.freecurrency/src/main/java/org/openhab/binding/freecurrency/internal/config/FreecurrencyServiceConfig.java
new file mode 100644 (file)
index 0000000..692fa5a
--- /dev/null
@@ -0,0 +1,26 @@
+/**
+ * Copyright (c) 2010-2024 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.binding.freecurrency.internal.config;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * The {@link FreecurrencyServiceConfig} class holds the service configuration
+ *
+ * @author Jan N. Klug - Initial contribution
+ */
+@NonNullByDefault
+public class FreecurrencyServiceConfig {
+    public String apiKey = "";
+    public String baseCurrency = "USD";
+}
diff --git a/bundles/org.openhab.binding.freecurrency/src/main/java/org/openhab/binding/freecurrency/internal/dto/CurrenciesDTO.java b/bundles/org.openhab.binding.freecurrency/src/main/java/org/openhab/binding/freecurrency/internal/dto/CurrenciesDTO.java
new file mode 100644 (file)
index 0000000..5e33792
--- /dev/null
@@ -0,0 +1,39 @@
+/**
+ * Copyright (c) 2010-2024 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.binding.freecurrency.internal.dto;
+
+import java.util.Map;
+
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * The {@link CurrenciesDTO} class is used to retrieve the available currencies
+ *
+ * @author Jan N. Klug - Initial contribution
+ */
+public class CurrenciesDTO {
+    public Map<String, CurrencyDTO> data;
+
+    public static class CurrencyDTO {
+        public String symbol;
+        public String name;
+        @SerializedName("symbol_native")
+        public String symbolNative;
+        @SerializedName("decimal_digits")
+        public int decimalDigits;
+        public int rounding;
+        public String code;
+        @SerializedName("name_plural")
+        public String namePlural;
+    }
+}
diff --git a/bundles/org.openhab.binding.freecurrency/src/main/java/org/openhab/binding/freecurrency/internal/dto/ExchangeRatesDTO.java b/bundles/org.openhab.binding.freecurrency/src/main/java/org/openhab/binding/freecurrency/internal/dto/ExchangeRatesDTO.java
new file mode 100644 (file)
index 0000000..5059675
--- /dev/null
@@ -0,0 +1,26 @@
+/**
+ * Copyright (c) 2010-2024 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.binding.freecurrency.internal.dto;
+
+import java.math.BigDecimal;
+import java.util.Map;
+
+/**
+ * The {@link ExchangeRatesDTO} class is used to retrieve the exchange-rates for all currencies
+ *
+ * @author Jan N. Klug - Initial contribution
+ */
+public class ExchangeRatesDTO {
+    public Map<String, String> meta;
+    public Map<String, BigDecimal> data;
+}
diff --git a/bundles/org.openhab.binding.freecurrency/src/main/resources/OH-INF/addon/addon.xml b/bundles/org.openhab.binding.freecurrency/src/main/resources/OH-INF/addon/addon.xml
new file mode 100644 (file)
index 0000000..942caa8
--- /dev/null
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<addon:addon id="freecurrency" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xmlns:addon="https://openhab.org/schemas/addon/v1.0.0"
+       xsi:schemaLocation="https://openhab.org/schemas/addon/v1.0.0 https://openhab.org/schemas/addon-1.0.0.xsd">
+
+       <type>binding</type>
+       <name>Freecurrency Binding</name>
+       <description>This is the binding to integrate Freecurrency API as currency provider.</description>
+       <connection>cloud</connection>
+
+       <config-description>
+               <parameter name="apiKey" type="text" required="true">
+                       <label>API Key</label>
+                       <description>The API key retrieved from freecurrencyapi.com</description>
+               </parameter>
+               <parameter name="baseCurrency" type="text">
+                       <label>Base Currency</label>
+                       <description>The base currency for this provider.</description>
+                       <default>USD</default>
+               </parameter>
+       </config-description>
+
+</addon:addon>
diff --git a/bundles/org.openhab.binding.freecurrency/src/main/resources/OH-INF/i18n/freecurrency.properties b/bundles/org.openhab.binding.freecurrency/src/main/resources/OH-INF/i18n/freecurrency.properties
new file mode 100644 (file)
index 0000000..1a36fb7
--- /dev/null
@@ -0,0 +1,29 @@
+# add-on
+
+addon.freecurrency.name = Freecurrency Binding
+addon.freecurrency.description = This is the binding to integrate Freecurrency API as currency provider.
+
+# add-on config
+
+addon.config.freecurrency.apiKey.label = API Key
+addon.config.freecurrency.apiKey.description = The API key retrieved from freecurrencyapi.com
+addon.config.freecurrency.baseCurrency.label = Base Currency
+addon.config.freecurrency.baseCurrency.description = The base currency for this provider.
+
+# thing types
+
+thing-type.freecurrency.info.label = Currency Information
+thing-type.freecurrency.info.description = Provide exchanges rates between currencies and service status information.
+
+# channel types
+
+channel-type.freecurrency.exchange-rate.label = Exchange Rate
+channel-type.freecurrency.exchange-rate.description = Exchange rate between two currencies
+channel-type.freecurrency.last-update.label = Last Update
+channel-type.freecurrency.last-update.description = The timestamp of the last retrieved set of exchange rates.
+
+# channel types config
+
+channel-type.config.freecurrency.exchange-rate.currency1.label = Currency 1
+channel-type.config.freecurrency.exchange-rate.currency2.label = Currency 2
+channel-type.config.freecurrency.exchange-rate.currency2.description = Optional, defaults to base currency if not configured.
diff --git a/bundles/org.openhab.binding.freecurrency/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.freecurrency/src/main/resources/OH-INF/thing/thing-types.xml
new file mode 100644 (file)
index 0000000..03e3cbb
--- /dev/null
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="freecurrency"
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
+       xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
+
+       <thing-type id="info" extensible="exchange-rate">
+               <label>Currency Information</label>
+               <description>Provide exchanges rates between currencies and service status information.</description>
+
+               <channels>
+                       <channel id="lastUpdate" typeId="last-update"/>
+               </channels>
+
+       </thing-type>
+
+       <channel-type id="last-update">
+               <item-type>DateTime</item-type>
+               <label>Last Update</label>
+               <description>The timestamp of the last retrieved set of exchange rates.</description>
+               <category>Time</category>
+               <state readOnly="true"/>
+       </channel-type>
+
+       <channel-type id="exchange-rate">
+               <item-type>Number</item-type>
+               <label>Exchange Rate</label>
+               <description>Exchange rate between two currencies</description>
+               <state pattern="%.6f" readOnly="true"/>
+
+               <config-description>
+                       <parameter name="currency1" type="text" required="true">
+                               <label>Currency 1</label>
+                       </parameter>
+                       <parameter name="currency2" type="text">
+                               <label>Currency 2</label>
+                               <description>Optional, defaults to base currency if not configured.</description>
+                       </parameter>
+               </config-description>
+       </channel-type>
+
+</thing:thing-descriptions>
index 14f345d1dfb22a16701b648511c7358aed6b79c1..f97ad738a32ffcf9e4fdaeb7c134c62c4f22c794 100644 (file)
     <module>org.openhab.binding.foobot</module>
     <module>org.openhab.binding.freebox</module>
     <module>org.openhab.binding.freeboxos</module>
+    <module>org.openhab.binding.freecurrency</module>
     <module>org.openhab.binding.fronius</module>
     <module>org.openhab.binding.fsinternetradio</module>
     <module>org.openhab.binding.ftpupload</module>