[ecowatt] Initial contribution (#13404)

* [ecowatt] Initial contribution

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.

Close #13351

Signed-off-by: Laurent Garnier <lg.hc@free.fr>

* Update bundles/org.openhab.binding.ecowatt/src/main/resources/OH-INF/thing/thing-types.xml

Signed-off-by: Fabian Wolter <github@fabian-wolter.de>

Signed-off-by: Laurent Garnier <lg.hc@free.fr>
Signed-off-by: Fabian Wolter <github@fabian-wolter.de>
Co-authored-by: Fabian Wolter <github@fabian-wolter.de>
This commit is contained in:
lolodomo 2022-09-18 17:52:18 +02:00 committed by GitHub
parent 798b3ede04
commit 6ebf20f183
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 975 additions and 0 deletions

View File

@ -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

View File

@ -391,6 +391,11 @@
<artifactId>org.openhab.binding.ecotouch</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.openhab.addons.bundles</groupId>
<artifactId>org.openhab.binding.ecowatt</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.openhab.addons.bundles</groupId>
<artifactId>org.openhab.binding.ekey</artifactId>

View File

@ -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

View File

@ -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
}
```

View File

@ -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>3.4.0-SNAPSHOT</version>
</parent>
<artifactId>org.openhab.binding.ecowatt</artifactId>
<name>openHAB Add-ons :: Bundles :: Ecowatt Binding</name>
</project>

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<features name="org.openhab.binding.ecowatt-${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-ecowatt" description="Ecowatt Binding" version="${project.version}">
<feature>openhab-runtime-base</feature>
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.ecowatt/${project.version}</bundle>
</feature>
</features>

View File

@ -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";
}

View File

@ -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<ThingTypeUID> 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;
}
}

View File

@ -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 = "";
}

View File

@ -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;
}
}

View File

@ -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<EcowattApiResponse> 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;
}
}

View File

@ -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<EcowattDaySignals> signals;
private @Nullable CommunicationException exception;
public EcowattApiResponse() {
this.signals = null;
this.exception = null;
}
public EcowattApiResponse(@Nullable List<EcowattDaySignals> 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<EcowattDaySignals> 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;
}
}

View File

@ -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<EcowattHourSignal> values;
public @Nullable ZonedDateTime getDay() {
return day;
}
public int getDaySignal() {
return value;
}
public @Nullable String getDayMessage() {
return message;
}
public int getHourSignal(int hour) {
List<EcowattHourSignal> localValues = values;
if (localValues != null) {
for (EcowattHourSignal hourSignal : localValues) {
if (hourSignal.hour == hour) {
return hourSignal.value;
}
}
}
return 0;
}
}

View File

@ -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;
}

View File

@ -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<ZonedDateTime>) (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);
}
}

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<binding:binding id="ecowatt" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:binding="https://openhab.org/schemas/binding/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/binding/v1.0.0 https://openhab.org/schemas/binding-1.0.0.xsd">
<name>Ecowatt Binding</name>
<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.</description>
</binding:binding>

View File

@ -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

View File

@ -0,0 +1,55 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="ecowatt"
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="signals">
<label>Electricity Forecast</label>
<description>The French electricity consumption forecasts</description>
<channels>
<channel id="todaySignal" typeId="signal">
<label>Today Signal</label>
<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.</description>
</channel>
<channel id="tomorrowSignal" typeId="signal">
<label>Tomorrow Signal</label>
<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.</description>
</channel>
<channel id="currentHourSignal" typeId="signal">
<label>Current Hour Signal</label>
<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.</description>
</channel>
</channels>
<config-description>
<parameter name="idClient" type="text" required="true">
<label>ID Client</label>
<description>ID client provided with the application you created in the RTE portal.</description>
</parameter>
<parameter name="idSecret" type="text" required="true">
<context>password</context>
<label>ID Secret</label>
<description>ID secret provided with the application you created in the RTE portal.</description>
</parameter>
</config-description>
</thing-type>
<channel-type id="signal">
<item-type>Number</item-type>
<label>Consumption Signal</label>
<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.</description>
<state readOnly="true">
<options>
<option value="1">Green (normal consumption)</option>
<option value="2">Orange (strained electrical system)</option>
<option value="3">Red (very strained electrical system)</option>
</options>
</state>
</channel-type>
</thing:thing-descriptions>

View File

@ -112,6 +112,7 @@
<module>org.openhab.binding.easee</module>
<module>org.openhab.binding.ecobee</module>
<module>org.openhab.binding.ecotouch</module>
<module>org.openhab.binding.ecowatt</module>
<module>org.openhab.binding.ekey</module>
<module>org.openhab.binding.electroluxair</module>
<module>org.openhab.binding.elerotransmitterstick</module>