Skip to content

Commit 6a840f7

Browse files
authored
[linky] Fixes for change in Enedis API on 2024 December 20 (openhab#17945)
* fixes for change in Enedis API on 2024 December 20 ! - URL for data is now mes-mesures-prm and not mes-mesures. - Dto format have changed : mainly merge data & date, field renaming, and moving. - Some changes on date format. * add timezone to thing config to allow overriding default timezone * add possible fix for 500 Internal Server Error * backport some fixes from linkyv2 branch to handle enedis website errors * remove condition so we can always get metadata to fix 500 error * remove the missingData stuff from previous commit, realize if was duplicate with check already done in getConsumptionAfterChecks * remove also the refreshinterval on ExpiringDayCache constructor, we don't need it anymore * remove nullable on action field Signed-off-by: Laurent ARNAL <[email protected]>
1 parent 6a1a41a commit 6a840f7

File tree

10 files changed

+310
-151
lines changed

10 files changed

+310
-151
lines changed

bundles/org.openhab.binding.linky/README.md

+8-5
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,12 @@ The binding has no configuration options, all configuration is done at Thing lev
2323

2424
The thing has the following configuration parameters:
2525

26-
| Parameter | Description |
27-
|----------------|--------------------------------|
28-
| username | Your Enedis platform username. |
29-
| password | Your Enedis platform password. |
30-
| internalAuthId | The internal authID |
26+
| Parameter | Description |
27+
|----------------|--------------------------------------------|
28+
| username | Your Enedis platform username. |
29+
| password | Your Enedis platform password. |
30+
| internalAuthId | The internal authID |
31+
| timezone | The timezone at the location of your linky |
3132

3233
This version is now compatible with the new API of Enedis (deployed from june 2020).
3334
To avoid the captcha login, it is necessary to log before on a classical browser (e.g Chrome, Firefox) and to retrieve the user cookies (internalAuthId).
@@ -43,6 +44,8 @@ Instructions given for Firefox :
4344
1. Disconnect from your Enedis account
4445
1. Repeat steps 1, 2. You should arrive directly on step 5, then open the developer tool window (F12) and select "Stockage" tab. In the "Cookies" entry, select "https://mon-compte-enedis.fr". You'll find an entry named "internalAuthId", copy this value in your openHAB configuration.
4546

47+
A new timezone parameter has been introduced. If you don't put a value, it will default to the timezone of your openHAB installation. This parameter can be useful if you read data from a Linky in a different timezone.
48+
4649
## Channels
4750

4851
The information that is retrieved is available as these channels:

bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/LinkyConfiguration.java

+1
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ public class LinkyConfiguration {
2626
public String username = "";
2727
public String password = "";
2828
public String internalAuthId = "";
29+
public String timezone = "";
2930

3031
public boolean seemsValid() {
3132
return !username.isBlank() && !password.isBlank() && !internalAuthId.isBlank();

bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/LinkyHandlerFactory.java

+35-6
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,17 @@
1212
*/
1313
package org.openhab.binding.linky.internal;
1414

15+
import static java.time.temporal.ChronoField.*;
1516
import static org.openhab.binding.linky.internal.LinkyBindingConstants.THING_TYPE_LINKY;
1617

1718
import java.security.KeyManagementException;
1819
import java.security.NoSuchAlgorithmException;
20+
import java.time.LocalDate;
21+
import java.time.LocalDateTime;
1922
import java.time.ZonedDateTime;
2023
import java.time.format.DateTimeFormatter;
24+
import java.time.format.DateTimeFormatterBuilder;
25+
import java.time.format.DateTimeParseException;
2126

2227
import javax.net.ssl.SSLContext;
2328
import javax.net.ssl.TrustManager;
@@ -27,7 +32,9 @@
2732
import org.eclipse.jetty.client.HttpClient;
2833
import org.eclipse.jetty.util.ssl.SslContextFactory;
2934
import org.openhab.binding.linky.internal.handler.LinkyHandler;
35+
import org.openhab.binding.linky.internal.utils.DoubleTypeAdapter;
3036
import org.openhab.core.i18n.LocaleProvider;
37+
import org.openhab.core.i18n.TimeZoneProvider;
3138
import org.openhab.core.io.net.http.HttpClientFactory;
3239
import org.openhab.core.io.net.http.TrustAllTrustManager;
3340
import org.openhab.core.thing.Thing;
@@ -55,21 +62,42 @@
5562
@Component(service = ThingHandlerFactory.class, configurationPid = "binding.linky")
5663
public class LinkyHandlerFactory extends BaseThingHandlerFactory {
5764
private static final DateTimeFormatter LINKY_FORMATTER = DateTimeFormatter.ofPattern("uuuu-MM-dd'T'HH:mm:ss.SSSX");
65+
private static final DateTimeFormatter LINKY_LOCALDATE_FORMATTER = DateTimeFormatter.ofPattern("uuuu-MM-dd");
66+
private static final DateTimeFormatter LINKY_LOCALDATETIME_FORMATTER = new DateTimeFormatterBuilder()
67+
.appendPattern("uuuu-MM-dd'T'HH:mm").optionalStart().appendLiteral(':').appendValue(SECOND_OF_MINUTE, 2)
68+
.optionalStart().appendFraction(NANO_OF_SECOND, 0, 9, true).toFormatter();
69+
5870
private static final int REQUEST_BUFFER_SIZE = 8000;
5971
private static final int RESPONSE_BUFFER_SIZE = 200000;
6072

6173
private final Logger logger = LoggerFactory.getLogger(LinkyHandlerFactory.class);
62-
private final Gson gson = new GsonBuilder().registerTypeAdapter(ZonedDateTime.class,
63-
(JsonDeserializer<ZonedDateTime>) (json, type, jsonDeserializationContext) -> ZonedDateTime
64-
.parse(json.getAsJsonPrimitive().getAsString(), LINKY_FORMATTER))
65-
.create();
74+
private final Gson gson = new GsonBuilder()
75+
.registerTypeAdapter(ZonedDateTime.class,
76+
(JsonDeserializer<ZonedDateTime>) (json, type, jsonDeserializationContext) -> ZonedDateTime
77+
.parse(json.getAsJsonPrimitive().getAsString(), LINKY_FORMATTER))
78+
.registerTypeAdapter(LocalDate.class,
79+
(JsonDeserializer<LocalDate>) (json, type, jsonDeserializationContext) -> LocalDate
80+
.parse(json.getAsJsonPrimitive().getAsString(), LINKY_LOCALDATE_FORMATTER))
81+
.registerTypeAdapter(LocalDateTime.class,
82+
(JsonDeserializer<LocalDateTime>) (json, type, jsonDeserializationContext) -> {
83+
try {
84+
return LocalDateTime.parse(json.getAsJsonPrimitive().getAsString(),
85+
LINKY_LOCALDATETIME_FORMATTER);
86+
} catch (DateTimeParseException ex) {
87+
return LocalDate.parse(json.getAsJsonPrimitive().getAsString(), LINKY_LOCALDATE_FORMATTER)
88+
.atStartOfDay();
89+
}
90+
})
91+
.registerTypeAdapter(Double.class, new DoubleTypeAdapter()).serializeNulls().create();
6692
private final LocaleProvider localeProvider;
6793
private final HttpClient httpClient;
94+
private final TimeZoneProvider timeZoneProvider;
6895

6996
@Activate
7097
public LinkyHandlerFactory(final @Reference LocaleProvider localeProvider,
71-
final @Reference HttpClientFactory httpClientFactory) {
98+
final @Reference HttpClientFactory httpClientFactory, final @Reference TimeZoneProvider timeZoneProvider) {
7299
this.localeProvider = localeProvider;
100+
this.timeZoneProvider = timeZoneProvider;
73101
SslContextFactory sslContextFactory = new SslContextFactory.Client();
74102
try {
75103
SSLContext sslContext = SSLContext.getInstance("SSL");
@@ -114,7 +142,8 @@ public boolean supportsThingType(ThingTypeUID thingTypeUID) {
114142

115143
@Override
116144
protected @Nullable ThingHandler createHandler(Thing thing) {
117-
return supportsThingType(thing.getThingTypeUID()) ? new LinkyHandler(thing, localeProvider, gson, httpClient)
145+
return supportsThingType(thing.getThingTypeUID())
146+
? new LinkyHandler(thing, localeProvider, gson, httpClient, timeZoneProvider)
118147
: null;
119148
}
120149
}

bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/api/EnedisHttpApi.java

+45-11
Original file line numberDiff line numberDiff line change
@@ -62,18 +62,18 @@
6262
*/
6363
@NonNullByDefault
6464
public class EnedisHttpApi {
65-
private static final DateTimeFormatter API_DATE_FORMAT = DateTimeFormatter.ofPattern("dd-MM-yyyy");
65+
private static final DateTimeFormatter API_DATE_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd");
6666
private static final String ENEDIS_DOMAIN = ".enedis.fr";
6767
private static final String URL_APPS_LINCS = "https://alex.microapplications" + ENEDIS_DOMAIN;
6868
private static final String URL_MON_COMPTE = "https://mon-compte" + ENEDIS_DOMAIN;
6969
private static final String URL_COMPTE_PART = URL_MON_COMPTE.replace("compte", "compte-particulier");
7070
private static final String URL_ENEDIS_AUTHENTICATE = URL_APPS_LINCS + "/authenticate?target=" + URL_COMPTE_PART;
7171
private static final String USER_INFO_CONTRACT_URL = URL_APPS_LINCS + "/mon-compte-client/api/private/v1/userinfos";
7272
private static final String USER_INFO_URL = URL_APPS_LINCS + "/userinfos";
73-
private static final String PRM_INFO_BASE_URL = URL_APPS_LINCS + "/mes-mesures/api/private/v1/personnes/";
73+
private static final String PRM_INFO_BASE_URL = URL_APPS_LINCS + "/mes-mesures-prm/api/private/v1/personnes/";
7474
private static final String PRM_INFO_URL = URL_APPS_LINCS + "/mes-prms-part/api/private/v2/personnes/%s/prms";
7575
private static final String MEASURE_URL = PRM_INFO_BASE_URL
76-
+ "%s/prms/%s/donnees-%s?dateDebut=%s&dateFin=%s&mesuretypecode=CONS";
76+
+ "%s/prms/%s/donnees-energetiques?mesuresTypeCode=%s&mesuresCorrigees=false&typeDonnees=CONS&dateDebut=%s";
7777
private static final URI COOKIE_URI = URI.create(URL_COMPTE_PART);
7878
private static final Pattern REQ_PATTERN = Pattern.compile("ReqID%(.*?)%26");
7979

@@ -90,10 +90,16 @@ public EnedisHttpApi(LinkyConfiguration config, Gson gson, HttpClient httpClient
9090
this.config = config;
9191
}
9292

93+
public void removeAllCookie() {
94+
httpClient.getCookieStore().removeAll();
95+
}
96+
9397
public void initialize() throws LinkyException {
9498
logger.debug("Starting login process for user: {}", config.username);
9599

96100
try {
101+
removeAllCookie();
102+
97103
addCookie(LinkyConfiguration.INTERNAL_AUTH_ID, config.internalAuthId);
98104
logger.debug("Step 1: getting authentification");
99105
String data = getContent(URL_ENEDIS_AUTHENTICATE);
@@ -237,10 +243,37 @@ private FormContentProvider getFormContent(String fieldName, String fieldValue)
237243

238244
private String getContent(String url) throws LinkyException {
239245
try {
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");
246+
Request request = httpClient.newRequest(url);
247+
248+
request = request.agent("Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:47.0) Gecko/20100101 Firefox/47.0");
242249
request = request.method(HttpMethod.GET);
243250
ContentResponse result = request.send();
251+
if (result.getStatus() == HttpStatus.TEMPORARY_REDIRECT_307
252+
|| result.getStatus() == HttpStatus.MOVED_TEMPORARILY_302) {
253+
String loc = result.getHeaders().get("Location");
254+
String newUrl = "";
255+
256+
if (loc.startsWith("http://") || loc.startsWith("https://")) {
257+
newUrl = loc;
258+
} else {
259+
newUrl = URL_APPS_LINCS + loc;
260+
}
261+
262+
request = httpClient.newRequest(newUrl);
263+
request = request.method(HttpMethod.GET);
264+
result = request.send();
265+
266+
if (result.getStatus() == HttpStatus.TEMPORARY_REDIRECT_307
267+
|| result.getStatus() == HttpStatus.MOVED_TEMPORARILY_302) {
268+
loc = result.getHeaders().get("Location");
269+
String[] urlParts = loc.split("/");
270+
if (urlParts.length < 4) {
271+
throw new LinkyException("malformed url : %s", loc);
272+
}
273+
return urlParts[3];
274+
}
275+
}
276+
244277
if (result.getStatus() != HttpStatus.OK_200) {
245278
throw new LinkyException("Error requesting '%s': %s", url, result.getContentAsString());
246279
}
@@ -261,7 +294,9 @@ private <T> T getData(String url, Class<T> clazz) throws LinkyException {
261294
throw new LinkyException("Requesting '%s' returned an empty response", url);
262295
}
263296
try {
264-
return Objects.requireNonNull(gson.fromJson(data, clazz));
297+
T result = Objects.requireNonNull(gson.fromJson(data, clazz));
298+
logger.trace("getData success {}: {}", clazz.getName(), url);
299+
return result;
265300
} catch (JsonSyntaxException e) {
266301
logger.debug("Invalid JSON response not matching {}: {}", clazz.getName(), data);
267302
throw new LinkyException(e, "Requesting '%s' returned an invalid JSON response", url);
@@ -289,17 +324,16 @@ public UserInfo getUserInfo() throws LinkyException {
289324

290325
private Consumption getMeasures(String userId, String prmId, LocalDate from, LocalDate to, String request)
291326
throws LinkyException {
292-
String url = String.format(MEASURE_URL, userId, prmId, request, from.format(API_DATE_FORMAT),
293-
to.format(API_DATE_FORMAT));
327+
String url = String.format(MEASURE_URL, userId, prmId, request, from.format(API_DATE_FORMAT));
294328
ConsumptionReport report = getData(url, ConsumptionReport.class);
295-
return report.firstLevel.consumptions;
329+
return report.consumptions;
296330
}
297331

298332
public Consumption getEnergyData(String userId, String prmId, LocalDate from, LocalDate to) throws LinkyException {
299-
return getMeasures(userId, prmId, from, to, "energie");
333+
return getMeasures(userId, prmId, from, to, "ENERGIE");
300334
}
301335

302336
public Consumption getPowerData(String userId, String prmId, LocalDate from, LocalDate to) throws LinkyException {
303-
return getMeasures(userId, prmId, from, to, "pmax");
337+
return getMeasures(userId, prmId, from, to, "PMAX");
304338
}
305339
}

bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/api/ExpiringDayCache.java

+6-3
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,9 @@ public class ExpiringDayCache<V> {
4343

4444
private final String name;
4545
private final int beginningHour;
46-
private final Supplier<@Nullable V> action;
46+
private final int beginningMinute;
47+
48+
private Supplier<@Nullable V> action;
4749

4850
private @Nullable V value;
4951
private LocalDateTime expiresAt;
@@ -55,9 +57,10 @@ public class ExpiringDayCache<V> {
5557
* @param beginningHour the hour in the day at which the validity period is starting
5658
* @param action the action to retrieve/calculate the value
5759
*/
58-
public ExpiringDayCache(String name, int beginningHour, Supplier<@Nullable V> action) {
60+
public ExpiringDayCache(String name, int beginningHour, int beginningMinute, Supplier<@Nullable V> action) {
5961
this.name = name;
6062
this.beginningHour = beginningHour;
63+
this.beginningMinute = beginningMinute;
6164
this.expiresAt = calcAlreadyExpired();
6265
this.action = action;
6366
}
@@ -99,7 +102,7 @@ public boolean isExpired() {
99102

100103
private LocalDateTime calcNextExpiresAt() {
101104
LocalDateTime now = LocalDateTime.now();
102-
LocalDateTime limit = now.withHour(beginningHour).truncatedTo(ChronoUnit.HOURS);
105+
LocalDateTime limit = now.withHour(beginningHour).withMinute(beginningMinute).truncatedTo(ChronoUnit.MINUTES);
103106
LocalDateTime result = now.isBefore(limit) ? limit : limit.plusDays(1);
104107
logger.debug("calcNextExpiresAt result = {}", result.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME));
105108
return result;

bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/ConsumptionReport.java

+19-20
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@
1212
*/
1313
package org.openhab.binding.linky.internal.dto;
1414

15-
import java.time.ZonedDateTime;
15+
import java.time.LocalDate;
16+
import java.time.LocalDateTime;
1617
import java.util.List;
1718

1819
import com.google.gson.annotations.SerializedName;
@@ -22,43 +23,41 @@
2223
* returned by API calls
2324
*
2425
* @author Gaël L'hopital - Initial contribution
26+
* @author Laurent Arnal - fix to handle new Dto format after enedis site modifications
2527
*/
2628
public class ConsumptionReport {
27-
public class Period {
28-
public String grandeurPhysiqueEnum;
29-
public ZonedDateTime dateDebut;
30-
public ZonedDateTime dateFin;
29+
30+
public class Data {
31+
public LocalDateTime dateDebut;
32+
public LocalDateTime dateFin;
33+
public Double valeur;
3134
}
3235

3336
public class Aggregate {
34-
public List<String> labels;
35-
public List<Period> periodes;
36-
public List<Double> datas;
37+
@SerializedName("donnees")
38+
public List<Data> datas;
39+
public String unite;
3740
}
3841

3942
public class ChronoData {
40-
@SerializedName("JOUR")
43+
@SerializedName("jour")
4144
public Aggregate days;
42-
@SerializedName("SEMAINE")
45+
@SerializedName("semaine")
4346
public Aggregate weeks;
44-
@SerializedName("MOIS")
47+
@SerializedName("mois")
4548
public Aggregate months;
46-
@SerializedName("ANNEE")
49+
@SerializedName("annee")
4750
public Aggregate years;
4851
}
4952

5053
public class Consumption {
5154
public ChronoData aggregats;
5255
public String grandeurMetier;
5356
public String grandeurPhysique;
54-
public String unite;
55-
}
56-
57-
public class FirstLevel {
58-
@SerializedName("CONS")
59-
public Consumption consumptions;
57+
public LocalDate dateDebut;
58+
public LocalDate dateFin;
6059
}
6160

62-
@SerializedName("1")
63-
public FirstLevel firstLevel;
61+
@SerializedName("cons")
62+
public Consumption consumptions;
6463
}

0 commit comments

Comments
 (0)