From d3de666f421ae50c47ebe3487c801648b66127ed Mon Sep 17 00:00:00 2001 From: Laurent ARNAL Date: Sat, 21 Dec 2024 12:01:50 +0100 Subject: [PATCH 01/13] 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. Signed-off-by: Laurent ARNAL --- .../linky/internal/LinkyHandlerFactory.java | 38 +++++++++-- .../linky/internal/api/EnedisHttpApi.java | 15 +++-- .../linky/internal/dto/ConsumptionReport.java | 39 ++++++------ .../linky/internal/handler/LinkyHandler.java | 63 ++++++++----------- 4 files changed, 86 insertions(+), 69 deletions(-) diff --git a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/LinkyHandlerFactory.java b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/LinkyHandlerFactory.java index ca60b18dd1da2..8e5d7f2be3c39 100644 --- a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/LinkyHandlerFactory.java +++ b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/LinkyHandlerFactory.java @@ -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; @@ -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; @@ -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) (json, type, jsonDeserializationContext) -> ZonedDateTime - .parse(json.getAsJsonPrimitive().getAsString(), LINKY_FORMATTER)) + private final Gson gson = new GsonBuilder() + .registerTypeAdapter(ZonedDateTime.class, + (JsonDeserializer) (json, type, jsonDeserializationContext) -> ZonedDateTime + .parse(json.getAsJsonPrimitive().getAsString(), LINKY_FORMATTER)) + .registerTypeAdapter(LocalDate.class, + (JsonDeserializer) (json, type, jsonDeserializationContext) -> LocalDate + .parse(json.getAsJsonPrimitive().getAsString(), LINKY_LOCALDATE_FORMATTER)) + .registerTypeAdapter(LocalDateTime.class, + (JsonDeserializer) (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"); @@ -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; } } diff --git a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/api/EnedisHttpApi.java b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/api/EnedisHttpApi.java index a2d2102ba912d..622561452bbd9 100644 --- a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/api/EnedisHttpApi.java +++ b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/api/EnedisHttpApi.java @@ -62,7 +62,7 @@ */ @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; @@ -70,10 +70,10 @@ public class EnedisHttpApi { 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"); @@ -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"); } } diff --git a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/ConsumptionReport.java b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/ConsumptionReport.java index 71e324f9f4cf9..6456cad7322f5 100644 --- a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/ConsumptionReport.java +++ b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/ConsumptionReport.java @@ -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; @@ -22,28 +23,30 @@ * 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 labels; - public List periodes; - public List datas; + @SerializedName("donnees") + public List 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; } @@ -51,14 +54,10 @@ 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; } diff --git a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/handler/LinkyHandler.java b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/handler/LinkyHandler.java index 4ad4815373a22..b7c1665963cd9 100644 --- a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/handler/LinkyHandler.java +++ b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/handler/LinkyHandler.java @@ -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; @@ -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; @@ -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(); @@ -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()))); }, () -> { updateKwhChannel(PEAK_POWER, Double.NaN); updateState(PEAK_TIMESTAMP, UnDefType.UNDEF); @@ -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); @@ -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); @@ -322,16 +328,15 @@ private List 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); } @@ -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); @@ -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); } } } From eca525d22f3ac14b569a4f82997d7f3934727a3d Mon Sep 17 00:00:00 2001 From: Laurent ARNAL Date: Wed, 8 Jan 2025 11:47:18 +0100 Subject: [PATCH 02/13] add timezone to thing config to allow overriding default timezone Signed-off-by: Laurent ARNAL --- .../linky/internal/LinkyConfiguration.java | 1 + .../linky/internal/handler/LinkyHandler.java | 20 +++++++++++++++++-- .../resources/OH-INF/thing/thing-types.xml | 8 ++++++++ 3 files changed, 27 insertions(+), 2 deletions(-) diff --git a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/LinkyConfiguration.java b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/LinkyConfiguration.java index 8b471ac673d9e..6021cae5ceab7 100644 --- a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/LinkyConfiguration.java +++ b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/LinkyConfiguration.java @@ -26,6 +26,7 @@ public class LinkyConfiguration { public String username = ""; public String password = ""; public String internalAuthId = ""; + public String timezone = ""; public boolean seemsValid() { return !username.isBlank() && !password.isBlank() && !internalAuthId.isBlank(); diff --git a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/handler/LinkyHandler.java b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/handler/LinkyHandler.java index b7c1665963cd9..51a447702e7a0 100644 --- a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/handler/LinkyHandler.java +++ b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/handler/LinkyHandler.java @@ -16,6 +16,7 @@ import java.time.LocalDate; import java.time.LocalDateTime; +import java.time.ZoneId; import java.time.ZonedDateTime; import java.time.format.DateTimeFormatter; import java.time.temporal.ChronoUnit; @@ -38,6 +39,7 @@ import org.openhab.binding.linky.internal.dto.PrmDetail; import org.openhab.binding.linky.internal.dto.PrmInfo; import org.openhab.binding.linky.internal.dto.UserInfo; +import org.openhab.core.config.core.Configuration; import org.openhab.core.i18n.LocaleProvider; import org.openhab.core.i18n.TimeZoneProvider; import org.openhab.core.library.types.DateTimeType; @@ -67,6 +69,7 @@ @NonNullByDefault public class LinkyHandler extends BaseThingHandler { private final TimeZoneProvider timeZoneProvider; + private ZoneId zoneId = ZoneId.systemDefault(); private static final int REFRESH_FIRST_HOUR_OF_DAY = 1; private static final int REFRESH_INTERVAL_IN_MIN = 120; @@ -155,6 +158,19 @@ public void initialize() { logger.debug("Initializing Linky handler."); updateStatus(ThingStatus.UNKNOWN); + // update the timezone if not set to default to openhab default timezone + Configuration thingConfig = getConfig(); + + Object val = thingConfig.get("timezone"); + if (val == null || "".equals(val)) { + zoneId = this.timeZoneProvider.getTimeZone(); + thingConfig.put("timezone", zoneId.getId()); + } else { + zoneId = ZoneId.of((String) val); + } + + updateConfiguration(thingConfig); + LinkyConfiguration config = getConfigAs(LinkyConfiguration.class); if (config.seemsValid()) { enedisApi = new EnedisHttpApi(config, gson, httpClient); @@ -217,8 +233,8 @@ private synchronized void updatePowerData() { cachedPowerData.getValue().ifPresentOrElse(values -> { Aggregate days = values.aggregats.days; 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()))); + updateState(PEAK_TIMESTAMP, + new DateTimeType(days.datas.get(days.datas.size() - 1).dateDebut.atZone(zoneId))); }, () -> { updateKwhChannel(PEAK_POWER, Double.NaN); updateState(PEAK_TIMESTAMP, UnDefType.UNDEF); diff --git a/bundles/org.openhab.binding.linky/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.linky/src/main/resources/OH-INF/thing/thing-types.xml index fbcdbb509e376..b094796bd972c 100644 --- a/bundles/org.openhab.binding.linky/src/main/resources/OH-INF/thing/thing-types.xml +++ b/bundles/org.openhab.binding.linky/src/main/resources/OH-INF/thing/thing-types.xml @@ -34,6 +34,14 @@ Authentication ID delivered after the captcha (see documentation). + + + The timezone associated with your Point of delivery. + Will default to openhab default timezone. + You will need to change this if your linky is located in a different timezone that your openhab location. + You can use an offset, or a label like Europe/Paris + + From bd721b838e8e26ba24d828f16bfb0dfa8cf06e32 Mon Sep 17 00:00:00 2001 From: Laurent ARNAL Date: Wed, 8 Jan 2025 11:49:27 +0100 Subject: [PATCH 03/13] spotless:apply Signed-off-by: Laurent ARNAL --- .../main/resources/OH-INF/thing/thing-types.xml | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/bundles/org.openhab.binding.linky/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.linky/src/main/resources/OH-INF/thing/thing-types.xml index b094796bd972c..8a66799dbccf6 100644 --- a/bundles/org.openhab.binding.linky/src/main/resources/OH-INF/thing/thing-types.xml +++ b/bundles/org.openhab.binding.linky/src/main/resources/OH-INF/thing/thing-types.xml @@ -35,13 +35,15 @@ Authentication ID delivered after the captcha (see documentation). - - The timezone associated with your Point of delivery. - Will default to openhab default timezone. - You will need to change this if your linky is located in a different timezone that your openhab location. - You can use an offset, or a label like Europe/Paris - - + + The timezone associated with your Point of delivery. + Will default to openhab default timezone. + You will + need to change this if your linky is located in a different timezone that your openhab location. + You can use an + offset, or a label like Europe/Paris + + From d43ab2a2dc6bd72abf3f14e01e0bc7b8dca95829 Mon Sep 17 00:00:00 2001 From: Laurent ARNAL Date: Wed, 8 Jan 2025 12:20:01 +0100 Subject: [PATCH 04/13] fixes for Jacob review Signed-off-by: Laurent ARNAL --- .../binding/linky/internal/handler/LinkyHandler.java | 6 +++--- .../src/main/resources/OH-INF/i18n/linky.properties | 4 +++- .../src/main/resources/OH-INF/thing/thing-types.xml | 4 ++-- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/handler/LinkyHandler.java b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/handler/LinkyHandler.java index 51a447702e7a0..50288bfc4c605 100644 --- a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/handler/LinkyHandler.java +++ b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/handler/LinkyHandler.java @@ -161,12 +161,12 @@ public void initialize() { // update the timezone if not set to default to openhab default timezone Configuration thingConfig = getConfig(); - Object val = thingConfig.get("timezone"); - if (val == null || "".equals(val)) { + String val = (String) thingConfig.get("timezone"); + if (val == null || val.isBlank()) { zoneId = this.timeZoneProvider.getTimeZone(); thingConfig.put("timezone", zoneId.getId()); } else { - zoneId = ZoneId.of((String) val); + zoneId = ZoneId.of(val); } updateConfiguration(thingConfig); diff --git a/bundles/org.openhab.binding.linky/src/main/resources/OH-INF/i18n/linky.properties b/bundles/org.openhab.binding.linky/src/main/resources/OH-INF/i18n/linky.properties index 9eeaf4460fdba..ecf55352b3a05 100644 --- a/bundles/org.openhab.binding.linky/src/main/resources/OH-INF/i18n/linky.properties +++ b/bundles/org.openhab.binding.linky/src/main/resources/OH-INF/i18n/linky.properties @@ -14,6 +14,8 @@ thing-type.config.linky.linky.internalAuthId.label = Auth ID thing-type.config.linky.linky.internalAuthId.description = Authentication ID delivered after the captcha (see documentation). thing-type.config.linky.linky.password.label = Password thing-type.config.linky.linky.password.description = Your Enedis Password +thing-type.config.linky.linky.timezone.label = timezone +thing-type.config.linky.linky.timezone.description = The timezone associated with your Point of delivery. Will default to openHAB default timezone. You will need to change this if your Linky is located in a different timezone that your openHAB location. You can use an offset, or a label like Europe/Paris thing-type.config.linky.linky.username.label = Username thing-type.config.linky.linky.username.description = Your Enedis Username @@ -41,6 +43,6 @@ channel-type.linky.power.label = Yesterday Peak Power channel-type.linky.power.description = Maximum power usage yesterday channel-type.linky.timestamp.label = Timestamp -# Thing status descriptions +# thing status descriptions offline.config-error-mandatory-settings = Username, password and authId are mandatory. diff --git a/bundles/org.openhab.binding.linky/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.linky/src/main/resources/OH-INF/thing/thing-types.xml index 8a66799dbccf6..51a489096c77e 100644 --- a/bundles/org.openhab.binding.linky/src/main/resources/OH-INF/thing/thing-types.xml +++ b/bundles/org.openhab.binding.linky/src/main/resources/OH-INF/thing/thing-types.xml @@ -37,9 +37,9 @@ The timezone associated with your Point of delivery. - Will default to openhab default timezone. + Will default to openHAB default timezone. You will - need to change this if your linky is located in a different timezone that your openhab location. + need to change this if your Linky is located in a different timezone that your openHAB location. You can use an offset, or a label like Europe/Paris From f8c760055b1f812c204e9ae63b089e729dc1e677 Mon Sep 17 00:00:00 2001 From: Laurent ARNAL Date: Sun, 26 Jan 2025 09:33:53 +0100 Subject: [PATCH 05/13] add possible fix for 500 Internal Server Error Signed-off-by: Laurent ARNAL --- .../linky/internal/api/EnedisHttpApi.java | 6 +++ .../linky/internal/handler/LinkyHandler.java | 49 ++++++++++++------- 2 files changed, 36 insertions(+), 19 deletions(-) diff --git a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/api/EnedisHttpApi.java b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/api/EnedisHttpApi.java index 622561452bbd9..d1bb0db2af50d 100644 --- a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/api/EnedisHttpApi.java +++ b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/api/EnedisHttpApi.java @@ -90,10 +90,16 @@ public EnedisHttpApi(LinkyConfiguration config, Gson gson, HttpClient httpClient this.config = config; } + public void removeAllCookie() { + httpClient.getCookieStore().removeAll(); + } + public void initialize() throws LinkyException { logger.debug("Starting login process for user: {}", config.username); try { + removeAllCookie(); + addCookie(LinkyConfiguration.INTERNAL_AUTH_ID, config.internalAuthId); logger.debug("Step 1: getting authentification"); String data = getContent(URL_ENEDIS_AUTHENTICATE); diff --git a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/handler/LinkyHandler.java b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/handler/LinkyHandler.java index 50288bfc4c605..c8f2e43b3dfe1 100644 --- a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/handler/LinkyHandler.java +++ b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/handler/LinkyHandler.java @@ -180,19 +180,6 @@ public void initialize() { api.initialize(); updateStatus(ThingStatus.ONLINE); - if (thing.getProperties().isEmpty()) { - UserInfo userInfo = api.getUserInfo(); - PrmInfo prmInfo = api.getPrmInfo(userInfo.userProperties.internId); - PrmDetail details = api.getPrmDetails(userInfo.userProperties.internId, prmInfo.idPrm); - updateProperties(Map.of(USER_ID, userInfo.userProperties.internId, PUISSANCE, - details.situationContractuelleDtos[0].structureTarifaire().puissanceSouscrite().valeur() - + " kVA", - PRM_ID, prmInfo.idPrm)); - } - - prmId = thing.getProperties().get(PRM_ID); - userId = thing.getProperties().get(USER_ID); - updateData(); disconnect(); @@ -214,17 +201,41 @@ public void initialize() { } } + private synchronized void updateMetaData() throws LinkyException { + EnedisHttpApi api = this.enedisApi; + if (api != null) { + if (thing.getProperties().isEmpty()) { + UserInfo userInfo = api.getUserInfo(); + PrmInfo prmInfo = api.getPrmInfo(userInfo.userProperties.internId); + PrmDetail details = api.getPrmDetails(userInfo.userProperties.internId, prmInfo.idPrm); + updateProperties(Map.of(USER_ID, userInfo.userProperties.internId, PUISSANCE, + details.situationContractuelleDtos[0].structureTarifaire().puissanceSouscrite().valeur() + + " kVA", + PRM_ID, prmInfo.idPrm)); + } + + prmId = thing.getProperties().get(PRM_ID); + userId = thing.getProperties().get(USER_ID); + } + } + /** * Request new data and updates channels */ private synchronized void updateData() { boolean connectedBefore = isConnected(); - updatePowerData(); - updateDailyWeeklyData(); - updateMonthlyData(); - updateYearlyData(); - if (!connectedBefore && isConnected()) { - disconnect(); + try { + updateMetaData(); + updatePowerData(); + updateDailyWeeklyData(); + updateMonthlyData(); + updateYearlyData(); + if (!connectedBefore && isConnected()) { + disconnect(); + } + } catch (LinkyException e) { + logger.error("Exception occurs during data update", e); + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage()); } } From 950878609a2c76779d491d28ab35d2b8f79798a9 Mon Sep 17 00:00:00 2001 From: Laurent ARNAL Date: Wed, 5 Feb 2025 10:25:21 +0100 Subject: [PATCH 06/13] backport some fixes from linkyv2 branch to handle enedis website errors Signed-off-by: Laurent ARNAL --- .../linky/internal/LinkyHandlerFactory.java | 4 +- .../linky/internal/api/EnedisHttpApi.java | 38 ++++- .../linky/internal/api/ExpiringDayCache.java | 62 ++++++-- .../linky/internal/handler/LinkyHandler.java | 135 ++++++++++++------ .../internal/utils/DoubleTypeAdapter.java | 56 ++++++++ 5 files changed, 234 insertions(+), 61 deletions(-) create mode 100644 bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/utils/DoubleTypeAdapter.java diff --git a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/LinkyHandlerFactory.java b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/LinkyHandlerFactory.java index 8e5d7f2be3c39..d1e1396e065d8 100644 --- a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/LinkyHandlerFactory.java +++ b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/LinkyHandlerFactory.java @@ -31,6 +31,7 @@ import org.eclipse.jetty.client.HttpClient; import org.eclipse.jetty.util.ssl.SslContextFactory; import org.openhab.binding.linky.internal.handler.LinkyHandler; +import org.openhab.binding.linky.internal.utils.DoubleTypeAdapter; import org.openhab.core.i18n.LocaleProvider; import org.openhab.core.i18n.TimeZoneProvider; import org.openhab.core.io.net.http.HttpClientFactory; @@ -86,8 +87,7 @@ public class LinkyHandlerFactory extends BaseThingHandlerFactory { .atStartOfDay(); } }) - - .create(); + .registerTypeAdapter(Double.class, new DoubleTypeAdapter()).serializeNulls().create(); private final LocaleProvider localeProvider; private final HttpClient httpClient; private final TimeZoneProvider timeZoneProvider; diff --git a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/api/EnedisHttpApi.java b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/api/EnedisHttpApi.java index d1bb0db2af50d..0cb4eb10f12ad 100644 --- a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/api/EnedisHttpApi.java +++ b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/api/EnedisHttpApi.java @@ -243,10 +243,37 @@ private FormContentProvider getFormContent(String fieldName, String fieldValue) private String getContent(String url) throws LinkyException { try { - Request request = httpClient.newRequest(url) - .agent("Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:47.0) Gecko/20100101 Firefox/47.0"); + Request request = httpClient.newRequest(url); + + request = request.agent("Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:47.0) Gecko/20100101 Firefox/47.0"); request = request.method(HttpMethod.GET); ContentResponse result = request.send(); + if (result.getStatus() == HttpStatus.TEMPORARY_REDIRECT_307 + || result.getStatus() == HttpStatus.MOVED_TEMPORARILY_302) { + String loc = result.getHeaders().get("Location"); + String newUrl = ""; + + if (loc.startsWith("http://") || loc.startsWith("https://")) { + newUrl = loc; + } else { + newUrl = URL_APPS_LINCS + loc; + } + + request = httpClient.newRequest(newUrl); + request = request.method(HttpMethod.GET); + result = request.send(); + + if (result.getStatus() == HttpStatus.TEMPORARY_REDIRECT_307 + || result.getStatus() == HttpStatus.MOVED_TEMPORARILY_302) { + loc = result.getHeaders().get("Location"); + String[] urlParts = loc.split("/"); + if (urlParts.length < 4) { + throw new LinkyException("malformed url : %s", loc); + } + return urlParts[3]; + } + } + if (result.getStatus() != HttpStatus.OK_200) { throw new LinkyException("Error requesting '%s': %s", url, result.getContentAsString()); } @@ -267,10 +294,15 @@ private T getData(String url, Class clazz) throws LinkyException { throw new LinkyException("Requesting '%s' returned an empty response", url); } try { - return Objects.requireNonNull(gson.fromJson(data, clazz)); + T result = Objects.requireNonNull(gson.fromJson(data, clazz)); + logger.debug("getData success {}: {}", clazz.getName(), url); + return result; } catch (JsonSyntaxException e) { logger.debug("Invalid JSON response not matching {}: {}", clazz.getName(), data); throw new LinkyException(e, "Requesting '%s' returned an invalid JSON response", url); + } catch (Exception e) { + logger.error("Error {}: {}", clazz.getName(), data, e); + throw new LinkyException(e, "Requesting '%s' returned an invalid JSON response", url); } } diff --git a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/api/ExpiringDayCache.java b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/api/ExpiringDayCache.java index 6c5f6e9713f5d..692042234ef5e 100644 --- a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/api/ExpiringDayCache.java +++ b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/api/ExpiringDayCache.java @@ -43,11 +43,17 @@ public class ExpiringDayCache { private final String name; private final int beginningHour; - private final Supplier<@Nullable V> action; + private final int beginningMinute; + private final int refreshInterval; + + @Nullable + private Supplier<@Nullable V> action; private @Nullable V value; private LocalDateTime expiresAt; + public boolean missingData = false; + /** * Create a new instance. * @@ -55,13 +61,35 @@ public class ExpiringDayCache { * @param beginningHour the hour in the day at which the validity period is starting * @param action the action to retrieve/calculate the value */ - public ExpiringDayCache(String name, int beginningHour, Supplier<@Nullable V> action) { + public ExpiringDayCache(String name, int beginningHour, int beginningMinute, int refreshInterval, + Supplier<@Nullable V> action) { this.name = name; this.beginningHour = beginningHour; + this.beginningMinute = beginningMinute; + this.refreshInterval = refreshInterval; this.expiresAt = calcAlreadyExpired(); this.action = action; } + /** + * Create a new instance. + * + * @param name the name of this cache + * @param beginningHour the hour in the day at which the validity period is starting + * @param action the action to retrieve/calculate the value + */ + public ExpiringDayCache(String name, int beginningHour, int beginningMinute, int refreshInterval) { + this.name = name; + this.beginningHour = beginningHour; + this.beginningMinute = beginningMinute; + this.refreshInterval = refreshInterval; + this.expiresAt = calcAlreadyExpired(); + } + + public void setAction(Supplier<@Nullable V> action) { + this.action = action; + } + /** * Returns the value - possibly from the cache, if it is still valid. */ @@ -83,9 +111,12 @@ public synchronized Optional getValue() { * @return the new value */ public synchronized @Nullable V refreshValue() { - value = action.get(); - expiresAt = calcNextExpiresAt(); - return value; + if (action != null) { + value = action.get(); + expiresAt = calcNextExpiresAt(); + return value; + } + return null; } /** @@ -97,12 +128,25 @@ public boolean isExpired() { return !LocalDateTime.now().isBefore(expiresAt); } + public void setMissingData(boolean missingData) { + this.missingData = missingData; + } + private LocalDateTime calcNextExpiresAt() { LocalDateTime now = LocalDateTime.now(); - LocalDateTime limit = now.withHour(beginningHour).truncatedTo(ChronoUnit.HOURS); - LocalDateTime result = now.isBefore(limit) ? limit : limit.plusDays(1); - logger.debug("calcNextExpiresAt result = {}", result.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME)); - return result; + + if (missingData) { + LocalDateTime result = now.plusMinutes(refreshInterval); + logger.debug("calcNextExpiresAt result = {}", result.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME)); + missingData = false; + return result; + } else { + LocalDateTime limit = now.withHour(beginningHour).withMinute(beginningMinute) + .truncatedTo(ChronoUnit.MINUTES); + LocalDateTime result = now.isBefore(limit) ? limit : limit.plusDays(1); + logger.debug("calcNextExpiresAt result = {}", result.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME)); + return result; + } } private LocalDateTime calcAlreadyExpired() { diff --git a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/handler/LinkyHandler.java b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/handler/LinkyHandler.java index c8f2e43b3dfe1..8c015dd4fcff9 100644 --- a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/handler/LinkyHandler.java +++ b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/handler/LinkyHandler.java @@ -24,6 +24,7 @@ import java.util.ArrayList; import java.util.List; import java.util.Map; +import java.util.Random; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; @@ -36,6 +37,7 @@ import org.openhab.binding.linky.internal.api.ExpiringDayCache; import org.openhab.binding.linky.internal.dto.ConsumptionReport.Aggregate; import org.openhab.binding.linky.internal.dto.ConsumptionReport.Consumption; +import org.openhab.binding.linky.internal.dto.ConsumptionReport.Data; import org.openhab.binding.linky.internal.dto.PrmDetail; import org.openhab.binding.linky.internal.dto.PrmInfo; import org.openhab.binding.linky.internal.dto.UserInfo; @@ -71,7 +73,9 @@ public class LinkyHandler extends BaseThingHandler { private final TimeZoneProvider timeZoneProvider; private ZoneId zoneId = ZoneId.systemDefault(); - private static final int REFRESH_FIRST_HOUR_OF_DAY = 1; + private static final Random randomNumbers = new Random(); + private static final int REFRESH_HOUR_OF_DAY = 1; + private static final int REFRESH_MINUTE_OF_DAY = randomNumbers.nextInt(60); private static final int REFRESH_INTERVAL_IN_MIN = 120; private final Logger logger = LoggerFactory.getLogger(LinkyHandler.class); @@ -104,53 +108,72 @@ public LinkyHandler(Thing thing, LocaleProvider localeProvider, Gson gson, HttpC this.weekFields = WeekFields.of(localeProvider.getLocale()); this.timeZoneProvider = timeZoneProvider; - this.cachedDailyData = new ExpiringDayCache<>("daily cache", REFRESH_FIRST_HOUR_OF_DAY, () -> { + this.cachedDailyData = new ExpiringDayCache<>("daily cache", REFRESH_HOUR_OF_DAY, REFRESH_MINUTE_OF_DAY, + REFRESH_INTERVAL_IN_MIN); + + this.cachedDailyData.setAction(() -> { LocalDate today = LocalDate.now(); Consumption consumption = getConsumptionData(today.minusDays(15), today); - if (consumption != null) { - logData(consumption.aggregats.days, "Day", false, DateTimeFormatter.ISO_LOCAL_DATE, Target.ALL); - logData(consumption.aggregats.weeks, "Week", true, DateTimeFormatter.ISO_LOCAL_DATE_TIME, Target.ALL); - consumption = getConsumptionAfterChecks(consumption, Target.LAST); - } - return consumption; - }); - this.cachedPowerData = new ExpiringDayCache<>("power cache", REFRESH_FIRST_HOUR_OF_DAY, () -> { - // We request data for yesterday and the day before yesterday, even if the data for the day before yesterday - // is not needed by the binding. This is only a workaround to an API bug that will return - // INTERNAL_SERVER_ERROR rather than the expected data with a NaN value when the data for yesterday is not - // yet available. - // By requesting two days, the API is not failing and you get the expected NaN value for yesterday when the - // data is not yet available. - LocalDate today = LocalDate.now(); - Consumption consumption = getPowerData(today.minusDays(2), today); if (consumption != null) { - logData(consumption.aggregats.days, "Day (peak)", true, DateTimeFormatter.ISO_LOCAL_DATE_TIME, - Target.ALL); - consumption = getConsumptionAfterChecks(consumption, Target.LAST); - } - return consumption; - }); + boolean missingData = checkMissingData(consumption); + if (missingData) { + logger.debug("API have returned incomplete data, we will recheck in {} mn", + REFRESH_INTERVAL_IN_MIN); + this.cachedDailyData.setMissingData(true); + } - this.cachedMonthlyData = new ExpiringDayCache<>("monthly cache", REFRESH_FIRST_HOUR_OF_DAY, () -> { - LocalDate today = LocalDate.now(); - Consumption consumption = getConsumptionData(today.withDayOfMonth(1).minusMonths(1), today); - if (consumption != null) { - logData(consumption.aggregats.months, "Month", true, DateTimeFormatter.ISO_LOCAL_DATE_TIME, Target.ALL); + logData(consumption.aggregats.days, "Day", false, DateTimeFormatter.ISO_LOCAL_DATE, Target.ALL); + logData(consumption.aggregats.weeks, "Week", true, DateTimeFormatter.ISO_LOCAL_DATE_TIME, Target.ALL); consumption = getConsumptionAfterChecks(consumption, Target.LAST); } return consumption; }); - this.cachedYearlyData = new ExpiringDayCache<>("yearly cache", REFRESH_FIRST_HOUR_OF_DAY, () -> { - LocalDate today = LocalDate.now(); - Consumption consumption = getConsumptionData(LocalDate.of(today.getYear() - 1, 1, 1), today); - if (consumption != null) { - logData(consumption.aggregats.years, "Year", true, DateTimeFormatter.ISO_LOCAL_DATE_TIME, Target.ALL); - consumption = getConsumptionAfterChecks(consumption, Target.LAST); - } - return consumption; - }); + this.cachedPowerData = new ExpiringDayCache<>("power cache", REFRESH_HOUR_OF_DAY, REFRESH_MINUTE_OF_DAY, + REFRESH_INTERVAL_IN_MIN, () -> { + // We request data for yesterday and the day before yesterday, even if the data for the day before + // yesterday + // is not needed by the binding. This is only a workaround to an API bug that will return + // INTERNAL_SERVER_ERROR rather than the expected data with a NaN value when the data for yesterday + // is not + // yet available. + // By requesting two days, the API is not failing and you get the expected NaN value for yesterday + // when the + // data is not yet available. + LocalDate today = LocalDate.now(); + Consumption consumption = getPowerData(today.minusDays(2), today); + if (consumption != null) { + logData(consumption.aggregats.days, "Day (peak)", true, DateTimeFormatter.ISO_LOCAL_DATE_TIME, + Target.ALL); + consumption = getConsumptionAfterChecks(consumption, Target.LAST); + } + return consumption; + }); + + this.cachedMonthlyData = new ExpiringDayCache<>("monthly cache", REFRESH_HOUR_OF_DAY, REFRESH_MINUTE_OF_DAY, + REFRESH_INTERVAL_IN_MIN, () -> { + LocalDate today = LocalDate.now(); + Consumption consumption = getConsumptionData(today.withDayOfMonth(1).minusMonths(1), today); + if (consumption != null) { + logData(consumption.aggregats.months, "Month", true, DateTimeFormatter.ISO_LOCAL_DATE_TIME, + Target.ALL); + consumption = getConsumptionAfterChecks(consumption, Target.LAST); + } + return consumption; + }); + + this.cachedYearlyData = new ExpiringDayCache<>("yearly cache", REFRESH_HOUR_OF_DAY, REFRESH_MINUTE_OF_DAY, + REFRESH_INTERVAL_IN_MIN, () -> { + LocalDate today = LocalDate.now(); + Consumption consumption = getConsumptionData(LocalDate.of(today.getYear() - 1, 1, 1), today); + if (consumption != null) { + logData(consumption.aggregats.years, "Year", true, DateTimeFormatter.ISO_LOCAL_DATE_TIME, + Target.ALL); + consumption = getConsumptionAfterChecks(consumption, Target.LAST); + } + return consumption; + }); } @Override @@ -185,8 +208,8 @@ public void initialize() { disconnect(); final LocalDateTime now = LocalDateTime.now(); - final LocalDateTime nextDayFirstTimeUpdate = now.plusDays(1).withHour(REFRESH_FIRST_HOUR_OF_DAY) - .truncatedTo(ChronoUnit.HOURS); + final LocalDateTime nextDayFirstTimeUpdate = now.plusDays(1).withHour(REFRESH_HOUR_OF_DAY) + .withMinute(REFRESH_MINUTE_OF_DAY).truncatedTo(ChronoUnit.MINUTES); refreshJob = scheduler.scheduleWithFixedDelay(this::updateData, ChronoUnit.MINUTES.between(now, nextDayFirstTimeUpdate) % REFRESH_INTERVAL_IN_MIN + 1, @@ -234,7 +257,7 @@ private synchronized void updateData() { disconnect(); } } catch (LinkyException e) { - logger.error("Exception occurs during data update", e); + logger.error("Exception occurs during data update {}", e.getMessage(), e); updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage()); } } @@ -505,6 +528,19 @@ public synchronized void handleCommand(ChannelUID channelUID, Command command) { return consumption; } + private boolean checkMissingData(@Nullable Consumption consumption) { + if (consumption != null) { + + for (int idx = 0; idx < consumption.aggregats.days.datas.size(); idx++) { + Data data = consumption.aggregats.days.datas.get(idx); + if (Double.isNaN(data.valeur)) { + return true; + } + } + } + return false; + } + private void checkData(Consumption consumption) throws LinkyException { if (consumption.aggregats.days.datas.isEmpty()) { throw new LinkyException("Invalid consumptions data: no day period"); @@ -554,12 +590,17 @@ 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.datas.get(index).dateDebut.format(dateTimeFormatter), - aggregate.datas.get(index).dateFin.format(dateTimeFormatter), aggregate.datas.get(index).valeur); - } else { - logger.debug("{} {} value {}", title, aggregate.datas.get(index).dateDebut.format(dateTimeFormatter), - aggregate.datas.get(index).valeur); + try { + if (withDateFin) { + 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.datas.get(index).dateDebut.format(dateTimeFormatter), + aggregate.datas.get(index).valeur); + } + } catch (Exception e) { + logger.error("error during logData", e); } } } diff --git a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/utils/DoubleTypeAdapter.java b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/utils/DoubleTypeAdapter.java new file mode 100644 index 0000000000000..c4ebf74997146 --- /dev/null +++ b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/utils/DoubleTypeAdapter.java @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2010-2025 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.linky.internal.utils; + +import java.io.IOException; + +import com.google.gson.TypeAdapter; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonToken; +import com.google.gson.stream.JsonWriter; + +/** + * {@link DoubleTypeAdapter} A type adapter for gson / double. + * + * Will prevent Null exception error when api return incomplete value. + * We can have this scenario when we ask to today consumption after midnight, but before enedis update the value. + * In this case, we don't want to failed all the data just because of one missing value. + * + * @author Laurent Arnal - Initial contribution + */ +public class DoubleTypeAdapter extends TypeAdapter { + + @Override + public Double read(JsonReader reader) throws IOException { + if (reader.peek() == JsonToken.NULL) { + reader.nextNull(); + return Double.NaN; + } + String stringValue = reader.nextString(); + try { + Double value = Double.valueOf(stringValue); + return value; + } catch (NumberFormatException e) { + return Double.NaN; + } + } + + @Override + public void write(JsonWriter writer, Double value) throws IOException { + if (value == null) { + writer.nullValue(); + return; + } + writer.value(value.doubleValue()); + } +} From 4b0045ea876f955181fe5af6d902456ae2f37a8c Mon Sep 17 00:00:00 2001 From: Laurent ARNAL Date: Thu, 6 Feb 2025 10:04:17 +0100 Subject: [PATCH 07/13] remove condition so we can always get metadata to fix 500 error Signed-off-by: Laurent ARNAL --- .../linky/internal/handler/LinkyHandler.java | 61 ++++++++++--------- 1 file changed, 32 insertions(+), 29 deletions(-) diff --git a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/handler/LinkyHandler.java b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/handler/LinkyHandler.java index 8c015dd4fcff9..f976df413b8d3 100644 --- a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/handler/LinkyHandler.java +++ b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/handler/LinkyHandler.java @@ -227,15 +227,12 @@ public void initialize() { private synchronized void updateMetaData() throws LinkyException { EnedisHttpApi api = this.enedisApi; if (api != null) { - if (thing.getProperties().isEmpty()) { - UserInfo userInfo = api.getUserInfo(); - PrmInfo prmInfo = api.getPrmInfo(userInfo.userProperties.internId); - PrmDetail details = api.getPrmDetails(userInfo.userProperties.internId, prmInfo.idPrm); - updateProperties(Map.of(USER_ID, userInfo.userProperties.internId, PUISSANCE, - details.situationContractuelleDtos[0].structureTarifaire().puissanceSouscrite().valeur() - + " kVA", - PRM_ID, prmInfo.idPrm)); - } + UserInfo userInfo = api.getUserInfo(); + PrmInfo prmInfo = api.getPrmInfo(userInfo.userProperties.internId); + PrmDetail details = api.getPrmDetails(userInfo.userProperties.internId, prmInfo.idPrm); + updateProperties(Map.of(USER_ID, userInfo.userProperties.internId, PUISSANCE, + details.situationContractuelleDtos[0].structureTarifaire().puissanceSouscrite().valeur() + " kVA", + PRM_ID, prmInfo.idPrm)); prmId = thing.getProperties().get(PRM_ID); userId = thing.getProperties().get(USER_ID); @@ -481,26 +478,32 @@ public synchronized void handleCommand(ChannelUID channelUID, Command command) { if (command instanceof RefreshType) { logger.debug("Refreshing channel {}", channelUID.getId()); boolean connectedBefore = isConnected(); - switch (channelUID.getId()) { - case YESTERDAY: - case LAST_WEEK: - case THIS_WEEK: - updateDailyWeeklyData(); - break; - case LAST_MONTH: - case THIS_MONTH: - updateMonthlyData(); - break; - case LAST_YEAR: - case THIS_YEAR: - updateYearlyData(); - break; - case PEAK_POWER: - case PEAK_TIMESTAMP: - updatePowerData(); - break; - default: - break; + + try { + updateMetaData(); + switch (channelUID.getId()) { + case YESTERDAY: + case LAST_WEEK: + case THIS_WEEK: + updateDailyWeeklyData(); + break; + case LAST_MONTH: + case THIS_MONTH: + updateMonthlyData(); + break; + case LAST_YEAR: + case THIS_YEAR: + updateYearlyData(); + break; + case PEAK_POWER: + case PEAK_TIMESTAMP: + updatePowerData(); + break; + default: + break; + } + } catch (LinkyException ex) { + logger.error("Unable to handleCommand refresh", ex); } if (!connectedBefore && isConnected()) { disconnect(); From fead647cc59c5637e1533d20c8a1c623645dbb2a Mon Sep 17 00:00:00 2001 From: Laurent ARNAL Date: Fri, 7 Feb 2025 09:16:18 +0100 Subject: [PATCH 08/13] remove the missingData stuff from previous commit, realize if was duplicate whith check already done in getConsumptionAfterChecks Signed-off-by: Laurent ARNAL --- .../linky/internal/api/ExpiringDayCache.java | 23 ++++--------------- .../linky/internal/handler/LinkyHandler.java | 21 ----------------- 2 files changed, 4 insertions(+), 40 deletions(-) diff --git a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/api/ExpiringDayCache.java b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/api/ExpiringDayCache.java index 692042234ef5e..7e1ccc6809bbc 100644 --- a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/api/ExpiringDayCache.java +++ b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/api/ExpiringDayCache.java @@ -52,8 +52,6 @@ public class ExpiringDayCache { private @Nullable V value; private LocalDateTime expiresAt; - public boolean missingData = false; - /** * Create a new instance. * @@ -128,25 +126,12 @@ public boolean isExpired() { return !LocalDateTime.now().isBefore(expiresAt); } - public void setMissingData(boolean missingData) { - this.missingData = missingData; - } - private LocalDateTime calcNextExpiresAt() { LocalDateTime now = LocalDateTime.now(); - - if (missingData) { - LocalDateTime result = now.plusMinutes(refreshInterval); - logger.debug("calcNextExpiresAt result = {}", result.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME)); - missingData = false; - return result; - } else { - LocalDateTime limit = now.withHour(beginningHour).withMinute(beginningMinute) - .truncatedTo(ChronoUnit.MINUTES); - LocalDateTime result = now.isBefore(limit) ? limit : limit.plusDays(1); - logger.debug("calcNextExpiresAt result = {}", result.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME)); - return result; - } + LocalDateTime limit = now.withHour(beginningHour).withMinute(beginningMinute).truncatedTo(ChronoUnit.MINUTES); + LocalDateTime result = now.isBefore(limit) ? limit : limit.plusDays(1); + logger.debug("calcNextExpiresAt result = {}", result.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME)); + return result; } private LocalDateTime calcAlreadyExpired() { diff --git a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/handler/LinkyHandler.java b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/handler/LinkyHandler.java index f976df413b8d3..1033b9034ad78 100644 --- a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/handler/LinkyHandler.java +++ b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/handler/LinkyHandler.java @@ -37,7 +37,6 @@ import org.openhab.binding.linky.internal.api.ExpiringDayCache; import org.openhab.binding.linky.internal.dto.ConsumptionReport.Aggregate; import org.openhab.binding.linky.internal.dto.ConsumptionReport.Consumption; -import org.openhab.binding.linky.internal.dto.ConsumptionReport.Data; import org.openhab.binding.linky.internal.dto.PrmDetail; import org.openhab.binding.linky.internal.dto.PrmInfo; import org.openhab.binding.linky.internal.dto.UserInfo; @@ -116,13 +115,6 @@ public LinkyHandler(Thing thing, LocaleProvider localeProvider, Gson gson, HttpC Consumption consumption = getConsumptionData(today.minusDays(15), today); if (consumption != null) { - boolean missingData = checkMissingData(consumption); - if (missingData) { - logger.debug("API have returned incomplete data, we will recheck in {} mn", - REFRESH_INTERVAL_IN_MIN); - this.cachedDailyData.setMissingData(true); - } - logData(consumption.aggregats.days, "Day", false, DateTimeFormatter.ISO_LOCAL_DATE, Target.ALL); logData(consumption.aggregats.weeks, "Week", true, DateTimeFormatter.ISO_LOCAL_DATE_TIME, Target.ALL); consumption = getConsumptionAfterChecks(consumption, Target.LAST); @@ -531,19 +523,6 @@ public synchronized void handleCommand(ChannelUID channelUID, Command command) { return consumption; } - private boolean checkMissingData(@Nullable Consumption consumption) { - if (consumption != null) { - - for (int idx = 0; idx < consumption.aggregats.days.datas.size(); idx++) { - Data data = consumption.aggregats.days.datas.get(idx); - if (Double.isNaN(data.valeur)) { - return true; - } - } - } - return false; - } - private void checkData(Consumption consumption) throws LinkyException { if (consumption.aggregats.days.datas.isEmpty()) { throw new LinkyException("Invalid consumptions data: no day period"); From d71513f8b56e03b26e96c4ffdd07c2412948ea2e Mon Sep 17 00:00:00 2001 From: Laurent ARNAL Date: Fri, 7 Feb 2025 09:22:06 +0100 Subject: [PATCH 09/13] remove also the refreshinterval on ExpiringDayCache constructor, we don't need it anymore Signed-off-by: Laurent ARNAL --- .../linky/internal/api/ExpiringDayCache.java | 24 +--------- .../linky/internal/handler/LinkyHandler.java | 48 +++++++++---------- 2 files changed, 23 insertions(+), 49 deletions(-) diff --git a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/api/ExpiringDayCache.java b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/api/ExpiringDayCache.java index 7e1ccc6809bbc..afde3ac73a6ea 100644 --- a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/api/ExpiringDayCache.java +++ b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/api/ExpiringDayCache.java @@ -44,7 +44,6 @@ public class ExpiringDayCache { private final String name; private final int beginningHour; private final int beginningMinute; - private final int refreshInterval; @Nullable private Supplier<@Nullable V> action; @@ -59,35 +58,14 @@ public class ExpiringDayCache { * @param beginningHour the hour in the day at which the validity period is starting * @param action the action to retrieve/calculate the value */ - public ExpiringDayCache(String name, int beginningHour, int beginningMinute, int refreshInterval, - Supplier<@Nullable V> action) { + public ExpiringDayCache(String name, int beginningHour, int beginningMinute, Supplier<@Nullable V> action) { this.name = name; this.beginningHour = beginningHour; this.beginningMinute = beginningMinute; - this.refreshInterval = refreshInterval; this.expiresAt = calcAlreadyExpired(); this.action = action; } - /** - * Create a new instance. - * - * @param name the name of this cache - * @param beginningHour the hour in the day at which the validity period is starting - * @param action the action to retrieve/calculate the value - */ - public ExpiringDayCache(String name, int beginningHour, int beginningMinute, int refreshInterval) { - this.name = name; - this.beginningHour = beginningHour; - this.beginningMinute = beginningMinute; - this.refreshInterval = refreshInterval; - this.expiresAt = calcAlreadyExpired(); - } - - public void setAction(Supplier<@Nullable V> action) { - this.action = action; - } - /** * Returns the value - possibly from the cache, if it is still valid. */ diff --git a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/handler/LinkyHandler.java b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/handler/LinkyHandler.java index 1033b9034ad78..2c01e080375bb 100644 --- a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/handler/LinkyHandler.java +++ b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/handler/LinkyHandler.java @@ -107,10 +107,7 @@ public LinkyHandler(Thing thing, LocaleProvider localeProvider, Gson gson, HttpC this.weekFields = WeekFields.of(localeProvider.getLocale()); this.timeZoneProvider = timeZoneProvider; - this.cachedDailyData = new ExpiringDayCache<>("daily cache", REFRESH_HOUR_OF_DAY, REFRESH_MINUTE_OF_DAY, - REFRESH_INTERVAL_IN_MIN); - - this.cachedDailyData.setAction(() -> { + this.cachedDailyData = new ExpiringDayCache<>("daily cache", REFRESH_HOUR_OF_DAY, REFRESH_MINUTE_OF_DAY, () -> { LocalDate today = LocalDate.now(); Consumption consumption = getConsumptionData(today.minusDays(15), today); @@ -122,29 +119,28 @@ public LinkyHandler(Thing thing, LocaleProvider localeProvider, Gson gson, HttpC return consumption; }); - this.cachedPowerData = new ExpiringDayCache<>("power cache", REFRESH_HOUR_OF_DAY, REFRESH_MINUTE_OF_DAY, - REFRESH_INTERVAL_IN_MIN, () -> { - // We request data for yesterday and the day before yesterday, even if the data for the day before - // yesterday - // is not needed by the binding. This is only a workaround to an API bug that will return - // INTERNAL_SERVER_ERROR rather than the expected data with a NaN value when the data for yesterday - // is not - // yet available. - // By requesting two days, the API is not failing and you get the expected NaN value for yesterday - // when the - // data is not yet available. - LocalDate today = LocalDate.now(); - Consumption consumption = getPowerData(today.minusDays(2), today); - if (consumption != null) { - logData(consumption.aggregats.days, "Day (peak)", true, DateTimeFormatter.ISO_LOCAL_DATE_TIME, - Target.ALL); - consumption = getConsumptionAfterChecks(consumption, Target.LAST); - } - return consumption; - }); + this.cachedPowerData = new ExpiringDayCache<>("power cache", REFRESH_HOUR_OF_DAY, REFRESH_MINUTE_OF_DAY, () -> { + // We request data for yesterday and the day before yesterday, even if the data for the day before + // yesterday + // is not needed by the binding. This is only a workaround to an API bug that will return + // INTERNAL_SERVER_ERROR rather than the expected data with a NaN value when the data for yesterday + // is not + // yet available. + // By requesting two days, the API is not failing and you get the expected NaN value for yesterday + // when the + // data is not yet available. + LocalDate today = LocalDate.now(); + Consumption consumption = getPowerData(today.minusDays(2), today); + if (consumption != null) { + logData(consumption.aggregats.days, "Day (peak)", true, DateTimeFormatter.ISO_LOCAL_DATE_TIME, + Target.ALL); + consumption = getConsumptionAfterChecks(consumption, Target.LAST); + } + return consumption; + }); this.cachedMonthlyData = new ExpiringDayCache<>("monthly cache", REFRESH_HOUR_OF_DAY, REFRESH_MINUTE_OF_DAY, - REFRESH_INTERVAL_IN_MIN, () -> { + () -> { LocalDate today = LocalDate.now(); Consumption consumption = getConsumptionData(today.withDayOfMonth(1).minusMonths(1), today); if (consumption != null) { @@ -156,7 +152,7 @@ public LinkyHandler(Thing thing, LocaleProvider localeProvider, Gson gson, HttpC }); this.cachedYearlyData = new ExpiringDayCache<>("yearly cache", REFRESH_HOUR_OF_DAY, REFRESH_MINUTE_OF_DAY, - REFRESH_INTERVAL_IN_MIN, () -> { + () -> { LocalDate today = LocalDate.now(); Consumption consumption = getConsumptionData(LocalDate.of(today.getYear() - 1, 1, 1), today); if (consumption != null) { From b76392cf6bd4d23f082c3e509b9e996a3212693c Mon Sep 17 00:00:00 2001 From: Laurent ARNAL Date: Fri, 7 Feb 2025 10:51:22 +0100 Subject: [PATCH 10/13] remove nullable on action field Signed-off-by: Laurent ARNAL --- .../binding/linky/internal/api/ExpiringDayCache.java | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/api/ExpiringDayCache.java b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/api/ExpiringDayCache.java index afde3ac73a6ea..5621534a75dc5 100644 --- a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/api/ExpiringDayCache.java +++ b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/api/ExpiringDayCache.java @@ -45,7 +45,6 @@ public class ExpiringDayCache { private final int beginningHour; private final int beginningMinute; - @Nullable private Supplier<@Nullable V> action; private @Nullable V value; @@ -87,12 +86,9 @@ public synchronized Optional getValue() { * @return the new value */ public synchronized @Nullable V refreshValue() { - if (action != null) { - value = action.get(); - expiresAt = calcNextExpiresAt(); - return value; - } - return null; + value = action.get(); + expiresAt = calcNextExpiresAt(); + return value; } /** From 48c5b82e6b1272721feba8925158e8ed1e8c6656 Mon Sep 17 00:00:00 2001 From: Laurent ARNAL Date: Sun, 9 Feb 2025 10:05:54 +0100 Subject: [PATCH 11/13] fix issues from lolodom review on 8 Feb Signed-off-by: Laurent ARNAL --- bundles/org.openhab.binding.linky/README.md | 13 ++-- .../linky/internal/LinkyHandlerFactory.java | 3 +- .../linky/internal/api/EnedisHttpApi.java | 5 +- .../linky/internal/dto/ConsumptionReport.java | 2 +- .../linky/internal/handler/LinkyHandler.java | 63 +++++++------------ .../resources/OH-INF/thing/thing-types.xml | 8 +-- 6 files changed, 39 insertions(+), 55 deletions(-) diff --git a/bundles/org.openhab.binding.linky/README.md b/bundles/org.openhab.binding.linky/README.md index 1f7aa92d1ee15..5c563a412c1d9 100644 --- a/bundles/org.openhab.binding.linky/README.md +++ b/bundles/org.openhab.binding.linky/README.md @@ -23,11 +23,12 @@ The binding has no configuration options, all configuration is done at Thing lev The thing has the following configuration parameters: -| Parameter | Description | -|----------------|--------------------------------| -| username | Your Enedis platform username. | -| password | Your Enedis platform password. | -| internalAuthId | The internal authID | +| Parameter | Description | +|----------------|--------------------------------------------| +| username | Your Enedis platform username. | +| password | Your Enedis platform password. | +| internalAuthId | The internal authID | +| timezone | The timezone at the location of your linky | This version is now compatible with the new API of Enedis (deployed from june 2020). 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 : 1. Disconnect from your Enedis account 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. +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. + ## Channels The information that is retrieved is available as these channels: diff --git a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/LinkyHandlerFactory.java b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/LinkyHandlerFactory.java index d1e1396e065d8..36ccf55c3c6eb 100644 --- a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/LinkyHandlerFactory.java +++ b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/LinkyHandlerFactory.java @@ -22,6 +22,7 @@ import java.time.ZonedDateTime; import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatterBuilder; +import java.time.format.DateTimeParseException; import javax.net.ssl.SSLContext; import javax.net.ssl.TrustManager; @@ -82,7 +83,7 @@ public class LinkyHandlerFactory extends BaseThingHandlerFactory { try { return LocalDateTime.parse(json.getAsJsonPrimitive().getAsString(), LINKY_LOCALDATETIME_FORMATTER); - } catch (Exception ex) { + } catch (DateTimeParseException ex) { return LocalDate.parse(json.getAsJsonPrimitive().getAsString(), LINKY_LOCALDATE_FORMATTER) .atStartOfDay(); } diff --git a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/api/EnedisHttpApi.java b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/api/EnedisHttpApi.java index 0cb4eb10f12ad..bce596ffd6966 100644 --- a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/api/EnedisHttpApi.java +++ b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/api/EnedisHttpApi.java @@ -295,14 +295,11 @@ private T getData(String url, Class clazz) throws LinkyException { } try { T result = Objects.requireNonNull(gson.fromJson(data, clazz)); - logger.debug("getData success {}: {}", clazz.getName(), url); + logger.trace("getData success {}: {}", clazz.getName(), url); return result; } catch (JsonSyntaxException e) { logger.debug("Invalid JSON response not matching {}: {}", clazz.getName(), data); throw new LinkyException(e, "Requesting '%s' returned an invalid JSON response", url); - } catch (Exception e) { - logger.error("Error {}: {}", clazz.getName(), data, e); - throw new LinkyException(e, "Requesting '%s' returned an invalid JSON response", url); } } diff --git a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/ConsumptionReport.java b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/ConsumptionReport.java index 6456cad7322f5..6f38eee866395 100644 --- a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/ConsumptionReport.java +++ b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/ConsumptionReport.java @@ -23,7 +23,7 @@ * returned by API calls * * @author Gaël L'hopital - Initial contribution - * @author Laurent ARNAL - fix to handle new Dto format after enedis site modifications + * @author Laurent Arnal - fix to handle new Dto format after enedis site modifications */ public class ConsumptionReport { diff --git a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/handler/LinkyHandler.java b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/handler/LinkyHandler.java index 2c01e080375bb..20f502a957290 100644 --- a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/handler/LinkyHandler.java +++ b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/handler/LinkyHandler.java @@ -40,7 +40,6 @@ import org.openhab.binding.linky.internal.dto.PrmDetail; import org.openhab.binding.linky.internal.dto.PrmInfo; import org.openhab.binding.linky.internal.dto.UserInfo; -import org.openhab.core.config.core.Configuration; import org.openhab.core.i18n.LocaleProvider; import org.openhab.core.i18n.TimeZoneProvider; import org.openhab.core.library.types.DateTimeType; @@ -69,14 +68,12 @@ @NonNullByDefault public class LinkyHandler extends BaseThingHandler { - private final TimeZoneProvider timeZoneProvider; - private ZoneId zoneId = ZoneId.systemDefault(); - private static final Random randomNumbers = new Random(); private static final int REFRESH_HOUR_OF_DAY = 1; private static final int REFRESH_MINUTE_OF_DAY = randomNumbers.nextInt(60); private static final int REFRESH_INTERVAL_IN_MIN = 120; + private final TimeZoneProvider timeZoneProvider; private final Logger logger = LoggerFactory.getLogger(LinkyHandler.class); private final HttpClient httpClient; private final Gson gson; @@ -87,6 +84,8 @@ public class LinkyHandler extends BaseThingHandler { private final ExpiringDayCache cachedMonthlyData; private final ExpiringDayCache cachedYearlyData; + private ZoneId zoneId = ZoneId.systemDefault(); + private @Nullable ScheduledFuture refreshJob; private @Nullable EnedisHttpApi enedisApi; @@ -120,15 +119,12 @@ public LinkyHandler(Thing thing, LocaleProvider localeProvider, Gson gson, HttpC }); this.cachedPowerData = new ExpiringDayCache<>("power cache", REFRESH_HOUR_OF_DAY, REFRESH_MINUTE_OF_DAY, () -> { - // We request data for yesterday and the day before yesterday, even if the data for the day before - // yesterday - // is not needed by the binding. This is only a workaround to an API bug that will return - // INTERNAL_SERVER_ERROR rather than the expected data with a NaN value when the data for yesterday - // is not - // yet available. + // We request data for yesterday and the day before yesterday, + // even if the data for the day before yesterday is not needed by the binding. + // This is only a workaround to an API bug that will return INTERNAL_SERVER_ERROR rather + // than the expected data with a NaN value when the data for yesterday is not yet available. // By requesting two days, the API is not failing and you get the expected NaN value for yesterday - // when the - // data is not yet available. + // when the data is not yet available. LocalDate today = LocalDate.now(); Consumption consumption = getPowerData(today.minusDays(2), today); if (consumption != null) { @@ -169,21 +165,15 @@ public void initialize() { logger.debug("Initializing Linky handler."); updateStatus(ThingStatus.UNKNOWN); - // update the timezone if not set to default to openhab default timezone - Configuration thingConfig = getConfig(); - - String val = (String) thingConfig.get("timezone"); - if (val == null || val.isBlank()) { - zoneId = this.timeZoneProvider.getTimeZone(); - thingConfig.put("timezone", zoneId.getId()); - } else { - zoneId = ZoneId.of(val); - } - - updateConfiguration(thingConfig); - LinkyConfiguration config = getConfigAs(LinkyConfiguration.class); if (config.seemsValid()) { + + if (config.timezone.isBlank()) { + zoneId = this.timeZoneProvider.getTimeZone(); + } else { + zoneId = ZoneId.of(config.timezone); + } + enedisApi = new EnedisHttpApi(config, gson, httpClient); scheduler.submit(() -> { try { @@ -242,7 +232,7 @@ private synchronized void updateData() { disconnect(); } } catch (LinkyException e) { - logger.error("Exception occurs during data update {}", e.getMessage(), e); + logger.debug("Exception occurs during data update {}", e.getMessage(), e); updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage()); } } @@ -491,7 +481,7 @@ public synchronized void handleCommand(ChannelUID channelUID, Command command) { break; } } catch (LinkyException ex) { - logger.error("Unable to handleCommand refresh", ex); + logger.debug("Unable to handleCommand refresh", ex); } if (!connectedBefore && isConnected()) { disconnect(); @@ -520,7 +510,7 @@ public synchronized void handleCommand(ChannelUID channelUID, Command command) { } private void checkData(Consumption consumption) throws LinkyException { - if (consumption.aggregats.days.datas.isEmpty()) { + if (consumption.aggregats.days != null && consumption.aggregats.days.datas.isEmpty()) { throw new LinkyException("Invalid consumptions data: no day period"); } if (consumption.aggregats.weeks != null && consumption.aggregats.weeks.datas.isEmpty()) { @@ -568,17 +558,12 @@ private void logData(Aggregate aggregate, String title, boolean withDateFin, Dat private void logData(Aggregate aggregate, int index, String title, boolean withDateFin, DateTimeFormatter dateTimeFormatter) { - try { - if (withDateFin) { - 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.datas.get(index).dateDebut.format(dateTimeFormatter), - aggregate.datas.get(index).valeur); - } - } catch (Exception e) { - logger.error("error during logData", e); + if (withDateFin) { + 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.datas.get(index).dateDebut.format(dateTimeFormatter), + aggregate.datas.get(index).valeur); } } } diff --git a/bundles/org.openhab.binding.linky/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.linky/src/main/resources/OH-INF/thing/thing-types.xml index 51a489096c77e..f6adab1985512 100644 --- a/bundles/org.openhab.binding.linky/src/main/resources/OH-INF/thing/thing-types.xml +++ b/bundles/org.openhab.binding.linky/src/main/resources/OH-INF/thing/thing-types.xml @@ -35,13 +35,11 @@ Authentication ID delivered after the captcha (see documentation). - + The timezone associated with your Point of delivery. Will default to openHAB default timezone. - You will - need to change this if your Linky is located in a different timezone that your openHAB location. - You can use an - offset, or a label like Europe/Paris + You will need to change this if your Linky is located in a different timezone that your openHAB location. + You can use an offset, or a label like Europe/Paris From 706147d35f7f996da3b2c18963f2922320b9eaca Mon Sep 17 00:00:00 2001 From: Laurent ARNAL Date: Sun, 9 Feb 2025 10:08:56 +0100 Subject: [PATCH 12/13] spotless:apply Signed-off-by: Laurent ARNAL --- .../src/main/resources/OH-INF/thing/thing-types.xml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/bundles/org.openhab.binding.linky/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.linky/src/main/resources/OH-INF/thing/thing-types.xml index f6adab1985512..ccc678058c05c 100644 --- a/bundles/org.openhab.binding.linky/src/main/resources/OH-INF/thing/thing-types.xml +++ b/bundles/org.openhab.binding.linky/src/main/resources/OH-INF/thing/thing-types.xml @@ -38,8 +38,10 @@ The timezone associated with your Point of delivery. Will default to openHAB default timezone. - You will need to change this if your Linky is located in a different timezone that your openHAB location. - You can use an offset, or a label like Europe/Paris + You will + need to change this if your Linky is located in a different timezone that your openHAB location. + You can use an + offset, or a label like Europe/Paris From de4f3f62cdf5f16801eab05d586257582db6413e Mon Sep 17 00:00:00 2001 From: Laurent ARNAL Date: Mon, 10 Feb 2025 11:13:10 +0100 Subject: [PATCH 13/13] update i18n files: mvn i18n:generate-default-translations Signed-off-by: Laurent ARNAL --- .../src/main/resources/OH-INF/i18n/linky.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bundles/org.openhab.binding.linky/src/main/resources/OH-INF/i18n/linky.properties b/bundles/org.openhab.binding.linky/src/main/resources/OH-INF/i18n/linky.properties index ecf55352b3a05..fa77fc096ce40 100644 --- a/bundles/org.openhab.binding.linky/src/main/resources/OH-INF/i18n/linky.properties +++ b/bundles/org.openhab.binding.linky/src/main/resources/OH-INF/i18n/linky.properties @@ -14,7 +14,7 @@ thing-type.config.linky.linky.internalAuthId.label = Auth ID thing-type.config.linky.linky.internalAuthId.description = Authentication ID delivered after the captcha (see documentation). thing-type.config.linky.linky.password.label = Password thing-type.config.linky.linky.password.description = Your Enedis Password -thing-type.config.linky.linky.timezone.label = timezone +thing-type.config.linky.linky.timezone.label = Timezone thing-type.config.linky.linky.timezone.description = The timezone associated with your Point of delivery. Will default to openHAB default timezone. You will need to change this if your Linky is located in a different timezone that your openHAB location. You can use an offset, or a label like Europe/Paris thing-type.config.linky.linky.username.label = Username thing-type.config.linky.linky.username.description = Your Enedis Username