]> git.basschouten.com Git - openhab-addons.git/commitdiff
[linky] Adressing issue #11642 (#12561)
authorGaël L'hopital <gael@lhopital.org>
Mon, 4 Apr 2022 08:14:43 +0000 (10:14 +0200)
committerGitHub <noreply@github.com>
Mon, 4 Apr 2022 08:14:43 +0000 (10:14 +0200)
* [linky] Addressing issue #11642
Apparently all the mess came from default write buffer size of Jetty client. Maybe during an update of jetty lib in the core.
I also corrected daily data that does not seem to accept J+1 anymore and three SAT findings

While I was here, enhanced LinkyException

Signed-off-by: clinique <gael@lhopital.org>
bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/LinkyException.java
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
bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/handler/LinkyHandler.java

index 6adcad55200e5683d72063088b5626fee31a2ffc..a4aceb2b5584e2e19ae13557d0eb9bc8b3880abf 100644 (file)
@@ -21,7 +21,6 @@ import org.eclipse.jdt.annotation.NonNullByDefault;
  */
 @NonNullByDefault
 public class LinkyException extends Exception {
-
     private static final long serialVersionUID = 3703839284673384018L;
 
     public LinkyException() {
@@ -32,7 +31,15 @@ public class LinkyException extends Exception {
         super(message);
     }
 
-    public LinkyException(String message, Exception e) {
+    public LinkyException(Exception e, String message) {
         super(message, e);
     }
+
+    public LinkyException(String message, Object... params) {
+        this(String.format(message, params));
+    }
+
+    public LinkyException(Exception e, String message, Object... params) {
+        this(e, String.format(message, params));
+    }
 }
index 83ff54b41f45ac6e5621e8070f401b611f575cc4..33b51ed42f2ef9a96b9680ebfb2d9dbdb89b6469 100644 (file)
@@ -48,29 +48,28 @@ import com.google.gson.JsonDeserializer;
 @Component(service = ThingHandlerFactory.class, configurationPid = "binding.linky")
 public class LinkyHandlerFactory extends BaseThingHandlerFactory {
     private static final DateTimeFormatter LINKY_FORMATTER = DateTimeFormatter.ofPattern("uuuu-MM-dd'T'HH:mm:ss.SSSX");
+    private static final int REQUEST_BUFFER_SIZE = 8000;
 
     private final Logger logger = LoggerFactory.getLogger(LinkyHandlerFactory.class);
-
+    private final Gson gson = new GsonBuilder().registerTypeAdapter(ZonedDateTime.class,
+            (JsonDeserializer<ZonedDateTime>) (json, type, jsonDeserializationContext) -> ZonedDateTime
+                    .parse(json.getAsJsonPrimitive().getAsString(), LINKY_FORMATTER))
+            .create();
     private final LocaleProvider localeProvider;
-    private final Gson gson;
     private final HttpClient httpClient;
 
     @Activate
     public LinkyHandlerFactory(final @Reference LocaleProvider localeProvider,
             final @Reference HttpClientFactory httpClientFactory) {
         this.localeProvider = localeProvider;
-        this.gson = new GsonBuilder().registerTypeAdapter(ZonedDateTime.class,
-                (JsonDeserializer<ZonedDateTime>) (json, type, jsonDeserializationContext) -> ZonedDateTime
-                        .parse(json.getAsJsonPrimitive().getAsString(), LINKY_FORMATTER))
-                .create();
         this.httpClient = httpClientFactory.createHttpClient(LinkyBindingConstants.BINDING_ID);
     }
 
     @Override
     protected void activate(ComponentContext componentContext) {
         super.activate(componentContext);
-        httpClient.getSslContextFactory().setExcludeCipherSuites(new String[0]);
         httpClient.setFollowRedirects(false);
+        httpClient.setRequestBufferSize(REQUEST_BUFFER_SIZE);
         try {
             httpClient.start();
         } catch (Exception e) {
@@ -95,8 +94,7 @@ public class LinkyHandlerFactory extends BaseThingHandlerFactory {
 
     @Override
     protected @Nullable ThingHandler createHandler(Thing thing) {
-        ThingTypeUID thingTypeUID = thing.getThingTypeUID();
-
-        return supportsThingType(thingTypeUID) ? new LinkyHandler(thing, localeProvider, gson, httpClient) : null;
+        return supportsThingType(thing.getThingTypeUID()) ? new LinkyHandler(thing, localeProvider, gson, httpClient)
+                : null;
     }
 }
index e190fe1d4af8756f07380102069fcb63e4f4e28e..74ede525e40593519991680feb8e5f3144b9a636 100644 (file)
@@ -55,31 +55,39 @@ import com.google.gson.JsonSyntaxException;
 @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 static final String ENEDIS_DOMAIN = ".enedis.fr";
+    private static final String URL_APPS_LINCS = "https://apps.lincs" + ENEDIS_DOMAIN;
+    private static final String URL_MON_COMPTE = "https://mon-compte" + ENEDIS_DOMAIN;
+    private static final String URL_COMPTE_PART = URL_MON_COMPTE.replace("compte", "compte-particulier");
+    private static final String URL_ENEDIS_AUTHENTICATE = URL_APPS_LINCS + "/authenticate?target=" + URL_COMPTE_PART;
+    private static final String USER_INFO_URL = URL_APPS_LINCS + "/userinfos";
+    private static final String PRM_INFO_BASE_URL = URL_APPS_LINCS + "/mes-mesures/api/private/v1/personnes/";
+    private static final String PRM_INFO_URL = PRM_INFO_BASE_URL + "null/prms";
+    private static final String MEASURE_URL = PRM_INFO_BASE_URL
+            + "%s/prms/%s/donnees-%s?dateDebut=%s&dateFin=%s&mesuretypecode=CONS";
+    private static final URI COOKIE_URI = URI.create(URL_COMPTE_PART);
+    private static final Pattern REQ_PATTERN = Pattern.compile("ReqID%(.*?)%26");
 
     private final Logger logger = LoggerFactory.getLogger(EnedisHttpApi.class);
     private final Gson gson;
     private final HttpClient httpClient;
-    private boolean connected = false;
     private final CookieStore cookieStore;
     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;
         this.cookieStore = httpClient.getCookieStore();
-        addCookie(LinkyConfiguration.INTERNAL_AUTH_ID, config.internalAuthId);
     }
 
     public void initialize() throws LinkyException {
         logger.debug("Starting login process for user : {}", config.username);
 
         try {
+            addCookie(LinkyConfiguration.INTERNAL_AUTH_ID, config.internalAuthId);
             logger.debug("Step 1 : getting authentification");
             String data = getData(URL_ENEDIS_AUTHENTICATE);
 
@@ -96,24 +104,22 @@ public class EnedisHttpApi {
             }
 
             logger.debug("Get the location and the ReqID");
-            Pattern p = Pattern.compile("ReqID%(.*?)%26");
-            Matcher m = p.matcher(getLocation(result));
+            Matcher m = REQ_PATTERN.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
+            String authenticateUrl = 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=";
+                    + reqId + "%26index%3Dnull%26acsURL%3D" + URL_APPS_LINCS
+                    + "/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).header("X-NoSession", "true").header("X-Password", "anonymous")
+            logger.debug("Step 3 : auth1 - retrieve the template, thanks to cookie internalAuthId user is already set");
+            result = httpClient.POST(authenticateUrl).header("X-NoSession", "true").header("X-Password", "anonymous")
                     .header("X-Requested-With", "XMLHttpRequest").header("X-Username", "anonymous").send();
             if (result.getStatus() != 200) {
-                throw new LinkyException("Connection failed step 3 - auth1 : " + result.getContentAsString());
+                throw new LinkyException("Connection failed step 3 - auth1 : %s", result.getContentAsString());
             }
 
             AuthData authData = gson.fromJson(result.getContentAsString(), AuthData.class);
@@ -125,18 +131,13 @@ public class EnedisHttpApi {
             }
 
             authData.callbacks.get(1).input.get(0).value = config.password;
-            url = URL_MON_COMPTE
-                    + "/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")
+            logger.debug("Step 4 : auth2 - send the auth data");
+            result = httpClient.POST(authenticateUrl).header(HttpHeader.CONTENT_TYPE, "application/json")
                     .header("X-NoSession", "true").header("X-Password", "anonymous")
                     .header("X-Requested-With", "XMLHttpRequest").header("X-Username", "anonymous")
                     .content(new StringContentProvider(gson.toJson(authData))).send();
             if (result.getStatus() != 200) {
-                throw new LinkyException("Connection failed step 3 - auth2 : " + result.getContentAsString());
+                throw new LinkyException("Connection failed step 3 - auth2 : %s", result.getContentAsString());
             }
 
             AuthResult authResult = gson.fromJson(result.getContentAsString(), AuthResult.class);
@@ -147,13 +148,13 @@ public class EnedisHttpApi {
             logger.debug("Add the tokenId cookie");
             addCookie("enedisExt", authResult.tokenId);
 
-            logger.debug("Step 4 : retrieve the SAMLresponse");
+            logger.debug("Step 5 : 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");
+            logger.debug("Step 6 : post the SAMLresponse to finish the authentication");
             result = httpClient.POST(el.attr("action")).content(getFormContent("SAMLResponse", samlInput.attr("value")))
                     .send();
             if (result.getStatus() != 302) {
@@ -161,7 +162,7 @@ public class EnedisHttpApi {
             }
             connected = true;
         } catch (InterruptedException | TimeoutException | ExecutionException | JsonSyntaxException e) {
-            throw new LinkyException("Error opening connection with Enedis webservice", e);
+            throw new LinkyException(e, "Error opening connection with Enedis webservice");
         }
     }
 
@@ -172,14 +173,14 @@ public class EnedisHttpApi {
     private void disconnect() throws LinkyException {
         if (connected) {
             logger.debug("Logout process");
+            connected = false;
             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));
+                getLocation(httpClient.GET(location));
                 cookieStore.removeAll();
-                connected = false;
             } catch (InterruptedException | ExecutionException | TimeoutException e) {
-                throw new LinkyException("Error while disconnecting from Enedis webservice", e);
+                throw new LinkyException(e, "Error while disconnecting from Enedis webservice");
             }
         }
     }
@@ -194,9 +195,9 @@ public class EnedisHttpApi {
 
     private void addCookie(String key, String value) {
         HttpCookie cookie = new HttpCookie(key, value);
-        cookie.setDomain(".enedis.fr");
+        cookie.setDomain(ENEDIS_DOMAIN);
         cookie.setPath("/");
-        cookieStore.add(URI.create(URL_COOKIE), cookie);
+        cookieStore.add(COOKIE_URI, cookie);
     }
 
     private FormContentProvider getFormContent(String fieldName, String fieldValue) {
@@ -209,11 +210,11 @@ public class EnedisHttpApi {
         try {
             ContentResponse result = httpClient.GET(url);
             if (result.getStatus() != 200) {
-                throw new LinkyException(String.format("Error requesting '%s' : %s", url, result.getContentAsString()));
+                throw new LinkyException("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);
+            throw new LinkyException(e, "Error getting url : '%s'", url);
         }
     }
 
@@ -221,10 +222,9 @@ public class EnedisHttpApi {
         if (!connected) {
             initialize();
         }
-        final String prm_info_url = URL_APPS_LINCS + "/mes-mesures/api/private/v1/personnes/null/prms";
-        String data = getData(prm_info_url);
+        String data = getData(PRM_INFO_URL);
         if (data.isEmpty()) {
-            throw new LinkyException(String.format("Requesting '%s' returned an empty response", prm_info_url));
+            throw new LinkyException("Requesting '%s' returned an empty response", PRM_INFO_URL);
         }
         try {
             PrmInfo[] prms = gson.fromJson(data, PrmInfo[].class);
@@ -234,8 +234,7 @@ public class EnedisHttpApi {
             return prms[0];
         } catch (JsonSyntaxException e) {
             logger.debug("invalid JSON response not matching PrmInfo[].class: {}", data);
-            throw new LinkyException(String.format("Requesting '%s' returned an invalid JSON response : %s",
-                    prm_info_url, e.getMessage()), e);
+            throw new LinkyException(e, "Requesting '%s' returned an invalid JSON response", PRM_INFO_URL);
         }
     }
 
@@ -243,29 +242,28 @@ public class EnedisHttpApi {
         if (!connected) {
             initialize();
         }
-        final String user_info_url = URL_APPS_LINCS + "/userinfos";
-        String data = getData(user_info_url);
+        String data = getData(USER_INFO_URL);
         if (data.isEmpty()) {
-            throw new LinkyException(String.format("Requesting '%s' returned an empty response", user_info_url));
+            throw new LinkyException("Requesting '%s' returned an empty response", USER_INFO_URL);
         }
         try {
             return Objects.requireNonNull(gson.fromJson(data, UserInfo.class));
         } catch (JsonSyntaxException e) {
             logger.debug("invalid JSON response not matching UserInfo.class: {}", data);
-            throw new LinkyException(String.format("Requesting '%s' returned an invalid JSON response : %s",
-                    user_info_url, e.getMessage()), e);
+            throw new LinkyException(e, "Requesting '%s' returned an invalid JSON response", USER_INFO_URL);
         }
     }
 
     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),
+        String url = String.format(MEASURE_URL, userId, prmId, request, from.format(API_DATE_FORMAT),
                 to.format(API_DATE_FORMAT));
+        if (!connected) {
+            initialize();
+        }
         String data = getData(url);
         if (data.isEmpty()) {
-            throw new LinkyException(String.format("Requesting '%s' returned an empty response", url));
+            throw new LinkyException("Requesting '%s' returned an empty response", url);
         }
         logger.trace("getData returned {}", data);
         try {
@@ -276,22 +274,15 @@ public class EnedisHttpApi {
             return report.firstLevel.consumptions;
         } catch (JsonSyntaxException e) {
             logger.debug("invalid JSON response not matching ConsumptionReport.class: {}", data);
-            throw new LinkyException(
-                    String.format("Requesting '%s' returned an invalid JSON response : %s", url, e.getMessage()), e);
+            throw new LinkyException(e, "Requesting '%s' returned an invalid JSON response", url);
         }
     }
 
     public Consumption getEnergyData(String userId, String prmId, LocalDate from, LocalDate to) throws LinkyException {
-        if (!connected) {
-            initialize();
-        }
         return getMeasures(userId, prmId, from, to, "energie");
     }
 
     public Consumption getPowerData(String userId, String prmId, LocalDate from, LocalDate to) throws LinkyException {
-        if (!connected) {
-            initialize();
-        }
         return getMeasures(userId, prmId, from, to, "pmax");
     }
 }
index 4872c7b8c6c4e991f90bf5ec34f6b90367dcf989..2b408e199862bc8575fa90f6e249587e282ecf23 100644 (file)
@@ -21,7 +21,6 @@ import java.time.format.DateTimeFormatter;
 import java.time.temporal.ChronoUnit;
 import java.time.temporal.WeekFields;
 import java.util.ArrayList;
-import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.concurrent.ScheduledFuture;
@@ -37,7 +36,6 @@ 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;
@@ -68,19 +66,18 @@ public class LinkyHandler extends BaseThingHandler {
     private static final int REFRESH_INTERVAL_IN_MIN = 120;
 
     private final Logger logger = LoggerFactory.getLogger(LinkyHandler.class);
-
     private final HttpClient httpClient;
     private final Gson gson;
     private final WeekFields weekFields;
 
-    private @Nullable ScheduledFuture<?> refreshJob;
-    private @Nullable EnedisHttpApi enedisApi;
-
     private final ExpiringDayCache<Consumption> cachedDailyData;
     private final ExpiringDayCache<Consumption> cachedPowerData;
     private final ExpiringDayCache<Consumption> cachedMonthlyData;
     private final ExpiringDayCache<Consumption> cachedYearlyData;
 
+    private @Nullable ScheduledFuture<?> refreshJob;
+    private @Nullable EnedisHttpApi enedisApi;
+
     private @NonNullByDefault({}) String prmId;
     private @NonNullByDefault({}) String userId;
 
@@ -108,8 +105,8 @@ public class LinkyHandler extends BaseThingHandler {
         });
 
         this.cachedPowerData = new ExpiringDayCache<>("power cache", REFRESH_FIRST_HOUR_OF_DAY, () -> {
-            LocalDate to = LocalDate.now().plusDays(1);
-            LocalDate from = to.minusDays(2);
+            LocalDate to = LocalDate.now();
+            LocalDate from = to.minusDays(1);
             Consumption consumption = getPowerData(from, to);
             if (consumption != null) {
                 logData(consumption.aggregats.days, "Day (peak)", true, DateTimeFormatter.ISO_LOCAL_DATE_TIME,
@@ -155,13 +152,9 @@ public class LinkyHandler extends BaseThingHandler {
                     updateStatus(ThingStatus.ONLINE);
 
                     if (thing.getProperties().isEmpty()) {
-                        Map<String, String> properties = new HashMap<>();
                         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);
-                        updateProperties(properties);
+                        updateProperties(Map.of(USER_ID, api.getUserInfo().userProperties.internId, PUISSANCE,
+                                prmInfo.puissanceSouscrite + " kVA", PRM_ID, prmInfo.prmId));
                     }
 
                     prmId = thing.getProperties().get(PRM_ID);
@@ -475,28 +468,28 @@ public class LinkyHandler extends BaseThingHandler {
 
     private void checkData(Consumption consumption) throws LinkyException {
         if (consumption.aggregats.days.periodes.size() == 0) {
-            throw new LinkyException("invalid consumptions data: no day period");
+            throw new LinkyException("Invalid consumptions data: no day period");
         }
         if (consumption.aggregats.days.periodes.size() != consumption.aggregats.days.datas.size()) {
-            throw new LinkyException("invalid consumptions data: not one data for each day period");
+            throw new LinkyException("Invalid consumptions data: not any data for each day period");
         }
         if (consumption.aggregats.weeks.periodes.size() == 0) {
-            throw new LinkyException("invalid consumptions data: no week period");
+            throw new LinkyException("Invalid consumptions data: no week period");
         }
         if (consumption.aggregats.weeks.periodes.size() != consumption.aggregats.weeks.datas.size()) {
-            throw new LinkyException("invalid consumptions data: not one data for each week period");
+            throw new LinkyException("Invalid consumptions data: not any data for each week period");
         }
         if (consumption.aggregats.months.periodes.size() == 0) {
-            throw new LinkyException("invalid consumptions data: no month period");
+            throw new LinkyException("Invalid consumptions data: no month period");
         }
         if (consumption.aggregats.months.periodes.size() != consumption.aggregats.months.datas.size()) {
-            throw new LinkyException("invalid consumptions data: not one data for each month period");
+            throw new LinkyException("Invalid consumptions data: not any data for each month period");
         }
         if (consumption.aggregats.years.periodes.size() == 0) {
-            throw new LinkyException("invalid consumptions data: no year period");
+            throw new LinkyException("Invalid consumptions data: no year period");
         }
         if (consumption.aggregats.years.periodes.size() != consumption.aggregats.years.datas.size()) {
-            throw new LinkyException("invalid consumptions data: not one data for each year period");
+            throw new LinkyException("Invalid consumptions data: not any data for each year period");
         }
     }