added migrated 2.x add-ons
Signed-off-by: Kai Kreuzer <kai@openhab.org>
This commit is contained in:
@@ -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>
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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";
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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"));
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user