16
16
import java .net .URI ;
17
17
import java .time .LocalDate ;
18
18
import java .time .format .DateTimeFormatter ;
19
+ import java .util .HashMap ;
20
+ import java .util .List ;
19
21
import java .util .Objects ;
22
+ import java .util .Optional ;
20
23
import java .util .concurrent .ExecutionException ;
21
24
import java .util .concurrent .TimeoutException ;
22
25
import java .util .regex .Matcher ;
23
26
import java .util .regex .Pattern ;
24
27
28
+ import javax .ws .rs .core .MediaType ;
29
+
25
30
import org .eclipse .jdt .annotation .NonNullByDefault ;
26
31
import org .eclipse .jetty .client .HttpClient ;
27
32
import org .eclipse .jetty .client .api .ContentResponse ;
33
+ import org .eclipse .jetty .client .api .Request ;
28
34
import org .eclipse .jetty .client .util .FormContentProvider ;
29
35
import org .eclipse .jetty .client .util .StringContentProvider ;
30
36
import org .eclipse .jetty .http .HttpHeader ;
37
+ import org .eclipse .jetty .http .HttpMethod ;
38
+ import org .eclipse .jetty .http .HttpStatus ;
31
39
import org .eclipse .jetty .util .Fields ;
32
40
import org .jsoup .Jsoup ;
33
41
import org .jsoup .nodes .Document ;
38
46
import org .openhab .binding .linky .internal .dto .AuthResult ;
39
47
import org .openhab .binding .linky .internal .dto .ConsumptionReport ;
40
48
import org .openhab .binding .linky .internal .dto .ConsumptionReport .Consumption ;
49
+ import org .openhab .binding .linky .internal .dto .PrmDetail ;
41
50
import org .openhab .binding .linky .internal .dto .PrmInfo ;
42
51
import org .openhab .binding .linky .internal .dto .UserInfo ;
43
52
import org .slf4j .Logger ;
@@ -59,9 +68,10 @@ public class EnedisHttpApi {
59
68
private static final String URL_MON_COMPTE = "https://mon-compte" + ENEDIS_DOMAIN ;
60
69
private static final String URL_COMPTE_PART = URL_MON_COMPTE .replace ("compte" , "compte-particulier" );
61
70
private static final String URL_ENEDIS_AUTHENTICATE = URL_APPS_LINCS + "/authenticate?target=" + URL_COMPTE_PART ;
71
+ private static final String USER_INFO_CONTRACT_URL = URL_APPS_LINCS + "/mon-compte-client/api/private/v1/userinfos" ;
62
72
private static final String USER_INFO_URL = URL_APPS_LINCS + "/userinfos" ;
63
73
private static final String PRM_INFO_BASE_URL = URL_APPS_LINCS + "/mes-mesures/api/private/v1/personnes/" ;
64
- private static final String PRM_INFO_URL = PRM_INFO_BASE_URL + "null /prms" ;
74
+ private static final String PRM_INFO_URL = URL_APPS_LINCS + "/mes-prms/api/private/v2/personnes/%s /prms" ;
65
75
private static final String MEASURE_URL = PRM_INFO_BASE_URL
66
76
+ "%s/prms/%s/donnees-%s?dateDebut=%s&dateFin=%s&mesuretypecode=CONS" ;
67
77
private static final URI COOKIE_URI = URI .create (URL_COMPTE_PART );
@@ -81,22 +91,22 @@ public EnedisHttpApi(LinkyConfiguration config, Gson gson, HttpClient httpClient
81
91
}
82
92
83
93
public void initialize () throws LinkyException {
84
- logger .debug ("Starting login process for user : {}" , config .username );
94
+ logger .debug ("Starting login process for user: {}" , config .username );
85
95
86
96
try {
87
97
addCookie (LinkyConfiguration .INTERNAL_AUTH_ID , config .internalAuthId );
88
- logger .debug ("Step 1 : getting authentification" );
89
- String data = getData (URL_ENEDIS_AUTHENTICATE );
98
+ logger .debug ("Step 1: getting authentification" );
99
+ String data = getContent (URL_ENEDIS_AUTHENTICATE );
90
100
91
101
logger .debug ("Reception request SAML" );
92
102
Document htmlDocument = Jsoup .parse (data );
93
103
Element el = htmlDocument .select ("form" ).first ();
94
104
Element samlInput = el .select ("input[name=SAMLRequest]" ).first ();
95
105
96
- logger .debug ("Step 2 : send SSO SAMLRequest" );
106
+ logger .debug ("Step 2: send SSO SAMLRequest" );
97
107
ContentResponse result = httpClient .POST (el .attr ("action" ))
98
108
.content (getFormContent ("SAMLRequest" , samlInput .attr ("value" ))).send ();
99
- if (result .getStatus () != 302 ) {
109
+ if (result .getStatus () != HttpStatus . FOUND_302 ) {
100
110
throw new LinkyException ("Connection failed step 2" );
101
111
}
102
112
@@ -112,11 +122,11 @@ public void initialize() throws LinkyException {
112
122
+ reqId + "%26index%3Dnull%26acsURL%3D" + URL_APPS_LINCS
113
123
+ "/saml/SSO%26spEntityID%3DSP-ODW-PROD%26binding%3Durn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST&AMAuthCookie=" ;
114
124
115
- logger .debug ("Step 3 : auth1 - retrieve the template, thanks to cookie internalAuthId user is already set" );
125
+ logger .debug ("Step 3: auth1 - retrieve the template, thanks to cookie internalAuthId user is already set" );
116
126
result = httpClient .POST (authenticateUrl ).header ("X-NoSession" , "true" ).header ("X-Password" , "anonymous" )
117
127
.header ("X-Requested-With" , "XMLHttpRequest" ).header ("X-Username" , "anonymous" ).send ();
118
- if (result .getStatus () != 200 ) {
119
- throw new LinkyException ("Connection failed step 3 - auth1 : %s" , result .getContentAsString ());
128
+ if (result .getStatus () != HttpStatus . OK_200 ) {
129
+ throw new LinkyException ("Connection failed step 3 - auth1: %s" , result .getContentAsString ());
120
130
}
121
131
122
132
AuthData authData = gson .fromJson (result .getContentAsString (), AuthData .class );
@@ -128,13 +138,13 @@ public void initialize() throws LinkyException {
128
138
}
129
139
130
140
authData .callbacks .get (1 ).input .get (0 ).value = config .password ;
131
- logger .debug ("Step 4 : auth2 - send the auth data" );
132
- result = httpClient .POST (authenticateUrl ).header (HttpHeader .CONTENT_TYPE , "application/json" )
141
+ logger .debug ("Step 4: auth2 - send the auth data" );
142
+ result = httpClient .POST (authenticateUrl ).header (HttpHeader .CONTENT_TYPE , MediaType . APPLICATION_JSON )
133
143
.header ("X-NoSession" , "true" ).header ("X-Password" , "anonymous" )
134
144
.header ("X-Requested-With" , "XMLHttpRequest" ).header ("X-Username" , "anonymous" )
135
145
.content (new StringContentProvider (gson .toJson (authData ))).send ();
136
- if (result .getStatus () != 200 ) {
137
- throw new LinkyException ("Connection failed step 3 - auth2 : %s" , result .getContentAsString ());
146
+ if (result .getStatus () != HttpStatus . OK_200 ) {
147
+ throw new LinkyException ("Connection failed step 3 - auth2: %s" , result .getContentAsString ());
138
148
}
139
149
140
150
AuthResult authResult = gson .fromJson (result .getContentAsString (), AuthResult .class );
@@ -145,18 +155,40 @@ public void initialize() throws LinkyException {
145
155
logger .debug ("Add the tokenId cookie" );
146
156
addCookie ("enedisExt" , authResult .tokenId );
147
157
148
- logger .debug ("Step 5 : retrieve the SAMLresponse" );
149
- data = getData (URL_MON_COMPTE + "/" + authResult .successUrl );
158
+ logger .debug ("Step 5: retrieve the SAMLresponse" );
159
+ data = getContent (URL_MON_COMPTE + "/" + authResult .successUrl );
150
160
htmlDocument = Jsoup .parse (data );
151
161
el = htmlDocument .select ("form" ).first ();
152
162
samlInput = el .select ("input[name=SAMLResponse]" ).first ();
153
163
154
- logger .debug ("Step 6 : post the SAMLresponse to finish the authentication" );
164
+ logger .debug ("Step 6: post the SAMLresponse to finish the authentication" );
155
165
result = httpClient .POST (el .attr ("action" )).content (getFormContent ("SAMLResponse" , samlInput .attr ("value" )))
156
166
.send ();
157
- if (result .getStatus () != 302 ) {
167
+ if (result .getStatus () != HttpStatus . FOUND_302 ) {
158
168
throw new LinkyException ("Connection failed step 6" );
159
169
}
170
+
171
+ logger .debug ("Step 7: retrieve cookieKey" );
172
+ result = httpClient .GET (USER_INFO_CONTRACT_URL );
173
+
174
+ @ SuppressWarnings ("unchecked" )
175
+ HashMap <String , String > hashRes = gson .fromJson (result .getContentAsString (), HashMap .class );
176
+
177
+ String cookieKey ;
178
+ if (hashRes != null && hashRes .containsKey ("cnAlex" )) {
179
+ cookieKey = "personne_for_" + hashRes .get ("cnAlex" );
180
+ } else {
181
+ throw new LinkyException ("Connection failed step 7, missing cookieKey" );
182
+ }
183
+
184
+ List <HttpCookie > lCookie = httpClient .getCookieStore ().getCookies ();
185
+ Optional <HttpCookie > cookie = lCookie .stream ().filter (it -> it .getName ().contains (cookieKey )).findFirst ();
186
+
187
+ String cookieVal = cookie .map (HttpCookie ::getValue )
188
+ .orElseThrow (() -> new LinkyException ("Connection failed step 7, missing cookieVal" ));
189
+
190
+ addCookie (cookieKey , cookieVal );
191
+
160
192
connected = true ;
161
193
} catch (InterruptedException | TimeoutException | ExecutionException | JsonSyntaxException e ) {
162
194
throw new LinkyException (e , "Error opening connection with Enedis webservice" );
@@ -203,76 +235,64 @@ private FormContentProvider getFormContent(String fieldName, String fieldValue)
203
235
return new FormContentProvider (fields );
204
236
}
205
237
206
- private String getData (String url ) throws LinkyException {
238
+ private String getContent (String url ) throws LinkyException {
207
239
try {
208
- ContentResponse result = httpClient .GET (url );
209
- if (result .getStatus () != 200 ) {
210
- throw new LinkyException ("Error requesting '%s' : %s" , url , result .getContentAsString ());
240
+ Request request = httpClient .newRequest (url )
241
+ .agent ("Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:47.0) Gecko/20100101 Firefox/47.0" );
242
+ request = request .method (HttpMethod .GET );
243
+ ContentResponse result = request .send ();
244
+ if (result .getStatus () != HttpStatus .OK_200 ) {
245
+ throw new LinkyException ("Error requesting '%s': %s" , url , result .getContentAsString ());
211
246
}
212
- return result .getContentAsString ();
247
+ String content = result .getContentAsString ();
248
+ logger .trace ("getContent returned {}" , content );
249
+ return content ;
213
250
} catch (InterruptedException | ExecutionException | TimeoutException e ) {
214
- throw new LinkyException (e , "Error getting url : '%s'" , url );
251
+ throw new LinkyException (e , "Error getting url: '%s'" , url );
215
252
}
216
253
}
217
254
218
- public PrmInfo getPrmInfo ( ) throws LinkyException {
255
+ private < T > T getData ( String url , Class < T > clazz ) throws LinkyException {
219
256
if (!connected ) {
220
257
initialize ();
221
258
}
222
- String data = getData ( PRM_INFO_URL );
259
+ String data = getContent ( url );
223
260
if (data .isEmpty ()) {
224
- throw new LinkyException ("Requesting '%s' returned an empty response" , PRM_INFO_URL );
261
+ throw new LinkyException ("Requesting '%s' returned an empty response" , url );
225
262
}
226
263
try {
227
- PrmInfo [] prms = gson .fromJson (data , PrmInfo [].class );
228
- if (prms == null || prms .length < 1 ) {
229
- throw new LinkyException ("Invalid prms data received" );
230
- }
231
- return prms [0 ];
264
+ return Objects .requireNonNull (gson .fromJson (data , clazz ));
232
265
} catch (JsonSyntaxException e ) {
233
- logger .debug ("invalid JSON response not matching PrmInfo[].class : {}" , data );
234
- throw new LinkyException (e , "Requesting '%s' returned an invalid JSON response" , PRM_INFO_URL );
266
+ logger .debug ("Invalid JSON response not matching {} : {}" , clazz . getName () , data );
267
+ throw new LinkyException (e , "Requesting '%s' returned an invalid JSON response" , url );
235
268
}
236
269
}
237
270
238
- public UserInfo getUserInfo () throws LinkyException {
239
- if (!connected ) {
240
- initialize ();
241
- }
242
- String data = getData (USER_INFO_URL );
243
- if (data .isEmpty ()) {
244
- throw new LinkyException ("Requesting '%s' returned an empty response" , USER_INFO_URL );
245
- }
246
- try {
247
- return Objects .requireNonNull (gson .fromJson (data , UserInfo .class ));
248
- } catch (JsonSyntaxException e ) {
249
- logger .debug ("invalid JSON response not matching UserInfo.class: {}" , data );
250
- throw new LinkyException (e , "Requesting '%s' returned an invalid JSON response" , USER_INFO_URL );
271
+ public PrmInfo getPrmInfo (String internId ) throws LinkyException {
272
+ String url = PRM_INFO_URL .formatted (internId );
273
+ PrmInfo [] prms = getData (url , PrmInfo [].class );
274
+ if (prms .length < 1 ) {
275
+ throw new LinkyException ("Invalid prms data received" );
251
276
}
277
+ return prms [0 ];
278
+ }
279
+
280
+ public PrmDetail getPrmDetails (String internId , String prmId ) throws LinkyException {
281
+ String url = PRM_INFO_URL .formatted (internId ) + "/" + prmId
282
+ + "?embed=SITALI&embed=SITCOM&embed=SITCON&embed=SYNCON" ;
283
+ return getData (url , PrmDetail .class );
284
+ }
285
+
286
+ public UserInfo getUserInfo () throws LinkyException {
287
+ return getData (USER_INFO_URL , UserInfo .class );
252
288
}
253
289
254
290
private Consumption getMeasures (String userId , String prmId , LocalDate from , LocalDate to , String request )
255
291
throws LinkyException {
256
292
String url = String .format (MEASURE_URL , userId , prmId , request , from .format (API_DATE_FORMAT ),
257
293
to .format (API_DATE_FORMAT ));
258
- if (!connected ) {
259
- initialize ();
260
- }
261
- String data = getData (url );
262
- if (data .isEmpty ()) {
263
- throw new LinkyException ("Requesting '%s' returned an empty response" , url );
264
- }
265
- logger .trace ("getData returned {}" , data );
266
- try {
267
- ConsumptionReport report = gson .fromJson (data , ConsumptionReport .class );
268
- if (report == null ) {
269
- throw new LinkyException ("No report data received" );
270
- }
271
- return report .firstLevel .consumptions ;
272
- } catch (JsonSyntaxException e ) {
273
- logger .debug ("invalid JSON response not matching ConsumptionReport.class: {}" , data );
274
- throw new LinkyException (e , "Requesting '%s' returned an invalid JSON response" , url );
275
- }
294
+ ConsumptionReport report = getData (url , ConsumptionReport .class );
295
+ return report .firstLevel .consumptions ;
276
296
}
277
297
278
298
public Consumption getEnergyData (String userId , String prmId , LocalDate from , LocalDate to ) throws LinkyException {
0 commit comments