added migrated 2.x add-ons

Signed-off-by: Kai Kreuzer <kai@openhab.org>
This commit is contained in:
Kai Kreuzer
2020-09-21 01:58:32 +02:00
parent bbf1a7fd29
commit 6df6783b60
11662 changed files with 1302875 additions and 11 deletions

View File

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

View File

@@ -0,0 +1,128 @@
/**
* Copyright (c) 2010-2020 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.linky.internal;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit;
import java.util.function.Supplier;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* This is a simple expiring and reloading cache implementation.
*
* There must be provided an action in order to retrieve/calculate the value. This action will be called only if the
* answer from the last calculation is not valid anymore, i.e. if it is expired.
*
* The cache expires after the current day; it is possible to shift the beginning time of the day.
*
* Soft Reference is not used to store the cached value because JVM Garbage Collector is clearing it too much often.
*
* @author Laurent Garnier - Initial contribution
*
* @param <V> the type of the value
*/
@NonNullByDefault
public class ExpiringDayCache<V> {
private final Logger logger = LoggerFactory.getLogger(ExpiringDayCache.class);
private final String name;
private final int beginningHour;
private final Supplier<@Nullable V> action;
private @Nullable V value;
private LocalDateTime expiresAt;
/**
* Create a new instance.
*
* @param name the name of this cache
* @param beginningHour the hour in the day at which the validity period is starting
* @param action the action to retrieve/calculate the value
*/
public ExpiringDayCache(String name, int beginningHour, Supplier<@Nullable V> action) {
this.name = name;
this.beginningHour = beginningHour;
this.expiresAt = calcAlreadyExpired();
this.action = action;
}
/**
* Returns the value - possibly from the cache, if it is still valid.
*/
public synchronized @Nullable V getValue() {
@Nullable
V cachedValue = value;
if (cachedValue == null || isExpired()) {
logger.debug("getValue from cache \"{}\" is requiring a fresh value", name);
cachedValue = refreshValue();
} else {
logger.debug("getValue from cache \"{}\" is returing a cached value", name);
}
return cachedValue;
}
/**
* Puts a new value into the cache.
*
* @param value the new value
*/
public final synchronized void putValue(@Nullable V value) {
this.value = value;
expiresAt = calcNextExpiresAt();
}
/**
* Invalidates the value in the cache.
*/
public final synchronized void invalidateValue() {
value = null;
expiresAt = calcAlreadyExpired();
}
/**
* Refreshes and returns the value in the cache.
*
* @return the new value
*/
public synchronized @Nullable V refreshValue() {
value = action.get();
expiresAt = calcNextExpiresAt();
return value;
}
/**
* Checks if the value is expired.
*
* @return true if the value is expired
*/
public boolean isExpired() {
return !LocalDateTime.now().isBefore(expiresAt);
}
private LocalDateTime calcNextExpiresAt() {
LocalDateTime now = LocalDateTime.now();
LocalDateTime limit = now.withHour(beginningHour).truncatedTo(ChronoUnit.HOURS);
LocalDateTime result = now.isBefore(limit) ? limit : limit.plusDays(1);
logger.debug("calcNextExpiresAt result = {}", result.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME));
return result;
}
private LocalDateTime calcAlreadyExpired() {
return LocalDateTime.now().minusDays(1);
}
}

View File

@@ -0,0 +1,40 @@
/**
* Copyright (c) 2010-2020 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.linky.internal;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.thing.ThingTypeUID;
/**
* The {@link LinkyBindingConstants} class defines common constants, which are
* used across the whole binding.
*
* @author Gaël L'hopital - Initial contribution
*/
@NonNullByDefault
public class LinkyBindingConstants {
public static final String BINDING_ID = "linky";
// List of all Thing Type UIDs
public static final ThingTypeUID THING_TYPE_LINKY = new ThingTypeUID(BINDING_ID, "linky");
// List of all Channel id's
public static final String YESTERDAY = "daily#yesterday";
public static final String THIS_WEEK = "weekly#thisWeek";
public static final String LAST_WEEK = "weekly#lastWeek";
public static final String THIS_MONTH = "monthly#thisMonth";
public static final String LAST_MONTH = "monthly#lastMonth";
public static final String THIS_YEAR = "yearly#thisYear";
public static final String LAST_YEAR = "yearly#lastYear";
}

