diff --git a/CODEOWNERS b/CODEOWNERS
index 316b6341670..86feb8041de 100644
--- a/CODEOWNERS
+++ b/CODEOWNERS
@@ -79,6 +79,7 @@
/bundles/org.openhab.binding.dwdunwetter/ @limdul79
/bundles/org.openhab.binding.ecobee/ @mhilbush
/bundles/org.openhab.binding.ecotouch/ @sibbi77
+/bundles/org.openhab.binding.ecowatt/ @lolodomo
/bundles/org.openhab.binding.ekey/ @hmerk
/bundles/org.openhab.binding.electroluxair/ @jannegpriv
/bundles/org.openhab.binding.elerotransmitterstick/ @vbier
diff --git a/bom/openhab-addons/pom.xml b/bom/openhab-addons/pom.xml
index fbadb8d5142..1b76eaccb38 100644
--- a/bom/openhab-addons/pom.xml
+++ b/bom/openhab-addons/pom.xml
@@ -391,6 +391,11 @@
org.openhab.binding.ecotouch${project.version}
+
+ org.openhab.addons.bundles
+ org.openhab.binding.ecowatt
+ ${project.version}
+ org.openhab.addons.bundlesorg.openhab.binding.ekey
diff --git a/bundles/org.openhab.binding.ecowatt/NOTICE b/bundles/org.openhab.binding.ecowatt/NOTICE
new file mode 100644
index 00000000000..38d625e3492
--- /dev/null
+++ b/bundles/org.openhab.binding.ecowatt/NOTICE
@@ -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.ecowatt/README.md b/bundles/org.openhab.binding.ecowatt/README.md
new file mode 100644
index 00000000000..6567b7ca39c
--- /dev/null
+++ b/bundles/org.openhab.binding.ecowatt/README.md
@@ -0,0 +1,71 @@
+# Ecowatt Binding
+
+This binding uses the Ecowatt API to expose clear signals to adopt the right gestures and to ensure a good supply of electricity for all in France.
+
+You can find more information about Ecowatt on this [site](https://www.monecowatt.fr).
+
+## Supported Things
+
+This binding supports only one thing type: `signals`.
+
+## Discovery
+
+Discovery is not supported.
+You have to add the thing manually.
+
+## Prerequisites before configuration
+
+You must create an account and an application on the RTE portal to obtain the OAuth2 credentials required to access the API.
+
+1. Open this [page](https://data.rte-france.com/catalog/-/api/consumption/Ecowatt/v4.0), find the "Ecowatt" tile and click on the "Abonnez-vous à l'API" button.
+2. Create an account by following the instructions (you will receive an email to validate your new account).
+3. Once logged in, create an application by entering a name (for example "openHAB Integration"), choosing "Web Server" as type, entering any description of your choice and finally clicking on the "Valider" button.
+4. You will then see your application details, in particular the "ID client" and "ID Secret" information which you will need later to set up your binding thing.
+
+## Binding Configuration
+
+There are no overall binding configuration settings that need to be set.
+All settings are through thing configuration parameters.
+
+## Thing Configuration
+
+| Name | Type | Description | Required |
+|-----------|---------|-----------------------------------------------------------------------|----------|
+| idClient | text | ID client provided with the application you created in the RTE portal | yes |
+| idSecret | text | ID secret provided with the application you created in the RTE portal | yes |
+
+## Channels
+
+All channels are read-only.
+
+| Channel | Type | Description |
+|-------------------|--------|------------------------------------------------------------------|
+| todaySignal | Number | The signal relating to the forecast consumption level for today. Values are 1 for normal consumption, 2 for strained electrical system and 3 for very strained electrical system. |
+| tomorrowSignal | Number | The signal relating to the forecast consumption level for tomorrow. Values are 1 for normal consumption, 2 for strained electrical system and 3 for very strained electrical system. |
+| currentHourSignal | Number | The signal relating to the forecast consumption level for the current hour. Values are 1 for normal consumption, 2 for strained electrical system and 3 for very strained electrical system. |
+
+## Full Example
+
+example.things:
+
+```
+Thing ecowatt:signals:signals "Ecowatt Signals" [ idClient="xxxxx", idSecret="yyyyy"]
+```
+
+example.items:
+
+```
+Number TodaySignal "Today [%s]" { channel="ecowatt:signals:signals:todaySignal" }
+Number TomorrowSignal "Tomorrow [%s]" { channel="ecowatt:signals:signals:tomorrowSignal" }
+Number CurrentHourSignal "Current hour [%s]" { channel="ecowatt:signals:signals:currentHourSignal" }
+```
+
+example.sitemap:
+
+```
+ Frame label="Ecowatt" {
+ Default item=TodaySignal
+ Default item=TomorrowSignal
+ Default item=CurrentHourSignal
+ }
+```
diff --git a/bundles/org.openhab.binding.ecowatt/pom.xml b/bundles/org.openhab.binding.ecowatt/pom.xml
new file mode 100644
index 00000000000..6b0fff0eb27
--- /dev/null
+++ b/bundles/org.openhab.binding.ecowatt/pom.xml
@@ -0,0 +1,17 @@
+
+
+
+ 4.0.0
+
+
+ org.openhab.addons.bundles
+ org.openhab.addons.reactor.bundles
+ 3.4.0-SNAPSHOT
+
+
+ org.openhab.binding.ecowatt
+
+ openHAB Add-ons :: Bundles :: Ecowatt Binding
+
+
diff --git a/bundles/org.openhab.binding.ecowatt/src/main/feature/feature.xml b/bundles/org.openhab.binding.ecowatt/src/main/feature/feature.xml
new file mode 100644
index 00000000000..7f18af571c0
--- /dev/null
+++ b/bundles/org.openhab.binding.ecowatt/src/main/feature/feature.xml
@@ -0,0 +1,9 @@
+
+
+ mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features
+
+
+ openhab-runtime-base
+ mvn:org.openhab.addons.bundles/org.openhab.binding.ecowatt/${project.version}
+
+
diff --git a/bundles/org.openhab.binding.ecowatt/src/main/java/org/openhab/binding/ecowatt/internal/EcowattBindingConstants.java b/bundles/org.openhab.binding.ecowatt/src/main/java/org/openhab/binding/ecowatt/internal/EcowattBindingConstants.java
new file mode 100644
index 00000000000..91f35dae9d8
--- /dev/null
+++ b/bundles/org.openhab.binding.ecowatt/src/main/java/org/openhab/binding/ecowatt/internal/EcowattBindingConstants.java
@@ -0,0 +1,36 @@
+/**
+ * Copyright (c) 2010-2022 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.ecowatt.internal;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.core.thing.ThingTypeUID;
+
+/**
+ * The {@link EcowattBindingConstants} class defines common constants, which are
+ * used across the whole binding.
+ *
+ * @author Laurent Garnier - Initial contribution
+ */
+@NonNullByDefault
+public class EcowattBindingConstants {
+
+ private static final String BINDING_ID = "ecowatt";
+
+ // List of all Thing Type UIDs
+ public static final ThingTypeUID THING_TYPE_SIGNALS = new ThingTypeUID(BINDING_ID, "signals");
+
+ // List of all Channel ids
+ public static final String CHANNEL_TODAY_SIGNAL = "todaySignal";
+ public static final String CHANNEL_TOMORROW_SIGNAL = "tomorrowSignal";
+ public static final String CHANNEL_CURRENT_HOUR_SIGNAL = "currentHourSignal";
+}
diff --git a/bundles/org.openhab.binding.ecowatt/src/main/java/org/openhab/binding/ecowatt/internal/EcowattHandlerFactory.java b/bundles/org.openhab.binding.ecowatt/src/main/java/org/openhab/binding/ecowatt/internal/EcowattHandlerFactory.java
new file mode 100644
index 00000000000..b290e59e7c5
--- /dev/null
+++ b/bundles/org.openhab.binding.ecowatt/src/main/java/org/openhab/binding/ecowatt/internal/EcowattHandlerFactory.java
@@ -0,0 +1,77 @@
+/**
+ * Copyright (c) 2010-2022 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.ecowatt.internal;
+
+import static org.openhab.binding.ecowatt.internal.EcowattBindingConstants.THING_TYPE_SIGNALS;
+
+import java.util.Set;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.eclipse.jetty.client.HttpClient;
+import org.openhab.binding.ecowatt.internal.handler.EcowattHandler;
+import org.openhab.core.auth.client.oauth2.OAuthFactory;
+import org.openhab.core.i18n.TimeZoneProvider;
+import org.openhab.core.i18n.TranslationProvider;
+import org.openhab.core.io.net.http.HttpClientFactory;
+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 EcowattHandlerFactory} is responsible for creating things and thing
+ * handlers.
+ *
+ * @author Laurent Garnier - Initial contribution
+ */
+@NonNullByDefault
+@Component(configurationPid = "binding.ecowatt", service = ThingHandlerFactory.class)
+public class EcowattHandlerFactory extends BaseThingHandlerFactory {
+
+ private static final Set SUPPORTED_THING_TYPES_UIDS = Set.of(THING_TYPE_SIGNALS);
+
+ private final OAuthFactory oAuthFactory;
+ private final HttpClient httpClient;
+ private final TranslationProvider i18nProvider;
+ private final TimeZoneProvider timeZoneProvider;
+
+ @Activate
+ public EcowattHandlerFactory(@Reference OAuthFactory oAuthFactory, @Reference HttpClientFactory httpClientFactory,
+ final @Reference TranslationProvider i18nProvider, final @Reference TimeZoneProvider timeZoneProvider) {
+ this.oAuthFactory = oAuthFactory;
+ this.httpClient = httpClientFactory.getCommonHttpClient();
+ this.i18nProvider = i18nProvider;
+ this.timeZoneProvider = timeZoneProvider;
+ }
+
+ @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_SIGNALS.equals(thingTypeUID)) {
+ return new EcowattHandler(thing, oAuthFactory, httpClient, i18nProvider, timeZoneProvider);
+ }
+
+ return null;
+ }
+}
diff --git a/bundles/org.openhab.binding.ecowatt/src/main/java/org/openhab/binding/ecowatt/internal/configuration/EcowattConfiguration.java b/bundles/org.openhab.binding.ecowatt/src/main/java/org/openhab/binding/ecowatt/internal/configuration/EcowattConfiguration.java
new file mode 100644
index 00000000000..903160a8e50
--- /dev/null
+++ b/bundles/org.openhab.binding.ecowatt/src/main/java/org/openhab/binding/ecowatt/internal/configuration/EcowattConfiguration.java
@@ -0,0 +1,27 @@
+/**
+ * Copyright (c) 2010-2022 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.ecowatt.internal.configuration;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * The {@link EcowattConfiguration} class contains fields mapping thing configuration parameters.
+ *
+ * @author Laurent Garnier - Initial contribution
+ */
+@NonNullByDefault
+public class EcowattConfiguration {
+
+ public String idClient = "";
+ public String idSecret = "";
+}
diff --git a/bundles/org.openhab.binding.ecowatt/src/main/java/org/openhab/binding/ecowatt/internal/exception/EcowattApiLimitException.java b/bundles/org.openhab.binding.ecowatt/src/main/java/org/openhab/binding/ecowatt/internal/exception/EcowattApiLimitException.java
new file mode 100644
index 00000000000..171e513d4d0
--- /dev/null
+++ b/bundles/org.openhab.binding.ecowatt/src/main/java/org/openhab/binding/ecowatt/internal/exception/EcowattApiLimitException.java
@@ -0,0 +1,37 @@
+/**
+ * Copyright (c) 2010-2022 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.ecowatt.internal.exception;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.core.i18n.CommunicationException;
+
+/**
+ * An exception used when the API limit is reached
+ *
+ * @author Laurent Garnier - Initial contribution
+ */
+@NonNullByDefault
+public class EcowattApiLimitException extends CommunicationException {
+ private static final long serialVersionUID = 1L;
+ private int retryAfter;
+
+ public EcowattApiLimitException(int retryAfter, String message, @Nullable Object @Nullable... msgParams) {
+ super(message, msgParams);
+ this.retryAfter = retryAfter;
+ }
+
+ public int getRetryAfter() {
+ return retryAfter;
+ }
+}
diff --git a/bundles/org.openhab.binding.ecowatt/src/main/java/org/openhab/binding/ecowatt/internal/handler/EcowattHandler.java b/bundles/org.openhab.binding.ecowatt/src/main/java/org/openhab/binding/ecowatt/internal/handler/EcowattHandler.java
new file mode 100644
index 00000000000..d6530b9b3d7
--- /dev/null
+++ b/bundles/org.openhab.binding.ecowatt/src/main/java/org/openhab/binding/ecowatt/internal/handler/EcowattHandler.java
@@ -0,0 +1,245 @@
+/**
+ * Copyright (c) 2010-2022 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.ecowatt.internal.handler;
+
+import static org.openhab.binding.ecowatt.internal.EcowattBindingConstants.*;
+
+import java.time.Duration;
+import java.time.LocalDateTime;
+import java.time.ZonedDateTime;
+import java.time.format.DateTimeFormatter;
+import java.time.temporal.ChronoUnit;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.eclipse.jetty.client.HttpClient;
+import org.openhab.binding.ecowatt.internal.configuration.EcowattConfiguration;
+import org.openhab.binding.ecowatt.internal.exception.EcowattApiLimitException;
+import org.openhab.binding.ecowatt.internal.restapi.EcowattApiResponse;
+import org.openhab.binding.ecowatt.internal.restapi.EcowattDaySignals;
+import org.openhab.binding.ecowatt.internal.restapi.EcowattRestApi;
+import org.openhab.core.auth.client.oauth2.OAuthFactory;
+import org.openhab.core.cache.ExpiringCache;
+import org.openhab.core.i18n.CommunicationException;
+import org.openhab.core.i18n.TimeZoneProvider;
+import org.openhab.core.i18n.TranslationProvider;
+import org.openhab.core.library.types.DecimalType;
+import org.openhab.core.thing.ChannelUID;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.thing.ThingStatus;
+import org.openhab.core.thing.ThingStatusDetail;
+import org.openhab.core.thing.binding.BaseThingHandler;
+import org.openhab.core.types.Command;
+import org.openhab.core.types.RefreshType;
+import org.openhab.core.types.State;
+import org.openhab.core.types.UnDefType;
+import org.osgi.framework.Bundle;
+import org.osgi.framework.FrameworkUtil;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The {@link EcowattHandler} is responsible for updating the state of the channels
+ *
+ * @author Laurent Garnier - Initial contribution
+ */
+@NonNullByDefault
+public class EcowattHandler extends BaseThingHandler {
+
+ private final Logger logger = LoggerFactory.getLogger(EcowattHandler.class);
+
+ private final OAuthFactory oAuthFactory;
+ private final HttpClient httpClient;
+ private final TranslationProvider i18nProvider;
+ private final TimeZoneProvider timeZoneProvider;
+ private final Bundle bundle;
+
+ private @Nullable EcowattRestApi api;
+ private ExpiringCache cachedApiResponse = new ExpiringCache<>(Duration.ofHours(4),
+ this::getApiResponse); // cache the API response during 4 hours
+
+ private @Nullable ScheduledFuture> updateJob;
+
+ public EcowattHandler(Thing thing, OAuthFactory oAuthFactory, HttpClient httpClient,
+ TranslationProvider i18nProvider, TimeZoneProvider timeZoneProvider) {
+ super(thing);
+ this.oAuthFactory = oAuthFactory;
+ this.httpClient = httpClient;
+ this.i18nProvider = i18nProvider;
+ this.timeZoneProvider = timeZoneProvider;
+ this.bundle = FrameworkUtil.getBundle(this.getClass());
+ }
+
+ @Override
+ public void handleCommand(ChannelUID channelUID, Command command) {
+ if (command == RefreshType.REFRESH) {
+ updateChannel(channelUID.getId());
+ }
+ }
+
+ @Override
+ public void initialize() {
+ EcowattConfiguration config = getConfigAs(EcowattConfiguration.class);
+
+ final String idClient = config.idClient;
+ final String idSecret = config.idSecret;
+
+ if (idClient.isBlank() || idSecret.isBlank()) {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
+ "@text/offline.config-error-unset-parameters");
+ } else {
+ api = new EcowattRestApi(oAuthFactory, httpClient, thing.getUID().getAsString(), idClient, idSecret);
+ updateStatus(ThingStatus.UNKNOWN);
+ scheduleNextUpdate(0, true);
+ }
+ }
+
+ @Override
+ public void dispose() {
+ stopScheduledJob();
+ EcowattRestApi localApi = api;
+ if (localApi != null) {
+ localApi.dispose();
+ api = null;
+ }
+ }
+
+ /**
+ * Schedule the next update of channels.
+ *
+ * After this update is run, a new update will be rescheduled, either just after the API is reachable again or at
+ * the beginning of the following hour.
+ *
+ * @param delayInSeconds the delay in seconds before running the next update
+ * @param retryIfApiLimitReached true if a retry is expected when the update fails due to reached API limit
+ */
+ private void scheduleNextUpdate(long delayInSeconds, boolean retryIfApiLimitReached) {
+ logger.debug("scheduleNextUpdate delay={}s retryIfLimitReached={}", delayInSeconds, retryIfApiLimitReached);
+ updateJob = scheduler.schedule(() -> {
+ int retryDelay = updateChannels(retryIfApiLimitReached);
+ long delayNextUpdate;
+ if (retryDelay > 0) {
+ // Schedule a new update just after the API is reachable again
+ logger.debug("retryDelay {}", retryDelay);
+ delayNextUpdate = retryDelay;
+ } else {
+ // Schedule a new update at the beginning of the following hour
+ final LocalDateTime now = LocalDateTime.now();
+ final LocalDateTime beginningNextHour = now.plusHours(1).truncatedTo(ChronoUnit.HOURS);
+ delayNextUpdate = ChronoUnit.SECONDS.between(now, beginningNextHour);
+ }
+ // Add 3s of additional delay for security...
+ delayNextUpdate += 3;
+ scheduleNextUpdate(delayNextUpdate, retryDelay == 0);
+ }, delayInSeconds, TimeUnit.SECONDS);
+ }
+
+ private void stopScheduledJob() {
+ ScheduledFuture> job = updateJob;
+ if (job != null) {
+ job.cancel(true);
+ updateJob = null;
+ }
+ }
+
+ private EcowattApiResponse getApiResponse() {
+ EcowattRestApi localApi = api;
+ if (localApi == null) {
+ return new EcowattApiResponse();
+ }
+
+ EcowattApiResponse response;
+ try {
+ response = localApi.getSignals();
+ } catch (CommunicationException e) {
+ Throwable cause = e.getCause();
+ if (cause != null) {
+ logger.warn("{}: {}", e.getMessage(bundle, i18nProvider), cause.getMessage());
+ } else {
+ logger.warn("{}", e.getMessage(bundle, i18nProvider));
+ }
+ response = new EcowattApiResponse(e);
+ }
+ return response;
+ }
+
+ private int updateChannels(boolean retryIfApiLimitReached) {
+ return updateChannel(null, retryIfApiLimitReached);
+ }
+
+ private void updateChannel(String channelId) {
+ updateChannel(channelId, false);
+ }
+
+ private synchronized int updateChannel(@Nullable String channelId, boolean retryIfApiLimitReached) {
+ logger.debug("updateChannel channelId={}, retryIfApiLimitReached={}", channelId, retryIfApiLimitReached);
+ int retryDelay = 0;
+ EcowattApiResponse response = cachedApiResponse.getValue();
+ if (response == null || !response.succeeded()) {
+ CommunicationException exception = response == null ? null : response.getException();
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
+ exception == null ? null : exception.getRawMessage());
+
+ // Invalidate the cache to be sure the next request will trigger the API
+ cachedApiResponse.invalidateValue();
+
+ if (retryIfApiLimitReached && exception instanceof EcowattApiLimitException
+ && ((EcowattApiLimitException) exception).getRetryAfter() > 0) {
+ // Will retry when the API is available again (just after the limit expired)
+ retryDelay = ((EcowattApiLimitException) exception).getRetryAfter();
+ }
+ } else {
+ updateStatus(ThingStatus.ONLINE);
+ }
+
+ ZonedDateTime now = ZonedDateTime.now(timeZoneProvider.getTimeZone());
+ logger.debug("now {}", now.format(DateTimeFormatter.ISO_ZONED_DATE_TIME));
+ if ((channelId == null || CHANNEL_TODAY_SIGNAL.equals(channelId)) && isLinked(CHANNEL_TODAY_SIGNAL)) {
+ updateState(CHANNEL_TODAY_SIGNAL, getDaySignalState(response, now));
+ }
+ if ((channelId == null || CHANNEL_TOMORROW_SIGNAL.equals(channelId)) && isLinked(CHANNEL_TOMORROW_SIGNAL)) {
+ updateState(CHANNEL_TOMORROW_SIGNAL, getDaySignalState(response, now.plusDays(1)));
+ }
+ if ((channelId == null || CHANNEL_CURRENT_HOUR_SIGNAL.equals(channelId))
+ && isLinked(CHANNEL_CURRENT_HOUR_SIGNAL)) {
+ updateState(CHANNEL_CURRENT_HOUR_SIGNAL, getHourSignalState(response, now));
+ }
+
+ return retryDelay;
+ }
+
+ private State getDaySignalState(@Nullable EcowattApiResponse response, ZonedDateTime dateTime) {
+ EcowattDaySignals signals = response == null ? null : response.getDaySignals(dateTime);
+ return signals != null && signals.getDaySignal() >= 1 && signals.getDaySignal() <= 3
+ ? new DecimalType(signals.getDaySignal())
+ : UnDefType.UNDEF;
+ }
+
+ private State getHourSignalState(@Nullable EcowattApiResponse response, ZonedDateTime dateTime) {
+ EcowattDaySignals signals = response == null ? null : response.getDaySignals(dateTime);
+ ZonedDateTime day = signals == null ? null : signals.getDay();
+ if (signals != null && day != null) {
+ // Move the current time to the same offset as the data returned by the API to get and use the right current
+ // hour index in these data
+ int hour = dateTime.withZoneSameInstant(day.getZone()).getHour();
+ int value = signals.getHourSignal(hour);
+ logger.debug("hour {} value {}", hour, value);
+ if (value >= 1 && value <= 3) {
+ return new DecimalType(value);
+ }
+ }
+ return UnDefType.UNDEF;
+ }
+}
diff --git a/bundles/org.openhab.binding.ecowatt/src/main/java/org/openhab/binding/ecowatt/internal/restapi/EcowattApiResponse.java b/bundles/org.openhab.binding.ecowatt/src/main/java/org/openhab/binding/ecowatt/internal/restapi/EcowattApiResponse.java
new file mode 100644
index 00000000000..b146e9a6d2e
--- /dev/null
+++ b/bundles/org.openhab.binding.ecowatt/src/main/java/org/openhab/binding/ecowatt/internal/restapi/EcowattApiResponse.java
@@ -0,0 +1,93 @@
+/**
+ * Copyright (c) 2010-2022 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.ecowatt.internal.restapi;
+
+import java.time.ZonedDateTime;
+import java.time.format.DateTimeFormatter;
+import java.time.temporal.ChronoUnit;
+import java.util.List;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.core.i18n.CommunicationException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The {@link EcowattApiResponse} class contains fields mapping the response to the Ecowatt API request /signals.
+ *
+ * It also includes an exception field to be set in case the API request fails.
+ *
+ * @author Laurent Garnier - Initial contribution
+ */
+@NonNullByDefault
+public class EcowattApiResponse {
+ private final Logger logger = LoggerFactory.getLogger(EcowattApiResponse.class);
+
+ public @Nullable List signals;
+ private @Nullable CommunicationException exception;
+
+ public EcowattApiResponse() {
+ this.signals = null;
+ this.exception = null;
+ }
+
+ public EcowattApiResponse(@Nullable List signals) {
+ this.signals = signals;
+ this.exception = null;
+ }
+
+ public EcowattApiResponse(CommunicationException exception) {
+ this.signals = null;
+ this.exception = exception;
+ }
+
+ /**
+ * Search the data for the day of the given date and time
+ *
+ * @param dateTime a date and time
+ * @return the data for the searched day or null if no data is found for this day
+ */
+ public @Nullable EcowattDaySignals getDaySignals(ZonedDateTime dateTime) {
+ List localSignals = signals;
+ if (localSignals != null) {
+ for (EcowattDaySignals daySignals : localSignals) {
+ ZonedDateTime zdt = daySignals.getDay();
+ if (zdt != null) {
+ // Adjust date/times to the same offset/zone
+ ZonedDateTime dateTime2 = dateTime.withZoneSameInstant(zdt.getZone());
+ logger.trace("zdt {} offset {} - dateTime2 {} offset {}",
+ zdt.format(DateTimeFormatter.ISO_ZONED_DATE_TIME), zdt.getOffset(),
+ dateTime2.format(DateTimeFormatter.ISO_ZONED_DATE_TIME), dateTime2.getOffset());
+ // Check if the two date/times are in the same day
+ if (zdt.truncatedTo(ChronoUnit.DAYS).toInstant()
+ .equals(dateTime2.truncatedTo(ChronoUnit.DAYS).toInstant())) {
+ logger.debug("getDaySignals for {} returns signal {} : {} ( {} )",
+ dateTime.format(DateTimeFormatter.ISO_ZONED_DATE_TIME), daySignals.getDaySignal(),
+ daySignals.getDayMessage(), zdt.format(DateTimeFormatter.ISO_ZONED_DATE_TIME));
+ return daySignals;
+ }
+ }
+ }
+ }
+ return null;
+ }
+
+ public boolean succeeded() {
+ return signals != null;
+ }
+
+ public @Nullable CommunicationException getException() {
+ return exception;
+ }
+}
diff --git a/bundles/org.openhab.binding.ecowatt/src/main/java/org/openhab/binding/ecowatt/internal/restapi/EcowattDaySignals.java b/bundles/org.openhab.binding.ecowatt/src/main/java/org/openhab/binding/ecowatt/internal/restapi/EcowattDaySignals.java
new file mode 100644
index 00000000000..009a3830019
--- /dev/null
+++ b/bundles/org.openhab.binding.ecowatt/src/main/java/org/openhab/binding/ecowatt/internal/restapi/EcowattDaySignals.java
@@ -0,0 +1,63 @@
+/**
+ * Copyright (c) 2010-2022 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.ecowatt.internal.restapi;
+
+import java.time.ZonedDateTime;
+import java.util.List;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * The {@link EcowattDaySignals} class contains fields mapping the content of each value of JSON table "signals" inside
+ * the API response
+ *
+ * @author Laurent Garnier - Initial contribution
+ */
+@NonNullByDefault
+public class EcowattDaySignals {
+ @SerializedName("GenerationFichier")
+ public @Nullable ZonedDateTime fileTimestamp;
+ @SerializedName("jour")
+ public @Nullable ZonedDateTime day;
+ @SerializedName("dvalue")
+ public int value;
+ public @Nullable String message;
+ public @Nullable List values;
+
+ public @Nullable ZonedDateTime getDay() {
+ return day;
+ }
+
+ public int getDaySignal() {
+ return value;
+ }
+
+ public @Nullable String getDayMessage() {
+ return message;
+ }
+
+ public int getHourSignal(int hour) {
+ List localValues = values;
+ if (localValues != null) {
+ for (EcowattHourSignal hourSignal : localValues) {
+ if (hourSignal.hour == hour) {
+ return hourSignal.value;
+ }
+ }
+ }
+ return 0;
+ }
+}
diff --git a/bundles/org.openhab.binding.ecowatt/src/main/java/org/openhab/binding/ecowatt/internal/restapi/EcowattHourSignal.java b/bundles/org.openhab.binding.ecowatt/src/main/java/org/openhab/binding/ecowatt/internal/restapi/EcowattHourSignal.java
new file mode 100644
index 00000000000..bc23c3cdb6f
--- /dev/null
+++ b/bundles/org.openhab.binding.ecowatt/src/main/java/org/openhab/binding/ecowatt/internal/restapi/EcowattHourSignal.java
@@ -0,0 +1,31 @@
+/**
+ * Copyright (c) 2010-2022 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.ecowatt.internal.restapi;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * The {@link EcowattHourSignal} class contains fields mapping the content of each value of JSON table "values" inside
+ * the API response
+ *
+ * @author Laurent Garnier - Initial contribution
+ */
+@NonNullByDefault
+public class EcowattHourSignal {
+ @SerializedName("pas")
+ public int hour = -1;
+ @SerializedName("hvalue")
+ public int value;
+}
diff --git a/bundles/org.openhab.binding.ecowatt/src/main/java/org/openhab/binding/ecowatt/internal/restapi/EcowattRestApi.java b/bundles/org.openhab.binding.ecowatt/src/main/java/org/openhab/binding/ecowatt/internal/restapi/EcowattRestApi.java
new file mode 100644
index 00000000000..084274ae7cb
--- /dev/null
+++ b/bundles/org.openhab.binding.ecowatt/src/main/java/org/openhab/binding/ecowatt/internal/restapi/EcowattRestApi.java
@@ -0,0 +1,141 @@
+/**
+ * Copyright (c) 2010-2022 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.ecowatt.internal.restapi;
+
+import java.io.IOException;
+import java.time.Instant;
+import java.time.OffsetDateTime;
+import java.time.ZonedDateTime;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jetty.client.HttpClient;
+import org.eclipse.jetty.client.api.ContentResponse;
+import org.eclipse.jetty.client.api.Request;
+import org.eclipse.jetty.http.HttpHeader;
+import org.eclipse.jetty.http.HttpMethod;
+import org.eclipse.jetty.http.HttpStatus;
+import org.openhab.binding.ecowatt.internal.exception.EcowattApiLimitException;
+import org.openhab.core.auth.client.oauth2.AccessTokenResponse;
+import org.openhab.core.auth.client.oauth2.OAuthClientService;
+import org.openhab.core.auth.client.oauth2.OAuthException;
+import org.openhab.core.auth.client.oauth2.OAuthFactory;
+import org.openhab.core.auth.client.oauth2.OAuthResponseException;
+import org.openhab.core.i18n.CommunicationException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.JsonDeserializer;
+import com.google.gson.JsonSyntaxException;
+
+/**
+ * The {@link EcowattRestApi} is responsible for handling all communication with the Ecowatt REST API
+ *
+ * @author Laurent Garnier - Initial contribution
+ */
+@NonNullByDefault
+public class EcowattRestApi {
+
+ private static final String ECOWATT_API_TOKEN_URL = "https://digital.iservices.rte-france.com/token/oauth/";
+ private static final String ECOWATT_API_GET_SIGNALS_URL = "https://digital.iservices.rte-france.com/open_api/ecowatt/v4/signals";
+
+ private final Logger logger = LoggerFactory.getLogger(EcowattRestApi.class);
+
+ private final OAuthFactory oAuthFactory;
+ private final HttpClient httpClient;
+ private final Gson gson;
+ private OAuthClientService authService;
+ private String authServiceHandle;
+
+ public EcowattRestApi(OAuthFactory oAuthFactory, HttpClient httpClient, String authServiceHandle, String idClient,
+ String idSecret) {
+ this.oAuthFactory = oAuthFactory;
+ this.httpClient = httpClient;
+ GsonBuilder gsonBuilder = new GsonBuilder();
+ gson = gsonBuilder.registerTypeAdapter(ZonedDateTime.class,
+ (JsonDeserializer) (json, type, jsonDeserializationContext) -> OffsetDateTime
+ .parse(json.getAsJsonPrimitive().getAsString()).toZonedDateTime())
+ .create();
+ this.authService = oAuthFactory.createOAuthClientService(authServiceHandle, ECOWATT_API_TOKEN_URL, null,
+ idClient, idSecret, null, true);
+ this.authServiceHandle = authServiceHandle;
+ }
+
+ public EcowattApiResponse getSignals() throws CommunicationException, EcowattApiLimitException {
+ logger.debug("API request signals");
+ String token = authenticate().getAccessToken();
+
+ final Request request = httpClient.newRequest(ECOWATT_API_GET_SIGNALS_URL).method(HttpMethod.GET)
+ .header(HttpHeader.AUTHORIZATION, "Bearer " + token).timeout(10, TimeUnit.SECONDS);
+
+ ContentResponse response;
+ try {
+ response = request.send();
+ } catch (TimeoutException | ExecutionException e) {
+ throw new CommunicationException("@text/exception.api-request-failed", e);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ throw new CommunicationException("@text/exception.api-request-failed", e);
+ }
+
+ int statusCode = response.getStatus();
+
+ if (statusCode == HttpStatus.TOO_MANY_REQUESTS_429) {
+ int retryAfter = -1;
+ if (response.getHeaders().contains(HttpHeader.RETRY_AFTER)) {
+ try {
+ retryAfter = Integer.parseInt(response.getHeaders().get(HttpHeader.RETRY_AFTER));
+ } catch (NumberFormatException e) {
+ }
+ }
+ throw new EcowattApiLimitException(retryAfter, "@text/exception.api-limit-reached");
+ } else if (statusCode != HttpStatus.OK_200) {
+ throw new CommunicationException("@text/exception.api-request-failed-params", statusCode,
+ response.getContentAsString());
+ }
+
+ try {
+ EcowattApiResponse deserializedResp = gson.fromJson(response.getContentAsString(),
+ EcowattApiResponse.class);
+ if (deserializedResp == null) {
+ throw new CommunicationException("@text/exception.empty-api-response");
+ }
+ return deserializedResp;
+ } catch (JsonSyntaxException e) {
+ throw new CommunicationException("@text/exception.parsing-api-response-failed", e);
+ }
+ }
+
+ private AccessTokenResponse authenticate() throws CommunicationException {
+ try {
+ AccessTokenResponse result = authService.getAccessTokenResponse();
+ if (result == null || result.isExpired(Instant.now(), 120)) {
+ logger.debug("Authentication required");
+ result = authService.getAccessTokenByClientCredentials(null);
+ }
+ logger.debug("Token {} of type {} created on {} expiring after {} seconds", result.getAccessToken(),
+ result.getTokenType(), result.getCreatedOn(), result.getExpiresIn());
+ return result;
+ } catch (OAuthException | IOException | OAuthResponseException e) {
+ throw new CommunicationException("@text/exception.authentication-failed", e);
+ }
+ }
+
+ public void dispose() {
+ oAuthFactory.ungetOAuthService(authServiceHandle);
+ }
+}
diff --git a/bundles/org.openhab.binding.ecowatt/src/main/resources/OH-INF/binding/binding.xml b/bundles/org.openhab.binding.ecowatt/src/main/resources/OH-INF/binding/binding.xml
new file mode 100644
index 00000000000..1b07d8283f2
--- /dev/null
+++ b/bundles/org.openhab.binding.ecowatt/src/main/resources/OH-INF/binding/binding.xml
@@ -0,0 +1,10 @@
+
+
+
+ Ecowatt Binding
+ This binding uses the Ecowatt API to expose clear signals to adopt the right gestures and to ensure a good
+ supply of electricity for all in Frances.
+
+
diff --git a/bundles/org.openhab.binding.ecowatt/src/main/resources/OH-INF/i18n/ecowatt.properties b/bundles/org.openhab.binding.ecowatt/src/main/resources/OH-INF/i18n/ecowatt.properties
new file mode 100644
index 00000000000..742223ac718
--- /dev/null
+++ b/bundles/org.openhab.binding.ecowatt/src/main/resources/OH-INF/i18n/ecowatt.properties
@@ -0,0 +1,43 @@
+# binding
+
+binding.ecowatt.name = Ecowatt Binding
+binding.ecowatt.description = This binding uses the Ecowatt API to expose clear signals to adopt the right gestures and to ensure a good supply of electricity for all in Frances.
+
+# thing types
+
+thing-type.ecowatt.signals.label = Electricity Forecast
+thing-type.ecowatt.signals.description = The French electricity consumption forecasts
+thing-type.ecowatt.signals.channel.currentHourSignal.label = Current Hour Signal
+thing-type.ecowatt.signals.channel.currentHourSignal.description = The signal relating to the forecast consumption level for the current hour. Values are 1 for normal consumption, 2 for strained electrical system and 3 for very strained electrical system.
+thing-type.ecowatt.signals.channel.todaySignal.label = Today Signal
+thing-type.ecowatt.signals.channel.todaySignal.description = The signal relating to the forecast consumption level for today. Values are 1 for normal consumption, 2 for strained electrical system and 3 for very strained electrical system.
+thing-type.ecowatt.signals.channel.tomorrowSignal.label = Tomorrow Signal
+thing-type.ecowatt.signals.channel.tomorrowSignal.description = The signal relating to the forecast consumption level for tomorrow. Values are 1 for normal consumption, 2 for strained electrical system and 3 for very strained electrical system.
+
+# thing types config
+
+thing-type.config.ecowatt.signals.idClient.label = ID Client
+thing-type.config.ecowatt.signals.idClient.description = ID client provided with the application you created in the RTE portal.
+thing-type.config.ecowatt.signals.idSecret.label = ID Secret
+thing-type.config.ecowatt.signals.idSecret.description = ID secret provided with the application you created in the RTE portal.
+
+# channel types
+
+channel-type.ecowatt.signal.label = Consumption Signal
+channel-type.ecowatt.signal.description = The signal relating to the forecast consumption level. Values are 1 for normal consumption, 2 for strained electrical system and 3 for very strained electrical system.
+channel-type.ecowatt.signal.state.option.1 = Green (normal consumption)
+channel-type.ecowatt.signal.state.option.2 = Orange (strained electrical system)
+channel-type.ecowatt.signal.state.option.3 = Red (very strained electrical system)
+
+# thing status descriptions
+
+offline.config-error-unset-parameters = Id client and/or id secret configuration parameters is not set
+
+# exceptions
+
+exception.authentication-failed = Authentication to the API failed
+exception.api-request-failed = REST API request failed
+exception.api-request-failed-params = REST API request failed: statusCode={0}, message={1}
+exception.empty-api-response = API response is empty
+exception.parsing-api-response-failed = Parsing of the API response failed
+exception.api-limit-reached = API limit reached; will retry later
diff --git a/bundles/org.openhab.binding.ecowatt/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.ecowatt/src/main/resources/OH-INF/thing/thing-types.xml
new file mode 100644
index 00000000000..43da2596a62
--- /dev/null
+++ b/bundles/org.openhab.binding.ecowatt/src/main/resources/OH-INF/thing/thing-types.xml
@@ -0,0 +1,55 @@
+
+
+
+
+
+ The French electricity consumption forecasts
+
+
+
+
+ The signal relating to the forecast consumption level for today. Values are 1 for normal consumption, 2
+ for strained electrical system and 3 for very strained electrical system.
+
+
+
+ The signal relating to the forecast consumption level for tomorrow. Values are 1 for normal
+ consumption, 2 for strained electrical system and 3 for very strained electrical system.
+
+
+
+ The signal relating to the forecast consumption level for the current hour. Values are 1 for normal
+ consumption, 2 for strained electrical system and 3 for very strained electrical system.
+
+
+
+
+
+
+ ID client provided with the application you created in the RTE portal.
+
+
+ password
+
+ ID secret provided with the application you created in the RTE portal.
+
+
+
+
+
+ Number
+
+ The signal relating to the forecast consumption level. Values are 1 for normal consumption, 2 for
+ strained electrical system and 3 for very strained electrical system.
+
+
+
+
+
+
+
+
+
diff --git a/bundles/pom.xml b/bundles/pom.xml
index 99ec72c728c..6f121148bab 100644
--- a/bundles/pom.xml
+++ b/bundles/pom.xml
@@ -112,6 +112,7 @@
org.openhab.binding.easeeorg.openhab.binding.ecobeeorg.openhab.binding.ecotouch
+ org.openhab.binding.ecowattorg.openhab.binding.ekeyorg.openhab.binding.electroluxairorg.openhab.binding.elerotransmitterstick