2 * Copyright (c) 2010-2020 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
7 * This program and the accompanying materials are made available under the
8 * terms of the Eclipse Public License 2.0 which is available at
9 * http://www.eclipse.org/legal/epl-2.0
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.linky.internal.api;
15 import java.net.CookieStore;
16 import java.net.HttpCookie;
18 import java.time.LocalDate;
19 import java.time.format.DateTimeFormatter;
20 import java.util.Objects;
21 import java.util.concurrent.ExecutionException;
22 import java.util.concurrent.TimeoutException;
23 import java.util.regex.Matcher;
24 import java.util.regex.Pattern;
26 import org.eclipse.jdt.annotation.NonNullByDefault;
27 import org.eclipse.jetty.client.HttpClient;
28 import org.eclipse.jetty.client.api.ContentResponse;
29 import org.eclipse.jetty.client.util.FormContentProvider;
30 import org.eclipse.jetty.client.util.StringContentProvider;
31 import org.eclipse.jetty.http.HttpHeader;
32 import org.eclipse.jetty.util.Fields;
33 import org.jsoup.Jsoup;
34 import org.jsoup.nodes.Document;
35 import org.jsoup.nodes.Element;
36 import org.openhab.binding.linky.internal.LinkyConfiguration;
37 import org.openhab.binding.linky.internal.LinkyException;
38 import org.openhab.binding.linky.internal.dto.AuthData;
39 import org.openhab.binding.linky.internal.dto.AuthResult;
40 import org.openhab.binding.linky.internal.dto.ConsumptionReport;
41 import org.openhab.binding.linky.internal.dto.ConsumptionReport.Consumption;
42 import org.openhab.binding.linky.internal.dto.PrmInfo;
43 import org.openhab.binding.linky.internal.dto.UserInfo;
44 import org.slf4j.Logger;
45 import org.slf4j.LoggerFactory;
47 import com.google.gson.Gson;
50 * {@link EnedisHttpApi} wraps the Enedis Webservice.
52 * @author Gaƫl L'hopital - Initial contribution
55 public class EnedisHttpApi {
56 private static final DateTimeFormatter API_DATE_FORMAT = DateTimeFormatter.ofPattern("dd-MM-yyyy");
57 private static final String URL_APPS_LINCS = "https://apps.lincs.enedis.fr";
58 private static final String URL_MON_COMPTE = "https://mon-compte.enedis.fr";
59 private static final String URL_ENEDIS_AUTHENTICATE = URL_APPS_LINCS
60 + "/authenticate?target=https://mon-compte-particulier.enedis.fr/suivi-de-mesure/";
61 private static final String URL_COOKIE = "https://mon-compte-particulier.enedis.fr";
63 private final Logger logger = LoggerFactory.getLogger(EnedisHttpApi.class);
64 private final Gson gson;
65 private final HttpClient httpClient;
66 private final LinkyConfiguration config;
67 private boolean connected = false;
69 public EnedisHttpApi(LinkyConfiguration config, Gson gson, HttpClient httpClient) {
71 this.httpClient = httpClient;
75 public void initialize() throws LinkyException {
76 httpClient.getSslContextFactory().setExcludeCipherSuites(new String[0]);
77 httpClient.setFollowRedirects(false);
80 } catch (Exception e) {
81 throw new LinkyException("Unable to start Jetty HttpClient", e);
86 private void connect() throws LinkyException {
87 addCookie(LinkyConfiguration.INTERNAL_AUTH_ID, config.internalAuthId);
89 logger.debug("Starting login process for user : {}", config.username);
92 logger.debug("Step 1 : getting authentification");
93 String data = getData(URL_ENEDIS_AUTHENTICATE);
95 logger.debug("Reception request SAML");
96 Document htmlDocument = Jsoup.parse(data);
97 Element el = htmlDocument.select("form").first();
98 Element samlInput = el.select("input[name=SAMLRequest]").first();
100 logger.debug("Step 2 : send SSO SAMLRequest");
101 ContentResponse result = httpClient.POST(el.attr("action"))
102 .content(getFormContent("SAMLRequest", samlInput.attr("value"))).send();
103 if (result.getStatus() != 302) {
104 throw new LinkyException("Connection failed step 2");
107 logger.debug("Get the location and the ReqID");
108 Pattern p = Pattern.compile("ReqID%(.*?)%26");
109 Matcher m = p.matcher(getLocation(result));
111 throw new LinkyException("Unable to locate ReqId in header");
114 String reqId = m.group(1);
115 String url = URL_MON_COMPTE
116 + "/auth/json/authenticate?realm=/enedis&forward=true&spEntityID=SP-ODW-PROD&goto=/auth/SSOPOST/metaAlias/enedis/providerIDP?ReqID%"
118 + "%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=";
121 "Step 3 : auth1 - retrieve the template, thanks to cookie internalAuthId, user is already set");
122 result = httpClient.POST(url).send();
123 if (result.getStatus() != 200) {
124 throw new LinkyException("Connection failed step 3 - auth1 : " + result.getContentAsString());
127 AuthData authData = gson.fromJson(result.getContentAsString(), AuthData.class);
128 if (authData.callbacks.size() < 2 || authData.callbacks.get(0).input.size() == 0
129 || authData.callbacks.get(1).input.size() == 0 || !config.username
130 .equals(Objects.requireNonNull(authData.callbacks.get(0).input.get(0)).valueAsString())) {
131 throw new LinkyException("Authentication error, the authentication_cookie is probably wrong");
134 authData.callbacks.get(1).input.get(0).value = config.password;
135 url = "https://mon-compte.enedis.fr/auth/json/authenticate?realm=/enedis&spEntityID=SP-ODW-PROD&goto=/auth/SSOPOST/metaAlias/enedis/providerIDP?ReqID%"
137 + "%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=";
139 logger.debug("Step 3 : auth2 - send the auth data");
140 result = httpClient.POST(url).header(HttpHeader.CONTENT_TYPE, "application/json")
141 .content(new StringContentProvider(gson.toJson(authData))).send();
142 if (result.getStatus() != 200) {
143 throw new LinkyException("Connection failed step 3 - auth2 : " + result.getContentAsString());
146 AuthResult authResult = gson.fromJson(result.getContentAsString(), AuthResult.class);
147 logger.debug("Add the tokenId cookie");
148 addCookie("enedisExt", authResult.tokenId);
150 logger.debug("Step 4 : retrieve the SAMLresponse");
151 data = getData(URL_MON_COMPTE + "/" + authResult.successUrl);
152 htmlDocument = Jsoup.parse(data);
153 el = htmlDocument.select("form").first();
154 samlInput = el.select("input[name=SAMLResponse]").first();
156 logger.debug("Step 5 : post the SAMLresponse to finish the authentication");
157 result = httpClient.POST(el.attr("action")).content(getFormContent("SAMLResponse", samlInput.attr("value")))
159 if (result.getStatus() != 302) {
160 throw new LinkyException("Connection failed step 5");
163 } catch (InterruptedException | TimeoutException | ExecutionException e) {
164 throw new LinkyException("Error opening connection with Enedis webservice", e);
168 public String getLocation(ContentResponse response) {
169 return response.getHeaders().get(HttpHeader.LOCATION);
172 public void disconnect() throws LinkyException {
174 try { // Three times in a row to get disconnected
175 String location = getLocation(httpClient.GET(URL_APPS_LINCS + "/logout"));
176 location = getLocation(httpClient.GET(location));
177 location = getLocation(httpClient.GET(location));
178 CookieStore cookieStore = httpClient.getCookieStore();
179 cookieStore.removeAll();
181 } catch (InterruptedException | ExecutionException | TimeoutException e) {
182 throw new LinkyException("Error while disconnecting from Enedis webservice", e);
187 public void dispose() throws LinkyException {
191 } catch (Exception e) {
192 throw new LinkyException("Error stopping Jetty client", e);
196 private void addCookie(String key, String value) {
197 CookieStore cookieStore = httpClient.getCookieStore();
198 HttpCookie cookie = new HttpCookie(key, value);
199 cookie.setDomain(".enedis.fr");
201 cookieStore.add(URI.create(URL_COOKIE), cookie);
204 private FormContentProvider getFormContent(String fieldName, String fieldValue) {
205 Fields fields = new Fields();
206 fields.put(fieldName, fieldValue);
207 return new FormContentProvider(fields);
210 private String getData(String url) throws LinkyException {
212 ContentResponse result = httpClient.GET(url);
213 if (result.getStatus() != 200) {
214 throw new LinkyException(String.format("Error requesting '%s' : %s", url, result.getContentAsString()));
216 return result.getContentAsString();
217 } catch (InterruptedException | ExecutionException | TimeoutException e) {
218 throw new LinkyException(String.format("Error getting url : '%s'", url), e);
222 public PrmInfo getPrmInfo() throws LinkyException {
223 final String prm_info_url = URL_APPS_LINCS + "/mes-mesures/api/private/v1/personnes/null/prms";
224 String data = getData(prm_info_url);
225 PrmInfo[] prms = gson.fromJson(data, PrmInfo[].class);
229 public UserInfo getUserInfo() throws LinkyException {
230 final String user_info_url = URL_APPS_LINCS + "/userinfos";
231 String data = getData(user_info_url);
232 return gson.fromJson(data, UserInfo.class);
235 private Consumption getMeasures(String userId, String prmId, LocalDate from, LocalDate to, String request)
236 throws LinkyException {
237 final String measure_url = URL_APPS_LINCS
238 + "/mes-mesures/api/private/v1/personnes/%s/prms/%s/donnees-%s?dateDebut=%s&dateFin=%s&mesuretypecode=CONS";
239 String url = String.format(measure_url, userId, prmId, request, from.format(API_DATE_FORMAT),
240 to.format(API_DATE_FORMAT));
241 String data = getData(url);
242 ConsumptionReport report = gson.fromJson(data, ConsumptionReport.class);
243 return report.firstLevel.consumptions;
246 public Consumption getEnergyData(String userId, String prmId, LocalDate from, LocalDate to) throws LinkyException {
247 return getMeasures(userId, prmId, from, to, "energie");
250 public Consumption getPowerData(String userId, String prmId, LocalDate from, LocalDate to) throws LinkyException {
251 return getMeasures(userId, prmId, from, to, "pmax");