View File

@@ -0,0 +1,24 @@
/**
* Copyright (c) 2010-2020 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.linky.internal;
/**
* The {@link LinkyConfiguration} is the class used to match the
* thing configuration.
*
* @author Gaël L'hopital - Initial contribution
*/
public class LinkyConfiguration {
public String username;
public String password;
}

View File

@@ -0,0 +1,61 @@
/**
* Copyright (c) 2010-2020 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.linky.internal;
import static org.openhab.binding.linky.internal.LinkyBindingConstants.THING_TYPE_LINKY;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.linky.internal.handler.LinkyHandler;
import org.openhab.core.i18n.LocaleProvider;
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 LinkyHandlerFactory} is responsible for creating things handlers.
*
* @author Gaël L'hopital - Initial contribution
*/
@NonNullByDefault
@Component(service = ThingHandlerFactory.class, configurationPid = "binding.linky")
public class LinkyHandlerFactory extends BaseThingHandlerFactory {
private final LocaleProvider localeProvider;
@Activate
public LinkyHandlerFactory(final @Reference LocaleProvider localeProvider) {
this.localeProvider = localeProvider;
}
@Override
public boolean supportsThingType(ThingTypeUID thingTypeUID) {
return thingTypeUID.equals(THING_TYPE_LINKY);
}
@Override
protected @Nullable ThingHandler createHandler(Thing thing) {
ThingTypeUID thingTypeUID = thing.getThingTypeUID();
if (thingTypeUID.equals(THING_TYPE_LINKY)) {
return new LinkyHandler(thing, localeProvider);
}
return null;
}
}

View File

@@ -0,0 +1,130 @@
/**
* Copyright (c) 2010-2020 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.linky.internal.console;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.util.Arrays;
import java.util.List;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.linky.internal.handler.LinkyHandler;
import org.openhab.core.io.console.Console;
import org.openhab.core.io.console.extensions.AbstractConsoleCommandExtension;
import org.openhab.core.io.console.extensions.ConsoleCommandExtension;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingRegistry;
import org.openhab.core.thing.ThingUID;
import org.openhab.core.thing.binding.ThingHandler;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
/**
* The {@link LinkyCommandExtension} is responsible for handling console commands
*
* @author Laurent Garnier - Initial contribution
*/
@NonNullByDefault
@Component(service = ConsoleCommandExtension.class)
public class LinkyCommandExtension extends AbstractConsoleCommandExtension {
private static final String REPORT = "report";
private final ThingRegistry thingRegistry;
@Activate
public LinkyCommandExtension(final @Reference ThingRegistry thingRegistry) {
super("linky", "Interact with the Linky binding.");
this.thingRegistry = thingRegistry;
}
@Override
public void execute(String[] args, Console console) {
if (args.length >= 2) {
Thing thing = null;
try {
ThingUID thingUID = new ThingUID(args[0]);
thing = thingRegistry.get(thingUID);
} catch (IllegalArgumentException e) {
thing = null;
}
ThingHandler thingHandler = null;
LinkyHandler handler = null;
if (thing != null) {
thingHandler = thing.getHandler();
if (thingHandler instanceof LinkyHandler) {
handler = (LinkyHandler) thingHandler;
}
}
if (thing == null) {
console.println("Bad thing id '" + args[0] + "'");
printUsage(console);
} else if (thingHandler == null) {
console.println("No handler initialized for the thing id '" + args[0] + "'");
printUsage(console);
} else if (handler == null) {
console.println("'" + args[0] + "' is not a Linky thing id");
printUsage(console);
} else if (REPORT.equals(args[1])) {
LocalDate now = LocalDate.now();
LocalDate start = now.minusDays(7);
LocalDate end = now.minusDays(1);
String separator = " ";
if (args.length >= 3) {
try {
start = LocalDate.parse(args[2], DateTimeFormatter.ISO_LOCAL_DATE);
} catch (DateTimeParseException e) {
console.println(
"Invalid format for start day '" + args[2] + "'; expected format is YYYY-MM-DD");
printUsage(console);
return;
}
}
if (args.length >= 4) {
try {
end = LocalDate.parse(args[3], DateTimeFormatter.ISO_LOCAL_DATE);
} catch (DateTimeParseException e) {
console.println("Invalid format for end day '" + args[3] + "'; expected format is YYYY-MM-DD");
printUsage(console);
return;
}
}
if (!start.isBefore(now) || start.isAfter(end)) {
console.println("Start day must be in the past and before the end day");
printUsage(console);
return;
}
if (end.isAfter(now.minusDays(1))) {
end = now.minusDays(1);
}
if (args.length >= 5) {
separator = args[4];
}
handler.reportValues(start, end, separator).forEach(console::println);
} else {
printUsage(console);
}
} else {
printUsage(console);
}
}
@Override
public List<String> getUsages() {
return Arrays.asList(buildCommandUsage("<thingUID> " + REPORT + " <start day> <end day> [<separator>]",
"report daily consumptions between two dates"));
}
}

