From 9fe74e14a2699c8b5091fc938fbf76415a5620ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABl=20L=27hopital?= Date: Mon, 4 Apr 2022 10:14:43 +0200 Subject: [PATCH] [linky] Adressing issue #11642 (#12561) * [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 --- .../linky/internal/LinkyException.java | 11 +- .../linky/internal/LinkyHandlerFactory.java | 18 ++- .../linky/internal/api/EnedisHttpApi.java | 103 ++++++++---------- .../linky/internal/handler/LinkyHandler.java | 37 +++---- 4 files changed, 79 insertions(+), 90 deletions(-) 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 index 1700214b9c4..b3b5b3ca466 100644 --- 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 @@ -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)); + } } diff --git a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/LinkyHandlerFactory.java b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/LinkyHandlerFactory.java index 1f2e28bdd5e..e89ea49bdef 100644 --- a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/LinkyHandlerFactory.java +++ b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/LinkyHandlerFactory.java @@ -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) (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) (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; } } 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 index 025dc2e86fc..d438568fcbe 100644 --- 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 @@ -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"); } } diff --git a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/handler/LinkyHandler.java b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/handler/LinkyHandler.java index 2e16205ad05..090ca1f041a 100644 --- a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/handler/LinkyHandler.java +++ b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/handler/LinkyHandler.java @@ -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 cachedDailyData; private final ExpiringDayCache cachedPowerData; private final ExpiringDayCache cachedMonthlyData; private final ExpiringDayCache 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 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"); } }