diff --git a/bundles/org.openhab.binding.linky/NOTICE b/bundles/org.openhab.binding.linky/NOTICE index e2657a475c7..3e2c49e0050 100644 --- a/bundles/org.openhab.binding.linky/NOTICE +++ b/bundles/org.openhab.binding.linky/NOTICE @@ -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 \ No newline at end of file +jsoup +* License: MIT License +* Project: https://jsoup.org/ +* Source: https://github.com/jhy/jsoup \ No newline at end of file diff --git a/bundles/org.openhab.binding.linky/README.md b/bundles/org.openhab.binding.linky/README.md index a72a141f389..47b4c7ec3d2 100644 --- a/bundles/org.openhab.binding.linky/README.md +++ b/bundles/org.openhab.binding.linky/README.md @@ -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%]" { channel="linky:linky:local:daily#yesterday" } -Number:Energy ConsoSemaineEnCours "Conso semaine en cours [%.0f %unit%]" { channel="linky:linky:local:weekly#thisWeek" } +Number:Energy ConsoSemaineEnCours "Conso cette semaine [%.0f %unit%]" { channel="linky:linky:local:weekly#thisWeek" } Number:Energy ConsoSemaineDerniere "Conso semaine dernière [%.0f %unit%]" { channel="linky:linky:local:weekly#lastWeek" } -Number:Energy ConsoMoisEnCours "Conso mois en cours [%.0f %unit%]" { channel="linky:linky:local:monthly#thisMonth" } +Number:Energy ConsoMoisEnCours "Conso ce mois [%.0f %unit%]" { channel="linky:linky:local:monthly#thisMonth" } Number:Energy ConsoMoisDernier "Conso mois dernier [%.0f %unit%]" { channel="linky:linky:local:monthly#lastMonth" } -Number:Energy ConsoAnneeEnCours "Conso année en cours [%.0f %unit%]" { channel="linky:linky:local:yearly#thisYear" } +Number:Energy ConsoAnneeEnCours "Conso cette année [%.0f %unit%]" { channel="linky:linky:local:yearly#thisYear" } Number:Energy ConsoAnneeDerniere "Conso année dernière [%.0f %unit%]" { channel="linky:linky:local:yearly#lastYear" } ``` diff --git a/bundles/org.openhab.binding.linky/pom.xml b/bundles/org.openhab.binding.linky/pom.xml index deae54163f1..6c64ec6d1ca 100644 --- a/bundles/org.openhab.binding.linky/pom.xml +++ b/bundles/org.openhab.binding.linky/pom.xml @@ -14,22 +14,13 @@ openHAB Add-ons :: Bundles :: Linky Binding - - !android.*,!com.android.org.*,!dalvik.*,!javax.annotation.meta.*,!org.apache.harmony.*,!org.conscrypt.*,!sun.* - - - com.squareup.okhttp3 - okhttp - 3.12.3 - compile - - - com.squareup.okio - okio - 1.15.0 + org.jsoup + jsoup + 1.8.3 compile + diff --git a/bundles/org.openhab.binding.linky/src/main/feature/feature.xml b/bundles/org.openhab.binding.linky/src/main/feature/feature.xml index 6da7cc23747..20b9446d27a 100644 --- a/bundles/org.openhab.binding.linky/src/main/feature/feature.xml +++ b/bundles/org.openhab.binding.linky/src/main/feature/feature.xml @@ -4,6 +4,7 @@ openhab-runtime-base + mvn:org.jsoup/jsoup/1.8.3 mvn:org.openhab.addons.bundles/org.openhab.binding.linky/${project.version} diff --git a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/LinkyBindingConstants.java b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/LinkyBindingConstants.java index e93fb59913c..80ff7b314da 100644 --- a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/LinkyBindingConstants.java +++ b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/LinkyBindingConstants.java @@ -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"; diff --git a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/LinkyConfiguration.java b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/LinkyConfiguration.java index 95b8e2b27c1..022c064dfd9 100644 --- a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/LinkyConfiguration.java +++ b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/LinkyConfiguration.java @@ -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; } diff --git a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/model/LinkyTimeScale.java b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/LinkyException.java similarity index 56% rename from bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/model/LinkyTimeScale.java rename to bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/LinkyException.java index 6b71bc776ed..da5f80594a4 100644 --- a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/model/LinkyTimeScale.java +++ b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/LinkyException.java @@ -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); } } diff --git a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/LinkyHandlerFactory.java b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/LinkyHandlerFactory.java index 3fc963f01a2..59c7fbaf434 100644 --- a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/LinkyHandlerFactory.java +++ b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/LinkyHandlerFactory.java @@ -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) (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; diff --git a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/api/EnedisHttpApi.java b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/api/EnedisHttpApi.java new file mode 100644 index 00000000000..ba656ec6416 --- /dev/null +++ b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/api/EnedisHttpApi.java @@ -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"); + } +} diff --git a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/ExpiringDayCache.java b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/api/ExpiringDayCache.java similarity index 96% rename from bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/ExpiringDayCache.java rename to bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/api/ExpiringDayCache.java index 018c4e384a1..3d2720a43db 100644 --- a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/ExpiringDayCache.java +++ b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/api/ExpiringDayCache.java @@ -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 { 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; } diff --git a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/console/LinkyCommandExtension.java b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/console/LinkyCommandExtension.java index 4751fa21668..36a8ec9ab27 100644 --- a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/console/LinkyCommandExtension.java +++ b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/console/LinkyCommandExtension.java @@ -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 getUsages() { - return Arrays.asList(buildCommandUsage(" " + REPORT + " []", - "report daily consumptions between two dates")); + return Arrays + .asList(buildCommandUsage(String.format(" %s []", REPORT), + "report daily consumptions between two dates")); } } diff --git a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/AuthData.java b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/AuthData.java new file mode 100644 index 00000000000..6e20453ccb9 --- /dev/null +++ b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/AuthData.java @@ -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 output = new ArrayList<>(); + public List input = new ArrayList<>(); + } + + public String authId; + public String template; + public String stage; + public String header; + public List callbacks = new ArrayList<>(); +} diff --git a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/AuthResult.java b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/AuthResult.java new file mode 100644 index 00000000000..fb9b895a264 --- /dev/null +++ b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/AuthResult.java @@ -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; +} diff --git a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/ConsumptionReport.java b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/ConsumptionReport.java new file mode 100644 index 00000000000..488ed24c540 --- /dev/null +++ b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/ConsumptionReport.java @@ -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 labels; + public List periodes; + public List 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; +} diff --git a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/PrmInfo.java b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/PrmInfo.java new file mode 100644 index 00000000000..7612de5d4e0 --- /dev/null +++ b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/PrmInfo.java @@ -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; +} diff --git a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/UserInfo.java b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/UserInfo.java new file mode 100644 index 00000000000..d3c42ee7e1b --- /dev/null +++ b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/UserInfo.java @@ -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; +} diff --git a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/handler/LinkyCookieJar.java b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/handler/LinkyCookieJar.java deleted file mode 100644 index 3e8fbd012d8..00000000000 --- a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/handler/LinkyCookieJar.java +++ /dev/null @@ -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 cookies = new ArrayList<>(); - - @Override - public void saveFromResponse(final HttpUrl url, final List cookies) { - this.cookies.addAll(cookies); - } - - @Override - public List loadForRequest(final HttpUrl url) { - if (LOGIN_URL_PATH.equals(url.url().getPath())) { - cookies = new ArrayList<>(); - } - return cookies; - } -} diff --git a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/handler/LinkyHandler.java b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/handler/LinkyHandler.java index aaee211d5db..d164e8aa1e2 100644 --- a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/handler/LinkyHandler.java +++ b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/handler/LinkyHandler.java @@ -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 cachedDaylyData; - private final ExpiringDayCache cachedMonthlyData; - private final ExpiringDayCache cachedYearlyData; + private final ExpiringDayCache cachedDaylyData; + private final ExpiringDayCache cachedPowerData; + private final ExpiringDayCache cachedMonthlyData; + private final ExpiringDayCache 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("daily cache", REFRESH_FIRST_HOUR_OF_DAY, - () -> { - final LocalDate today = LocalDate.now(); - return getConsumptionData(DAILY, today.minusDays(13), today, true); - }); - this.cachedMonthlyData = new ExpiringDayCache("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("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 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 discoverAttributes() throws LinkyException { + Map 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 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 diff --git a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/model/LinkyConsumptionData.java b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/model/LinkyConsumptionData.java deleted file mode 100644 index b6ed156029b..00000000000 --- a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/model/LinkyConsumptionData.java +++ /dev/null @@ -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 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 = 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; - } - } -} diff --git a/bundles/org.openhab.binding.linky/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.linky/src/main/resources/OH-INF/thing/thing-types.xml index 4b503cd85d8..30fde724fba 100644 --- a/bundles/org.openhab.binding.linky/src/main/resources/OH-INF/thing/thing-types.xml +++ b/bundles/org.openhab.binding.linky/src/main/resources/OH-INF/thing/thing-types.xml @@ -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"> - @@ -31,6 +30,10 @@ password Your Enedis Password + + + Authentication ID delivered after the captcha (see documentation). + @@ -40,6 +43,8 @@ + + @@ -47,10 +52,10 @@ - + - + @@ -59,7 +64,7 @@ - + @@ -71,7 +76,7 @@ - + @@ -79,7 +84,6 @@ - Number:Energy @@ -87,4 +91,18 @@ + + Number:Power + + Maximum power usage yesterday + + + + + DateTime + + Maximum power usage timestamp + + +