View File

@@ -0,0 +1,46 @@
/**
* Copyright (c) 2010-2020 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.linky.internal.handler;
import java.util.ArrayList;
import java.util.List;
import okhttp3.Cookie;
import okhttp3.CookieJar;
import okhttp3.HttpUrl;
/**
* The {@link LinkyCookieJar} is responsible to holds cookies
* during API session
*
* @author Gaël L'hopital - Initial contribution
*/
public class LinkyCookieJar implements CookieJar {
private static final String LOGIN_URL_PATH = "/auth/UI/Login";
private List<Cookie> cookies = new ArrayList<>();
@Override
public void saveFromResponse(final HttpUrl url, final List<Cookie> cookies) {
this.cookies.addAll(cookies);
}
@Override
public List<Cookie> loadForRequest(final HttpUrl url) {
if (LOGIN_URL_PATH.equals(url.url().getPath())) {
cookies = new ArrayList<>();
}
return cookies;
}
}

View File

@@ -0,0 +1,387 @@
/**
* Copyright (c) 2010-2020 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.linky.internal.handler;
import static org.openhab.binding.linky.internal.LinkyBindingConstants.*;
import static org.openhab.binding.linky.internal.model.LinkyTimeScale.*;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit;
import java.time.temporal.WeekFields;
import java.util.ArrayList;
import java.util.Base64;
import java.util.List;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.linky.internal.ExpiringDayCache;
import org.openhab.binding.linky.internal.LinkyConfiguration;
import org.openhab.binding.linky.internal.model.LinkyConsumptionData;
import org.openhab.binding.linky.internal.model.LinkyTimeScale;
import org.openhab.core.i18n.LocaleProvider;
import org.openhab.core.library.types.QuantityType;
import org.openhab.core.library.unit.SmartHomeUnits;
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.UnDefType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gson.Gson;
import com.google.gson.JsonSyntaxException;
import okhttp3.FormBody;
import okhttp3.FormBody.Builder;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
/**
* The {@link LinkyHandler} is responsible for handling commands, which are
* sent to one of the channels.
*
* @author Gaël L'hopital - Initial contribution
*/
@NonNullByDefault
public class LinkyHandler extends BaseThingHandler {
private final Logger logger = LoggerFactory.getLogger(LinkyHandler.class);
private static final String LOGIN_BASE_URI = "https://espace-client-connexion.enedis.fr/auth/UI/Login";
private static final String API_BASE_URI = "https://espace-client-particuliers.enedis.fr/group/espace-particuliers/suivi-de-consommation";
private static final DateTimeFormatter API_DATE_FORMAT = DateTimeFormatter.ofPattern("dd/MM/yyyy");
private static final int REFRESH_FIRST_HOUR_OF_DAY = 5;
private static final int REFRESH_INTERVAL_IN_MIN = 360;
private final OkHttpClient client = new OkHttpClient.Builder().followRedirects(false)
.cookieJar(new LinkyCookieJar()).build();
private final Gson gson = new Gson();
private @NonNullByDefault({}) ScheduledFuture<?> refreshJob;
private final WeekFields weekFields;
private final ExpiringDayCache<LinkyConsumptionData> cachedDaylyData;
private final ExpiringDayCache<LinkyConsumptionData> cachedMonthlyData;
private final ExpiringDayCache<LinkyConsumptionData> cachedYearlyData;
public LinkyHandler(Thing thing, LocaleProvider localeProvider) {
super(thing);
this.weekFields = WeekFields.of(localeProvider.getLocale());
this.cachedDaylyData = new ExpiringDayCache<LinkyConsumptionData>("daily cache", REFRESH_FIRST_HOUR_OF_DAY,
() -> {
final LocalDate today = LocalDate.now();
return getConsumptionData(DAILY, today.minusDays(13), today, true);
});
this.cachedMonthlyData = new ExpiringDayCache<LinkyConsumptionData>("monthly cache", REFRESH_FIRST_HOUR_OF_DAY,
() -> {
final LocalDate today = LocalDate.now();
return getConsumptionData(MONTHLY, today.withDayOfMonth(1).minusMonths(1), today, true);
});
this.cachedYearlyData = new ExpiringDayCache<LinkyConsumptionData>("yearly cache", REFRESH_FIRST_HOUR_OF_DAY,
() -> {
final LocalDate today = LocalDate.now();
return getConsumptionData(YEARLY, LocalDate.of(today.getYear() - 1, 1, 1), today, true);
});
}
@Override
public void initialize() {
logger.debug("Initializing Linky handler.");
updateStatus(ThingStatus.UNKNOWN);
scheduler.submit(this::login);
final LocalDateTime now = LocalDateTime.now();
final LocalDateTime nextDayFirstTimeUpdate = now.plusDays(1).withHour(REFRESH_FIRST_HOUR_OF_DAY)
.truncatedTo(ChronoUnit.HOURS);
refreshJob = scheduler.scheduleWithFixedDelay(this::updateData,
ChronoUnit.MINUTES.between(now, nextDayFirstTimeUpdate) % REFRESH_INTERVAL_IN_MIN + 1,
REFRESH_INTERVAL_IN_MIN, TimeUnit.MINUTES);
}
private static Builder getLoginBodyBuilder() {
return new FormBody.Builder().add("encoded", "true").add("gx_charset", "UTF-8").add("SunQueryParamsString",
Base64.getEncoder().encodeToString("realm=particuliers".getBytes(StandardCharsets.UTF_8)));
}
private synchronized boolean login() {
logger.debug("login");
LinkyConfiguration config = getConfigAs(LinkyConfiguration.class);
Request requestLogin = new Request.Builder().url(LOGIN_BASE_URI)
.post(getLoginBodyBuilder().add("IDToken1", config.username).add("IDToken2", config.password).build())
.build();
try (Response response = client.newCall(requestLogin).execute()) {
if (response.isRedirect()) {
logger.debug("Response status {} {} redirects to {}", response.code(), response.message(),
response.header("Location"));
} else {
logger.debug("Response status {} {}", response.code(), response.message());
}
// Do a first call to get data; this first call will fail with code 302
getConsumptionData(DAILY, LocalDate.now(), LocalDate.now(), false);
updateStatus(ThingStatus.ONLINE);
return true;
} catch (IOException e) {
logger.debug("Exception while trying to login: {}", e.getMessage(), e);
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getMessage());
return false;
}
}
/**
* Request new data and updates channels
*/
private void updateData() {
updateDailyData();
updateMonthlyData();
updateYearlyData();
}
/**
* Request new dayly/weekly data and updates channels
*/
private synchronized void updateDailyData() {
if (!isLinked(YESTERDAY) && !isLinked(LAST_WEEK) && !isLinked(THIS_WEEK)) {
return;
}
double lastWeek = Double.NaN;
double thisWeek = Double.NaN;
double yesterday = Double.NaN;
LinkyConsumptionData result = cachedDaylyData.getValue();
if (result != null && result.success()) {
LocalDate rangeStart = LocalDate.now().minusDays(13);
int jump = result.getDecalage();
while (rangeStart.getDayOfWeek() != weekFields.getFirstDayOfWeek()) {
rangeStart = rangeStart.plusDays(1);
jump++;
}
int lastWeekNumber = rangeStart.get(weekFields.weekOfWeekBasedYear());
lastWeek = 0.0;
thisWeek = 0.0;
yesterday = Double.NaN;
while (jump < result.getData().size()) {
double consumption = result.getData().get(jump).valeur;
if (consumption > 0) {
if (rangeStart.get(weekFields.weekOfWeekBasedYear()) == lastWeekNumber) {
lastWeek += consumption;
logger.trace("Consumption at index {} added to last week: {}", jump, consumption);
} else {
thisWeek += consumption;
logger.trace("Consumption at index {} added to current week: {}", jump, consumption);
}
yesterday = consumption;
}
jump++;
rangeStart = rangeStart.plusDays(1);
}
} else {
cachedDaylyData.invalidateValue();
}
updateKwhChannel(YESTERDAY, yesterday);
updateKwhChannel(THIS_WEEK, thisWeek);
updateKwhChannel(LAST_WEEK, lastWeek);
}
/**
* Request new monthly data and updates channels
*/
private synchronized void updateMonthlyData() {
if (!isLinked(LAST_MONTH) && !isLinked(THIS_MONTH)) {
return;
}
double lastMonth = Double.NaN;
double thisMonth = Double.NaN;
LinkyConsumptionData result = cachedMonthlyData.getValue();
if (result != null && result.success()) {
int jump = result.getDecalage();
lastMonth = result.getData().get(jump).valeur;
thisMonth = result.getData().get(jump + 1).valeur;
if (thisMonth < 0) {
thisMonth = 0.0;
}
} else {
cachedMonthlyData.invalidateValue();
}
updateKwhChannel(LAST_MONTH, lastMonth);
updateKwhChannel(THIS_MONTH, thisMonth);
}
/**
* Request new yearly data and updates channels
*/
private synchronized void updateYearlyData() {
if (!isLinked(LAST_YEAR) && !isLinked(THIS_YEAR)) {
return;
}
double thisYear = Double.NaN;
double lastYear = Double.NaN;
LinkyConsumptionData result = cachedYearlyData.getValue();
if (result != null && result.success()) {
int elementQuantity = result.getData().size();
thisYear = elementQuantity > 0 ? result.getData().get(elementQuantity - 1).valeur : Double.NaN;
lastYear = elementQuantity > 1 ? result.getData().get(elementQuantity - 2).valeur : Double.NaN;
} else {
cachedYearlyData.invalidateValue();
}
updateKwhChannel(LAST_YEAR, lastYear);
updateKwhChannel(THIS_YEAR, thisYear);
}
private void updateKwhChannel(String channelId, double consumption) {
logger.debug("Update channel {} with {}", channelId, consumption);
updateState(channelId,
!Double.isNaN(consumption) ? new QuantityType<>(consumption, SmartHomeUnits.KILOWATT_HOUR)
: UnDefType.UNDEF);
}
/**
* Produce a report of all daily values between two dates
*
* @param startDay the start day of the report
* @param endDay the end day of the report
* @param separator the separator to be used betwwen the date and the value
*
* @return the report as a string
*/
public List<String> reportValues(LocalDate startDay, LocalDate endDay, @Nullable String separator) {
List<String> report = new ArrayList<>();
if (startDay.getYear() == endDay.getYear() && startDay.getMonthValue() == endDay.getMonthValue()) {
// All values in the same month
LinkyConsumptionData result = getConsumptionData(DAILY, startDay, endDay, true);
if (result != null && result.success()) {
LocalDate currentDay = startDay;
int jump = result.getDecalage();
while (jump < result.getData().size() && !currentDay.isAfter(endDay)) {
double consumption = result.getData().get(jump).valeur;
String line = currentDay.format(DateTimeFormatter.ISO_LOCAL_DATE) + separator;
if (consumption >= 0) {
line += String.valueOf(consumption);
}
report.add(line);
jump++;
currentDay = currentDay.plusDays(1);
}
} else {
LocalDate currentDay = startDay;
while (!currentDay.isAfter(endDay)) {
report.add(currentDay.format(DateTimeFormatter.ISO_LOCAL_DATE) + separator);
currentDay = currentDay.plusDays(1);
}
}
} else {
// Concatenate the report produced for each month between the two dates
LocalDate first = startDay;
do {
LocalDate last = first.withDayOfMonth(first.lengthOfMonth());
if (last.isAfter(endDay)) {
last = endDay;
}
report.addAll(reportValues(first, last, separator));
first = last.plusDays(1);
} while (!first.isAfter(endDay));
}
return report;
}
private @Nullable LinkyConsumptionData getConsumptionData(LinkyTimeScale timeScale, LocalDate from, LocalDate to,
boolean reLog) {
logger.debug("getConsumptionData {}", timeScale);
LinkyConsumptionData result = null;
boolean tryRelog = false;
FormBody formBody = new FormBody.Builder().add("p_p_id", "lincspartdisplaycdc_WAR_lincspartcdcportlet")
.add("p_p_lifecycle", "2").add("p_p_resource_id", timeScale.getId())
.add("_lincspartdisplaycdc_WAR_lincspartcdcportlet_dateDebut", from.format(API_DATE_FORMAT))
.add("_lincspartdisplaycdc_WAR_lincspartcdcportlet_dateFin", to.format(API_DATE_FORMAT)).build();
Request requestData = new Request.Builder().url(API_BASE_URI).post(formBody).build();
try (Response response = client.newCall(requestData).execute()) {
if (response.isRedirect()) {
String location = response.header("Location");
logger.debug("Response status {} {} redirects to {}", response.code(), response.message(), location);
if (reLog && location != null && location.startsWith(LOGIN_BASE_URI)) {
tryRelog = true;
}
} else {
String body = (response.body() != null) ? response.body().string() : null;
logger.debug("Response status {} {} : {}", response.code(), response.message(), body);
if (body != null && !body.isEmpty()) {
result = gson.fromJson(body, LinkyConsumptionData.class);
}
}
} catch (IOException e) {
logger.debug("Exception calling API : {} - {}", e.getClass().getCanonicalName(), e.getMessage());
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, e.getMessage());
} catch (JsonSyntaxException e) {
logger.debug("Exception while converting JSON response : {}", e.getMessage());
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.NONE, e.getMessage());
}
if (tryRelog && login()) {
result = getConsumptionData(timeScale, from, to, false);
}
return result;
}
@Override
public void dispose() {
logger.debug("Disposing the Linky handler.");
if (refreshJob != null && !refreshJob.isCancelled()) {
refreshJob.cancel(true);
refreshJob = null;
}
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
if (command instanceof RefreshType) {
logger.debug("Refreshing channel {}", channelUID.getId());
switch (channelUID.getId()) {
case YESTERDAY:
case LAST_WEEK:
case THIS_WEEK:
updateDailyData();
break;
case LAST_MONTH:
case THIS_MONTH:
updateMonthlyData();
break;
case LAST_YEAR:
case THIS_YEAR:
updateYearlyData();
break;
default:
break;
}
} else {
logger.debug("The Linky binding is read-only and can not handle command {}", command);
}
}
}

