]> git.basschouten.com Git - openhab-addons.git/blob
fb4fad25cb1b1968c262cafbfb6fcaf55731492d
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2021 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
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
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.binding.linky.internal.api;
14
15 import java.net.CookieStore;
16 import java.net.HttpCookie;
17 import java.net.URI;
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;
25
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;
46
47 import com.google.gson.Gson;
48 import com.google.gson.JsonSyntaxException;
49
50 /**
51  * {@link EnedisHttpApi} wraps the Enedis Webservice.
52  *
53  * @author GaĆ«l L'hopital - Initial contribution
54  */
55 @NonNullByDefault
56 public class EnedisHttpApi {
57     private static final DateTimeFormatter API_DATE_FORMAT = DateTimeFormatter.ofPattern("dd-MM-yyyy");
58     private static final String URL_APPS_LINCS = "https://apps.lincs.enedis.fr";
59     private static final String URL_MON_COMPTE = "https://mon-compte.enedis.fr";
60     private static final String URL_ENEDIS_AUTHENTICATE = URL_APPS_LINCS
61             + "/authenticate?target=https://mon-compte-particulier.enedis.fr/suivi-de-mesure/";
62     private static final String URL_COOKIE = "https://mon-compte-particulier.enedis.fr";
63
64     private final Logger logger = LoggerFactory.getLogger(EnedisHttpApi.class);
65     private final Gson gson;
66     private final HttpClient httpClient;
67     private final LinkyConfiguration config;
68     private boolean connected = false;
69
70     public EnedisHttpApi(LinkyConfiguration config, Gson gson, HttpClient httpClient) {
71         this.gson = gson;
72         this.httpClient = httpClient;
73         this.config = config;
74     }
75
76     public void initialize() throws LinkyException {
77         addCookie(LinkyConfiguration.INTERNAL_AUTH_ID, config.internalAuthId);
78
79         logger.debug("Starting login process for user : {}", config.username);
80
81         try {
82             logger.debug("Step 1 : getting authentification");
83             String data = getData(URL_ENEDIS_AUTHENTICATE);
84
85             logger.debug("Reception request SAML");
86             Document htmlDocument = Jsoup.parse(data);
87             Element el = htmlDocument.select("form").first();
88             Element samlInput = el.select("input[name=SAMLRequest]").first();
89
90             logger.debug("Step 2 : send SSO SAMLRequest");
91             ContentResponse result = httpClient.POST(el.attr("action"))
92                     .content(getFormContent("SAMLRequest", samlInput.attr("value"))).send();
93             if (result.getStatus() != 302) {
94                 throw new LinkyException("Connection failed step 2");
95             }
96
97             logger.debug("Get the location and the ReqID");
98             Pattern p = Pattern.compile("ReqID%(.*?)%26");
99             Matcher m = p.matcher(getLocation(result));
100             if (!m.find()) {
101                 throw new LinkyException("Unable to locate ReqId in header");
102             }
103
104             String reqId = m.group(1);
105             String url = URL_MON_COMPTE
106                     + "/auth/json/authenticate?realm=/enedis&forward=true&spEntityID=SP-ODW-PROD&goto=/auth/SSOPOST/metaAlias/enedis/providerIDP?ReqID%"
107                     + reqId
108                     + "%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=";
109
110             logger.debug(
111                     "Step 3 : auth1 - retrieve the template, thanks to cookie internalAuthId, user is already set");
112             result = httpClient.POST(url).send();
113             if (result.getStatus() != 200) {
114                 throw new LinkyException("Connection failed step 3 - auth1 : " + result.getContentAsString());
115             }
116
117             AuthData authData = gson.fromJson(result.getContentAsString(), AuthData.class);
118             if (authData.callbacks.size() < 2 || authData.callbacks.get(0).input.size() == 0
119                     || authData.callbacks.get(1).input.size() == 0 || !config.username
120                             .equals(Objects.requireNonNull(authData.callbacks.get(0).input.get(0)).valueAsString())) {
121                 throw new LinkyException("Authentication error, the authentication_cookie is probably wrong");
122             }
123
124             authData.callbacks.get(1).input.get(0).value = config.password;
125             url = "https://mon-compte.enedis.fr/auth/json/authenticate?realm=/enedis&spEntityID=SP-ODW-PROD&goto=/auth/SSOPOST/metaAlias/enedis/providerIDP?ReqID%"
126                     + reqId
127                     + "%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=";
128
129             logger.debug("Step 3 : auth2 - send the auth data");
130             result = httpClient.POST(url).header(HttpHeader.CONTENT_TYPE, "application/json")
131                     .content(new StringContentProvider(gson.toJson(authData))).send();
132             if (result.getStatus() != 200) {
133                 throw new LinkyException("Connection failed step 3 - auth2 : " + result.getContentAsString());
134             }
135
136             AuthResult authResult = gson.fromJson(result.getContentAsString(), AuthResult.class);
137             logger.debug("Add the tokenId cookie");
138             addCookie("enedisExt", authResult.tokenId);
139
140             logger.debug("Step 4 : retrieve the SAMLresponse");
141             data = getData(URL_MON_COMPTE + "/" + authResult.successUrl);
142             htmlDocument = Jsoup.parse(data);
143             el = htmlDocument.select("form").first();
144             samlInput = el.select("input[name=SAMLResponse]").first();
145
146             logger.debug("Step 5 : post the SAMLresponse to finish the authentication");
147             result = httpClient.POST(el.attr("action")).content(getFormContent("SAMLResponse", samlInput.attr("value")))
148                     .send();
149             if (result.getStatus() != 302) {
150                 throw new LinkyException("Connection failed step 5");
151             }
152             connected = true;
153         } catch (InterruptedException | TimeoutException | ExecutionException | JsonSyntaxException e) {
154             throw new LinkyException("Error opening connection with Enedis webservice", e);
155         }
156     }
157
158     public String getLocation(ContentResponse response) {
159         return response.getHeaders().get(HttpHeader.LOCATION);
160     }
161
162     public void disconnect() throws LinkyException {
163         if (connected) {
164             logger.debug("Logout process");
165             try { // Three times in a row to get disconnected
166                 String location = getLocation(httpClient.GET(URL_APPS_LINCS + "/logout"));
167                 location = getLocation(httpClient.GET(location));
168                 location = getLocation(httpClient.GET(location));
169                 CookieStore cookieStore = httpClient.getCookieStore();
170                 cookieStore.removeAll();
171                 connected = false;
172             } catch (InterruptedException | ExecutionException | TimeoutException e) {
173                 throw new LinkyException("Error while disconnecting from Enedis webservice", e);
174             }
175         }
176     }
177
178     public boolean isConnected() {
179         return connected;
180     }
181
182     public void dispose() throws LinkyException {
183         disconnect();
184     }
185
186     private void addCookie(String key, String value) {
187         CookieStore cookieStore = httpClient.getCookieStore();
188         HttpCookie cookie = new HttpCookie(key, value);
189         cookie.setDomain(".enedis.fr");
190         cookie.setPath("/");
191         cookieStore.add(URI.create(URL_COOKIE), cookie);
192     }
193
194     private FormContentProvider getFormContent(String fieldName, String fieldValue) {
195         Fields fields = new Fields();
196         fields.put(fieldName, fieldValue);
197         return new FormContentProvider(fields);
198     }
199
200     private String getData(String url) throws LinkyException {
201         try {
202             ContentResponse result = httpClient.GET(url);
203             if (result.getStatus() != 200) {
204                 throw new LinkyException(String.format("Error requesting '%s' : %s", url, result.getContentAsString()));
205             }
206             return result.getContentAsString();
207         } catch (InterruptedException | ExecutionException | TimeoutException e) {
208             throw new LinkyException(String.format("Error getting url : '%s'", url), e);
209         }
210     }
211
212     public PrmInfo getPrmInfo() throws LinkyException {
213         if (!connected) {
214             initialize();
215         }
216         final String prm_info_url = URL_APPS_LINCS + "/mes-mesures/api/private/v1/personnes/null/prms";
217         String data = getData(prm_info_url);
218         if (data.isEmpty()) {
219             throw new LinkyException(String.format("Requesting '%s' returned an empty response", prm_info_url));
220         }
221         try {
222             PrmInfo[] prms = gson.fromJson(data, PrmInfo[].class);
223             return prms[0];
224         } catch (JsonSyntaxException e) {
225             logger.debug("invalid JSON response not matching PrmInfo[].class: {}", data);
226             throw new LinkyException(String.format("Requesting '%s' returned an invalid JSON response : %s",
227                     prm_info_url, e.getMessage()), e);
228         }
229     }
230
231     public UserInfo getUserInfo() throws LinkyException {
232         if (!connected) {
233             initialize();
234         }
235         final String user_info_url = URL_APPS_LINCS + "/userinfos";
236         String data = getData(user_info_url);
237         if (data.isEmpty()) {
238             throw new LinkyException(String.format("Requesting '%s' returned an empty response", user_info_url));
239         }
240         try {
241             return Objects.requireNonNull(gson.fromJson(data, UserInfo.class));
242         } catch (JsonSyntaxException e) {
243             logger.debug("invalid JSON response not matching UserInfo.class: {}", data);
244             throw new LinkyException(String.format("Requesting '%s' returned an invalid JSON response : %s",
245                     user_info_url, e.getMessage()), e);
246         }
247     }
248
249     private Consumption getMeasures(String userId, String prmId, LocalDate from, LocalDate to, String request)
250             throws LinkyException {
251         final String measure_url = URL_APPS_LINCS
252                 + "/mes-mesures/api/private/v1/personnes/%s/prms/%s/donnees-%s?dateDebut=%s&dateFin=%s&mesuretypecode=CONS";
253         String url = String.format(measure_url, userId, prmId, request, from.format(API_DATE_FORMAT),
254                 to.format(API_DATE_FORMAT));
255         String data = getData(url);
256         if (data.isEmpty()) {
257             throw new LinkyException(String.format("Requesting '%s' returned an empty response", url));
258         }
259         logger.trace("getData returned {}", data);
260         try {
261             ConsumptionReport report = gson.fromJson(data, ConsumptionReport.class);
262             return report.firstLevel.consumptions;
263         } catch (JsonSyntaxException e) {
264             logger.debug("invalid JSON response not matching ConsumptionReport.class: {}", data);
265             throw new LinkyException(
266                     String.format("Requesting '%s' returned an invalid JSON response : %s", url, e.getMessage()), e);
267         }
268     }
269
270     public Consumption getEnergyData(String userId, String prmId, LocalDate from, LocalDate to) throws LinkyException {
271         if (!connected) {
272             initialize();
273         }
274         return getMeasures(userId, prmId, from, to, "energie");
275     }
276
277     public Consumption getPowerData(String userId, String prmId, LocalDate from, LocalDate to) throws LinkyException {
278         if (!connected) {
279             initialize();
280         }
281         return getMeasures(userId, prmId, from, to, "pmax");
282     }
283 }