Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[linky] Yet another website underlaying API modification #17538

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
2 changes: 1 addition & 1 deletion bundles/org.openhab.binding.linky/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ In case you are running openHAB inside Docker, the binding will work only if you
### Thing

```java
Thing linky:linky:local "Compteur Linky" [ username="example@domaine.fr", password="******" ]
Thing linky:linky:local "Compteur Linky" [ username="example@domaine.fr", password="******", internalAuthId="******" ]
```

### Items
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,10 @@ public LinkyException(Exception e, String message) {
}

public LinkyException(String message, Object... params) {
this(String.format(message, params));
this(message.formatted(params));
}

public LinkyException(Exception e, String message, Object... params) {
this(e, String.format(message, params));
this(e, message.formatted(params));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@
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 static final int RESPONSE_BUFFER_SIZE = 200000;
lolodomo marked this conversation as resolved.
Show resolved Hide resolved

private final Logger logger = LoggerFactory.getLogger(LinkyHandlerFactory.class);
private final Gson gson = new GsonBuilder().registerTypeAdapter(ZonedDateTime.class,
Expand Down Expand Up @@ -83,6 +84,7 @@ public LinkyHandlerFactory(final @Reference LocaleProvider localeProvider,
this.httpClient = httpClientFactory.createHttpClient(LinkyBindingConstants.BINDING_ID, sslContextFactory);
httpClient.setFollowRedirects(false);
httpClient.setRequestBufferSize(REQUEST_BUFFER_SIZE);
httpClient.setResponseBufferSize(RESPONSE_BUFFER_SIZE);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
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.PrmDetail;
import org.openhab.binding.linky.internal.dto.PrmInfo;
import org.openhab.binding.linky.internal.dto.UserInfo;
import org.slf4j.Logger;
Expand All @@ -61,7 +62,7 @@ public class EnedisHttpApi {
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 PRM_INFO_URL = URL_APPS_LINCS + "/mes-prms/api/private/v2/personnes/%s/prms";
private static final String MEASURE_URL = PRM_INFO_BASE_URL
+ "%s/prms/%s/donnees-%s?dateDebut=%s&dateFin=%s&mesuretypecode=CONS";
lolodomo marked this conversation as resolved.
Show resolved Hide resolved
private static final URI COOKIE_URI = URI.create(URL_COMPTE_PART);
Expand All @@ -81,19 +82,19 @@ public EnedisHttpApi(LinkyConfiguration config, Gson gson, HttpClient httpClient
}

public void initialize() throws LinkyException {
logger.debug("Starting login process for user : {}", config.username);
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);
logger.debug("Step 1: getting authentification");
String data = getContent(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");
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) {
Expand All @@ -112,7 +113,7 @@ public void initialize() throws LinkyException {
+ 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");
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) {
Expand All @@ -128,7 +129,7 @@ public void initialize() throws LinkyException {
}

authData.callbacks.get(1).input.get(0).value = config.password;
logger.debug("Step 4 : auth2 - send the auth data");
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")
Expand All @@ -145,13 +146,13 @@ public void initialize() throws LinkyException {
logger.debug("Add the tokenId cookie");
addCookie("enedisExt", authResult.tokenId);

logger.debug("Step 5 : retrieve the SAMLresponse");
data = getData(URL_MON_COMPTE + "/" + authResult.successUrl);
logger.debug("Step 5: retrieve the SAMLresponse");
data = getContent(URL_MON_COMPTE + "/" + authResult.successUrl);
htmlDocument = Jsoup.parse(data);
el = htmlDocument.select("form").first();
samlInput = el.select("input[name=SAMLResponse]").first();

logger.debug("Step 6 : 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) {
Expand Down Expand Up @@ -203,76 +204,61 @@ private FormContentProvider getFormContent(String fieldName, String fieldValue)
return new FormContentProvider(fields);
}

private String getData(String url) throws LinkyException {
private String getContent(String url) throws LinkyException {
try {
ContentResponse result = httpClient.GET(url);
if (result.getStatus() != 200) {
throw new LinkyException("Error requesting '%s' : %s", url, result.getContentAsString());
throw new LinkyException("Error requesting '%s': %s", url, result.getContentAsString());
}
return result.getContentAsString();
String content = result.getContentAsString();
logger.trace("getContent returned {}", content);
return content;
} catch (InterruptedException | ExecutionException | TimeoutException e) {
throw new LinkyException(e, "Error getting url : '%s'", url);
throw new LinkyException(e, "Error getting url: '%s'", url);
}
}

public PrmInfo getPrmInfo() throws LinkyException {
private <T> T getData(String url, Class<T> clazz) throws LinkyException {
if (!connected) {
initialize();
}
String data = getData(PRM_INFO_URL);
String data = getContent(url);
if (data.isEmpty()) {
throw new LinkyException("Requesting '%s' returned an empty response", PRM_INFO_URL);
throw new LinkyException("Requesting '%s' returned an empty response", url);
}
try {
PrmInfo[] prms = gson.fromJson(data, PrmInfo[].class);
if (prms == null || prms.length < 1) {
throw new LinkyException("Invalid prms data received");
}
return prms[0];
return Objects.requireNonNull(gson.fromJson(data, clazz));
} catch (JsonSyntaxException e) {
logger.debug("invalid JSON response not matching PrmInfo[].class: {}", data);
throw new LinkyException(e, "Requesting '%s' returned an invalid JSON response", PRM_INFO_URL);
logger.debug("Invalid JSON response not matching {}: {}", clazz.getName(), data);
throw new LinkyException(e, "Requesting '%s' returned an invalid JSON response", url);
}
}

public UserInfo getUserInfo() throws LinkyException {
if (!connected) {
initialize();
}
String data = getData(USER_INFO_URL);
if (data.isEmpty()) {
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(e, "Requesting '%s' returned an invalid JSON response", USER_INFO_URL);
public PrmInfo getPrmInfo(String internId) throws LinkyException {
String url = PRM_INFO_URL.formatted(internId);
PrmInfo[] prms = getData(url, PrmInfo[].class);
if (prms.length < 1) {
throw new LinkyException("Invalid prms data received");
}
return prms[0];
}

public PrmDetail getPrmDetails(String internId, String prmId) throws LinkyException {
String url = PRM_INFO_URL.formatted(internId) + "/" + prmId
+ "?embed=SITALI&embed=SITCOM&embed=SITCON&embed=SYNCON";
return getData(url, PrmDetail.class);
}

public UserInfo getUserInfo() throws LinkyException {
return getData(USER_INFO_URL, UserInfo.class);
}

private Consumption getMeasures(String userId, String prmId, LocalDate from, LocalDate to, String request)
throws LinkyException {
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("Requesting '%s' returned an empty response", url);
}
logger.trace("getData returned {}", data);
try {
ConsumptionReport report = gson.fromJson(data, ConsumptionReport.class);
if (report == null) {
throw new LinkyException("No report data received");
}
return report.firstLevel.consumptions;
} catch (JsonSyntaxException e) {
logger.debug("invalid JSON response not matching ConsumptionReport.class: {}", data);
throw new LinkyException(e, "Requesting '%s' returned an invalid JSON response", url);
}
ConsumptionReport report = getData(url, ConsumptionReport.class);
return report.firstLevel.consumptions;
}

public Consumption getEnergyData(String userId, String prmId, LocalDate from, LocalDate to) throws LinkyException {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
/**
* Copyright (c) 2010-2024 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;

/**
* The {@link PrmDetail} holds detailed informations about prm configuration
*
* @author Gaël L'hopital - Initial contribution
*/
public class PrmDetail {
public record Adresse(String ligne2, String ligne3, String ligne4, String ligne5, String ligne6) {
lolodomo marked this conversation as resolved.
Show resolved Hide resolved
}

public record DicEntry(String code, String libelle) {
}

public record Measure(String unite, String valeur) {
}

public record AlimentationPrincipale(Object puissanceRaccordementInjection,
Measure puissanceRaccordementSoutirage) {
}

public record Compteur(boolean accessibilite, boolean ticActivee, boolean ticStandard) {
}

public record Contrat(DicEntry typeContrat, String referenceContrat) {
}

public record Disjoncteur(DicEntry calibre) {
}

public record DispositifComptage(DicEntry typeComptage) {
}

public record GrilleFournisseur(DicEntry calendrier, Object classeTemporelle) {
}

public record InformationsContractuelles(Contrat contrat, DicEntry etatContractuel, SiContractuel siContractuel) {
}

public record SiContractuel(DicEntry application) {
}

public record SituationAlimentationDto(AlimentationPrincipale alimentationPrincipale) {
}

public record SituationComptageDto(ArrayList<Compteur> compteurs, Disjoncteur disjoncteur,
DispositifComptage dispositifComptage) {
}

public record SituationContractuelleDto(InformationsContractuelles informationsContractuelles,
StructureTarifaire structureTarifaire, String fournisseur, DicEntry segment) {
}

public record StructureTarifaire(Measure puissanceSouscrite, GrilleFournisseur grilleFournisseur) {
}

public record SyntheseContractuelleDto(DicEntry niveauOuvertureServices) {
}

public Adresse adresse;
public String segment;
public SyntheseContractuelleDto syntheseContractuelleDto;
public SituationContractuelleDto[] situationContractuelleDtos;
public SituationAlimentationDto situationAlimentationDto;
public SituationComptageDto situationComptageDto;
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,35 +13,11 @@
package org.openhab.binding.linky.internal.dto;

/**
* The {@link UserInfo} holds informations about energy delivery point
* The {@link UserInfo} holds ids of existing Prms
*
* @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;
public String idPrm;
}
Loading