View File

@@ -0,0 +1,72 @@
/**
* Copyright (c) 2010-2020 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.linky.internal.model;
import java.util.ArrayList;
import java.util.List;
/**
* The {@link LinkyConsumptionData} is responsible for holding values
* returned by API calls
*
* @author Gaël L'hopital - Initial contribution
*/
public class LinkyConsumptionData {
private Etat etat;
private Graphe graphe;
public Etat getEtat() {
return etat;
}
public boolean isInactive() {
return "nonActive".equalsIgnoreCase(etat.valeur);
}
public boolean success() {
return "termine".equalsIgnoreCase(etat.valeur);
}
public List<Data> getData() {
return graphe.data;
}
public int getDecalage() {
return graphe.decalage;
}
private static class Etat {
public String valeur;
}
public static class Graphe {
public int puissanceSouscrite;
public int decalage;
public Periode periode;
public List<Data> data = new ArrayList<>();
}
private static class Periode {
public String dateDebut;
public String dateFin;
}
public static class Data {
public double valeur;
public int ordre;
public boolean isPositive() {
return valeur > 0;
}
}
}

View File

@@ -0,0 +1,39 @@
/**
* Copyright (c) 2010-2020 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.linky.internal.model;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The {@link LinkyTimeScale} enumerates all possible time scale
* for API queries
*
* @author Gaël L'hopital - Initial contribution
*/
@NonNullByDefault
public enum LinkyTimeScale {
HOURLY("urlCdcHeure"),
DAILY("urlCdcJour"),
MONTHLY("urlCdcMois"),
YEARLY("urlCdcAn");
private String id;
private LinkyTimeScale(String id) {
this.id = id;
}
public String getId() {
return this.id;
}
}

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<binding:binding id="linky" 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>Linky Binding</name>
<description>Retrieves your energy consumption data from Enedis website</description>
<author>Gaël L'hopital</author>
</binding:binding>

