Skip to content

Commit

Permalink
Yet another website underlaying API modification
Browse files Browse the repository at this point in the history
Signed-off-by: Gaël L'hopital <[email protected]>
  • Loading branch information
clinique committed Oct 10, 2024
1 parent d923eb9 commit 94cba60
Show file tree
Hide file tree
Showing 8 changed files with 139 additions and 90 deletions.
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="[email protected]", password="******" ]
Thing linky:linky:local "Compteur Linky" [ username="[email protected]", password="******", internalAuthId="******" ]
```

### Items
Expand Down
4 changes: 2 additions & 2 deletions bundles/org.openhab.binding.linky/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@
<dependency>
<groupId>org.jsoup</groupId>
<artifactId>jsoup</artifactId>
<version>1.14.3</version>
<scope>provided</scope>
<version>1.15.4</version>
<scope>compile</scope>
</dependency>
</dependencies>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +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.14.3</bundle>
<bundle dependency="true">mvn:org.jsoup/jsoup/1.15.4</bundle>
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.linky/${project.version}</bundle>
</feature>
</features>
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 @@ -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";
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,81 @@
/**
* 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) {

}

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;
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,9 @@
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.PrmDetail;
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;
Expand Down Expand Up @@ -157,9 +159,13 @@ public void initialize() {
updateStatus(ThingStatus.ONLINE);

if (thing.getProperties().isEmpty()) {
PrmInfo prmInfo = api.getPrmInfo();
updateProperties(Map.of(USER_ID, api.getUserInfo().userProperties.internId, PUISSANCE,
prmInfo.puissanceSouscrite + " kVA", PRM_ID, prmInfo.prmId));
UserInfo userInfo = api.getUserInfo();
PrmInfo prmInfo = api.getPrmInfo(userInfo.userProperties.internId);
PrmDetail details = api.getPrmDetails(userInfo.userProperties.internId, prmInfo.idPrm);
updateProperties(Map.of(USER_ID, userInfo.userProperties.internId, PUISSANCE,
details.situationContractuelleDtos[0].structureTarifaire().puissanceSouscrite().valeur()
+ " kVA",
PRM_ID, prmInfo.idPrm));
}

prmId = thing.getProperties().get(PRM_ID);
Expand Down

0 comments on commit 94cba60

Please sign in to comment.