Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[linky] Fixes for change in Enedis API on 2024 December 20 #17945

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,16 @@
*/
package org.openhab.binding.linky.internal;

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

import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeFormatterBuilder;

import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;
Expand All @@ -28,6 +32,7 @@
import org.eclipse.jetty.util.ssl.SslContextFactory;
import org.openhab.binding.linky.internal.handler.LinkyHandler;
import org.openhab.core.i18n.LocaleProvider;
import org.openhab.core.i18n.TimeZoneProvider;
import org.openhab.core.io.net.http.HttpClientFactory;
import org.openhab.core.io.net.http.TrustAllTrustManager;
import org.openhab.core.thing.Thing;
Expand Down Expand Up @@ -55,21 +60,43 @@
@Component(service = ThingHandlerFactory.class, configurationPid = "binding.linky")
public class LinkyHandlerFactory extends BaseThingHandlerFactory {
private static final DateTimeFormatter LINKY_FORMATTER = DateTimeFormatter.ofPattern("uuuu-MM-dd'T'HH:mm:ss.SSSX");
private static final DateTimeFormatter LINKY_LOCALDATE_FORMATTER = DateTimeFormatter.ofPattern("uuuu-MM-dd");
private static final DateTimeFormatter LINKY_LOCALDATETIME_FORMATTER = new DateTimeFormatterBuilder()
.appendPattern("uuuu-MM-dd'T'HH:mm").optionalStart().appendLiteral(':').appendValue(SECOND_OF_MINUTE, 2)
.optionalStart().appendFraction(NANO_OF_SECOND, 0, 9, true).toFormatter();

private static final int REQUEST_BUFFER_SIZE = 8000;
private static final int RESPONSE_BUFFER_SIZE = 200000;

private final Logger logger = LoggerFactory.getLogger(LinkyHandlerFactory.class);
private final Gson gson = new GsonBuilder().registerTypeAdapter(ZonedDateTime.class,
(JsonDeserializer<ZonedDateTime>) (json, type, jsonDeserializationContext) -> ZonedDateTime
.parse(json.getAsJsonPrimitive().getAsString(), LINKY_FORMATTER))
private final Gson gson = new GsonBuilder()
.registerTypeAdapter(ZonedDateTime.class,
(JsonDeserializer<ZonedDateTime>) (json, type, jsonDeserializationContext) -> ZonedDateTime
.parse(json.getAsJsonPrimitive().getAsString(), LINKY_FORMATTER))
.registerTypeAdapter(LocalDate.class,
(JsonDeserializer<LocalDate>) (json, type, jsonDeserializationContext) -> LocalDate
.parse(json.getAsJsonPrimitive().getAsString(), LINKY_LOCALDATE_FORMATTER))
.registerTypeAdapter(LocalDateTime.class,
(JsonDeserializer<LocalDateTime>) (json, type, jsonDeserializationContext) -> {
try {
return LocalDateTime.parse(json.getAsJsonPrimitive().getAsString(),
LINKY_LOCALDATETIME_FORMATTER);
} catch (Exception ex) {
return LocalDate.parse(json.getAsJsonPrimitive().getAsString(), LINKY_LOCALDATE_FORMATTER)
.atStartOfDay();
}
})

.create();
private final LocaleProvider localeProvider;
private final HttpClient httpClient;
private final TimeZoneProvider timeZoneProvider;

@Activate
public LinkyHandlerFactory(final @Reference LocaleProvider localeProvider,
final @Reference HttpClientFactory httpClientFactory) {
final @Reference HttpClientFactory httpClientFactory, final @Reference TimeZoneProvider timeZoneProvider) {
this.localeProvider = localeProvider;
this.timeZoneProvider = timeZoneProvider;
SslContextFactory sslContextFactory = new SslContextFactory.Client();
try {
SSLContext sslContext = SSLContext.getInstance("SSL");
Expand Down Expand Up @@ -114,7 +141,8 @@ public boolean supportsThingType(ThingTypeUID thingTypeUID) {

@Override
protected @Nullable ThingHandler createHandler(Thing thing) {
return supportsThingType(thing.getThingTypeUID()) ? new LinkyHandler(thing, localeProvider, gson, httpClient)
return supportsThingType(thing.getThingTypeUID())
? new LinkyHandler(thing, localeProvider, gson, httpClient, timeZoneProvider)
: null;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -62,18 +62,18 @@
*/
@NonNullByDefault
public class EnedisHttpApi {
private static final DateTimeFormatter API_DATE_FORMAT = DateTimeFormatter.ofPattern("dd-MM-yyyy");
private static final DateTimeFormatter API_DATE_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd");
private static final String ENEDIS_DOMAIN = ".enedis.fr";
private static final String URL_APPS_LINCS = "https://alex.microapplications" + ENEDIS_DOMAIN;
private static final String URL_MON_COMPTE = "https://mon-compte" + ENEDIS_DOMAIN;
private static final String URL_COMPTE_PART = URL_MON_COMPTE.replace("compte", "compte-particulier");
private static final String URL_ENEDIS_AUTHENTICATE = URL_APPS_LINCS + "/authenticate?target=" + URL_COMPTE_PART;
private static final String USER_INFO_CONTRACT_URL = URL_APPS_LINCS + "/mon-compte-client/api/private/v1/userinfos";
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_BASE_URL = URL_APPS_LINCS + "/mes-mesures-prm/api/private/v1/personnes/";
private static final String PRM_INFO_URL = URL_APPS_LINCS + "/mes-prms-part/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";
+ "%s/prms/%s/donnees-energetiques?mesuresTypeCode=%s&mesuresCorrigees=false&typeDonnees=CONS&dateDebut=%s";
private static final URI COOKIE_URI = URI.create(URL_COMPTE_PART);
private static final Pattern REQ_PATTERN = Pattern.compile("ReqID%(.*?)%26");

Expand Down Expand Up @@ -289,17 +289,16 @@ public UserInfo getUserInfo() throws LinkyException {

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));
String url = String.format(MEASURE_URL, userId, prmId, request, from.format(API_DATE_FORMAT));
ConsumptionReport report = getData(url, ConsumptionReport.class);
return report.firstLevel.consumptions;
return report.consumptions;
}

public Consumption getEnergyData(String userId, String prmId, LocalDate from, LocalDate to) throws LinkyException {
return getMeasures(userId, prmId, from, to, "energie");
return getMeasures(userId, prmId, from, to, "ENERGIE");
}

public Consumption getPowerData(String userId, String prmId, LocalDate from, LocalDate to) throws LinkyException {
return getMeasures(userId, prmId, from, to, "pmax");
return getMeasures(userId, prmId, from, to, "PMAX");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@
*/
package org.openhab.binding.linky.internal.dto;

import java.time.ZonedDateTime;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.List;

import com.google.gson.annotations.SerializedName;
Expand All @@ -22,43 +23,41 @@
* returned by API calls
*
* @author Gaël L'hopital - Initial contribution
* @author Laurent ARNAL - fix to handle new Dto format after enedis site modifications
*/
public class ConsumptionReport {
public class Period {
public String grandeurPhysiqueEnum;
public ZonedDateTime dateDebut;
public ZonedDateTime dateFin;

public class Data {
public LocalDateTime dateDebut;
public LocalDateTime dateFin;
public Double valeur;
}

public class Aggregate {
public List<String> labels;
public List<Period> periodes;
public List<Double> datas;
@SerializedName("donnees")
public List<Data> datas;
public String unite;
}

public class ChronoData {
@SerializedName("JOUR")
@SerializedName("jour")
public Aggregate days;
@SerializedName("SEMAINE")
@SerializedName("semaine")
public Aggregate weeks;
@SerializedName("MOIS")
@SerializedName("mois")
public Aggregate months;
@SerializedName("ANNEE")
@SerializedName("annee")
public Aggregate years;
}

public class Consumption {
public ChronoData aggregats;
public String grandeurMetier;
public String grandeurPhysique;
public String unite;
}

public class FirstLevel {
@SerializedName("CONS")
public Consumption consumptions;
public LocalDate dateDebut;
public LocalDate dateFin;
}

@SerializedName("1")
public FirstLevel firstLevel;
@SerializedName("cons")
public Consumption consumptions;
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
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.i18n.TimeZoneProvider;
import org.openhab.core.library.types.DateTimeType;
import org.openhab.core.library.types.QuantityType;
import org.openhab.core.library.unit.MetricPrefix;
Expand All @@ -65,6 +66,8 @@

@NonNullByDefault
public class LinkyHandler extends BaseThingHandler {
private final TimeZoneProvider timeZoneProvider;

private static final int REFRESH_FIRST_HOUR_OF_DAY = 1;
private static final int REFRESH_INTERVAL_IN_MIN = 120;

Expand All @@ -90,11 +93,13 @@ private enum Target {
ALL
}

public LinkyHandler(Thing thing, LocaleProvider localeProvider, Gson gson, HttpClient httpClient) {
public LinkyHandler(Thing thing, LocaleProvider localeProvider, Gson gson, HttpClient httpClient,
TimeZoneProvider timeZoneProvider) {
super(thing);
this.gson = gson;
this.httpClient = httpClient;
this.weekFields = WeekFields.of(localeProvider.getLocale());
this.timeZoneProvider = timeZoneProvider;

this.cachedDailyData = new ExpiringDayCache<>("daily cache", REFRESH_FIRST_HOUR_OF_DAY, () -> {
LocalDate today = LocalDate.now();
Expand Down Expand Up @@ -211,8 +216,9 @@ private synchronized void updatePowerData() {
if (isLinked(PEAK_POWER) || isLinked(PEAK_TIMESTAMP)) {
cachedPowerData.getValue().ifPresentOrElse(values -> {
Aggregate days = values.aggregats.days;
updatekVAChannel(PEAK_POWER, days.datas.get(days.datas.size() - 1));
updateState(PEAK_TIMESTAMP, new DateTimeType(days.periodes.get(days.datas.size() - 1).dateDebut));
updatekVAChannel(PEAK_POWER, days.datas.get(days.datas.size() - 1).valeur);
updateState(PEAK_TIMESTAMP, new DateTimeType(
days.datas.get(days.datas.size() - 1).dateDebut.atZone(this.timeZoneProvider.getTimeZone())));
Comment on lines +220 to +221
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was changed very recently, just before releasing 4.3. We don't need to pass a ZonedDateTipe; DateTimeType just needs an Instant as parameter. So you do not need TimeZoneProvider .

}, () -> {
updateKwhChannel(PEAK_POWER, Double.NaN);
updateState(PEAK_TIMESTAMP, UnDefType.UNDEF);
Expand All @@ -224,9 +230,9 @@ private void setCurrentAndPrevious(Aggregate periods, String currentChannel, Str
double currentValue = 0.0;
double previousValue = 0.0;
if (!periods.datas.isEmpty()) {
currentValue = periods.datas.get(periods.datas.size() - 1);
currentValue = periods.datas.get(periods.datas.size() - 1).valeur;
if (periods.datas.size() > 1) {
previousValue = periods.datas.get(periods.datas.size() - 2);
previousValue = periods.datas.get(periods.datas.size() - 2).valeur;
}
}
updateKwhChannel(currentChannel, currentValue);
Expand All @@ -240,7 +246,7 @@ private synchronized void updateDailyWeeklyData() {
if (isLinked(YESTERDAY) || isLinked(LAST_WEEK) || isLinked(THIS_WEEK)) {
cachedDailyData.getValue().ifPresentOrElse(values -> {
Aggregate days = values.aggregats.days;
updateKwhChannel(YESTERDAY, days.datas.get(days.datas.size() - 1));
updateKwhChannel(YESTERDAY, days.datas.get(days.datas.size() - 1).valeur);
setCurrentAndPrevious(values.aggregats.weeks, THIS_WEEK, LAST_WEEK);
}, () -> {
updateKwhChannel(YESTERDAY, Double.NaN);
Expand Down Expand Up @@ -322,16 +328,15 @@ private List<String> buildReport(LocalDate startDay, LocalDate endDay, @Nullable
Consumption result = getConsumptionData(startDay, endDay.plusDays(1));
if (result != null) {
Aggregate days = result.aggregats.days;
int size = (days.datas == null || days.periodes == null) ? 0
: (days.datas.size() <= days.periodes.size() ? days.datas.size() : days.periodes.size());
int size = (days.datas == null) ? 0 : days.datas.size();
for (int i = 0; i < size; i++) {
double consumption = days.datas.get(i);
LocalDate day = days.periodes.get(i).dateDebut.toLocalDate();
double consumption = days.datas.get(i).valeur;
LocalDate day = days.datas.get(i).dateDebut.toLocalDate();
// Filter data in case it contains data from dates outside the requested period
if (day.isBefore(startDay) || day.isAfter(endDay)) {
continue;
}
String line = days.periodes.get(i).dateDebut.format(DateTimeFormatter.ISO_LOCAL_DATE) + separator;
String line = days.datas.get(i).dateDebut.format(DateTimeFormatter.ISO_LOCAL_DATE) + separator;
if (consumption >= 0) {
line += String.valueOf(consumption);
}
Expand Down Expand Up @@ -474,50 +479,36 @@ public synchronized void handleCommand(ChannelUID channelUID, Command command) {
}

private void checkData(Consumption consumption) throws LinkyException {
if (consumption.aggregats.days.periodes.isEmpty()) {
if (consumption.aggregats.days.datas.isEmpty()) {
throw new LinkyException("Invalid consumptions data: no day period");
}
if (consumption.aggregats.days.periodes.size() != consumption.aggregats.days.datas.size()) {
throw new LinkyException("Invalid consumptions data: not any data for each day period");
}
if (consumption.aggregats.weeks.periodes.isEmpty()) {
if (consumption.aggregats.weeks != null && consumption.aggregats.weeks.datas.isEmpty()) {
throw new LinkyException("Invalid consumptions data: no week period");
}
if (consumption.aggregats.weeks.periodes.size() != consumption.aggregats.weeks.datas.size()) {
throw new LinkyException("Invalid consumptions data: not any data for each week period");
}
if (consumption.aggregats.months.periodes.isEmpty()) {
if (consumption.aggregats.months != null && consumption.aggregats.months.datas.isEmpty()) {
throw new LinkyException("Invalid consumptions data: no month period");
}
if (consumption.aggregats.months.periodes.size() != consumption.aggregats.months.datas.size()) {
throw new LinkyException("Invalid consumptions data: not any data for each month period");
}
if (consumption.aggregats.years.periodes.isEmpty()) {
if (consumption.aggregats.years != null && consumption.aggregats.years.datas.isEmpty()) {
throw new LinkyException("Invalid consumptions data: no year period");
}
if (consumption.aggregats.years.periodes.size() != consumption.aggregats.years.datas.size()) {
throw new LinkyException("Invalid consumptions data: not any data for each year period");
}
}

private boolean isDataFirstDayAvailable(Consumption consumption) {
Aggregate days = consumption.aggregats.days;
logData(days, "First day", false, DateTimeFormatter.ISO_LOCAL_DATE, Target.FIRST);
return days.datas != null && !days.datas.isEmpty() && !days.datas.get(0).isNaN();
return days.datas != null && !days.datas.isEmpty() && !days.datas.get(0).valeur.isNaN();
}

private boolean isDataLastDayAvailable(Consumption consumption) {
Aggregate days = consumption.aggregats.days;
logData(days, "Last day", false, DateTimeFormatter.ISO_LOCAL_DATE, Target.LAST);
return days.datas != null && !days.datas.isEmpty() && !days.datas.get(days.datas.size() - 1).isNaN();
return days.datas != null && !days.datas.isEmpty() && !days.datas.get(days.datas.size() - 1).valeur.isNaN();
}

private void logData(Aggregate aggregate, String title, boolean withDateFin, DateTimeFormatter dateTimeFormatter,
Target target) {
if (logger.isDebugEnabled()) {
int size = (aggregate.datas == null || aggregate.periodes == null) ? 0
: (aggregate.datas.size() <= aggregate.periodes.size() ? aggregate.datas.size()
: aggregate.periodes.size());
int size = (aggregate.datas == null) ? 0 : aggregate.datas.size();
if (target == Target.FIRST) {
if (size > 0) {
logData(aggregate, 0, title, withDateFin, dateTimeFormatter);
Expand All @@ -537,11 +528,11 @@ private void logData(Aggregate aggregate, String title, boolean withDateFin, Dat
private void logData(Aggregate aggregate, int index, String title, boolean withDateFin,
DateTimeFormatter dateTimeFormatter) {
if (withDateFin) {
logger.debug("{} {} {} value {}", title, aggregate.periodes.get(index).dateDebut.format(dateTimeFormatter),
aggregate.periodes.get(index).dateFin.format(dateTimeFormatter), aggregate.datas.get(index));
logger.debug("{} {} {} value {}", title, aggregate.datas.get(index).dateDebut.format(dateTimeFormatter),
aggregate.datas.get(index).dateFin.format(dateTimeFormatter), aggregate.datas.get(index).valeur);
} else {
logger.debug("{} {} value {}", title, aggregate.periodes.get(index).dateDebut.format(dateTimeFormatter),
aggregate.datas.get(index));
logger.debug("{} {} value {}", title, aggregate.datas.get(index).dateDebut.format(dateTimeFormatter),
aggregate.datas.get(index).valeur);
}
}
}