[Linky] Linky issue 7610 (#8678)

* Staging work

* Refactoring the binding for OH3
Adressing Issue #7610
Added new channels

* spotless apply
* Pleasing Travis
* Code review and added disconnection logic.
* Adressing code review comments

Signed-off-by: clinique <gael@lhopital.org>
This commit is contained in:
Gaël L'hopital 2020-10-15 19:58:38 +02:00 committed by GitHub
parent f90f91ff1f
commit 9b7fb69e8d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 772 additions and 379 deletions

View File

@ -14,12 +14,7 @@ https://github.com/openhab/openhab-addons
== Third-party Content
okhttp
* License: Apache License 2.0
* Project: https://square.github.io/okhttp/
* Source: https://github.com/square/okhttp
okio
* License: Apache 2.0 License
* Project: https://square.github.io/okio/2.x/okio/jvm/okio
* Source: https://github.com/square/okio
jsoup
* License: MIT License
* Project: https://jsoup.org/
* Source: https://github.com/jhy/jsoup

View File

@ -23,24 +23,40 @@ The binding has no configuration options, all configuration is done at Thing lev
The thing has the following configuration parameters:
| Parameter | Description |
|-----------------|--------------------------------|
| username | Your Enedis platform username. |
| password | Your Enedis platform password. |
| Parameter | Description |
|----------------|--------------------------------|
| username | Your Enedis platform username. |
| password | Your Enedis platform password. |
| internalAuthId | The internal authID |
This version is now compatible with the new API of Enedis (deployed from june 2020).
To avoid the captcha login, it is necessary to log before on a classical browser (e.g Chrome, Firefox) and to retrieve the user cookies (internalAuthId).
Instructions given for Firefox :
1. Go to https://mon-compte-client.enedis.fr/.
2. Select "Particulier" in the drop down list and click on the "Connexion" button.
3. You'll be redirected to a page where you'll have to enter you Enedis account email address and check the "Je ne suis pas un robot" checkbox.
4. Clic on "Suivant".
5. In the login page, prefilled with your mail address, enter your Enedis account password and click on "Connexion à Espace Client Enedis".
6. You will be directed to your Enedis account environment. Get back to previous page in you browser.
7. Open the developper tool window (F12) and select "Stockage" tab. In the "Cookies" entry, select "https://mon-compte-enedis.fr". You should see an entry named "internalAuthId", copy this value in your Openhab configuration.
## Channels
The information that is retrieved is available as these channels:
| Channel ID | Item Type | Description |
|-------------------|---------------|----------------------------|
| daily#yesterday | Number:Energy | Yesterday energy usage |
| weekly#thisWeek | Number:Energy | Current week energy usage |
| weekly#lastWeek | Number:Energy | Last week energy usage |
| monthly#thisMonth | Number:Energy | Current month energy usage |
| monthly#lastMonth | Number:Energy | Last month energy usage |
| yearly#thisYear | Number:Energy | Current year energy usage |
| yearly#lastYear | Number:Energy | Last year energy usage |
| Channel ID | Item Type | Description |
|-------------------|---------------|------------------------------|
| daily#yesterday | Number:Energy | Yesterday energy usage |
| daily#power | Number:Power | Yesterday's peak power usage |
| daily#timestamp | DateTime | Timestamp of the power peak |
| weekly#thisWeek | Number:Energy | Current week energy usage |
| weekly#lastWeek | Number:Energy | Last week energy usage |
| monthly#thisMonth | Number:Energy | Current month energy usage |
| monthly#lastMonth | Number:Energy | Last month energy usage |
| yearly#thisYear | Number:Energy | Current year energy usage |
| yearly#lastYear | Number:Energy | Last year energy usage |
## Console Commands
@ -70,10 +86,10 @@ Thing linky:linky:local "Compteur Linky" [ username="example@domaine.fr", passwo
```
Number:Energy ConsoHier "Conso hier [%.0f %unit%]" <energy> { channel="linky:linky:local:daily#yesterday" }
Number:Energy ConsoSemaineEnCours "Conso semaine en cours [%.0f %unit%]" <energy> { channel="linky:linky:local:weekly#thisWeek" }
Number:Energy ConsoSemaineEnCours "Conso cette semaine [%.0f %unit%]" <energy> { channel="linky:linky:local:weekly#thisWeek" }
Number:Energy ConsoSemaineDerniere "Conso semaine dernière [%.0f %unit%]" <energy> { channel="linky:linky:local:weekly#lastWeek" }
Number:Energy ConsoMoisEnCours "Conso mois en cours [%.0f %unit%]" <energy> { channel="linky:linky:local:monthly#thisMonth" }
Number:Energy ConsoMoisEnCours "Conso ce mois [%.0f %unit%]" <energy> { channel="linky:linky:local:monthly#thisMonth" }
Number:Energy ConsoMoisDernier "Conso mois dernier [%.0f %unit%]" <energy> { channel="linky:linky:local:monthly#lastMonth" }
Number:Energy ConsoAnneeEnCours "Conso année en cours [%.0f %unit%]" <energy> { channel="linky:linky:local:yearly#thisYear" }
Number:Energy ConsoAnneeEnCours "Conso cette année [%.0f %unit%]" <energy> { channel="linky:linky:local:yearly#thisYear" }
Number:Energy ConsoAnneeDerniere "Conso année dernière [%.0f %unit%]" <energy> { channel="linky:linky:local:yearly#lastYear" }
```

View File

@ -14,22 +14,13 @@
<name>openHAB Add-ons :: Bundles :: Linky Binding</name>
<properties>
<bnd.importpackage>!android.*,!com.android.org.*,!dalvik.*,!javax.annotation.meta.*,!org.apache.harmony.*,!org.conscrypt.*,!sun.*</bnd.importpackage>
</properties>
<dependencies>
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>3.12.3</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.squareup.okio</groupId>
<artifactId>okio</artifactId>
<version>1.15.0</version>
<groupId>org.jsoup</groupId>
<artifactId>jsoup</artifactId>
<version>1.8.3</version>
<scope>compile</scope>
</dependency>
</dependencies>
</project>

View File

@ -4,6 +4,7 @@
<feature name="openhab-binding-linky" description="Linky Binding" version="${project.version}">
<feature>openhab-runtime-base</feature>
<bundle dependency="true">mvn:org.jsoup/jsoup/1.8.3</bundle>
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.linky/${project.version}</bundle>
</feature>
</features>

View File

@ -29,8 +29,15 @@ public class LinkyBindingConstants {
// List of all Thing Type UIDs
public static final ThingTypeUID THING_TYPE_LINKY = new ThingTypeUID(BINDING_ID, "linky");
// Thing properties
public static final String PUISSANCE = "puissance";
public static final String PRM_ID = "prmId";
public static final String USER_ID = "av2_interne_id";
// List of all Channel id's
public static final String YESTERDAY = "daily#yesterday";
public static final String PEAK_POWER = "daily#power";
public static final String PEAK_TIMESTAMP = "daily#timestamp";
public static final String THIS_WEEK = "weekly#thisWeek";
public static final String LAST_WEEK = "weekly#lastWeek";
public static final String THIS_MONTH = "monthly#thisMonth";

View File

@ -19,6 +19,8 @@ package org.openhab.binding.linky.internal;
* @author Gaël L'hopital - Initial contribution
*/
public class LinkyConfiguration {
public static final String INTERNAL_AUTH_ID = "internalAuthId";
public String username;
public String password;
public String internalAuthId;
}

View File

@ -10,30 +10,29 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.linky.internal.model;
package org.openhab.binding.linky.internal;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The {@link LinkyTimeScale} enumerates all possible time scale
* for API queries
* Will be thrown for cloud errors
*
* @author Gaël L'hopital - Initial contribution
*/
@NonNullByDefault
public enum LinkyTimeScale {
HOURLY("urlCdcHeure"),
DAILY("urlCdcJour"),
MONTHLY("urlCdcMois"),
YEARLY("urlCdcAn");
public class LinkyException extends Exception {
private String id;
private static final long serialVersionUID = 3703839284673384018L;
private LinkyTimeScale(String id) {
this.id = id;
public LinkyException() {
super();
}
public String getId() {
return this.id;
public LinkyException(String message) {
super(message);
}
public LinkyException(String message, Exception e) {
super(message, e);
}
}

View File

@ -14,10 +14,15 @@ package org.openhab.binding.linky.internal;
import static org.openhab.binding.linky.internal.LinkyBindingConstants.THING_TYPE_LINKY;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.client.HttpClient;
import org.openhab.binding.linky.internal.handler.LinkyHandler;
import org.openhab.core.i18n.LocaleProvider;
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;
@ -27,6 +32,10 @@ import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonDeserializer;
/**
* The {@link LinkyHandlerFactory} is responsible for creating things handlers.
*
@ -35,25 +44,33 @@ import org.osgi.service.component.annotations.Reference;
@NonNullByDefault
@Component(service = ThingHandlerFactory.class, configurationPid = "binding.linky")
public class LinkyHandlerFactory extends BaseThingHandlerFactory {
private static final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("uuuu-MM-dd'T'HH:mm:ss.SSSX");
private final LocaleProvider localeProvider;
private final Gson gson;
private final HttpClient httpClient;
@Activate
public LinkyHandlerFactory(final @Reference LocaleProvider localeProvider) {
public LinkyHandlerFactory(final @Reference LocaleProvider localeProvider,
final @Reference HttpClientFactory httpClientFactory) {
this.localeProvider = localeProvider;
this.httpClient = httpClientFactory.createHttpClient(LinkyBindingConstants.BINDING_ID);
this.gson = new GsonBuilder().registerTypeAdapter(ZonedDateTime.class,
(JsonDeserializer<ZonedDateTime>) (json, type, jsonDeserializationContext) -> ZonedDateTime
.parse(json.getAsJsonPrimitive().getAsString(), formatter))
.create();
}
@Override
public boolean supportsThingType(ThingTypeUID thingTypeUID) {
return thingTypeUID.equals(THING_TYPE_LINKY);
return THING_TYPE_LINKY.equals(thingTypeUID);
}
@Override
protected @Nullable ThingHandler createHandler(Thing thing) {
ThingTypeUID thingTypeUID = thing.getThingTypeUID();
if (thingTypeUID.equals(THING_TYPE_LINKY)) {
return new LinkyHandler(thing, localeProvider);
if (supportsThingType(thingTypeUID)) {
return new LinkyHandler(thing, localeProvider, gson, httpClient);
}
return null;

View File

@ -0,0 +1,252 @@
/**
* 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.api;
import java.net.CookieStore;
import java.net.HttpCookie;
import java.net.URI;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeoutException;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.client.api.ContentResponse;
import org.eclipse.jetty.client.util.FormContentProvider;
import org.eclipse.jetty.client.util.StringContentProvider;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.util.Fields;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.openhab.binding.linky.internal.LinkyConfiguration;
import org.openhab.binding.linky.internal.LinkyException;
import org.openhab.binding.linky.internal.dto.AuthData;
import org.openhab.binding.linky.internal.dto.AuthResult;
import org.openhab.binding.linky.internal.dto.ConsumptionReport;
import org.openhab.binding.linky.internal.dto.ConsumptionReport.Consumption;
import org.openhab.binding.linky.internal.dto.PrmInfo;
import org.openhab.binding.linky.internal.dto.UserInfo;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gson.Gson;
/**
* {@link EnedisHttpApi} wraps the Enedis Webservice.
*
* @author Gaël L'hopital - Initial contribution
*/
@NonNullByDefault
public class EnedisHttpApi {
private static final DateTimeFormatter API_DATE_FORMAT = DateTimeFormatter.ofPattern("dd-MM-yyyy");
private static final String URL_APPS_LINCS = "https://apps.lincs.enedis.fr";
private static final String URL_MON_COMPTE = "https://mon-compte.enedis.fr";
private static final String URL_ENEDIS_AUTHENTICATE = URL_APPS_LINCS
+ "/authenticate?target=https://mon-compte-particulier.enedis.fr/suivi-de-mesure/";
private static final String URL_COOKIE = "https://mon-compte-particulier.enedis.fr";
private final Logger logger = LoggerFactory.getLogger(EnedisHttpApi.class);
private final Gson gson;
private final HttpClient httpClient;
private final LinkyConfiguration config;
private boolean connected = false;
public EnedisHttpApi(LinkyConfiguration config, Gson gson, HttpClient httpClient) {
this.gson = gson;
this.httpClient = httpClient;
this.config = config;
}
public void initialize() throws LinkyException {
httpClient.getSslContextFactory().setExcludeCipherSuites(new String[0]);
httpClient.setFollowRedirects(false);
try {
httpClient.start();
} catch (Exception e) {
throw new LinkyException("Unable to start Jetty HttpClient", e);
}
connect();
}
private void connect() throws LinkyException {
addCookie(LinkyConfiguration.INTERNAL_AUTH_ID, config.internalAuthId);
logger.debug("Starting login process for user : {}", config.username);
try {
logger.debug("Step 1 : getting authentification");
String data = getData(URL_ENEDIS_AUTHENTICATE);
logger.debug("Reception request SAML");
Document htmlDocument = Jsoup.parse(data);
Element el = htmlDocument.select("form").first();
Element samlInput = el.select("input[name=SAMLRequest]").first();
logger.debug("Step 2 : send SSO SAMLRequest");
ContentResponse result = httpClient.POST(el.attr("action"))
.content(getFormContent("SAMLRequest", samlInput.attr("value"))).send();
if (result.getStatus() != 302) {
throw new LinkyException("Connection failed step 2");
}
logger.debug("Get the location and the ReqID");
Pattern p = Pattern.compile("ReqID%(.*?)%26");
Matcher m = p.matcher(getLocation(result));
if (!m.find()) {
throw new LinkyException("Unable to locate ReqId in header");
}
String reqId = m.group(1);
String url = URL_MON_COMPTE
+ "/auth/json/authenticate?realm=/enedis&forward=true&spEntityID=SP-ODW-PROD&goto=/auth/SSOPOST/metaAlias/enedis/providerIDP?ReqID%"
+ reqId
+ "%26index%3Dnull%26acsURL%3Dhttps://apps.lincs.enedis.fr/saml/SSO%26spEntityID%3DSP-ODW-PROD%26binding%3Durn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST&AMAuthCookie=";
logger.debug(
"Step 3 : auth1 - retrieve the template, thanks to cookie internalAuthId, user is already set");
result = httpClient.POST(url).send();
if (result.getStatus() != 200) {
throw new LinkyException("Connection failed step 3 - auth1 : " + result.getContentAsString());
}
AuthData authData = gson.fromJson(result.getContentAsString(), AuthData.class);
if (authData.callbacks.size() < 2 || authData.callbacks.get(0).input.size() == 0
|| authData.callbacks.get(1).input.size() == 0
|| !config.username.contentEquals(authData.callbacks.get(0).input.get(0).valueAsString())) {
throw new LinkyException("Authentication error, the authentication_cookie is probably wrong");
}
authData.callbacks.get(1).input.get(0).value = config.password;
url = "https://mon-compte.enedis.fr/auth/json/authenticate?realm=/enedis&spEntityID=SP-ODW-PROD&goto=/auth/SSOPOST/metaAlias/enedis/providerIDP?ReqID%"
+ reqId
+ "%26index%3Dnull%26acsURL%3Dhttps://apps.lincs.enedis.fr/saml/SSO%26spEntityID%3DSP-ODW-PROD%26binding%3Durn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST&AMAuthCookie=";
logger.debug("Step 3 : auth2 - send the auth data");
result = httpClient.POST(url).header(HttpHeader.CONTENT_TYPE, "application/json")
.content(new StringContentProvider(gson.toJson(authData))).send();
if (result.getStatus() != 200) {
throw new LinkyException("Connection failed step 3 - auth2 : " + result.getContentAsString());
}
AuthResult authResult = gson.fromJson(result.getContentAsString(), AuthResult.class);
logger.debug("Add the tokenId cookie");
addCookie("enedisExt", authResult.tokenId);
logger.debug("Step 4 : retrieve the SAMLresponse");
data = getData(URL_MON_COMPTE + "/" + authResult.successUrl);
htmlDocument = Jsoup.parse(data);
el = htmlDocument.select("form").first();
samlInput = el.select("input[name=SAMLResponse]").first();
logger.debug("Step 5 : post the SAMLresponse to finish the authentication");
result = httpClient.POST(el.attr("action")).content(getFormContent("SAMLResponse", samlInput.attr("value")))
.send();
if (result.getStatus() != 302) {
throw new LinkyException("Connection failed step 5");
}
connected = true;
} catch (InterruptedException | TimeoutException | ExecutionException e) {
throw new LinkyException("Error opening connection with Enedis webservice", e);
}
}
public String getLocation(ContentResponse response) {
return response.getHeaders().get(HttpHeader.LOCATION);
}
public void disconnect() throws LinkyException {
if (connected) {
try { // Three times in a row to get disconnected
String location = getLocation(httpClient.GET(URL_APPS_LINCS + "/logout"));
location = getLocation(httpClient.GET(location));
location = getLocation(httpClient.GET(location));
CookieStore cookieStore = httpClient.getCookieStore();
cookieStore.removeAll();
connected = false;
} catch (InterruptedException | ExecutionException | TimeoutException e) {
throw new LinkyException("Error while disconnecting from Enedis webservice", e);
}
}
}
public void dispose() throws LinkyException {
try {
disconnect();
httpClient.stop();
} catch (Exception e) {
throw new LinkyException("Error stopping Jetty client", e);
}
}
private void addCookie(String key, String value) {
CookieStore cookieStore = httpClient.getCookieStore();
HttpCookie cookie = new HttpCookie(key, value);
cookie.setDomain(".enedis.fr");
cookie.setPath("/");
cookieStore.add(URI.create(URL_COOKIE), cookie);
}
private FormContentProvider getFormContent(String fieldName, String fieldValue) {
Fields fields = new Fields();
fields.put(fieldName, fieldValue);
return new FormContentProvider(fields);
}
private String getData(String url) throws LinkyException {
try {
ContentResponse result = httpClient.GET(url);
if (result.getStatus() != 200) {
throw new LinkyException(String.format("Error requesting '%s' : %s", url, result.getContentAsString()));
}
return result.getContentAsString();
} catch (InterruptedException | ExecutionException | TimeoutException e) {
throw new LinkyException(String.format("Error getting url : '%s'", url), e);
}
}
public PrmInfo getPrmInfo() throws LinkyException {
final String prm_info_url = URL_APPS_LINCS + "/mes-mesures/api/private/v1/personnes/null/prms";
String data = getData(prm_info_url);
PrmInfo[] prms = gson.fromJson(data, PrmInfo[].class);
return prms[0];
}
public UserInfo getUserInfo() throws LinkyException {
final String user_info_url = URL_APPS_LINCS + "/userinfos";
String data = getData(user_info_url);
return gson.fromJson(data, UserInfo.class);
}
private Consumption getMeasures(String userId, String prmId, LocalDate from, LocalDate to, String request)
throws LinkyException {
final String measure_url = URL_APPS_LINCS
+ "/mes-mesures/api/private/v1/personnes/%s/prms/%s/donnees-%s?dateDebut=%s&dateFin=%s&mesuretypecode=CONS";
String url = String.format(measure_url, userId, prmId, request, from.format(API_DATE_FORMAT),
to.format(API_DATE_FORMAT));
String data = getData(url);
ConsumptionReport report = gson.fromJson(data, ConsumptionReport.class);
return report.firstLevel.consumptions;
}
public Consumption getEnergyData(String userId, String prmId, LocalDate from, LocalDate to) throws LinkyException {
return getMeasures(userId, prmId, from, to, "energie");
}
public Consumption getPowerData(String userId, String prmId, LocalDate from, LocalDate to) throws LinkyException {
return getMeasures(userId, prmId, from, to, "pmax");
}
}

View File

@ -10,7 +10,7 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.linky.internal;
package org.openhab.binding.linky.internal.api;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
@ -71,7 +71,7 @@ public class ExpiringDayCache<V> {
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);
logger.debug("getValue from cache \"{}\" is returning a cached value", name);
}
return cachedValue;
}

View File

@ -19,6 +19,7 @@ import java.util.Arrays;
import java.util.List;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.linky.internal.LinkyBindingConstants;
import org.openhab.binding.linky.internal.handler.LinkyHandler;
import org.openhab.core.io.console.Console;
import org.openhab.core.io.console.extensions.AbstractConsoleCommandExtension;
@ -47,7 +48,7 @@ public class LinkyCommandExtension extends AbstractConsoleCommandExtension {
@Activate
public LinkyCommandExtension(final @Reference ThingRegistry thingRegistry) {
super("linky", "Interact with the Linky binding.");
super(LinkyBindingConstants.BINDING_ID, "Interact with the Linky binding.");
this.thingRegistry = thingRegistry;
}
@ -70,13 +71,13 @@ public class LinkyCommandExtension extends AbstractConsoleCommandExtension {
}
}
if (thing == null) {
console.println("Bad thing id '" + args[0] + "'");
console.println(String.format("Bad thing id '%s'", args[0]));
printUsage(console);
} else if (thingHandler == null) {
console.println("No handler initialized for the thing id '" + args[0] + "'");
console.println(String.format("No handler initialized for the thing id '%s'", args[0]));
printUsage(console);
} else if (handler == null) {
console.println("'" + args[0] + "' is not a Linky thing id");
console.println(String.format("'%s' is not a Linky thing id", args[0]));
printUsage(console);
} else if (REPORT.equals(args[1])) {
LocalDate now = LocalDate.now();
@ -87,8 +88,8 @@ public class LinkyCommandExtension extends AbstractConsoleCommandExtension {
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");
console.println(String
.format("Invalid format for start day '%s'; expected format is YYYY-MM-DD", args[2]));
printUsage(console);
return;
}
@ -97,7 +98,8 @@ public class LinkyCommandExtension extends AbstractConsoleCommandExtension {
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");
console.println(String.format("Invalid format for end day '%s'; expected format is YYYY-MM-DD",
args[3]));
printUsage(console);
return;
}
@ -124,7 +126,8 @@ public class LinkyCommandExtension extends AbstractConsoleCommandExtension {
@Override
public List<String> getUsages() {
return Arrays.asList(buildCommandUsage("<thingUID> " + REPORT + " <start day> <end day> [<separator>]",
"report daily consumptions between two dates"));
return Arrays
.asList(buildCommandUsage(String.format("<thingUID> %s <start day> <end day> [<separator>]", REPORT),
"report daily consumptions between two dates"));
}
}

View File

@ -0,0 +1,51 @@
/**
* 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.dto;
import java.util.ArrayList;
import java.util.List;
import org.eclipse.jdt.annotation.Nullable;
/**
* The {@link AuthData} holds authentication information
*
* @author Gaël L'hopital - Initial contribution
*/
public class AuthData {
public class AuthDataCallBack {
public class NameValuePair {
public String name;
public Object value;
public @Nullable String valueAsString() {
if (value instanceof String) {
return (String) value;
}
return null;
}
}
public String type;
public List<NameValuePair> output = new ArrayList<>();
public List<NameValuePair> input = new ArrayList<>();
}
public String authId;
public String template;
public String stage;
public String header;
public List<AuthDataCallBack> callbacks = new ArrayList<>();
}

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.dto;
/**
* The {@link AuthResult} holds informations about the ongoing authentication process
*
* @author Gaël L'hopital - Initial contribution
*/
public class AuthResult {
public String successUrl;
public String tokenId;
}

View File

@ -0,0 +1,64 @@
/**
* 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.dto;
import java.time.ZonedDateTime;
import java.util.List;
import com.google.gson.annotations.SerializedName;
/**
* The {@link ConsumptionReport} is responsible for holding values
* returned by API calls
*
* @author Gaël L'hopital - Initial contribution
*/
public class ConsumptionReport {
public class Period {
public String grandeurPhysiqueEnum;
public ZonedDateTime dateDebut;
public ZonedDateTime dateFin;
}
public class Aggregate {
public List<String> labels;
public List<Period> periodes;
public List<Double> datas;
}
public class ChronoData {
@SerializedName("JOUR")
public Aggregate days;
@SerializedName("SEMAINE")
public Aggregate weeks;
@SerializedName("MOIS")
public Aggregate months;
@SerializedName("ANNEE")
public Aggregate years;
}
public class Consumption {
public ChronoData aggregats;
public String grandeurMetier;
public String grandeurPhysique;
public String unite;
}
public class FirstLevel {
@SerializedName("CONS")
public Consumption consumptions;
}
@SerializedName("1")
public FirstLevel firstLevel;
}

View File

@ -0,0 +1,47 @@
/**
* 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.dto;
/**
* The {@link UserInfo} holds informations about energy delivery point
*
* @author Gaël L'hopital - Initial contribution
*/
public class PrmInfo {
public class Adresse {
public Object adresseLigneUn;
public String adresseLigneDeux;
public Object adresseLigneTrois;
public String adresseLigneQuatre;
public Object adresseLigneCinq;
public String adresseLigneSix;
public String adresseLigneSept;
}
public String prmId;
public String dateFinRole;
public String segment;
public Adresse adresse;
public String typeCompteur;
public String niveauOuvertureServices;
public String communiquant;
public long dateSoutirage;
public String dateInjection;
public int departement;
public int puissanceSouscrite;
public String codeCalendrier;
public String codeTitulaire;
public boolean collecteActivee;
public boolean multiTitulaire;
}

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.dto;
import com.google.gson.annotations.SerializedName;
/**
* The {@link UserInfo} holds informations about the user account
*
* @author Gaël L'hopital - Initial contribution
*/
public class UserInfo {
public class UserProperties {
@SerializedName("av2_interne_id")
public String internId;
@SerializedName("av2_prenom")
public String firstName;
@SerializedName("av2_mail")
public String mail;
@SerializedName("av2_nom")
public String name;
@SerializedName("av2_infos_personnalisees")
public String personalInfo;
}
public String username;
public boolean connected;
public UserProperties userProperties;
}

View File

@ -1,46 +0,0 @@
/**
* 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

@ -13,28 +13,32 @@
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.HashMap;
import java.util.List;
import java.util.Map;
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.eclipse.jetty.client.HttpClient;
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.binding.linky.internal.LinkyException;
import org.openhab.binding.linky.internal.api.EnedisHttpApi;
import org.openhab.binding.linky.internal.api.ExpiringDayCache;
import org.openhab.binding.linky.internal.dto.ConsumptionReport.Aggregate;
import org.openhab.binding.linky.internal.dto.ConsumptionReport.Consumption;
import org.openhab.binding.linky.internal.dto.PrmInfo;
import org.openhab.binding.linky.internal.dto.UserInfo;
import org.openhab.core.i18n.LocaleProvider;
import org.openhab.core.library.types.DateTimeType;
import org.openhab.core.library.types.QuantityType;
import org.openhab.core.library.unit.SmartHomeUnits;
import org.openhab.core.thing.ChannelUID;
@ -49,13 +53,6 @@ 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
@ -68,189 +65,181 @@ import okhttp3.Response;
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 final HttpClient httpClient;
private final Gson gson;
private @NonNullByDefault({}) ScheduledFuture<?> refreshJob;
private @Nullable ScheduledFuture<?> refreshJob;
private @Nullable EnedisHttpApi enedisApi;
private final WeekFields weekFields;
private final ExpiringDayCache<LinkyConsumptionData> cachedDaylyData;
private final ExpiringDayCache<LinkyConsumptionData> cachedMonthlyData;
private final ExpiringDayCache<LinkyConsumptionData> cachedYearlyData;
private final ExpiringDayCache<Consumption> cachedDaylyData;
private final ExpiringDayCache<Consumption> cachedPowerData;
private final ExpiringDayCache<Consumption> cachedMonthlyData;
private final ExpiringDayCache<Consumption> cachedYearlyData;
public LinkyHandler(Thing thing, LocaleProvider localeProvider) {
private @NonNullByDefault({}) String prmId;
private @NonNullByDefault({}) String userId;
public LinkyHandler(Thing thing, LocaleProvider localeProvider, Gson gson, HttpClient httpClient) {
super(thing);
this.gson = gson;
this.httpClient = httpClient;
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);
});
this.cachedDaylyData = new ExpiringDayCache<>("daily cache", REFRESH_FIRST_HOUR_OF_DAY, () -> {
LocalDate today = LocalDate.now();
return getConsumptionData(today.minusDays(13), today);
});
this.cachedPowerData = new ExpiringDayCache<>("power cache", REFRESH_FIRST_HOUR_OF_DAY, () -> {
LocalDate to = LocalDate.now().plusDays(1);
LocalDate from = to.minusDays(2);
return getPowerData(from, to);
});
this.cachedMonthlyData = new ExpiringDayCache<>("monthly cache", REFRESH_FIRST_HOUR_OF_DAY, () -> {
LocalDate today = LocalDate.now();
return getConsumptionData(today.withDayOfMonth(1).minusMonths(1), today);
});
this.cachedYearlyData = new ExpiringDayCache<>("yearly cache", REFRESH_FIRST_HOUR_OF_DAY, () -> {
LocalDate today = LocalDate.now();
return getConsumptionData(LocalDate.of(today.getYear() - 1, 1, 1), today);
});
}
@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);
enedisApi = new EnedisHttpApi(config, gson, httpClient);
try {
enedisApi.initialize();
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;
if (thing.getProperties().isEmpty()) {
Map<String, String> properties = discoverAttributes();
updateProperties(properties);
}
prmId = thing.getProperties().get(PRM_ID);
userId = thing.getProperties().get(USER_ID);
final LocalDateTime now = LocalDateTime.now();
final LocalDateTime nextDayFirstTimeUpdate = now.plusDays(1).withHour(REFRESH_FIRST_HOUR_OF_DAY)
.truncatedTo(ChronoUnit.HOURS);
updateData();
refreshJob = scheduler.scheduleWithFixedDelay(this::updateData,
ChronoUnit.MINUTES.between(now, nextDayFirstTimeUpdate) % REFRESH_INTERVAL_IN_MIN + 1,
REFRESH_INTERVAL_IN_MIN, TimeUnit.MINUTES);
} catch (LinkyException e) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
}
}
private Map<String, String> discoverAttributes() throws LinkyException {
Map<String, String> properties = new HashMap<>();
EnedisHttpApi api = this.enedisApi;
if (api != null) {
PrmInfo prmInfo = api.getPrmInfo();
UserInfo userInfo = api.getUserInfo();
properties.put(USER_ID, userInfo.userProperties.internId);
properties.put(PUISSANCE, prmInfo.puissanceSouscrite + " kVA");
properties.put(PRM_ID, prmInfo.prmId);
}
return properties;
}
/**
* Request new data and updates channels
*/
private void updateData() {
updatePowerData();
updateDailyData();
updateMonthlyData();
updateYearlyData();
}
private synchronized void updatePowerData() {
if (isLinked(PEAK_POWER) || isLinked(PEAK_TIMESTAMP)) {
Consumption result = cachedPowerData.getValue();
if (result != null) {
updateVAChannel(PEAK_POWER, result.aggregats.days.datas.get(0));
updateState(PEAK_TIMESTAMP, new DateTimeType(result.aggregats.days.periodes.get(0).dateDebut));
}
}
}
/**
* Request new dayly/weekly data and updates channels
*/
private synchronized void updateDailyData() {
if (!isLinked(YESTERDAY) && !isLinked(LAST_WEEK) && !isLinked(THIS_WEEK)) {
return;
}
if (isLinked(YESTERDAY) || isLinked(LAST_WEEK) || isLinked(THIS_WEEK)) {
Consumption result = cachedDaylyData.getValue();
if (result != null) {
Aggregate days = result.aggregats.days;
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 maxValue = days.periodes.size() - 1;
int thisWeekNumber = days.periodes.get(maxValue).dateDebut.get(weekFields.weekOfWeekBasedYear());
double yesterday = days.datas.get(maxValue);
double lastWeek = 0.0;
double thisWeek = 0.0;
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);
for (int i = maxValue; i >= 0; i--) {
int weekNumber = days.periodes.get(i).dateDebut.get(weekFields.weekOfWeekBasedYear());
if (weekNumber == thisWeekNumber) {
thisWeek += days.datas.get(i);
} else if (weekNumber == thisWeekNumber - 1) {
lastWeek += days.datas.get(i);
} else {
thisWeek += consumption;
logger.trace("Consumption at index {} added to current week: {}", jump, consumption);
break;
}
yesterday = consumption;
}
jump++;
rangeStart = rangeStart.plusDays(1);
updateKwhChannel(YESTERDAY, yesterday);
updateKwhChannel(THIS_WEEK, thisWeek);
updateKwhChannel(LAST_WEEK, lastWeek);
}
} 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;
if (isLinked(LAST_MONTH) || isLinked(THIS_MONTH)) {
Consumption result = cachedMonthlyData.getValue();
if (result != null) {
Aggregate months = result.aggregats.months;
updateKwhChannel(LAST_MONTH, months.datas.get(0));
updateKwhChannel(THIS_MONTH, months.datas.get(1));
}
} 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;
if (isLinked(LAST_YEAR) || isLinked(THIS_YEAR)) {
Consumption result = cachedYearlyData.getValue();
if (result != null) {
Aggregate years = result.aggregats.years;
updateKwhChannel(LAST_YEAR, years.datas.get(0));
updateKwhChannel(THIS_YEAR, years.datas.get(1));
}
}
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) {
@ -260,6 +249,12 @@ public class LinkyHandler extends BaseThingHandler {
: UnDefType.UNDEF);
}
private void updateVAChannel(String channelId, double power) {
logger.debug("Update channel {} with {}", channelId, power);
updateState(channelId,
!Double.isNaN(power) ? new QuantityType<>(power, SmartHomeUnits.VOLT_AMPERE) : UnDefType.UNDEF);
}
/**
* Produce a report of all daily values between two dates
*
@ -273,19 +268,16 @@ public class LinkyHandler extends BaseThingHandler {
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;
Consumption result = getConsumptionData(startDay, endDay);
if (result != null) {
Aggregate days = result.aggregats.days;
for (int i = 0; i < days.datas.size(); i++) {
double consumption = days.datas.get(i);
String line = days.periodes.get(i).dateDebut.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;
@ -309,54 +301,46 @@ public class LinkyHandler extends BaseThingHandler {
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);
}
private @Nullable Consumption getConsumptionData(LocalDate from, LocalDate to) {
EnedisHttpApi api = this.enedisApi;
if (api != null) {
try {
return api.getEnergyData(userId, prmId, from, to);
} catch (LinkyException e) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, e.getMessage());
}
} 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 null;
}
private @Nullable Consumption getPowerData(LocalDate from, LocalDate to) {
EnedisHttpApi api = this.enedisApi;
if (api != null) {
try {
return api.getPowerData(userId, prmId, from, to);
} catch (LinkyException e) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, e.getMessage());
}
}
return result;
return null;
}
@Override
public void dispose() {
logger.debug("Disposing the Linky handler.");
if (refreshJob != null && !refreshJob.isCancelled()) {
refreshJob.cancel(true);
ScheduledFuture<?> job = this.refreshJob;
if (job != null && !job.isCancelled()) {
job.cancel(true);
refreshJob = null;
}
EnedisHttpApi api = this.enedisApi;
if (api != null) {
try {
api.dispose();
enedisApi = null;
} catch (LinkyException ignore) {
}
}
}
@Override

View File

@ -1,72 +0,0 @@
/**
* 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

@ -4,7 +4,6 @@
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>
@ -31,6 +30,10 @@
<context>password</context>
<description>Your Enedis Password</description>
</parameter>
<parameter name="internalAuthId" type="text" required="true">
<label>Auth ID</label>
<description>Authentication ID delivered after the captcha (see documentation).</description>
</parameter>
</config-description>
</thing-type>
@ -40,6 +43,8 @@
<channel id="yesterday" typeId="consumption">
<label>Yesterday Consumption</label>
</channel>
<channel id="power" typeId="power"/>
<channel id="timestamp" typeId="timestamp"/>
</channels>
</channel-group-type>
@ -47,10 +52,10 @@
<label>Weekly consumption</label>
<channels>
<channel id="thisWeek" typeId="consumption">
<label>Current Week Consumption</label>
<label>This Week Consumption</label>
</channel>
<channel id="lastWeek" typeId="consumption">
<label>Last Week Consumption</label>
<label>Maximum power usage yesterday</label>
</channel>
</channels>
</channel-group-type>
@ -59,7 +64,7 @@
<label>Monthly consumption</label>
<channels>
<channel id="thisMonth" typeId="consumption">
<label>Current Month Consumption</label>
<label>This Month Consumption</label>
</channel>
<channel id="lastMonth" typeId="consumption">
<label>Last Month Consumption</label>
@ -71,7 +76,7 @@
<label>Yearly consumption</label>
<channels>
<channel id="thisYear" typeId="consumption">
<label>Current Year Consumption</label>
<label>This Year Consumption</label>
</channel>
<channel id="lastYear" typeId="consumption">
<label>Last Year Consumption</label>
@ -79,7 +84,6 @@
</channels>
</channel-group-type>
<channel-type id="consumption">
<item-type>Number:Energy</item-type>
<label>Total Consumption</label>
@ -87,4 +91,18 @@
<state readOnly="true" pattern="%.3f %unit%"></state>
</channel-type>
<channel-type id="power">
<item-type>Number:Power</item-type>
<label>Peak Power</label>
<description>Maximum power usage yesterday</description>
<state readOnly="true" pattern="%.3f %unit%"></state>
</channel-type>
<channel-type id="timestamp">
<item-type>DateTime</item-type>
<label>Peak Timestamp</label>
<description>Maximum power usage timestamp</description>
<state readOnly="true">
</state>
</channel-type>
</thing:thing-descriptions>