View File

@@ -0,0 +1,90 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="linky"
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">
<!-- Linky Thing -->
<thing-type id="linky">
<label>Linky</label>
<description>
Provides your energy consumption data.
In order to receive the data, you must activate your account at
https://espace-client-particuliers.enedis.fr/web/espace-particuliers/compteur-linky.
</description>
<channel-groups>
<channel-group typeId="daily" id="daily"/>
<channel-group typeId="weekly" id="weekly"/>
<channel-group typeId="monthly" id="monthly"/>
<channel-group typeId="yearly" id="yearly"/>
</channel-groups>
<config-description>
<parameter name="username" type="text" required="true">
<label>Username</label>
<context>email</context>
<description>Your Enedis Username</description>
</parameter>
<parameter name="password" type="text" required="true">
<label>Password</label>
<context>password</context>
<description>Your Enedis Password</description>
</parameter>
</config-description>
</thing-type>
<channel-group-type id="daily">
<label>Daily consumption</label>
<channels>
<channel id="yesterday" typeId="consumption">
<label>Yesterday Consumption</label>
</channel>
</channels>
</channel-group-type>
<channel-group-type id="weekly">
<label>Weekly consumption</label>
<channels>
<channel id="thisWeek" typeId="consumption">
<label>Current Week Consumption</label>
</channel>
<channel id="lastWeek" typeId="consumption">
<label>Last Week Consumption</label>
</channel>
</channels>
</channel-group-type>
<channel-group-type id="monthly">
<label>Monthly consumption</label>
<channels>
<channel id="thisMonth" typeId="consumption">
<label>Current Month Consumption</label>
</channel>
<channel id="lastMonth" typeId="consumption">
<label>Last Month Consumption</label>
</channel>
</channels>
</channel-group-type>
<channel-group-type id="yearly">
<label>Yearly consumption</label>
<channels>
<channel id="thisYear" typeId="consumption">
<label>Current Year Consumption</label>
</channel>
<channel id="lastYear" typeId="consumption">
<label>Last Year Consumption</label>
</channel>
</channels>
</channel-group-type>
<channel-type id="consumption">
<item-type>Number:Energy</item-type>
<label>Total Consumption</label>
<description>Consumption at given time interval</description>
<state readOnly="true" pattern="%.3f %unit%"></state>
</channel-type>
</thing:thing-descriptions>