]> git.basschouten.com Git - openhab-addons.git/commitdiff
[Linky] Linky issue 7610 (#8678)
authorGaël L'hopital <gael@lhopital.org>
Thu, 15 Oct 2020 17:58:38 +0000 (19:58 +0200)
committerGitHub <noreply@github.com>
Thu, 15 Oct 2020 17:58:38 +0000 (10:58 -0700)
* 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>
22 files changed:
bundles/org.openhab.binding.linky/NOTICE
bundles/org.openhab.binding.linky/README.md
bundles/org.openhab.binding.linky/pom.xml
bundles/org.openhab.binding.linky/src/main/feature/feature.xml
bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/ExpiringDayCache.java [deleted file]
bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/LinkyBindingConstants.java
bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/LinkyConfiguration.java
bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/LinkyException.java [new file with mode: 0644]
bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/LinkyHandlerFactory.java
bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/api/EnedisHttpApi.java [new file with mode: 0644]
bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/api/ExpiringDayCache.java [new file with mode: 0644]
bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/console/LinkyCommandExtension.java
bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/AuthData.java [new file with mode: 0644]
bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/AuthResult.java [new file with mode: 0644]
bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/ConsumptionReport.java [new file with mode: 0644]
bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/PrmInfo.java [new file with mode: 0644]
bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/UserInfo.java [new file with mode: 0644]
bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/handler/LinkyCookieJar.java [deleted file]
bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/handler/LinkyHandler.java
bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/model/LinkyConsumptionData.java [deleted file]
bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/model/LinkyTimeScale.java [deleted file]
bundles/org.openhab.binding.linky/src/main/resources/OH-INF/thing/thing-types.xml

index e2657a475c706e9986dce4108368e52c112dc357..3e2c49e0050b8b3e1511b828ea6417b5124c1b9e 100644 (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
\ 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
index a72a141f3890d62ed5c87459fbad3ecf8bbd7d56..47b4c7ec3d201072059d2ac42660f2917c34c3b6 100644 (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" }
 ```
index deae54163f1b9455c26be31559fc1b011f556562..6c64ec6d1ca99bbd709e9b4642dee245b3c48792 100644 (file)
 
   <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>
index 6da7cc23747e51f82c6c13f0b9aa6ae206ab0aa8..20b9446d27af56fec4aa3ff8da241382e2a05311 100644 (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>
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/ExpiringDayCache.java
deleted file mode 100644 (file)
index 018c4e3..0000000
+++ /dev/null
@@ -1,128 +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;
-
-import java.time.LocalDateTime;
-import java.time.format.DateTimeFormatter;
-import java.time.temporal.ChronoUnit;
-import java.util.function.Supplier;
-
-import org.eclipse.jdt.annotation.NonNullByDefault;
-import org.eclipse.jdt.annotation.Nullable;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-/**
- * This is a simple expiring and reloading cache implementation.
- *
- * There must be provided an action in order to retrieve/calculate the value. This action will be called only if the
- * answer from the last calculation is not valid anymore, i.e. if it is expired.
- *
- * The cache expires after the current day; it is possible to shift the beginning time of the day.
- *
- * Soft Reference is not used to store the cached value because JVM Garbage Collector is clearing it too much often.
- *
- * @author Laurent Garnier - Initial contribution
- *
- * @param <V> the type of the value
- */
-@NonNullByDefault
-public class ExpiringDayCache<V> {
-    private final Logger logger = LoggerFactory.getLogger(ExpiringDayCache.class);
-
-    private final String name;
-    private final int beginningHour;
-    private final Supplier<@Nullable V> action;
-
-    private @Nullable V value;
-    private LocalDateTime expiresAt;
-
-    /**
-     * Create a new instance.
-     *
-     * @param name the name of this cache
-     * @param beginningHour the hour in the day at which the validity period is starting
-     * @param action the action to retrieve/calculate the value
-     */
-    public ExpiringDayCache(String name, int beginningHour, Supplier<@Nullable V> action) {
-        this.name = name;
-        this.beginningHour = beginningHour;
-        this.expiresAt = calcAlreadyExpired();
-        this.action = action;
-    }
-
-    /**
-     * Returns the value - possibly from the cache, if it is still valid.
-     */
-    public synchronized @Nullable V getValue() {
-        @Nullable
-        V cachedValue = value;
-        if (cachedValue == null || isExpired()) {
-            logger.debug("getValue from cache \"{}\" is requiring a fresh value", name);
-            cachedValue = refreshValue();
-        } else {
-            logger.debug("getValue from cache \"{}\" is returing a cached value", name);
-        }
-        return cachedValue;
-    }
-
-    /**
-     * Puts a new value into the cache.
-     *
-     * @param value the new value
-     */
-    public final synchronized void putValue(@Nullable V value) {
-        this.value = value;
-        expiresAt = calcNextExpiresAt();
-    }
-
-    /**
-     * Invalidates the value in the cache.
-     */
-    public final synchronized void invalidateValue() {
-        value = null;
-        expiresAt = calcAlreadyExpired();
-    }
-
-    /**
-     * Refreshes and returns the value in the cache.
-     *
-     * @return the new value
-     */
-    public synchronized @Nullable V refreshValue() {
-        value = action.get();
-        expiresAt = calcNextExpiresAt();
-        return value;
-    }
-
-    /**
-     * Checks if the value is expired.
-     *
-     * @return true if the value is expired
-     */
-    public boolean isExpired() {
-        return !LocalDateTime.now().isBefore(expiresAt);
-    }
-
-    private LocalDateTime calcNextExpiresAt() {
-        LocalDateTime now = LocalDateTime.now();
-        LocalDateTime limit = now.withHour(beginningHour).truncatedTo(ChronoUnit.HOURS);
-        LocalDateTime result = now.isBefore(limit) ? limit : limit.plusDays(1);
-        logger.debug("calcNextExpiresAt result = {}", result.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME));
-        return result;
-    }
-
-    private LocalDateTime calcAlreadyExpired() {
-        return LocalDateTime.now().minusDays(1);
-    }
-}
index e93fb59913c5b5396c3d5c243c686f9f89f79400..80ff7b314dafd7fd2350dd1ab5221fb947544705 100644 (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";
index 95b8e2b27c1fee2560700888dfea83fb54def307..022c064dfd955434890726382681cb0fd18eeaba 100644 (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;
 }
diff --git a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/LinkyException.java b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/LinkyException.java
new file mode 100644 (file)
index 0000000..da5f805
--- /dev/null
@@ -0,0 +1,38 @@
+/**
+ * Copyright (c) 2010-2020 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.linky.internal;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * Will be thrown for cloud errors
+ *
+ * @author Gaël L'hopital - Initial contribution
+ */
+@NonNullByDefault
+public class LinkyException extends Exception {
+
+    private static final long serialVersionUID = 3703839284673384018L;
+
+    public LinkyException() {
+        super();
+    }
+
+    public LinkyException(String message) {
+        super(message);
+    }
+
+    public LinkyException(String message, Exception e) {
+        super(message, e);
+    }
+}
index 3fc963f01a21317602878d8a0c6d4f99335a5b73..59c7fbaf434a0528031f4767f12d302966b77596 100644 (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;
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 (file)
index 0000000..ba656ec
--- /dev/null
@@ -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/api/ExpiringDayCache.java b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/api/ExpiringDayCache.java
new file mode 100644 (file)
index 0000000..3d2720a
--- /dev/null
@@ -0,0 +1,128 @@
+/**
+ * Copyright (c) 2010-2020 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.linky.internal.api;
+
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+import java.time.temporal.ChronoUnit;
+import java.util.function.Supplier;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * This is a simple expiring and reloading cache implementation.
+ *
+ * There must be provided an action in order to retrieve/calculate the value. This action will be called only if the
+ * answer from the last calculation is not valid anymore, i.e. if it is expired.
+ *
+ * The cache expires after the current day; it is possible to shift the beginning time of the day.
+ *
+ * Soft Reference is not used to store the cached value because JVM Garbage Collector is clearing it too much often.
+ *
+ * @author Laurent Garnier - Initial contribution
+ *
+ * @param <V> the type of the value
+ */
+@NonNullByDefault
+public class ExpiringDayCache<V> {
+    private final Logger logger = LoggerFactory.getLogger(ExpiringDayCache.class);
+
+    private final String name;
+    private final int beginningHour;
+    private final Supplier<@Nullable V> action;
+
+    private @Nullable V value;
+    private LocalDateTime expiresAt;
+
+    /**
+     * Create a new instance.
+     *
+     * @param name the name of this cache
+     * @param beginningHour the hour in the day at which the validity period is starting
+     * @param action the action to retrieve/calculate the value
+     */
+    public ExpiringDayCache(String name, int beginningHour, Supplier<@Nullable V> action) {
+        this.name = name;
+        this.beginningHour = beginningHour;
+        this.expiresAt = calcAlreadyExpired();
+        this.action = action;
+    }
+
+    /**
+     * Returns the value - possibly from the cache, if it is still valid.
+     */
+    public synchronized @Nullable V getValue() {
+        @Nullable
+        V cachedValue = value;
+        if (cachedValue == null || isExpired()) {
+            logger.debug("getValue from cache \"{}\" is requiring a fresh value", name);
+            cachedValue = refreshValue();
+        } else {
+            logger.debug("getValue from cache \"{}\" is returning a cached value", name);
+        }
+        return cachedValue;
+    }
+
+    /**
+     * Puts a new value into the cache.
+     *
+     * @param value the new value
+     */
+    public final synchronized void putValue(@Nullable V value) {
+        this.value = value;
+        expiresAt = calcNextExpiresAt();
+    }
+
+    /**
+     * Invalidates the value in the cache.
+     */
+    public final synchronized void invalidateValue() {
+        value = null;
+        expiresAt = calcAlreadyExpired();
+    }
+
+    /**
+     * Refreshes and returns the value in the cache.
+     *
+     * @return the new value
+     */
+    public synchronized @Nullable V refreshValue() {
+        value = action.get();
+        expiresAt = calcNextExpiresAt();
+        return value;
+    }
+
+    /**
+     * Checks if the value is expired.
+     *
+     * @return true if the value is expired
+     */
+    public boolean isExpired() {
+        return !LocalDateTime.now().isBefore(expiresAt);
+    }
+
+    private LocalDateTime calcNextExpiresAt() {
+        LocalDateTime now = LocalDateTime.now();
+        LocalDateTime limit = now.withHour(beginningHour).truncatedTo(ChronoUnit.HOURS);
+        LocalDateTime result = now.isBefore(limit) ? limit : limit.plusDays(1);
+        logger.debug("calcNextExpiresAt result = {}", result.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME));
+        return result;
+    }
+
+    private LocalDateTime calcAlreadyExpired() {
+        return LocalDateTime.now().minusDays(1);
+    }
+}
index 4751fa21668cbaef3f132c35322e40064ef890c3..36a8ec9ab27e1dfcec067867d67d8ee0becd54e1 100644 (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"));
     }
 }
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 (file)
index 0000000..6e20453
--- /dev/null
@@ -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<>();
+}
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 (file)
index 0000000..fb9b895
--- /dev/null
@@ -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 (file)
index 0000000..488ed24
--- /dev/null
@@ -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;
+}
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 (file)
index 0000000..7612de5
--- /dev/null
@@ -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 (file)
index 0000000..d3c42ee
--- /dev/null
@@ -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 (file)
index 3e8fbd0..0000000
+++ /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<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;
-    }
-}
index aaee211d5db7d9a78f1757437703192286a28df9..d164e8aa1e298122c2641965c7186523070f438a 100644 (file)
 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;
+
+    private @NonNullByDefault({}) String prmId;
+    private @NonNullByDefault({}) String userId;
 
-    public LinkyHandler(Thing thing, LocaleProvider localeProvider) {
+    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)));
-    }
+        LinkyConfiguration config = getConfigAs(LinkyConfiguration.class);
+        enedisApi = new EnedisHttpApi(config, gson, httpClient);
 
-    private synchronized boolean login() {
-        logger.debug("login");
+        try {
+            enedisApi.initialize();
+            updateStatus(ThingStatus.ONLINE);
 
-        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());
+            if (thing.getProperties().isEmpty()) {
+                Map<String, String> properties = discoverAttributes();
+                updateProperties(properties);
             }
-            // Do a first call to get data; this first call will fail with code 302
-            getConsumptionData(DAILY, LocalDate.now(), LocalDate.now(), false);
-            updateStatus(ThingStatus.ONLINE);
-            return true;
-        } catch (IOException e) {
-            logger.debug("Exception while trying to login: {}", e.getMessage(), e);
-            updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getMessage());
-            return false;
+
+            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;
-        }
-
-        double lastWeek = Double.NaN;
-        double thisWeek = Double.NaN;
-        double yesterday = Double.NaN;
-        LinkyConsumptionData result = cachedDaylyData.getValue();
-        if (result != null && result.success()) {
-            LocalDate rangeStart = LocalDate.now().minusDays(13);
-            int jump = result.getDecalage();
-            while (rangeStart.getDayOfWeek() != weekFields.getFirstDayOfWeek()) {
-                rangeStart = rangeStart.plusDays(1);
-                jump++;
-            }
-
-            int lastWeekNumber = rangeStart.get(weekFields.weekOfWeekBasedYear());
-
-            lastWeek = 0.0;
-            thisWeek = 0.0;
-            yesterday = Double.NaN;
-            while (jump < result.getData().size()) {
-                double consumption = result.getData().get(jump).valeur;
-                if (consumption > 0) {
-                    if (rangeStart.get(weekFields.weekOfWeekBasedYear()) == lastWeekNumber) {
-                        lastWeek += consumption;
-                        logger.trace("Consumption at index {} added to last week: {}", jump, consumption);
+        if (isLinked(YESTERDAY) || isLinked(LAST_WEEK) || isLinked(THIS_WEEK)) {
+            Consumption result = cachedDaylyData.getValue();
+            if (result != null) {
+                Aggregate days = result.aggregats.days;
+
+                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;
+
+                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;
-        }
-
-        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();
+        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));
+            }
         }
-        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
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 (file)
index b6ed156..0000000
+++ /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<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;
-        }
-    }
-}
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/model/LinkyTimeScale.java
deleted file mode 100644 (file)
index 6b71bc7..0000000
+++ /dev/null
@@ -1,39 +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 org.eclipse.jdt.annotation.NonNullByDefault;
-
-/**
- * The {@link LinkyTimeScale} enumerates all possible time scale
- * for API queries
- *
- * @author Gaël L'hopital - Initial contribution
- */
-@NonNullByDefault
-public enum LinkyTimeScale {
-    HOURLY("urlCdcHeure"),
-    DAILY("urlCdcJour"),
-    MONTHLY("urlCdcMois"),
-    YEARLY("urlCdcAn");
-
-    private String id;
-
-    private LinkyTimeScale(String id) {
-        this.id = id;
-    }
-
-    public String getId() {
-        return this.id;
-    }
-}
index 4b503cd85d825d2916dc6737a11641b41c92eedc..30fde724fba96972900288575d94d81ae1552c34 100644 (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>
                                <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>
 
                <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>
                <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>