[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 <gael@lhopital.org>
This commit is contained in:
Gaël L'hopital 2022-04-04 10:14:43 +02:00 committed by Wouter Born
parent b011c8439e
commit 9fe74e14a2
No known key found for this signature in database
GPG Key ID: A58E5C231E0E72B5
4 changed files with 79 additions and 90 deletions

View 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));
}
}

View 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;
}
}

View 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");
}
}

View 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");
}
}