From 2b565eb385d45d589399f99b119f6a2f1e6a1fcb Mon Sep 17 00:00:00 2001 From: Laurent ARNAL Date: Mon, 11 Aug 2025 17:50:20 +0200 Subject: [PATCH 01/34] start implementing new reading method using index api (will support fournisseur and distributor dispatch like heure creuse / heure pleine or tempo tarif). Signed-off-by: Laurent ARNAL --- .../linky/internal/api/EnedisHttpApi.java | 15 ++- .../linky/internal/dto/Calendrier.java | 26 +++++ .../internal/dto/ClassesTemporelles.java | 26 +++++ .../linky/internal/dto/ConsumptionReport.java | 3 + .../linky/internal/dto/IntervalReading.java | 2 + .../linky/internal/dto/MeterReading.java | 107 ++++++++++++++++-- .../handler/BridgeRemoteBaseHandler.java | 2 + .../handler/BridgeRemoteEnedisHandler.java | 7 ++ .../handler/BridgeRemoteEnedisWebHandler.java | 8 ++ .../BridgeRemoteMyElectricalDataHandler.java | 7 ++ .../handler/ThingLinkyRemoteHandler.java | 102 ++++++++++++++++- .../internal/utils/DoubleTypeAdapter.java | 3 + 12 files changed, 293 insertions(+), 15 deletions(-) create mode 100644 bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/Calendrier.java create mode 100644 bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/ClassesTemporelles.java 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 ba070cfa040d4..7368c5976c1fc 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 @@ -259,7 +259,7 @@ public Contact getContact(ThingLinkyRemoteHandler handler, String prmId) throws } private MeterReading getMeasures(ThingLinkyRemoteHandler handler, String apiUrl, String mps, String prmId, - LocalDate from, LocalDate to) throws LinkyException { + LocalDate from, LocalDate to, boolean useIndex) throws LinkyException { String dtStart = from.format(linkyBridgeHandler.getApiDateFormat()); String dtEnd = to.format(linkyBridgeHandler.getApiDateFormat()); @@ -270,23 +270,28 @@ private MeterReading getMeasures(ThingLinkyRemoteHandler handler, String apiUrl, } else { String url = String.format(apiUrl, mps, prmId, dtStart, dtEnd); ConsumptionReport consomptionReport = getData(handler, url, ConsumptionReport.class); - return MeterReading.convertFromComsumptionReport(consomptionReport); + return MeterReading.convertFromComsumptionReport(consomptionReport, useIndex); } } public MeterReading getEnergyData(ThingLinkyRemoteHandler handler, String mps, String prmId, LocalDate from, LocalDate to) throws LinkyException { - return getMeasures(handler, linkyBridgeHandler.getDailyConsumptionUrl(), mps, prmId, from, to); + return getMeasures(handler, linkyBridgeHandler.getDailyConsumptionUrl(), mps, prmId, from, to, false); + } + + public MeterReading getEnergyIndex(ThingLinkyRemoteHandler handler, String mps, String prmId, LocalDate from, + LocalDate to) throws LinkyException { + return getMeasures(handler, linkyBridgeHandler.getDailyIndexUrl(), mps, prmId, from, to, true); } public MeterReading getLoadCurveData(ThingLinkyRemoteHandler handler, String mps, String prmId, LocalDate from, LocalDate to) throws LinkyException { - return getMeasures(handler, linkyBridgeHandler.getLoadCurveUrl(), mps, prmId, from, to); + return getMeasures(handler, linkyBridgeHandler.getLoadCurveUrl(), mps, prmId, from, to, false); } public MeterReading getPowerData(ThingLinkyRemoteHandler handler, String mps, String prmId, LocalDate from, LocalDate to) throws LinkyException { - return getMeasures(handler, linkyBridgeHandler.getMaxPowerUrl(), mps, prmId, from, to); + return getMeasures(handler, linkyBridgeHandler.getMaxPowerUrl(), mps, prmId, from, to, false); } public ResponseTempo getTempoData(ThingBaseRemoteHandler handler, LocalDate from, LocalDate to) diff --git a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/Calendrier.java b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/Calendrier.java new file mode 100644 index 0000000000000..24da25d5459c2 --- /dev/null +++ b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/Calendrier.java @@ -0,0 +1,26 @@ +/* + * 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.dto; + +/** + * The {@link Calendrier} holds informations about energy consumption + * + * @author Gaël L'hopital - Initial contribution + * @author Laurent Arnal - Rewrite addon to use official dataconect API + */ + +public class Calendrier { + public String idCalendrier; + public String libelleCalendrier; + +} diff --git a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/ClassesTemporelles.java b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/ClassesTemporelles.java new file mode 100644 index 0000000000000..e3b2648e47cd3 --- /dev/null +++ b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/ClassesTemporelles.java @@ -0,0 +1,26 @@ +/* + * 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.dto; + +/** + * The {@link ClassesTemporelles} holds informations about energy consumption + * + * @author Gaël L'hopital - Initial contribution + * @author Laurent Arnal - Rewrite addon to use official dataconect API + */ + +public class ClassesTemporelles { + public String libelle; + public Double valeur = 0.0; + +} 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 1034872825294..12caafcfbb53d 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 @@ -31,6 +31,9 @@ public class Data { public LocalDateTime dateDebut; public LocalDateTime dateFin; public Double valeur; + public ClassesTemporelles[] classesTemporellesFournisseur; + public ClassesTemporelles[] classesTemporellesDistributeur; + public Calendrier[] calendrier; } public class Aggregate { diff --git a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/IntervalReading.java b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/IntervalReading.java index 31bc99aba9157..afe6b4dca9c36 100644 --- a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/IntervalReading.java +++ b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/IntervalReading.java @@ -23,5 +23,7 @@ public class IntervalReading { public Double value = 0.0; + public Double[] valueFromFournisseur; + public Double[] valueFromDistributeur; public LocalDateTime date; } diff --git a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/MeterReading.java b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/MeterReading.java index 92d849e777e6f..1414bd7168938 100644 --- a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/MeterReading.java +++ b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/MeterReading.java @@ -44,30 +44,119 @@ public class MeterReading { public IntervalReading[] monthValue; public IntervalReading[] yearValue; - public static MeterReading convertFromComsumptionReport(ConsumptionReport comsumptionReport) { + public static MeterReading convertFromComsumptionReport(ConsumptionReport comsumptionReport, boolean useIndex) { MeterReading result = new MeterReading(); result.readingType = new ReadingType(); if (comsumptionReport.consumptions.aggregats != null) { if (comsumptionReport.consumptions.aggregats.days != null) { - result.baseValue = fromAgregat(comsumptionReport.consumptions.aggregats.days); + result.baseValue = fromAgregat(comsumptionReport.consumptions.aggregats.days, useIndex); } else if (comsumptionReport.consumptions.aggregats.heure != null) { - result.baseValue = fromAgregat(comsumptionReport.consumptions.aggregats.heure); + result.baseValue = fromAgregat(comsumptionReport.consumptions.aggregats.heure, useIndex); } } return result; } - public static IntervalReading[] fromAgregat(ConsumptionReport.Aggregate agregat) { + public static IntervalReading[] fromAgregat(ConsumptionReport.Aggregate agregat, boolean useIndex) { int size = agregat.datas.size(); IntervalReading[] result = new IntervalReading[size]; - for (int i = 0; i < size; i++) { - Data dataObj = agregat.datas.get(i); - result[i] = new IntervalReading(); - result[i].value = Double.valueOf(dataObj.valeur); - result[i].date = dataObj.dateDebut; + if (!useIndex) { + for (int i = 0; i < size; i++) { + Data dataObj = agregat.datas.get(i); + result[i] = new IntervalReading(); + result[i].value = Double.valueOf(dataObj.valeur); + result[i].date = dataObj.dateDebut; + } + } else { + Double lastVal = 0.0; + Double[] lastValFournisseur = new Double[6]; + lastValFournisseur[0] = 0.0; + lastValFournisseur[1] = 0.0; + lastValFournisseur[2] = 0.0; + lastValFournisseur[3] = 0.0; + lastValFournisseur[4] = 0.0; + lastValFournisseur[5] = 0.0; + + for (int i = 0; i < size; i++) { + Data dataObj = agregat.datas.get(i); + Double value = Double.valueOf(dataObj.valeur); + Double[] valueFournisseur = new Double[6]; + valueFournisseur[0] = 0.0; + valueFournisseur[1] = 0.0; + valueFournisseur[2] = 0.0; + valueFournisseur[3] = 0.0; + valueFournisseur[4] = 0.0; + valueFournisseur[5] = 0.0; + + if (i > 0) { + result[i - 1] = new IntervalReading(); + result[i - 1].value = value - lastVal; + result[i - 1].date = dataObj.dateDebut; + result[i - 1].valueFromFournisseur = new Double[6]; + result[i - 1].valueFromDistributeur = new Double[4]; + + result[i - 1].valueFromFournisseur[0] = 0.0; + result[i - 1].valueFromFournisseur[1] = 0.0; + result[i - 1].valueFromFournisseur[2] = 0.0; + result[i - 1].valueFromFournisseur[3] = 0.0; + result[i - 1].valueFromFournisseur[4] = 0.0; + result[i - 1].valueFromFournisseur[5] = 0.0; + + result[i - 1].valueFromDistributeur[0] = 0.0; + result[i - 1].valueFromDistributeur[1] = 0.0; + result[i - 1].valueFromDistributeur[2] = 0.0; + result[i - 1].valueFromDistributeur[3] = 0.0; + + if (dataObj.classesTemporellesFournisseur != null) { + for (ClassesTemporelles ct : dataObj.classesTemporellesFournisseur) { + int idxFournisseur = -1; + if ("Base".equals(ct.libelle)) { + idxFournisseur = 0; + } else if ("Blanc Heures Creuses".equals(ct.libelle)) { + idxFournisseur = 3; + } else if ("Blanc Heures Pleines".equals(ct.libelle)) { + idxFournisseur = 2; + } else if ("Bleu Heures Creuses".equals(ct.libelle)) { + idxFournisseur = 1; + } else if ("Bleu Heures Pleines".equals(ct.libelle)) { + idxFournisseur = 0; + } else if ("Rouge Heures Creuses".equals(ct.libelle)) { + idxFournisseur = 5; + } else if ("Rouge Heures Pleines".equals(ct.libelle)) { + idxFournisseur = 4; + } else if ("Heures Pleines".equals(ct.libelle)) { + idxFournisseur = 0; + } else if ("Heures Creuses".equals(ct.libelle)) { + idxFournisseur = 1; + } else if ("Heures Creuses Hiver / Saison Haute".equals(ct.libelle)) { + idxFournisseur = 4; + } else if ("Heures Creuses Saison Basse".equals(ct.libelle)) { + idxFournisseur = 4; + } else if ("Heures Pleines Hiver / Saison Haute".equals(ct.libelle)) { + idxFournisseur = 4; + } else if ("Heures Pleines Saison Basse".equals(ct.libelle)) { + idxFournisseur = 4; + } + + if (idxFournisseur == -1) { + continue; + } + + valueFournisseur[idxFournisseur] = Double.valueOf(ct.valeur); + result[i - 1].valueFromFournisseur[idxFournisseur] = (valueFournisseur[idxFournisseur] + - lastValFournisseur[idxFournisseur]); + + } + } + + } + lastVal = value; + lastValFournisseur = valueFournisseur; + } + } return result; diff --git a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/handler/BridgeRemoteBaseHandler.java b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/handler/BridgeRemoteBaseHandler.java index 4ff0bf0595631..0df3a1abed6a9 100644 --- a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/handler/BridgeRemoteBaseHandler.java +++ b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/handler/BridgeRemoteBaseHandler.java @@ -178,6 +178,8 @@ public void handleCommand(ChannelUID channelUID, Command command) { public abstract String getDailyConsumptionUrl(); + public abstract String getDailyIndexUrl(); + public abstract String getMaxPowerUrl(); public abstract String getLoadCurveUrl(); diff --git a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/handler/BridgeRemoteEnedisHandler.java b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/handler/BridgeRemoteEnedisHandler.java index acb014d60d420..87671a5a5f38d 100644 --- a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/handler/BridgeRemoteEnedisHandler.java +++ b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/handler/BridgeRemoteEnedisHandler.java @@ -51,6 +51,8 @@ public class BridgeRemoteEnedisHandler extends BridgeRemoteApiHandler { private static final String ADDRESS_URL = "customers_upa/v5/usage_points/addresses?usage_point_id=%s"; private static final String MEASURE_DAILY_CONSUMPTION_URL = "metering_data_dc/v5/daily_consumption?usage_point_id=%s&start=%s&end=%s"; + private static final String MEASURE_DAILY_INDEX_URL = MEASURE_DAILY_CONSUMPTION_URL; + private static final String MEASURE_MAX_POWER_URL = "metering_data_dcmp/v5/daily_consumption_max_power?usage_point_id=%s&start=%s&end=%s"; private static final String LOAD_CURVE_CONSUMPTION_URL = "metering_data_clc/v5/consumption_load_curve?usage_point_id=%s&start=%s&end=%s"; @@ -179,6 +181,11 @@ public String getDailyConsumptionUrl() { return getBaseUrl() + MEASURE_DAILY_CONSUMPTION_URL; } + @Override + public String getDailyIndexUrl() { + return getBaseUrl() + MEASURE_DAILY_INDEX_URL; + } + @Override public String getMaxPowerUrl() { return getBaseUrl() + MEASURE_MAX_POWER_URL; diff --git a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/handler/BridgeRemoteEnedisWebHandler.java b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/handler/BridgeRemoteEnedisWebHandler.java index 035311b1c64c2..734f18676e9bd 100644 --- a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/handler/BridgeRemoteEnedisWebHandler.java +++ b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/handler/BridgeRemoteEnedisWebHandler.java @@ -73,6 +73,9 @@ public class BridgeRemoteEnedisWebHandler extends BridgeRemoteBaseHandler { private static final String MEASURE_DAILY_CONSUMPTION_URL = PRM_INFO_BASE_URL + "%s/prms/%s/donnees-energetiques?mesuresTypeCode=ENERGIE&mesuresCorrigees=false&typeDonnees=CONS"; + private static final String MEASURE_DAILY_INDEX_URL = PRM_INFO_BASE_URL + + "%s/prms/%s/donnees-energetiques?mesuresTypeCode=INDEX&mesuresCorrigees=false&typeDonnees=CONS"; + private static final String MEASURE_MAX_POWER_URL = PRM_INFO_BASE_URL + "%s/prms/%s/donnees-energetiques?mesuresTypeCode=PMAX&mesuresCorrigees=false&typeDonnees=CONS"; @@ -144,6 +147,11 @@ public String getDailyConsumptionUrl() { return MEASURE_DAILY_CONSUMPTION_URL; } + @Override + public String getDailyIndexUrl() { + return MEASURE_DAILY_INDEX_URL; + } + @Override public String getMaxPowerUrl() { return MEASURE_MAX_POWER_URL; diff --git a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/handler/BridgeRemoteMyElectricalDataHandler.java b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/handler/BridgeRemoteMyElectricalDataHandler.java index f27dd895e4bd5..ac37fca9803fc 100644 --- a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/handler/BridgeRemoteMyElectricalDataHandler.java +++ b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/handler/BridgeRemoteMyElectricalDataHandler.java @@ -49,6 +49,8 @@ public class BridgeRemoteMyElectricalDataHandler extends BridgeRemoteApiHandler private static final String CONTACT_URL = BASE_URL + "contact/%s/cache/"; private static final String ADDRESS_URL = BASE_URL + "addresses/%s/cache/"; private static final String MEASURE_DAILY_CONSUMPTION_URL = BASE_URL + "daily_consumption/%s/start/%s/end/%s/cache"; + private static final String MEASURE_DAILY_INDEX_URL = MEASURE_DAILY_CONSUMPTION_URL; + private static final String MEASURE_MAX_POWER_URL = BASE_URL + "daily_consumption_max_power/%s/start/%s/end/%s/cache"; private static final String LOAD_CURVE_CONSUMPTION_URL = BASE_URL @@ -192,6 +194,11 @@ public String getDailyConsumptionUrl() { return MEASURE_DAILY_CONSUMPTION_URL; } + @Override + public String getDailyIndexUrl() { + return MEASURE_DAILY_INDEX_URL; + } + @Override public String getMaxPowerUrl() { return MEASURE_MAX_POWER_URL; diff --git a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/handler/ThingLinkyRemoteHandler.java b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/handler/ThingLinkyRemoteHandler.java index 7e1a439127ddf..67120c4a6b30e 100644 --- a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/handler/ThingLinkyRemoteHandler.java +++ b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/handler/ThingLinkyRemoteHandler.java @@ -89,6 +89,7 @@ public class ThingLinkyRemoteHandler extends ThingBaseRemoteHandler { private final ExpiringDayCache metaData; private final ExpiringDayCache dailyConsumption; + private final ExpiringDayCache dailyIndex; private final ExpiringDayCache dailyConsumptionMaxPower; private final ExpiringDayCache loadCurveConsumption; @@ -129,6 +130,17 @@ public ThingLinkyRemoteHandler(Thing thing, LocaleProvider localeProvider, TimeZ return meterReading; }); + this.dailyIndex = new ExpiringDayCache<>("dailyIndex", REFRESH_HOUR_OF_DAY, REFRESH_MINUTE_OF_DAY, () -> { + LocalDate today = LocalDate.now(); + MeterReading meterReading = getConsumptionIndex(today.minusDays(1095), today); + meterReading = getMeterReadingAfterChecks(meterReading); + if (meterReading != null) { + logData(meterReading.baseValue, "Day", DateTimeFormatter.ISO_LOCAL_DATE, Target.ALL); + logData(meterReading.weekValue, "Week", DateTimeFormatter.ISO_LOCAL_DATE_TIME, Target.ALL); + } + return meterReading; + }); + // We request data for yesterday and the day before yesterday // even if the data for the day before yesterday // This is only a workaround to an API bug that will return INTERNAL_SERVER_ERROR rather @@ -368,6 +380,7 @@ private synchronized void updateData() { } updateEnergyData(); + updateEnergyIndex(); updatePowerData(); updateLoadCurveData(); } @@ -479,6 +492,76 @@ private synchronized void updateEnergyData() { }); } + /** + * Request new daily/weekly data and updates channels + */ + private synchronized void updateEnergyIndex() { + dailyIndex.getValue().ifPresentOrElse(values -> { + int dSize = values.baseValue.length; + + /* + * updateKwhChannel(LINKY_REMOTE_DAILY_GROUP, CHANNEL_DAY_MINUS_1, values.baseValue[dSize - 1].value); + * updateKwhChannel(LINKY_REMOTE_DAILY_GROUP, CHANNEL_DAY_MINUS_2, values.baseValue[dSize - 2].value); + * updateKwhChannel(LINKY_REMOTE_DAILY_GROUP, CHANNEL_DAY_MINUS_3, values.baseValue[dSize - 3].value); + * + * int idxCurrentYear = values.yearValue.length - 1; + * int idxCurrentWeek = values.weekValue.length - 1; + * int idxCurrentMonth = values.monthValue.length - 1; + * + * updateKwhChannel(LINKY_REMOTE_WEEKLY_GROUP, CHANNEL_WEEK_MINUS_0, + * values.weekValue[idxCurrentWeek].value); + * updateKwhChannel(LINKY_REMOTE_WEEKLY_GROUP, CHANNEL_WEEK_MINUS_1, + * values.weekValue[idxCurrentWeek - 1].value); + * if (idxCurrentWeek - 2 >= 0) { + * updateKwhChannel(LINKY_REMOTE_WEEKLY_GROUP, CHANNEL_WEEK_MINUS_2, + * values.weekValue[idxCurrentWeek - 2].value); + * } + * + * updateKwhChannel(LINKY_REMOTE_MONTHLY_GROUP, CHANNEL_MONTH_MINUS_0, + * values.monthValue[idxCurrentMonth].value); + * updateKwhChannel(LINKY_REMOTE_MONTHLY_GROUP, CHANNEL_MONTH_MINUS_1, + * values.monthValue[idxCurrentMonth - 1].value); + * if (idxCurrentMonth - 2 >= 0) { + * updateKwhChannel(LINKY_REMOTE_MONTHLY_GROUP, CHANNEL_MONTH_MINUS_2, + * values.monthValue[idxCurrentMonth - 2].value); + * } + * + * updateKwhChannel(LINKY_REMOTE_YEARLY_GROUP, CHANNEL_YEAR_MINUS_0, + * values.yearValue[idxCurrentYear].value); + * updateKwhChannel(LINKY_REMOTE_YEARLY_GROUP, CHANNEL_YEAR_MINUS_1, + * values.yearValue[idxCurrentYear - 1].value); + * if (idxCurrentYear - 2 >= 0) { + * updateKwhChannel(LINKY_REMOTE_YEARLY_GROUP, CHANNEL_YEAR_MINUS_2, + * values.yearValue[idxCurrentYear - 2].value); + * } + * + * updateTimeSeries(LINKY_REMOTE_DAILY_GROUP, CHANNEL_CONSUMPTION, values.baseValue, Units.KILOWATT_HOUR); + * updateTimeSeries(LINKY_REMOTE_WEEKLY_GROUP, CHANNEL_CONSUMPTION, values.weekValue, Units.KILOWATT_HOUR); + * updateTimeSeries(LINKY_REMOTE_MONTHLY_GROUP, CHANNEL_CONSUMPTION, values.monthValue, + * Units.KILOWATT_HOUR); + * updateTimeSeries(LINKY_REMOTE_YEARLY_GROUP, CHANNEL_CONSUMPTION, values.yearValue, Units.KILOWATT_HOUR); + */ + }, () -> { + /* + * updateKwhChannel(LINKY_REMOTE_DAILY_GROUP, CHANNEL_DAY_MINUS_1, Double.NaN); + * updateKwhChannel(LINKY_REMOTE_DAILY_GROUP, CHANNEL_DAY_MINUS_2, Double.NaN); + * updateKwhChannel(LINKY_REMOTE_DAILY_GROUP, CHANNEL_DAY_MINUS_3, Double.NaN); + * + * updateKwhChannel(LINKY_REMOTE_WEEKLY_GROUP, CHANNEL_WEEK_MINUS_0, Double.NaN); + * updateKwhChannel(LINKY_REMOTE_WEEKLY_GROUP, CHANNEL_WEEK_MINUS_1, Double.NaN); + * updateKwhChannel(LINKY_REMOTE_WEEKLY_GROUP, CHANNEL_WEEK_MINUS_2, Double.NaN); + * + * updateKwhChannel(LINKY_REMOTE_MONTHLY_GROUP, CHANNEL_MONTH_MINUS_0, Double.NaN); + * updateKwhChannel(LINKY_REMOTE_MONTHLY_GROUP, CHANNEL_MONTH_MINUS_1, Double.NaN); + * updateKwhChannel(LINKY_REMOTE_MONTHLY_GROUP, CHANNEL_MONTH_MINUS_2, Double.NaN); + * + * updateKwhChannel(LINKY_REMOTE_YEARLY_GROUP, CHANNEL_YEAR_MINUS_0, Double.NaN); + * updateKwhChannel(LINKY_REMOTE_YEARLY_GROUP, CHANNEL_YEAR_MINUS_1, Double.NaN); + * updateKwhChannel(LINKY_REMOTE_YEARLY_GROUP, CHANNEL_YEAR_MINUS_2, Double.NaN); + */ + }); + } + /** * Request new loadCurve data and updates channels */ @@ -611,6 +694,23 @@ private List buildReport(LocalDate startDay, LocalDate endDay, @Nullable return null; } + private @Nullable MeterReading getConsumptionIndex(LocalDate from, LocalDate to) { + logger.debug("getConsumptionIndex for {} from {} to {}", config.prmId, + from.format(DateTimeFormatter.ISO_LOCAL_DATE), to.format(DateTimeFormatter.ISO_LOCAL_DATE)); + + EnedisHttpApi api = this.enedisApi; + if (api != null) { + try { + return api.getEnergyIndex(this, this.userId, config.prmId, from, to); + } catch (LinkyException e) { + logger.debug("Exception when getting consumption data for {} : {}", config.prmId, e.getMessage(), e); + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, e.getMessage()); + } + } + + return null; + } + private @Nullable MeterReading getLoadCurveConsumption(LocalDate from, LocalDate to) { logger.debug("getLoadCurveConsumption for {} from {} to {}", config.prmId, from.format(DateTimeFormatter.ISO_LOCAL_DATE), to.format(DateTimeFormatter.ISO_LOCAL_DATE)); @@ -772,7 +872,7 @@ private boolean isDataLastDayAvailable(@Nullable MeterReading meterReading) { IntervalReading[] iv = meterReading.baseValue; logData(iv, "Last day", DateTimeFormatter.ISO_LOCAL_DATE, Target.LAST); - return iv.length != 0 && !iv[iv.length - 1].value.isNaN(); + return iv != null && iv.length != 0 && iv[iv.length - 1] != null && !iv[iv.length - 1].value.isNaN(); } return false; 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 index c3c927be81cf5..22ba90c6fcf70 100644 --- 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 @@ -44,6 +44,9 @@ public class DoubleTypeAdapter extends TypeAdapter { return Double.NaN; } String stringValue = reader.nextString(); + if (stringValue != null) { + stringValue = stringValue.replace(',', '.'); + } try { Double value = Double.valueOf(stringValue); return value; From a5b1bfc36e0d6efe6d3d90b4052379871beedd78 Mon Sep 17 00:00:00 2001 From: Laurent ARNAL Date: Mon, 11 Aug 2025 18:44:53 +0200 Subject: [PATCH 02/34] add new channel for idx Fournisseur data Signed-off-by: Laurent ARNAL --- .../constants/LinkyBindingConstants.java | 2 + .../handler/ThingLinkyRemoteHandler.java | 40 +++++++++++++------ .../OH-INF/thing/group-linky-remote-daily.xml | 8 ++++ 3 files changed, 38 insertions(+), 12 deletions(-) diff --git a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/constants/LinkyBindingConstants.java b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/constants/LinkyBindingConstants.java index bccd2b5d1d3fe..e62535784cadb 100644 --- a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/constants/LinkyBindingConstants.java +++ b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/constants/LinkyBindingConstants.java @@ -59,6 +59,8 @@ public class LinkyBindingConstants { // List of all Channel id's public static final String CHANNEL_CONSUMPTION = "consumption"; + public static final String CHANNEL_CONSUMPTION_IDX0 = "consumptionIdx0"; + public static final String CHANNEL_CONSUMPTION_IDX1 = "consumptionIdx1"; public static final String CHANNEL_MAX_POWER = "max-power"; public static final String CHANNEL_POWER = "power"; public static final String CHANNEL_TIMESTAMP_CHANNEL = "power"; diff --git a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/handler/ThingLinkyRemoteHandler.java b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/handler/ThingLinkyRemoteHandler.java index 67120c4a6b30e..b2734397783b1 100644 --- a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/handler/ThingLinkyRemoteHandler.java +++ b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/handler/ThingLinkyRemoteHandler.java @@ -405,16 +405,16 @@ private synchronized void updatePowerData() { updateState(LINKY_REMOTE_DAILY_GROUP, CHANNEL_PEAK_POWER_TS_DAY_MINUS_3, new DateTimeType(values.baseValue[dSize - 3].date.atZone(zoneId))); - updateTimeSeries(LINKY_REMOTE_DAILY_GROUP, CHANNEL_MAX_POWER, values.baseValue, + updateTimeSeries(LINKY_REMOTE_DAILY_GROUP, CHANNEL_MAX_POWER, values.baseValue, -1, MetricPrefix.KILO(Units.VOLT_AMPERE)); - updateTimeSeries(LINKY_REMOTE_WEEKLY_GROUP, CHANNEL_MAX_POWER, values.weekValue, + updateTimeSeries(LINKY_REMOTE_WEEKLY_GROUP, CHANNEL_MAX_POWER, values.weekValue, -1, MetricPrefix.KILO(Units.VOLT_AMPERE)); - updateTimeSeries(LINKY_REMOTE_MONTHLY_GROUP, CHANNEL_MAX_POWER, values.monthValue, + updateTimeSeries(LINKY_REMOTE_MONTHLY_GROUP, CHANNEL_MAX_POWER, values.monthValue, -1, MetricPrefix.KILO(Units.VOLT_AMPERE)); - updateTimeSeries(LINKY_REMOTE_YEARLY_GROUP, CHANNEL_MAX_POWER, values.yearValue, + updateTimeSeries(LINKY_REMOTE_YEARLY_GROUP, CHANNEL_MAX_POWER, values.yearValue, -1, MetricPrefix.KILO(Units.VOLT_AMPERE)); }, () -> { updateKwhChannel(LINKY_REMOTE_DAILY_GROUP, CHANNEL_PEAK_POWER_DAY_MINUS_1, Double.NaN); @@ -469,10 +469,11 @@ private synchronized void updateEnergyData() { values.yearValue[idxCurrentYear - 2].value); } - updateTimeSeries(LINKY_REMOTE_DAILY_GROUP, CHANNEL_CONSUMPTION, values.baseValue, Units.KILOWATT_HOUR); - updateTimeSeries(LINKY_REMOTE_WEEKLY_GROUP, CHANNEL_CONSUMPTION, values.weekValue, Units.KILOWATT_HOUR); - updateTimeSeries(LINKY_REMOTE_MONTHLY_GROUP, CHANNEL_CONSUMPTION, values.monthValue, Units.KILOWATT_HOUR); - updateTimeSeries(LINKY_REMOTE_YEARLY_GROUP, CHANNEL_CONSUMPTION, values.yearValue, Units.KILOWATT_HOUR); + updateTimeSeries(LINKY_REMOTE_DAILY_GROUP, CHANNEL_CONSUMPTION, values.baseValue, -1, Units.KILOWATT_HOUR); + updateTimeSeries(LINKY_REMOTE_WEEKLY_GROUP, CHANNEL_CONSUMPTION, values.weekValue, -1, Units.KILOWATT_HOUR); + updateTimeSeries(LINKY_REMOTE_MONTHLY_GROUP, CHANNEL_CONSUMPTION, values.monthValue, -1, + Units.KILOWATT_HOUR); + updateTimeSeries(LINKY_REMOTE_YEARLY_GROUP, CHANNEL_CONSUMPTION, values.yearValue, -1, Units.KILOWATT_HOUR); }, () -> { updateKwhChannel(LINKY_REMOTE_DAILY_GROUP, CHANNEL_DAY_MINUS_1, Double.NaN); updateKwhChannel(LINKY_REMOTE_DAILY_GROUP, CHANNEL_DAY_MINUS_2, Double.NaN); @@ -499,6 +500,11 @@ private synchronized void updateEnergyIndex() { dailyIndex.getValue().ifPresentOrElse(values -> { int dSize = values.baseValue.length; + updateTimeSeries(LINKY_REMOTE_DAILY_GROUP, CHANNEL_CONSUMPTION_IDX0, values.baseValue, 0, + Units.KILOWATT_HOUR); + updateTimeSeries(LINKY_REMOTE_DAILY_GROUP, CHANNEL_CONSUMPTION_IDX1, values.baseValue, 1, + Units.KILOWATT_HOUR); + /* * updateKwhChannel(LINKY_REMOTE_DAILY_GROUP, CHANNEL_DAY_MINUS_1, values.baseValue[dSize - 1].value); * updateKwhChannel(LINKY_REMOTE_DAILY_GROUP, CHANNEL_DAY_MINUS_2, values.baseValue[dSize - 2].value); @@ -568,7 +574,7 @@ private synchronized void updateEnergyIndex() { private synchronized void updateLoadCurveData() { if (isLinked(LINKY_REMOTE_LOAD_CURVE_GROUP, CHANNEL_POWER)) { loadCurveConsumption.getValue().ifPresentOrElse(values -> { - updateTimeSeries(LINKY_REMOTE_LOAD_CURVE_GROUP, CHANNEL_POWER, values.baseValue, + updateTimeSeries(LINKY_REMOTE_LOAD_CURVE_GROUP, CHANNEL_POWER, values.baseValue, -1, MetricPrefix.KILO(Units.VOLT_AMPERE)); }, () -> { }); @@ -576,11 +582,14 @@ private synchronized void updateLoadCurveData() { } private synchronized > void updateTimeSeries(String groupId, String channelId, - IntervalReading[] iv, Unit unit) { + IntervalReading[] iv, int idx, Unit unit) { TimeSeries timeSeries = new TimeSeries(Policy.REPLACE); for (int i = 0; i < iv.length; i++) { try { + if (iv[i] == null) { + continue; + } if (iv[i].date == null) { continue; } @@ -590,7 +599,13 @@ private synchronized > void updateTimeSeries(String groupI if (Double.isNaN(iv[i].value)) { continue; } - timeSeries.add(timestamp, new QuantityType<>(iv[i].value, unit)); + if (idx != -1) { + if (iv[i + 1] != null) { + timeSeries.add(timestamp, new QuantityType<>(iv[i + 1].valueFromFournisseur[idx], unit)); + } + } else { + timeSeries.add(timestamp, new QuantityType<>(iv[i].value, unit)); + } } catch (Exception ex) { logger.error("error occurs durring updatePowerTimeSeries for {} : {}", config.prmId, ex.getMessage(), ex); @@ -782,7 +797,8 @@ public void handleCommand(ChannelUID channelUID, Command command) { if (!isDataLastDayAvailable(meterReading)) { logger.debug("Data including yesterday are not yet available"); - return null; + return meterReading; + // return null; } if (meterReading != null) { diff --git a/bundles/org.openhab.binding.linky/src/main/resources/OH-INF/thing/group-linky-remote-daily.xml b/bundles/org.openhab.binding.linky/src/main/resources/OH-INF/thing/group-linky-remote-daily.xml index 8b9d09f8b0ed7..b5c82a853bc7c 100644 --- a/bundles/org.openhab.binding.linky/src/main/resources/OH-INF/thing/group-linky-remote-daily.xml +++ b/bundles/org.openhab.binding.linky/src/main/resources/OH-INF/thing/group-linky-remote-daily.xml @@ -26,6 +26,14 @@ The energy consumption + + + The energy consumption Idx0 + + + + The energy consumption Idx1 + From c517d5b1be85445066d0e97414357bc4c4c89662 Mon Sep 17 00:00:00 2001 From: Laurent ARNAL Date: Sat, 6 Sep 2025 19:27:13 +0200 Subject: [PATCH 03/34] backport fixes from branch linky-fix-2025-09 Signed-off-by: Laurent ARNAL --- .../binding/linky/internal/api/EnedisHttpApi.java | 2 +- .../handler/BridgeRemoteEnedisWebHandler.java | 15 +++++++++++---- .../internal/handler/ThingLinkyRemoteHandler.java | 11 +++++++++++ .../linky/internal/types/LinkyException.java | 1 - 4 files changed, 23 insertions(+), 6 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 7368c5976c1fc..2525b3d56e7cf 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 @@ -268,7 +268,7 @@ private MeterReading getMeasures(ThingLinkyRemoteHandler handler, String apiUrl, ResponseMeter meterResponse = getData(handler, url, ResponseMeter.class); return meterResponse.meterReading; } else { - String url = String.format(apiUrl, mps, prmId, dtStart, dtEnd); + String url = String.format(apiUrl, mps, prmId, "C5", dtStart, dtEnd); ConsumptionReport consomptionReport = getData(handler, url, ConsumptionReport.class); return MeterReading.convertFromComsumptionReport(consomptionReport, useIndex); } diff --git a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/handler/BridgeRemoteEnedisWebHandler.java b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/handler/BridgeRemoteEnedisWebHandler.java index 734f18676e9bd..a71735f14bc6d 100644 --- a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/handler/BridgeRemoteEnedisWebHandler.java +++ b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/handler/BridgeRemoteEnedisWebHandler.java @@ -67,20 +67,20 @@ public class BridgeRemoteEnedisWebHandler extends BridgeRemoteBaseHandler { private static final String USER_INFO_CONTRACT_URL = BASE_URL + "/mon-compte/api/private/v2/userinfos"; private static final String USER_INFO_URL = BASE_URL + "/userinfos"; - private static final String PRM_INFO_BASE_URL = BASE_URL + "/mes-mesures-prm/api/private/v1/personnes/"; + private static final String PRM_INFO_BASE_URL = BASE_URL + "/mes-mesures-prm/api/private/v2/personnes/"; private static final String PRM_INFO_URL = BASE_URL + "/mes-prms-part/api/private/v2/personnes/%s/prms"; private static final String MEASURE_DAILY_CONSUMPTION_URL = PRM_INFO_BASE_URL - + "%s/prms/%s/donnees-energetiques?mesuresTypeCode=ENERGIE&mesuresCorrigees=false&typeDonnees=CONS"; + + "%s/prms/%s/donnees-energetiques?mesuresTypeCode=ENERGIE&mesuresCorrigees=false&typeDonnees=CONS&segments=%s"; private static final String MEASURE_DAILY_INDEX_URL = PRM_INFO_BASE_URL + "%s/prms/%s/donnees-energetiques?mesuresTypeCode=INDEX&mesuresCorrigees=false&typeDonnees=CONS"; private static final String MEASURE_MAX_POWER_URL = PRM_INFO_BASE_URL - + "%s/prms/%s/donnees-energetiques?mesuresTypeCode=PMAX&mesuresCorrigees=false&typeDonnees=CONS"; + + "%s/prms/%s/donnees-energetiques?mesuresTypeCode=PMAX&mesuresCorrigees=false&typeDonnees=CONS&segments=%s"; private static final String LOAD_CURVE_CONSUMPTION_URL = PRM_INFO_BASE_URL - + "%s/prms/%s/donnees-energetiques?mesuresTypeCode=COURBE&mesuresCorrigees=false&typeDonnees=CONS&dateDebut=%s"; + + "%s/prms/%s/donnees-energetiques?mesuresTypeCode=COURBE&mesuresCorrigees=false&typeDonnees=CONS&segments=%s&dateDebut=%s"; private static final DateTimeFormatter API_DATE_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd"); private static final DateTimeFormatter API_DATE_FORMAT_YEAR_FIRST = DateTimeFormatter.ofPattern("yyyy-MM-dd"); @@ -90,6 +90,8 @@ public class BridgeRemoteEnedisWebHandler extends BridgeRemoteBaseHandler { private static final String BASE_MYELECT_URL = "https://www.myelectricaldata.fr/"; private static final String TEMPO_URL = BASE_MYELECT_URL + "rte/tempo/%s/%s"; + private String idPersonne = ""; + public BridgeRemoteEnedisWebHandler(Bridge bridge, final @Reference HttpClientFactory httpClientFactory, final @Reference OAuthFactory oAuthFactory, final @Reference HttpService httpService, ComponentContext componentContext, Gson gson) { @@ -341,6 +343,7 @@ public synchronized void connectionInit() throws LinkyException { if (hashRes != null && hashRes.containsKey("cnAlex")) { cookieKey = "personne_for_" + hashRes.get("cnAlex"); + idPersonne = Objects.requireNonNull(hashRes.get("idPersonne")); } else { throw new LinkyException("Connection failed step 5, missing cookieKey"); } @@ -363,4 +366,8 @@ public synchronized void connectionInit() throws LinkyException { public boolean supportNewApiFormat() { return false; } + + public String getIdPersonne() { + return idPersonne; + } } diff --git a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/handler/ThingLinkyRemoteHandler.java b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/handler/ThingLinkyRemoteHandler.java index b2734397783b1..a3c233979b6b4 100644 --- a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/handler/ThingLinkyRemoteHandler.java +++ b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/handler/ThingLinkyRemoteHandler.java @@ -283,8 +283,19 @@ private synchronized void updateMetaData() { if (values.identity.internId == null) { values.identity.internId = values.identity.firstname + " " + values.identity.lastname; } + userId = values.identity.internId; + Bridge bridge = getBridge(); + BridgeRemoteBaseHandler bridgeHandler = null; + if (bridge != null) { + bridgeHandler = (BridgeRemoteBaseHandler) bridge.getHandler(); + } + + if (bridgeHandler instanceof BridgeRemoteEnedisWebHandler) { + userId = ((BridgeRemoteEnedisWebHandler) bridgeHandler).getIdPersonne(); + } + addProps(props, USER_ID, userId); addProps(props, PROPERTY_USAGEPOINT_ID, values.usagePoint.usagePointId); diff --git a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/types/LinkyException.java b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/types/LinkyException.java index de3d10bb20652..283914b352489 100644 --- a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/types/LinkyException.java +++ b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/types/LinkyException.java @@ -24,7 +24,6 @@ public class LinkyException extends Exception { private static final long serialVersionUID = 3703839284673384018L; public LinkyException() { - super(); } public LinkyException(String message) { From bce7914b2813653e89771f91b906c9d974205103 Mon Sep 17 00:00:00 2001 From: Laurent ARNAL Date: Sun, 7 Sep 2025 09:16:30 +0200 Subject: [PATCH 04/34] backport fixes from branch linky-fix-2025-09 Signed-off-by: Laurent ARNAL --- .../linky/internal/api/EnedisHttpApi.java | 26 +++++++++---------- .../handler/ThingLinkyRemoteHandler.java | 12 +++++---- 2 files changed, 20 insertions(+), 18 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 2525b3d56e7cf..123ccf176edcc 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 @@ -259,7 +259,7 @@ public Contact getContact(ThingLinkyRemoteHandler handler, String prmId) throws } private MeterReading getMeasures(ThingLinkyRemoteHandler handler, String apiUrl, String mps, String prmId, - LocalDate from, LocalDate to, boolean useIndex) throws LinkyException { + String segment, LocalDate from, LocalDate to, boolean useIndex) throws LinkyException { String dtStart = from.format(linkyBridgeHandler.getApiDateFormat()); String dtEnd = to.format(linkyBridgeHandler.getApiDateFormat()); @@ -274,24 +274,24 @@ private MeterReading getMeasures(ThingLinkyRemoteHandler handler, String apiUrl, } } - public MeterReading getEnergyData(ThingLinkyRemoteHandler handler, String mps, String prmId, LocalDate from, - LocalDate to) throws LinkyException { - return getMeasures(handler, linkyBridgeHandler.getDailyConsumptionUrl(), mps, prmId, from, to, false); + public MeterReading getEnergyData(ThingLinkyRemoteHandler handler, String mps, String prmId, String segment, + LocalDate from, LocalDate to) throws LinkyException { + return getMeasures(handler, linkyBridgeHandler.getDailyConsumptionUrl(), mps, prmId, segment, from, to, false); } - public MeterReading getEnergyIndex(ThingLinkyRemoteHandler handler, String mps, String prmId, LocalDate from, - LocalDate to) throws LinkyException { - return getMeasures(handler, linkyBridgeHandler.getDailyIndexUrl(), mps, prmId, from, to, true); + public MeterReading getEnergyIndex(ThingLinkyRemoteHandler handler, String mps, String prmId, String segment, + LocalDate from, LocalDate to) throws LinkyException { + return getMeasures(handler, linkyBridgeHandler.getDailyIndexUrl(), mps, prmId, segment, from, to, true); } - public MeterReading getLoadCurveData(ThingLinkyRemoteHandler handler, String mps, String prmId, LocalDate from, - LocalDate to) throws LinkyException { - return getMeasures(handler, linkyBridgeHandler.getLoadCurveUrl(), mps, prmId, from, to, false); + public MeterReading getLoadCurveData(ThingLinkyRemoteHandler handler, String mps, String prmId, String segment, + LocalDate from, LocalDate to) throws LinkyException { + return getMeasures(handler, linkyBridgeHandler.getLoadCurveUrl(), mps, prmId, segment, from, to, false); } - public MeterReading getPowerData(ThingLinkyRemoteHandler handler, String mps, String prmId, LocalDate from, - LocalDate to) throws LinkyException { - return getMeasures(handler, linkyBridgeHandler.getMaxPowerUrl(), mps, prmId, from, to, false); + public MeterReading getPowerData(ThingLinkyRemoteHandler handler, String mps, String prmId, String segment, + LocalDate from, LocalDate to) throws LinkyException { + return getMeasures(handler, linkyBridgeHandler.getMaxPowerUrl(), mps, prmId, segment, from, to, false); } public ResponseTempo getTempoData(ThingBaseRemoteHandler handler, LocalDate from, LocalDate to) diff --git a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/handler/ThingLinkyRemoteHandler.java b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/handler/ThingLinkyRemoteHandler.java index a3c233979b6b4..3424547f9bea0 100644 --- a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/handler/ThingLinkyRemoteHandler.java +++ b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/handler/ThingLinkyRemoteHandler.java @@ -99,6 +99,7 @@ public class ThingLinkyRemoteHandler extends ThingBaseRemoteHandler { private double divider = 1.00; public String userId = ""; + public String segment = ""; private @Nullable ScheduledFuture pollingJob = null; @@ -302,7 +303,8 @@ private synchronized void updateMetaData() { addProps(props, PROPERTY_IDENTITY, title + " " + firstName + " " + lastName); - addProps(props, PROPERTY_CONTRACT_SEGMENT, values.contract.segment); + segment = values.contract.segment; + addProps(props, PROPERTY_CONTRACT_SEGMENT, segment); addProps(props, PROPERTY_CONTRACT_CONTRACT_STATUS, values.contract.contractStatus); addProps(props, PROPERTY_CONTRACT_CONTRACT_TYPE, values.contract.contractType); addProps(props, PROPERTY_CONTRACT_DISTRIBUTION_TARIFF, values.contract.distributionTariff); @@ -710,7 +712,7 @@ private List buildReport(LocalDate startDay, LocalDate endDay, @Nullable EnedisHttpApi api = this.enedisApi; if (api != null) { try { - return api.getEnergyData(this, this.userId, config.prmId, from, to); + return api.getEnergyData(this, this.userId, config.prmId, segment, from, to); } catch (LinkyException e) { logger.debug("Exception when getting consumption data for {} : {}", config.prmId, e.getMessage(), e); updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, e.getMessage()); @@ -727,7 +729,7 @@ private List buildReport(LocalDate startDay, LocalDate endDay, @Nullable EnedisHttpApi api = this.enedisApi; if (api != null) { try { - return api.getEnergyIndex(this, this.userId, config.prmId, from, to); + return api.getEnergyIndex(this, this.userId, config.prmId, segment, from, to); } catch (LinkyException e) { logger.debug("Exception when getting consumption data for {} : {}", config.prmId, e.getMessage(), e); updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, e.getMessage()); @@ -744,7 +746,7 @@ private List buildReport(LocalDate startDay, LocalDate endDay, @Nullable EnedisHttpApi api = this.enedisApi; if (api != null) { try { - return api.getLoadCurveData(this, this.userId, config.prmId, from, to); + return api.getLoadCurveData(this, this.userId, config.prmId, segment, from, to); } catch (LinkyException e) { logger.debug("Exception when getting consumption data: {}", e.getMessage(), e); updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, e.getMessage()); @@ -761,7 +763,7 @@ private List buildReport(LocalDate startDay, LocalDate endDay, @Nullable EnedisHttpApi api = this.enedisApi; if (api != null) { try { - return api.getPowerData(this, this.userId, config.prmId, from, to); + return api.getPowerData(this, this.userId, config.prmId, segment, from, to); } catch (LinkyException e) { logger.debug("Exception when getting power data: {}", e.getMessage(), e); updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, e.getMessage()); From a6ea2d13af07cd7d4520f49ddc8eb13ff5deece1 Mon Sep 17 00:00:00 2001 From: Laurent ARNAL Date: Sun, 7 Sep 2025 10:17:27 +0200 Subject: [PATCH 05/34] fix error on URL Signed-off-by: Laurent ARNAL --- .../linky/internal/handler/BridgeRemoteEnedisWebHandler.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/handler/BridgeRemoteEnedisWebHandler.java b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/handler/BridgeRemoteEnedisWebHandler.java index a71735f14bc6d..3d711be5eeea4 100644 --- a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/handler/BridgeRemoteEnedisWebHandler.java +++ b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/handler/BridgeRemoteEnedisWebHandler.java @@ -74,7 +74,7 @@ public class BridgeRemoteEnedisWebHandler extends BridgeRemoteBaseHandler { + "%s/prms/%s/donnees-energetiques?mesuresTypeCode=ENERGIE&mesuresCorrigees=false&typeDonnees=CONS&segments=%s"; private static final String MEASURE_DAILY_INDEX_URL = PRM_INFO_BASE_URL - + "%s/prms/%s/donnees-energetiques?mesuresTypeCode=INDEX&mesuresCorrigees=false&typeDonnees=CONS"; + + "%s/prms/%s/donnees-energetiques?mesuresTypeCode=INDEX&mesuresCorrigees=false&typeDonnees=CONS&segments=%s"; private static final String MEASURE_MAX_POWER_URL = PRM_INFO_BASE_URL + "%s/prms/%s/donnees-energetiques?mesuresTypeCode=PMAX&mesuresCorrigees=false&typeDonnees=CONS&segments=%s"; From 671729bf5c7ad2073db7db8ca3a36a645124a79f Mon Sep 17 00:00:00 2001 From: Laurent ARNAL Date: Sun, 14 Sep 2025 07:37:14 +0200 Subject: [PATCH 06/34] move dto to double to simplify code Signed-off-by: Laurent ARNAL --- .../linky/internal/dto/IntervalReading.java | 4 +- .../linky/internal/dto/MeterReading.java | 40 ++++--------------- .../handler/ThingLinkyRemoteHandler.java | 10 +++-- 3 files changed, 16 insertions(+), 38 deletions(-) diff --git a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/IntervalReading.java b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/IntervalReading.java index afe6b4dca9c36..00b1e245776e3 100644 --- a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/IntervalReading.java +++ b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/IntervalReading.java @@ -23,7 +23,7 @@ public class IntervalReading { public Double value = 0.0; - public Double[] valueFromFournisseur; - public Double[] valueFromDistributeur; + public double[] valueFromFournisseur; + public double[] valueFromDistributeur; public LocalDateTime date; } diff --git a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/MeterReading.java b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/MeterReading.java index 1414bd7168938..3678b32fb7350 100644 --- a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/MeterReading.java +++ b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/MeterReading.java @@ -67,48 +67,24 @@ public static IntervalReading[] fromAgregat(ConsumptionReport.Aggregate agregat, for (int i = 0; i < size; i++) { Data dataObj = agregat.datas.get(i); result[i] = new IntervalReading(); - result[i].value = Double.valueOf(dataObj.valeur); + result[i].value = dataObj.valeur; result[i].date = dataObj.dateDebut; } } else { - Double lastVal = 0.0; - Double[] lastValFournisseur = new Double[6]; - lastValFournisseur[0] = 0.0; - lastValFournisseur[1] = 0.0; - lastValFournisseur[2] = 0.0; - lastValFournisseur[3] = 0.0; - lastValFournisseur[4] = 0.0; - lastValFournisseur[5] = 0.0; + double lastVal = 0.0; + double[] lastValFournisseur = new double[6]; for (int i = 0; i < size; i++) { Data dataObj = agregat.datas.get(i); - Double value = Double.valueOf(dataObj.valeur); - Double[] valueFournisseur = new Double[6]; - valueFournisseur[0] = 0.0; - valueFournisseur[1] = 0.0; - valueFournisseur[2] = 0.0; - valueFournisseur[3] = 0.0; - valueFournisseur[4] = 0.0; - valueFournisseur[5] = 0.0; + double value = dataObj.valeur; + double[] valueFournisseur = new double[6]; if (i > 0) { result[i - 1] = new IntervalReading(); result[i - 1].value = value - lastVal; result[i - 1].date = dataObj.dateDebut; - result[i - 1].valueFromFournisseur = new Double[6]; - result[i - 1].valueFromDistributeur = new Double[4]; - - result[i - 1].valueFromFournisseur[0] = 0.0; - result[i - 1].valueFromFournisseur[1] = 0.0; - result[i - 1].valueFromFournisseur[2] = 0.0; - result[i - 1].valueFromFournisseur[3] = 0.0; - result[i - 1].valueFromFournisseur[4] = 0.0; - result[i - 1].valueFromFournisseur[5] = 0.0; - - result[i - 1].valueFromDistributeur[0] = 0.0; - result[i - 1].valueFromDistributeur[1] = 0.0; - result[i - 1].valueFromDistributeur[2] = 0.0; - result[i - 1].valueFromDistributeur[3] = 0.0; + result[i - 1].valueFromFournisseur = new double[6]; + result[i - 1].valueFromDistributeur = new double[4]; if (dataObj.classesTemporellesFournisseur != null) { for (ClassesTemporelles ct : dataObj.classesTemporellesFournisseur) { @@ -145,7 +121,7 @@ public static IntervalReading[] fromAgregat(ConsumptionReport.Aggregate agregat, continue; } - valueFournisseur[idxFournisseur] = Double.valueOf(ct.valeur); + valueFournisseur[idxFournisseur] = ct.valeur; result[i - 1].valueFromFournisseur[idxFournisseur] = (valueFournisseur[idxFournisseur] - lastValFournisseur[idxFournisseur]); diff --git a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/handler/ThingLinkyRemoteHandler.java b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/handler/ThingLinkyRemoteHandler.java index 3424547f9bea0..7d92c89b5b16e 100644 --- a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/handler/ThingLinkyRemoteHandler.java +++ b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/handler/ThingLinkyRemoteHandler.java @@ -83,6 +83,8 @@ public class ThingLinkyRemoteHandler extends ThingBaseRemoteHandler { private static final int REFRESH_HOUR_OF_DAY = 1; private static final int REFRESH_MINUTE_OF_DAY = RANDOM_NUMBERS.nextInt(60); private static final int REFRESH_INTERVAL_IN_MIN = 120; + // private static final int NUMBER_OF_DATA_DAY = 1095; + private static final int NUMBER_OF_DATA_DAY = 90; private final TimeZoneProvider timeZoneProvider; private final Logger logger = LoggerFactory.getLogger(ThingLinkyRemoteHandler.class); @@ -122,7 +124,7 @@ public ThingLinkyRemoteHandler(Thing thing, LocaleProvider localeProvider, TimeZ this.dailyConsumption = new ExpiringDayCache<>("dailyConsumption", REFRESH_HOUR_OF_DAY, REFRESH_MINUTE_OF_DAY, () -> { LocalDate today = LocalDate.now(); - MeterReading meterReading = getConsumptionData(today.minusDays(1095), today); + MeterReading meterReading = getConsumptionData(today.minusDays(NUMBER_OF_DATA_DAY), today); meterReading = getMeterReadingAfterChecks(meterReading); if (meterReading != null) { logData(meterReading.baseValue, "Day", DateTimeFormatter.ISO_LOCAL_DATE, Target.ALL); @@ -133,7 +135,7 @@ public ThingLinkyRemoteHandler(Thing thing, LocaleProvider localeProvider, TimeZ this.dailyIndex = new ExpiringDayCache<>("dailyIndex", REFRESH_HOUR_OF_DAY, REFRESH_MINUTE_OF_DAY, () -> { LocalDate today = LocalDate.now(); - MeterReading meterReading = getConsumptionIndex(today.minusDays(1095), today); + MeterReading meterReading = getConsumptionIndex(today.minusDays(NUMBER_OF_DATA_DAY), today); meterReading = getMeterReadingAfterChecks(meterReading); if (meterReading != null) { logData(meterReading.baseValue, "Day", DateTimeFormatter.ISO_LOCAL_DATE, Target.ALL); @@ -151,7 +153,7 @@ public ThingLinkyRemoteHandler(Thing thing, LocaleProvider localeProvider, TimeZ this.dailyConsumptionMaxPower = new ExpiringDayCache<>("dailyConsumptionMaxPower", REFRESH_HOUR_OF_DAY, REFRESH_MINUTE_OF_DAY, () -> { LocalDate today = LocalDate.now(); - MeterReading meterReading = getPowerData(today.minusDays(1095), today); + MeterReading meterReading = getPowerData(today.minusDays(NUMBER_OF_DATA_DAY), today); meterReading = getMeterReadingAfterChecks(meterReading); if (meterReading != null) { logData(meterReading.baseValue, "Day (peak)", DateTimeFormatter.ISO_LOCAL_DATE, Target.ALL); @@ -901,7 +903,7 @@ private boolean isDataLastDayAvailable(@Nullable MeterReading meterReading) { IntervalReading[] iv = meterReading.baseValue; logData(iv, "Last day", DateTimeFormatter.ISO_LOCAL_DATE, Target.LAST); - return iv != null && iv.length != 0 && iv[iv.length - 1] != null && !iv[iv.length - 1].value.isNaN(); + return iv != null && iv.length != 0 && iv[iv.length - 1] != null; } return false; From 177694bfbabc41df83ac9cfed28f57175384eab9 Mon Sep 17 00:00:00 2001 From: Laurent ARNAL Date: Sun, 14 Sep 2025 07:48:08 +0200 Subject: [PATCH 07/34] spotless:apply Signed-off-by: Laurent ARNAL --- .../binding/linky/internal/dto/Calendrier.java | 1 - .../linky/internal/dto/ClassesTemporelles.java | 1 - .../OH-INF/thing/group-linky-remote-daily.xml | 16 ++++++++-------- 3 files changed, 8 insertions(+), 10 deletions(-) diff --git a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/Calendrier.java b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/Calendrier.java index 24da25d5459c2..49316391f3235 100644 --- a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/Calendrier.java +++ b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/Calendrier.java @@ -22,5 +22,4 @@ public class Calendrier { public String idCalendrier; public String libelleCalendrier; - } diff --git a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/ClassesTemporelles.java b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/ClassesTemporelles.java index e3b2648e47cd3..c9e245af12865 100644 --- a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/ClassesTemporelles.java +++ b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/ClassesTemporelles.java @@ -22,5 +22,4 @@ public class ClassesTemporelles { public String libelle; public Double valeur = 0.0; - } diff --git a/bundles/org.openhab.binding.linky/src/main/resources/OH-INF/thing/group-linky-remote-daily.xml b/bundles/org.openhab.binding.linky/src/main/resources/OH-INF/thing/group-linky-remote-daily.xml index b5c82a853bc7c..8e04b91c3be61 100644 --- a/bundles/org.openhab.binding.linky/src/main/resources/OH-INF/thing/group-linky-remote-daily.xml +++ b/bundles/org.openhab.binding.linky/src/main/resources/OH-INF/thing/group-linky-remote-daily.xml @@ -26,14 +26,14 @@ The energy consumption - - - The energy consumption Idx0 - - - - The energy consumption Idx1 - + + + The energy consumption Idx0 + + + + The energy consumption Idx1 + From f6ea9456ecbe6fe4b33a7387f7ae4a1d651614f4 Mon Sep 17 00:00:00 2001 From: Laurent ARNAL Date: Sun, 14 Sep 2025 08:25:26 +0200 Subject: [PATCH 08/34] review agregat conversion to simplify code Signed-off-by: Laurent ARNAL --- .../linky/internal/dto/ConsumptionReport.java | 6 +- .../linky/internal/dto/IntervalReading.java | 6 +- .../linky/internal/dto/MeterReading.java | 69 +++++++------------ .../handler/ThingLinkyRemoteHandler.java | 2 +- 4 files changed, 34 insertions(+), 49 deletions(-) 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 12caafcfbb53d..f17b3bd5a43df 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 @@ -31,8 +31,10 @@ public class Data { public LocalDateTime dateDebut; public LocalDateTime dateFin; public Double valeur; - public ClassesTemporelles[] classesTemporellesFournisseur; - public ClassesTemporelles[] classesTemporellesDistributeur; + @SerializedName("classesTemporellesFournisseur") + public ClassesTemporelles[] classesTemporellesSupplier; + @SerializedName("classesTemporellesDistributeur") + public ClassesTemporelles[] classesTemporellesDistributor; public Calendrier[] calendrier; } diff --git a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/IntervalReading.java b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/IntervalReading.java index 00b1e245776e3..faaa2b983302b 100644 --- a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/IntervalReading.java +++ b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/IntervalReading.java @@ -23,7 +23,9 @@ public class IntervalReading { public Double value = 0.0; - public double[] valueFromFournisseur; - public double[] valueFromDistributeur; + public double[] valueSupplier; + public double[] valueDistributor; + public String[] supplierLabel; + public String[] distributorLabel; public LocalDateTime date; } diff --git a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/MeterReading.java b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/MeterReading.java index 3678b32fb7350..fd8160fcb42e5 100644 --- a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/MeterReading.java +++ b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/MeterReading.java @@ -72,65 +72,46 @@ public static IntervalReading[] fromAgregat(ConsumptionReport.Aggregate agregat, } } else { double lastVal = 0.0; - double[] lastValFournisseur = new double[6]; + double[] lastValueSupplier = new double[6]; + double[] lastValueDistributor = new double[6]; for (int i = 0; i < size; i++) { Data dataObj = agregat.datas.get(i); double value = dataObj.valeur; - double[] valueFournisseur = new double[6]; if (i > 0) { result[i - 1] = new IntervalReading(); result[i - 1].value = value - lastVal; result[i - 1].date = dataObj.dateDebut; - result[i - 1].valueFromFournisseur = new double[6]; - result[i - 1].valueFromDistributeur = new double[4]; - - if (dataObj.classesTemporellesFournisseur != null) { - for (ClassesTemporelles ct : dataObj.classesTemporellesFournisseur) { - int idxFournisseur = -1; - if ("Base".equals(ct.libelle)) { - idxFournisseur = 0; - } else if ("Blanc Heures Creuses".equals(ct.libelle)) { - idxFournisseur = 3; - } else if ("Blanc Heures Pleines".equals(ct.libelle)) { - idxFournisseur = 2; - } else if ("Bleu Heures Creuses".equals(ct.libelle)) { - idxFournisseur = 1; - } else if ("Bleu Heures Pleines".equals(ct.libelle)) { - idxFournisseur = 0; - } else if ("Rouge Heures Creuses".equals(ct.libelle)) { - idxFournisseur = 5; - } else if ("Rouge Heures Pleines".equals(ct.libelle)) { - idxFournisseur = 4; - } else if ("Heures Pleines".equals(ct.libelle)) { - idxFournisseur = 0; - } else if ("Heures Creuses".equals(ct.libelle)) { - idxFournisseur = 1; - } else if ("Heures Creuses Hiver / Saison Haute".equals(ct.libelle)) { - idxFournisseur = 4; - } else if ("Heures Creuses Saison Basse".equals(ct.libelle)) { - idxFournisseur = 4; - } else if ("Heures Pleines Hiver / Saison Haute".equals(ct.libelle)) { - idxFournisseur = 4; - } else if ("Heures Pleines Saison Basse".equals(ct.libelle)) { - idxFournisseur = 4; - } - - if (idxFournisseur == -1) { - continue; - } - - valueFournisseur[idxFournisseur] = ct.valeur; - result[i - 1].valueFromFournisseur[idxFournisseur] = (valueFournisseur[idxFournisseur] - - lastValFournisseur[idxFournisseur]); + result[i - 1].valueSupplier = new double[10]; + result[i - 1].valueDistributor = new double[4]; + result[i - 1].supplierLabel = new String[10]; + result[i - 1].distributorLabel = new String[4]; + if (dataObj.classesTemporellesSupplier != null) { + for (int idxSupplier = 0; idxSupplier < dataObj.classesTemporellesSupplier.length; idxSupplier++) { + ClassesTemporelles ct = dataObj.classesTemporellesSupplier[idxSupplier]; + + result[i - 1].valueSupplier[idxSupplier] = (ct.valeur - lastValueSupplier[idxSupplier]); + result[i - 1].supplierLabel[idxSupplier] = ct.libelle; + + lastValueSupplier[idxSupplier] = ct.valeur; } } + if (dataObj.classesTemporellesDistributor != null) { + for (int idxDistributor = 0; idxDistributor < dataObj.classesTemporellesDistributor.length; idxDistributor++) { + ClassesTemporelles ct = dataObj.classesTemporellesDistributor[idxDistributor]; + + result[i - 1].valueDistributor[idxDistributor] = (ct.valeur + - lastValueDistributor[idxDistributor]); + result[i - 1].distributorLabel[idxDistributor] = ct.libelle; + + lastValueDistributor[idxDistributor] = ct.valeur; + } + } } lastVal = value; - lastValFournisseur = valueFournisseur; } } diff --git a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/handler/ThingLinkyRemoteHandler.java b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/handler/ThingLinkyRemoteHandler.java index 7d92c89b5b16e..f7a2299cd23f7 100644 --- a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/handler/ThingLinkyRemoteHandler.java +++ b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/handler/ThingLinkyRemoteHandler.java @@ -616,7 +616,7 @@ private synchronized > void updateTimeSeries(String groupI } if (idx != -1) { if (iv[i + 1] != null) { - timeSeries.add(timestamp, new QuantityType<>(iv[i + 1].valueFromFournisseur[idx], unit)); + timeSeries.add(timestamp, new QuantityType<>(iv[i + 1].valueSupplier[idx], unit)); } } else { timeSeries.add(timestamp, new QuantityType<>(iv[i].value, unit)); From 12d96aafae657a7ba8414008ccfed43c36959263 Mon Sep 17 00:00:00 2001 From: Laurent ARNAL Date: Wed, 17 Sep 2025 08:50:30 +0200 Subject: [PATCH 09/34] add support for other distributor and supplier index Signed-off-by: Laurent ARNAL --- .../constants/LinkyBindingConstants.java | 4 +- .../handler/ThingLinkyRemoteHandler.java | 13 ++-- .../OH-INF/thing/group-linky-remote-daily.xml | 63 ++++++++++++++++--- 3 files changed, 67 insertions(+), 13 deletions(-) diff --git a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/constants/LinkyBindingConstants.java b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/constants/LinkyBindingConstants.java index e62535784cadb..044fdbbeafdce 100644 --- a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/constants/LinkyBindingConstants.java +++ b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/constants/LinkyBindingConstants.java @@ -59,8 +59,8 @@ public class LinkyBindingConstants { // List of all Channel id's public static final String CHANNEL_CONSUMPTION = "consumption"; - public static final String CHANNEL_CONSUMPTION_IDX0 = "consumptionIdx0"; - public static final String CHANNEL_CONSUMPTION_IDX1 = "consumptionIdx1"; + public static final String CHANNEL_CONSUMPTION_SUPPLIER_IDX = "consumptionSupplierIdx"; + public static final String CHANNEL_CONSUMPTION_DISTRIBUTOR_IDX = "consumptionDistributorIdx"; public static final String CHANNEL_MAX_POWER = "max-power"; public static final String CHANNEL_POWER = "power"; public static final String CHANNEL_TIMESTAMP_CHANNEL = "power"; diff --git a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/handler/ThingLinkyRemoteHandler.java b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/handler/ThingLinkyRemoteHandler.java index f7a2299cd23f7..fd07c84fcb639 100644 --- a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/handler/ThingLinkyRemoteHandler.java +++ b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/handler/ThingLinkyRemoteHandler.java @@ -515,10 +515,15 @@ private synchronized void updateEnergyIndex() { dailyIndex.getValue().ifPresentOrElse(values -> { int dSize = values.baseValue.length; - updateTimeSeries(LINKY_REMOTE_DAILY_GROUP, CHANNEL_CONSUMPTION_IDX0, values.baseValue, 0, - Units.KILOWATT_HOUR); - updateTimeSeries(LINKY_REMOTE_DAILY_GROUP, CHANNEL_CONSUMPTION_IDX1, values.baseValue, 1, - Units.KILOWATT_HOUR); + for (int idx = 0; idx < 10; idx++) { + updateTimeSeries(LINKY_REMOTE_DAILY_GROUP, CHANNEL_CONSUMPTION_SUPPLIER_IDX + idx, values.baseValue, + idx, Units.KILOWATT_HOUR); + } + + for (int idx = 0; idx < 4; idx++) { + updateTimeSeries(LINKY_REMOTE_DAILY_GROUP, CHANNEL_CONSUMPTION_DISTRIBUTOR_IDX + idx, values.baseValue, + idx, Units.KILOWATT_HOUR); + } /* * updateKwhChannel(LINKY_REMOTE_DAILY_GROUP, CHANNEL_DAY_MINUS_1, values.baseValue[dSize - 1].value); diff --git a/bundles/org.openhab.binding.linky/src/main/resources/OH-INF/thing/group-linky-remote-daily.xml b/bundles/org.openhab.binding.linky/src/main/resources/OH-INF/thing/group-linky-remote-daily.xml index 8e04b91c3be61..9f849b01871ea 100644 --- a/bundles/org.openhab.binding.linky/src/main/resources/OH-INF/thing/group-linky-remote-daily.xml +++ b/bundles/org.openhab.binding.linky/src/main/resources/OH-INF/thing/group-linky-remote-daily.xml @@ -26,14 +26,63 @@ The energy consumption - - - The energy consumption Idx0 - - - - The energy consumption Idx1 + + + The supplier energy consumption Idx0 + + + The supplier energy consumption Idx1 + + + + The supplier energy consumption Idx2 + + + + The supplier energy consumption Idx3 + + + + The supplier energy consumption Idx4 + + + + The supplier energy consumption Idx5 + + + + The supplier energy consumption Idx6 + + + + The supplier energy consumption Idx7 + + + + The supplier energy consumption Idx8 + + + + The supplier energy consumption Idx9 + + + + + The distributor energy consumption Idx0 + + + + The distributor energy consumption Idx1 + + + + The distributor energy consumption Idx2 + + + + The distributor energy consumption Idx3 + From 916f06f25762ba60e89f8ce44f3387deb61bfccd Mon Sep 17 00:00:00 2001 From: Laurent ARNAL Date: Thu, 18 Sep 2025 09:12:23 +0200 Subject: [PATCH 10/34] add channel for Index Signed-off-by: Laurent ARNAL --- .../handler/ThingLinkyRemoteHandler.java | 72 ++++++++++++++++++- .../OH-INF/thing/group-linky-remote-daily.xml | 1 + .../thing/group-linky-remote-monthly.xml | 58 +++++++++++++++ .../thing/group-linky-remote-weekly.xml | 59 +++++++++++++++ .../thing/group-linky-remote-yearly.xml | 59 +++++++++++++++ 5 files changed, 246 insertions(+), 3 deletions(-) diff --git a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/handler/ThingLinkyRemoteHandler.java b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/handler/ThingLinkyRemoteHandler.java index fd07c84fcb639..1481e0fe356c6 100644 --- a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/handler/ThingLinkyRemoteHandler.java +++ b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/handler/ThingLinkyRemoteHandler.java @@ -525,6 +525,36 @@ private synchronized void updateEnergyIndex() { idx, Units.KILOWATT_HOUR); } + for (int idx = 0; idx < 10; idx++) { + updateTimeSeries(LINKY_REMOTE_WEEKLY_GROUP, CHANNEL_CONSUMPTION_SUPPLIER_IDX + idx, values.weekValue, + idx, Units.KILOWATT_HOUR); + } + + for (int idx = 0; idx < 4; idx++) { + updateTimeSeries(LINKY_REMOTE_WEEKLY_GROUP, CHANNEL_CONSUMPTION_DISTRIBUTOR_IDX + idx, values.weekValue, + idx, Units.KILOWATT_HOUR); + } + + for (int idx = 0; idx < 10; idx++) { + updateTimeSeries(LINKY_REMOTE_MONTHLY_GROUP, CHANNEL_CONSUMPTION_SUPPLIER_IDX + idx, values.monthValue, + idx, Units.KILOWATT_HOUR); + } + + for (int idx = 0; idx < 4; idx++) { + updateTimeSeries(LINKY_REMOTE_MONTHLY_GROUP, CHANNEL_CONSUMPTION_DISTRIBUTOR_IDX + idx, + values.monthValue, idx, Units.KILOWATT_HOUR); + } + + for (int idx = 0; idx < 10; idx++) { + updateTimeSeries(LINKY_REMOTE_YEARLY_GROUP, CHANNEL_CONSUMPTION_SUPPLIER_IDX + idx, values.yearValue, + idx, Units.KILOWATT_HOUR); + } + + for (int idx = 0; idx < 4; idx++) { + updateTimeSeries(LINKY_REMOTE_YEARLY_GROUP, CHANNEL_CONSUMPTION_DISTRIBUTOR_IDX + idx, values.yearValue, + idx, Units.KILOWATT_HOUR); + } + /* * updateKwhChannel(LINKY_REMOTE_DAILY_GROUP, CHANNEL_DAY_MINUS_1, values.baseValue[dSize - 1].value); * updateKwhChannel(LINKY_REMOTE_DAILY_GROUP, CHANNEL_DAY_MINUS_2, values.baseValue[dSize - 2].value); @@ -620,7 +650,7 @@ private synchronized > void updateTimeSeries(String groupI continue; } if (idx != -1) { - if (iv[i + 1] != null) { + if (i + 1 < iv.length && iv[i + 1] != null) { timeSeries.add(timestamp, new QuantityType<>(iv[i + 1].valueSupplier[idx], unit)); } } else { @@ -817,14 +847,19 @@ public void handleCommand(ChannelUID channelUID, Command command) { if (!isDataLastDayAvailable(meterReading)) { logger.debug("Data including yesterday are not yet available"); - return meterReading; + // return meterReading; // return null; } if (meterReading != null) { if (meterReading.weekValue == null) { LocalDate startDate = meterReading.baseValue[0].date.toLocalDate(); - LocalDate endDate = meterReading.baseValue[meterReading.baseValue.length - 1].date.toLocalDate(); + LocalDate endDate; + if (meterReading.baseValue[meterReading.baseValue.length - 1] != null) { + endDate = meterReading.baseValue[meterReading.baseValue.length - 1].date.toLocalDate(); + } else { + endDate = meterReading.baseValue[meterReading.baseValue.length - 2].date.toLocalDate(); + } int startWeek = startDate.get(WeekFields.of(Locale.FRANCE).weekOfYear()); int endWeek = endDate.get(WeekFields.of(Locale.FRANCE).weekOfYear()); @@ -856,6 +891,9 @@ public void handleCommand(ChannelUID channelUID, Command command) { for (int idx = 0; idx < size; idx++) { IntervalReading ir = meterReading.baseValue[idx]; + if (ir == null) { + continue; + } LocalDateTime dt = ir.date; double value = ir.value; value = value / divider; @@ -869,6 +907,7 @@ public void handleCommand(ChannelUID channelUID, Command command) { if (idxWeek < weeksNum) { meterReading.weekValue[idxWeek].value += value; + if (meterReading.weekValue[idxWeek].date == null) { meterReading.weekValue[idxWeek].date = dt; } @@ -886,6 +925,33 @@ public void handleCommand(ChannelUID channelUID, Command command) { meterReading.yearValue[idxYear].date = LocalDateTime.of(dt.getYear(), 1, 1, 0, 0); } } + + if (ir.valueSupplier != null) { + if (meterReading.weekValue[idxWeek].valueSupplier == null) { + meterReading.weekValue[idxWeek].valueSupplier = new double[10]; + } + if (meterReading.monthValue[idxMonth].valueSupplier == null) { + meterReading.monthValue[idxMonth].valueSupplier = new double[10]; + } + if (meterReading.yearValue[idxYear].valueSupplier == null) { + meterReading.yearValue[idxYear].valueSupplier = new double[10]; + } + for (int idxSupplier = 0; idxSupplier < 10; idxSupplier++) { + double valueSupplier = ir.valueSupplier[idxSupplier]; + + if (idxWeek < weeksNum) { + meterReading.weekValue[idxWeek].valueSupplier[idxSupplier] += valueSupplier; + } + if (idxMonth < monthsNum) { + meterReading.monthValue[idxMonth].valueSupplier[idxSupplier] += valueSupplier; + } + + if (idxYear < yearsNum) { + meterReading.yearValue[idxYear].valueSupplier[idxSupplier] += valueSupplier; + } + + } + } } } } diff --git a/bundles/org.openhab.binding.linky/src/main/resources/OH-INF/thing/group-linky-remote-daily.xml b/bundles/org.openhab.binding.linky/src/main/resources/OH-INF/thing/group-linky-remote-daily.xml index 9f849b01871ea..e9b3ef3320a93 100644 --- a/bundles/org.openhab.binding.linky/src/main/resources/OH-INF/thing/group-linky-remote-daily.xml +++ b/bundles/org.openhab.binding.linky/src/main/resources/OH-INF/thing/group-linky-remote-daily.xml @@ -26,6 +26,7 @@ The energy consumption + The supplier energy consumption Idx0 diff --git a/bundles/org.openhab.binding.linky/src/main/resources/OH-INF/thing/group-linky-remote-monthly.xml b/bundles/org.openhab.binding.linky/src/main/resources/OH-INF/thing/group-linky-remote-monthly.xml index 3b6695f8d70e3..ad809b9df2427 100644 --- a/bundles/org.openhab.binding.linky/src/main/resources/OH-INF/thing/group-linky-remote-monthly.xml +++ b/bundles/org.openhab.binding.linky/src/main/resources/OH-INF/thing/group-linky-remote-monthly.xml @@ -27,6 +27,64 @@ The energy consumption + + + The supplier energy consumption Idx0 + + + + The supplier energy consumption Idx1 + + + + The supplier energy consumption Idx2 + + + + The supplier energy consumption Idx3 + + + + The supplier energy consumption Idx4 + + + + The supplier energy consumption Idx5 + + + + The supplier energy consumption Idx6 + + + + The supplier energy consumption Idx7 + + + + The supplier energy consumption Idx8 + + + + The supplier energy consumption Idx9 + + + + + The distributor energy consumption Idx0 + + + + The distributor energy consumption Idx1 + + + + The distributor energy consumption Idx2 + + + + The distributor energy consumption Idx3 + + Maximum power usage value diff --git a/bundles/org.openhab.binding.linky/src/main/resources/OH-INF/thing/group-linky-remote-weekly.xml b/bundles/org.openhab.binding.linky/src/main/resources/OH-INF/thing/group-linky-remote-weekly.xml index 3b2e59ca14187..6e95ccb95f469 100644 --- a/bundles/org.openhab.binding.linky/src/main/resources/OH-INF/thing/group-linky-remote-weekly.xml +++ b/bundles/org.openhab.binding.linky/src/main/resources/OH-INF/thing/group-linky-remote-weekly.xml @@ -23,6 +23,65 @@ The energy consumption + + + + The supplier energy consumption Idx0 + + + + The supplier energy consumption Idx1 + + + + The supplier energy consumption Idx2 + + + + The supplier energy consumption Idx3 + + + + The supplier energy consumption Idx4 + + + + The supplier energy consumption Idx5 + + + + The supplier energy consumption Idx6 + + + + The supplier energy consumption Idx7 + + + + The supplier energy consumption Idx8 + + + + The supplier energy consumption Idx9 + + + + + The distributor energy consumption Idx0 + + + + The distributor energy consumption Idx1 + + + + The distributor energy consumption Idx2 + + + + The distributor energy consumption Idx3 + + Maximum power usage value diff --git a/bundles/org.openhab.binding.linky/src/main/resources/OH-INF/thing/group-linky-remote-yearly.xml b/bundles/org.openhab.binding.linky/src/main/resources/OH-INF/thing/group-linky-remote-yearly.xml index 9ad761e6eaddf..7d4e3cb3a8eb0 100644 --- a/bundles/org.openhab.binding.linky/src/main/resources/OH-INF/thing/group-linky-remote-yearly.xml +++ b/bundles/org.openhab.binding.linky/src/main/resources/OH-INF/thing/group-linky-remote-yearly.xml @@ -23,6 +23,65 @@ The energy consumption + + + + The supplier energy consumption Idx0 + + + + The supplier energy consumption Idx1 + + + + The supplier energy consumption Idx2 + + + + The supplier energy consumption Idx3 + + + + The supplier energy consumption Idx4 + + + + The supplier energy consumption Idx5 + + + + The supplier energy consumption Idx6 + + + + The supplier energy consumption Idx7 + + + + The supplier energy consumption Idx8 + + + + The supplier energy consumption Idx9 + + + + + The distributor energy consumption Idx0 + + + + The distributor energy consumption Idx1 + + + + The distributor energy consumption Idx2 + + + + The distributor energy consumption Idx3 + + Maximum power usage value From 302ff6598d64d0f6c562507bc9b763cbaf7518d9 Mon Sep 17 00:00:00 2001 From: Laurent ARNAL Date: Thu, 18 Sep 2025 10:27:08 +0200 Subject: [PATCH 11/34] fix bad indexing for supplier & distributor index Signed-off-by: Laurent ARNAL --- .../linky/internal/handler/ThingLinkyRemoteHandler.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/handler/ThingLinkyRemoteHandler.java b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/handler/ThingLinkyRemoteHandler.java index 1481e0fe356c6..b5b3e912389eb 100644 --- a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/handler/ThingLinkyRemoteHandler.java +++ b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/handler/ThingLinkyRemoteHandler.java @@ -650,8 +650,8 @@ private synchronized > void updateTimeSeries(String groupI continue; } if (idx != -1) { - if (i + 1 < iv.length && iv[i + 1] != null) { - timeSeries.add(timestamp, new QuantityType<>(iv[i + 1].valueSupplier[idx], unit)); + if (i < iv.length && iv[i] != null) { + timeSeries.add(timestamp, new QuantityType<>(iv[i].valueSupplier[idx], unit)); } } else { timeSeries.add(timestamp, new QuantityType<>(iv[i].value, unit)); From 8261bee143610be1c7b38a75805da1f5b37e7205 Mon Sep 17 00:00:00 2001 From: Laurent ARNAL Date: Thu, 18 Sep 2025 12:49:24 +0200 Subject: [PATCH 12/34] fix error on index date provoking bad data alignment Signed-off-by: Laurent ARNAL --- .../org/openhab/binding/linky/internal/dto/MeterReading.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/MeterReading.java b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/MeterReading.java index fd8160fcb42e5..a07d925226720 100644 --- a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/MeterReading.java +++ b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/MeterReading.java @@ -82,7 +82,8 @@ public static IntervalReading[] fromAgregat(ConsumptionReport.Aggregate agregat, if (i > 0) { result[i - 1] = new IntervalReading(); result[i - 1].value = value - lastVal; - result[i - 1].date = dataObj.dateDebut; + // The index in on nextDay N, but index difference give consumption for day N-1 + result[i - 1].date = dataObj.dateDebut.minusDays(1); result[i - 1].valueSupplier = new double[10]; result[i - 1].valueDistributor = new double[4]; result[i - 1].supplierLabel = new String[10]; From 925585e2465a0af9418083b9b07c064e4ac36159 Mon Sep 17 00:00:00 2001 From: Laurent ARNAL Date: Thu, 18 Sep 2025 12:59:03 +0200 Subject: [PATCH 13/34] review data array bound when using index remove unnecessary code after last fixes Signed-off-by: Laurent ARNAL --- .../binding/linky/internal/dto/MeterReading.java | 10 +++++++++- .../internal/handler/ThingLinkyRemoteHandler.java | 13 ++----------- 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/MeterReading.java b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/MeterReading.java index a07d925226720..ff16019e580dd 100644 --- a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/MeterReading.java +++ b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/MeterReading.java @@ -61,7 +61,15 @@ public static MeterReading convertFromComsumptionReport(ConsumptionReport comsum public static IntervalReading[] fromAgregat(ConsumptionReport.Aggregate agregat, boolean useIndex) { int size = agregat.datas.size(); - IntervalReading[] result = new IntervalReading[size]; + IntervalReading[] result = null; + + // For some unknown reason, index API don't return the index for day N-1. + // So array length is cut off 1 + if (useIndex) { + result = new IntervalReading[size - 1]; + } else { + result = new IntervalReading[size]; + } if (!useIndex) { for (int i = 0; i < size; i++) { diff --git a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/handler/ThingLinkyRemoteHandler.java b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/handler/ThingLinkyRemoteHandler.java index b5b3e912389eb..48e163fe3b6b2 100644 --- a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/handler/ThingLinkyRemoteHandler.java +++ b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/handler/ThingLinkyRemoteHandler.java @@ -847,19 +847,13 @@ public void handleCommand(ChannelUID channelUID, Command command) { if (!isDataLastDayAvailable(meterReading)) { logger.debug("Data including yesterday are not yet available"); - // return meterReading; - // return null; + return meterReading; } if (meterReading != null) { if (meterReading.weekValue == null) { LocalDate startDate = meterReading.baseValue[0].date.toLocalDate(); - LocalDate endDate; - if (meterReading.baseValue[meterReading.baseValue.length - 1] != null) { - endDate = meterReading.baseValue[meterReading.baseValue.length - 1].date.toLocalDate(); - } else { - endDate = meterReading.baseValue[meterReading.baseValue.length - 2].date.toLocalDate(); - } + LocalDate endDate = meterReading.baseValue[meterReading.baseValue.length - 1].date.toLocalDate(); int startWeek = startDate.get(WeekFields.of(Locale.FRANCE).weekOfYear()); int endWeek = endDate.get(WeekFields.of(Locale.FRANCE).weekOfYear()); @@ -891,9 +885,6 @@ public void handleCommand(ChannelUID channelUID, Command command) { for (int idx = 0; idx < size; idx++) { IntervalReading ir = meterReading.baseValue[idx]; - if (ir == null) { - continue; - } LocalDateTime dt = ir.date; double value = ir.value; value = value / divider; From 04a072578e21815be4404a100653f0852b178d66 Mon Sep 17 00:00:00 2001 From: Laurent ARNAL Date: Fri, 19 Sep 2025 16:24:16 +0200 Subject: [PATCH 14/34] add code to handle missing value in data retrieve Signed-off-by: Laurent ARNAL --- .../openhab/binding/linky/internal/dto/MeterReading.java | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/MeterReading.java b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/MeterReading.java index ff16019e580dd..3d86e2c182538 100644 --- a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/MeterReading.java +++ b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/MeterReading.java @@ -97,6 +97,14 @@ public static IntervalReading[] fromAgregat(ConsumptionReport.Aggregate agregat, result[i - 1].supplierLabel = new String[10]; result[i - 1].distributorLabel = new String[4]; + if (dataObj.classesTemporellesSupplier == null) { + dataObj.classesTemporellesSupplier = agregat.datas.get(i - 1).classesTemporellesSupplier; + } + + if (dataObj.classesTemporellesDistributor == null) { + dataObj.classesTemporellesDistributor = agregat.datas.get(i - 1).classesTemporellesDistributor; + } + if (dataObj.classesTemporellesSupplier != null) { for (int idxSupplier = 0; idxSupplier < dataObj.classesTemporellesSupplier.length; idxSupplier++) { ClassesTemporelles ct = dataObj.classesTemporellesSupplier[idxSupplier]; From 75cde7fa6ee50d6fbe09453845f7825388de1d0b Mon Sep 17 00:00:00 2001 From: Laurent ARNAL Date: Fri, 19 Sep 2025 16:24:52 +0200 Subject: [PATCH 15/34] remove Idx channel name from resource, will be handle dynamically by the binding Signed-off-by: Laurent ARNAL --- .../OH-INF/thing/group-linky-remote-daily.xml | 57 ------------------ .../thing/group-linky-remote-monthly.xml | 57 ------------------ .../thing/group-linky-remote-weekly.xml | 58 ------------------- .../thing/group-linky-remote-yearly.xml | 58 ------------------- 4 files changed, 230 deletions(-) diff --git a/bundles/org.openhab.binding.linky/src/main/resources/OH-INF/thing/group-linky-remote-daily.xml b/bundles/org.openhab.binding.linky/src/main/resources/OH-INF/thing/group-linky-remote-daily.xml index e9b3ef3320a93..1f66f743741c2 100644 --- a/bundles/org.openhab.binding.linky/src/main/resources/OH-INF/thing/group-linky-remote-daily.xml +++ b/bundles/org.openhab.binding.linky/src/main/resources/OH-INF/thing/group-linky-remote-daily.xml @@ -27,63 +27,6 @@ The energy consumption - - - The supplier energy consumption Idx0 - - - - The supplier energy consumption Idx1 - - - - The supplier energy consumption Idx2 - - - - The supplier energy consumption Idx3 - - - - The supplier energy consumption Idx4 - - - - The supplier energy consumption Idx5 - - - - The supplier energy consumption Idx6 - - - - The supplier energy consumption Idx7 - - - - The supplier energy consumption Idx8 - - - - The supplier energy consumption Idx9 - - - - - The distributor energy consumption Idx0 - - - - The distributor energy consumption Idx1 - - - - The distributor energy consumption Idx2 - - - - The distributor energy consumption Idx3 - diff --git a/bundles/org.openhab.binding.linky/src/main/resources/OH-INF/thing/group-linky-remote-monthly.xml b/bundles/org.openhab.binding.linky/src/main/resources/OH-INF/thing/group-linky-remote-monthly.xml index ad809b9df2427..761fb22d209a1 100644 --- a/bundles/org.openhab.binding.linky/src/main/resources/OH-INF/thing/group-linky-remote-monthly.xml +++ b/bundles/org.openhab.binding.linky/src/main/resources/OH-INF/thing/group-linky-remote-monthly.xml @@ -27,63 +27,6 @@ The energy consumption - - - The supplier energy consumption Idx0 - - - - The supplier energy consumption Idx1 - - - - The supplier energy consumption Idx2 - - - - The supplier energy consumption Idx3 - - - - The supplier energy consumption Idx4 - - - - The supplier energy consumption Idx5 - - - - The supplier energy consumption Idx6 - - - - The supplier energy consumption Idx7 - - - - The supplier energy consumption Idx8 - - - - The supplier energy consumption Idx9 - - - - - The distributor energy consumption Idx0 - - - - The distributor energy consumption Idx1 - - - - The distributor energy consumption Idx2 - - - - The distributor energy consumption Idx3 - diff --git a/bundles/org.openhab.binding.linky/src/main/resources/OH-INF/thing/group-linky-remote-weekly.xml b/bundles/org.openhab.binding.linky/src/main/resources/OH-INF/thing/group-linky-remote-weekly.xml index 6e95ccb95f469..6e8c7af72f4e6 100644 --- a/bundles/org.openhab.binding.linky/src/main/resources/OH-INF/thing/group-linky-remote-weekly.xml +++ b/bundles/org.openhab.binding.linky/src/main/resources/OH-INF/thing/group-linky-remote-weekly.xml @@ -23,64 +23,6 @@ The energy consumption - - - - The supplier energy consumption Idx0 - - - - The supplier energy consumption Idx1 - - - - The supplier energy consumption Idx2 - - - - The supplier energy consumption Idx3 - - - - The supplier energy consumption Idx4 - - - - The supplier energy consumption Idx5 - - - - The supplier energy consumption Idx6 - - - - The supplier energy consumption Idx7 - - - - The supplier energy consumption Idx8 - - - - The supplier energy consumption Idx9 - - - - - The distributor energy consumption Idx0 - - - - The distributor energy consumption Idx1 - - - - The distributor energy consumption Idx2 - - - - The distributor energy consumption Idx3 - diff --git a/bundles/org.openhab.binding.linky/src/main/resources/OH-INF/thing/group-linky-remote-yearly.xml b/bundles/org.openhab.binding.linky/src/main/resources/OH-INF/thing/group-linky-remote-yearly.xml index 7d4e3cb3a8eb0..2d02b6a6d2ad0 100644 --- a/bundles/org.openhab.binding.linky/src/main/resources/OH-INF/thing/group-linky-remote-yearly.xml +++ b/bundles/org.openhab.binding.linky/src/main/resources/OH-INF/thing/group-linky-remote-yearly.xml @@ -24,64 +24,6 @@ The energy consumption - - - The supplier energy consumption Idx0 - - - - The supplier energy consumption Idx1 - - - - The supplier energy consumption Idx2 - - - - The supplier energy consumption Idx3 - - - - The supplier energy consumption Idx4 - - - - The supplier energy consumption Idx5 - - - - The supplier energy consumption Idx6 - - - - The supplier energy consumption Idx7 - - - - The supplier energy consumption Idx8 - - - - The supplier energy consumption Idx9 - - - - - The distributor energy consumption Idx0 - - - - The distributor energy consumption Idx1 - - - - The distributor energy consumption Idx2 - - - - The distributor energy consumption Idx3 - - Maximum power usage value From 14a74606e6aaaae0814f1c7fef62bf962981598d Mon Sep 17 00:00:00 2001 From: Laurent ARNAL Date: Fri, 19 Sep 2025 16:26:51 +0200 Subject: [PATCH 16/34] introduce support for different tarif inside a dataset result: - Will create channel dynamically to represent each available tarif. - Will split the dataset result to the bound of tarif change. - Will update each time series accordly. - Graph will be able to display silmutaneous tarif on the same graph. Signed-off-by: Laurent ARNAL --- .../handler/ThingLinkyRemoteHandler.java | 211 ++++++++++++------ 1 file changed, 143 insertions(+), 68 deletions(-) diff --git a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/handler/ThingLinkyRemoteHandler.java b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/handler/ThingLinkyRemoteHandler.java index 48e163fe3b6b2..de3554956bc22 100644 --- a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/handler/ThingLinkyRemoteHandler.java +++ b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/handler/ThingLinkyRemoteHandler.java @@ -14,6 +14,7 @@ import static org.openhab.binding.linky.internal.constants.LinkyBindingConstants.*; +import java.text.Normalizer; import java.time.Instant; import java.time.LocalDate; import java.time.LocalDateTime; @@ -22,6 +23,7 @@ import java.time.temporal.ChronoUnit; import java.time.temporal.WeekFields; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import java.util.Locale; import java.util.Map; @@ -37,6 +39,7 @@ import org.openhab.binding.linky.internal.api.EnedisHttpApi; import org.openhab.binding.linky.internal.api.ExpiringDayCache; import org.openhab.binding.linky.internal.config.LinkyThingRemoteConfiguration; +import org.openhab.binding.linky.internal.constants.LinkyBindingConstants; import org.openhab.binding.linky.internal.dto.Contact; import org.openhab.binding.linky.internal.dto.Contract; import org.openhab.binding.linky.internal.dto.Identity; @@ -56,10 +59,13 @@ import org.openhab.core.library.unit.MetricPrefix; import org.openhab.core.library.unit.Units; import org.openhab.core.thing.Bridge; +import org.openhab.core.thing.Channel; import org.openhab.core.thing.ChannelUID; import org.openhab.core.thing.Thing; import org.openhab.core.thing.ThingStatus; import org.openhab.core.thing.ThingStatusDetail; +import org.openhab.core.thing.binding.builder.ChannelBuilder; +import org.openhab.core.thing.type.ChannelTypeUID; import org.openhab.core.types.Command; import org.openhab.core.types.RefreshType; import org.openhab.core.types.State; @@ -508,31 +514,159 @@ private synchronized void updateEnergyData() { }); } + private void addChannel(List channels, ChannelTypeUID chanTypeUid, String channelGroup, String channelName, + String channelLabel, String channelDesc) { + ChannelUID channelUid = new ChannelUID(this.getThing().getUID(), channelGroup, channelName); + Channel channel = ChannelBuilder.create(channelUid).withType(chanTypeUid).withDescription(channelDesc) + .withLabel(channelLabel).build(); + + if (!channels.contains(channel)) { + channels.add(channel); + } + + } + + /** + * The methods remove specific local character (like 'é'/'ê','â') so we have a correctly formated UID from a + * localize item label + * + * @param label + * @return the label without invalid character + */ + public static String sanetizeId(String label) { + String result = label; + + if (!Normalizer.isNormalized(label, Normalizer.Form.NFKD)) { + result = Normalizer.normalize(label, Normalizer.Form.NFKD); + result = result.replaceAll("\\p{M}", ""); + } + + result = result.replaceAll("[^a-zA-Z0-9_]", ""); + result = result.substring(0, 1).toLowerCase() + result.substring(1); + + return result; + } + + private void handleDynamicChannel(MeterReading values) { + ChannelTypeUID chanTypeUid = new ChannelTypeUID(LinkyBindingConstants.BINDING_ID, "consumption"); + List channels = new ArrayList(); + + for (IntervalReading ir : values.baseValue) { + for (String st : ir.distributorLabel) { + + } + for (String st : ir.supplierLabel) { + if (st == null) { + continue; + } + + String channelName = sanetizeId(st); + String channelLabel = st; + String channelDesc = "The " + st; + + addChannel(channels, chanTypeUid, LINKY_REMOTE_DAILY_GROUP, channelName, channelLabel, channelDesc); + + } + } + + for (int idx = 0; idx < 10; idx++) { + String channelName = CHANNEL_CONSUMPTION_SUPPLIER_IDX + idx; + String channelLabel = "Supplier Consumption " + idx; + String channelDesc = "The Supplier Consumption for index " + idx; + + addChannel(channels, chanTypeUid, LINKY_REMOTE_DAILY_GROUP, channelName, channelLabel, channelDesc); + addChannel(channels, chanTypeUid, LINKY_REMOTE_WEEKLY_GROUP, channelName, channelLabel, channelDesc); + addChannel(channels, chanTypeUid, LINKY_REMOTE_MONTHLY_GROUP, channelName, channelLabel, channelDesc); + addChannel(channels, chanTypeUid, LINKY_REMOTE_YEARLY_GROUP, channelName, channelLabel, channelDesc); + } + + for (int idx = 0; idx < 4; idx++) { + String channelName = CHANNEL_CONSUMPTION_DISTRIBUTOR_IDX + idx; + String channelLabel = "Distributor Consumption " + idx; + String channelDesc = "The Distributor Consumption for index " + idx; + + addChannel(channels, chanTypeUid, LINKY_REMOTE_DAILY_GROUP, channelName, channelLabel, channelDesc); + addChannel(channels, chanTypeUid, LINKY_REMOTE_WEEKLY_GROUP, channelName, channelLabel, channelDesc); + addChannel(channels, chanTypeUid, LINKY_REMOTE_MONTHLY_GROUP, channelName, channelLabel, channelDesc); + addChannel(channels, chanTypeUid, LINKY_REMOTE_YEARLY_GROUP, channelName, channelLabel, channelDesc); + } + + if (channels.size() > 0) { + Thing thing = this.getThing(); + + for (Channel chan : thing.getChannels()) { + channels.add(chan); + } + + // channels.add(chanTest); + updateThing(editThing().withChannels(channels).build()); + } + + } + + private @Nullable List cut(@Nullable IntervalReading[] irs) { + List result = new ArrayList(); + String currentTarif = ""; + int lastIdx = 0; + for (int idx = 0; idx < irs.length; idx++) { + String tarif = String.join("#", irs[idx].supplierLabel); + + if ((!tarif.equals(currentTarif) && !currentTarif.equals("")) || (idx == irs.length - 1)) { + logger.debug("tarif change:" + lastIdx + "/" + (idx - 1)); + + IntervalReading[] subArray; + if (idx == irs.length - 1) { + subArray = Arrays.copyOfRange(irs, lastIdx, idx + 1); + } else { + subArray = Arrays.copyOfRange(irs, lastIdx, idx); + } + result.add(subArray); + lastIdx = idx; + } + + currentTarif = tarif; + logger.debug(""); + } + return result; + } + /** * Request new daily/weekly data and updates channels */ private synchronized void updateEnergyIndex() { dailyIndex.getValue().ifPresentOrElse(values -> { int dSize = values.baseValue.length; + String channelName = ""; + + handleDynamicChannel(values); for (int idx = 0; idx < 10; idx++) { - updateTimeSeries(LINKY_REMOTE_DAILY_GROUP, CHANNEL_CONSUMPTION_SUPPLIER_IDX + idx, values.baseValue, - idx, Units.KILOWATT_HOUR); + channelName = CHANNEL_CONSUMPTION_SUPPLIER_IDX + idx; + updateTimeSeries(LINKY_REMOTE_DAILY_GROUP, channelName, values.baseValue, idx, Units.KILOWATT_HOUR); + + List irs = cut(values.baseValue); + for (IntervalReading[] ir : irs) { + if (ir[0].supplierLabel[idx] == null) { + continue; + } + channelName = sanetizeId(ir[0].supplierLabel[idx]); + updateTimeSeries(LINKY_REMOTE_DAILY_GROUP, channelName, ir, idx, Units.KILOWATT_HOUR); + } } for (int idx = 0; idx < 4; idx++) { - updateTimeSeries(LINKY_REMOTE_DAILY_GROUP, CHANNEL_CONSUMPTION_DISTRIBUTOR_IDX + idx, values.baseValue, - idx, Units.KILOWATT_HOUR); + channelName = CHANNEL_CONSUMPTION_SUPPLIER_IDX + idx; + updateTimeSeries(LINKY_REMOTE_DAILY_GROUP, channelName, values.baseValue, idx, Units.KILOWATT_HOUR); } for (int idx = 0; idx < 10; idx++) { - updateTimeSeries(LINKY_REMOTE_WEEKLY_GROUP, CHANNEL_CONSUMPTION_SUPPLIER_IDX + idx, values.weekValue, - idx, Units.KILOWATT_HOUR); + channelName = CHANNEL_CONSUMPTION_SUPPLIER_IDX + idx; + updateTimeSeries(LINKY_REMOTE_WEEKLY_GROUP, channelName, values.weekValue, idx, Units.KILOWATT_HOUR); } for (int idx = 0; idx < 4; idx++) { - updateTimeSeries(LINKY_REMOTE_WEEKLY_GROUP, CHANNEL_CONSUMPTION_DISTRIBUTOR_IDX + idx, values.weekValue, - idx, Units.KILOWATT_HOUR); + channelName = CHANNEL_CONSUMPTION_SUPPLIER_IDX + idx; + updateTimeSeries(LINKY_REMOTE_WEEKLY_GROUP, channelName, values.weekValue, idx, Units.KILOWATT_HOUR); } for (int idx = 0; idx < 10; idx++) { @@ -554,67 +688,7 @@ private synchronized void updateEnergyIndex() { updateTimeSeries(LINKY_REMOTE_YEARLY_GROUP, CHANNEL_CONSUMPTION_DISTRIBUTOR_IDX + idx, values.yearValue, idx, Units.KILOWATT_HOUR); } - - /* - * updateKwhChannel(LINKY_REMOTE_DAILY_GROUP, CHANNEL_DAY_MINUS_1, values.baseValue[dSize - 1].value); - * updateKwhChannel(LINKY_REMOTE_DAILY_GROUP, CHANNEL_DAY_MINUS_2, values.baseValue[dSize - 2].value); - * updateKwhChannel(LINKY_REMOTE_DAILY_GROUP, CHANNEL_DAY_MINUS_3, values.baseValue[dSize - 3].value); - * - * int idxCurrentYear = values.yearValue.length - 1; - * int idxCurrentWeek = values.weekValue.length - 1; - * int idxCurrentMonth = values.monthValue.length - 1; - * - * updateKwhChannel(LINKY_REMOTE_WEEKLY_GROUP, CHANNEL_WEEK_MINUS_0, - * values.weekValue[idxCurrentWeek].value); - * updateKwhChannel(LINKY_REMOTE_WEEKLY_GROUP, CHANNEL_WEEK_MINUS_1, - * values.weekValue[idxCurrentWeek - 1].value); - * if (idxCurrentWeek - 2 >= 0) { - * updateKwhChannel(LINKY_REMOTE_WEEKLY_GROUP, CHANNEL_WEEK_MINUS_2, - * values.weekValue[idxCurrentWeek - 2].value); - * } - * - * updateKwhChannel(LINKY_REMOTE_MONTHLY_GROUP, CHANNEL_MONTH_MINUS_0, - * values.monthValue[idxCurrentMonth].value); - * updateKwhChannel(LINKY_REMOTE_MONTHLY_GROUP, CHANNEL_MONTH_MINUS_1, - * values.monthValue[idxCurrentMonth - 1].value); - * if (idxCurrentMonth - 2 >= 0) { - * updateKwhChannel(LINKY_REMOTE_MONTHLY_GROUP, CHANNEL_MONTH_MINUS_2, - * values.monthValue[idxCurrentMonth - 2].value); - * } - * - * updateKwhChannel(LINKY_REMOTE_YEARLY_GROUP, CHANNEL_YEAR_MINUS_0, - * values.yearValue[idxCurrentYear].value); - * updateKwhChannel(LINKY_REMOTE_YEARLY_GROUP, CHANNEL_YEAR_MINUS_1, - * values.yearValue[idxCurrentYear - 1].value); - * if (idxCurrentYear - 2 >= 0) { - * updateKwhChannel(LINKY_REMOTE_YEARLY_GROUP, CHANNEL_YEAR_MINUS_2, - * values.yearValue[idxCurrentYear - 2].value); - * } - * - * updateTimeSeries(LINKY_REMOTE_DAILY_GROUP, CHANNEL_CONSUMPTION, values.baseValue, Units.KILOWATT_HOUR); - * updateTimeSeries(LINKY_REMOTE_WEEKLY_GROUP, CHANNEL_CONSUMPTION, values.weekValue, Units.KILOWATT_HOUR); - * updateTimeSeries(LINKY_REMOTE_MONTHLY_GROUP, CHANNEL_CONSUMPTION, values.monthValue, - * Units.KILOWATT_HOUR); - * updateTimeSeries(LINKY_REMOTE_YEARLY_GROUP, CHANNEL_CONSUMPTION, values.yearValue, Units.KILOWATT_HOUR); - */ }, () -> { - /* - * updateKwhChannel(LINKY_REMOTE_DAILY_GROUP, CHANNEL_DAY_MINUS_1, Double.NaN); - * updateKwhChannel(LINKY_REMOTE_DAILY_GROUP, CHANNEL_DAY_MINUS_2, Double.NaN); - * updateKwhChannel(LINKY_REMOTE_DAILY_GROUP, CHANNEL_DAY_MINUS_3, Double.NaN); - * - * updateKwhChannel(LINKY_REMOTE_WEEKLY_GROUP, CHANNEL_WEEK_MINUS_0, Double.NaN); - * updateKwhChannel(LINKY_REMOTE_WEEKLY_GROUP, CHANNEL_WEEK_MINUS_1, Double.NaN); - * updateKwhChannel(LINKY_REMOTE_WEEKLY_GROUP, CHANNEL_WEEK_MINUS_2, Double.NaN); - * - * updateKwhChannel(LINKY_REMOTE_MONTHLY_GROUP, CHANNEL_MONTH_MINUS_0, Double.NaN); - * updateKwhChannel(LINKY_REMOTE_MONTHLY_GROUP, CHANNEL_MONTH_MINUS_1, Double.NaN); - * updateKwhChannel(LINKY_REMOTE_MONTHLY_GROUP, CHANNEL_MONTH_MINUS_2, Double.NaN); - * - * updateKwhChannel(LINKY_REMOTE_YEARLY_GROUP, CHANNEL_YEAR_MINUS_0, Double.NaN); - * updateKwhChannel(LINKY_REMOTE_YEARLY_GROUP, CHANNEL_YEAR_MINUS_1, Double.NaN); - * updateKwhChannel(LINKY_REMOTE_YEARLY_GROUP, CHANNEL_YEAR_MINUS_2, Double.NaN); - */ }); } @@ -1024,4 +1098,5 @@ private boolean isLinkedPowerData() { private boolean isLinked(String groupName, String channelName) { return isLinked(groupName + "#" + channelName); } + } From 5ffe047a0ad3c52f88db1384814b73c82499f8eb Mon Sep 17 00:00:00 2001 From: Laurent ARNAL Date: Fri, 19 Sep 2025 19:03:24 +0200 Subject: [PATCH 17/34] progress on tarif support Signed-off-by: Laurent ARNAL --- .../handler/ThingLinkyRemoteHandler.java | 114 ++++++++++++++++-- 1 file changed, 101 insertions(+), 13 deletions(-) diff --git a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/handler/ThingLinkyRemoteHandler.java b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/handler/ThingLinkyRemoteHandler.java index de3554956bc22..78bf5773a843f 100644 --- a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/handler/ThingLinkyRemoteHandler.java +++ b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/handler/ThingLinkyRemoteHandler.java @@ -565,6 +565,9 @@ private void handleDynamicChannel(MeterReading values) { String channelDesc = "The " + st; addChannel(channels, chanTypeUid, LINKY_REMOTE_DAILY_GROUP, channelName, channelLabel, channelDesc); + addChannel(channels, chanTypeUid, LINKY_REMOTE_WEEKLY_GROUP, channelName, channelLabel, channelDesc); + addChannel(channels, chanTypeUid, LINKY_REMOTE_MONTHLY_GROUP, channelName, channelLabel, channelDesc); + addChannel(channels, chanTypeUid, LINKY_REMOTE_YEARLY_GROUP, channelName, channelLabel, channelDesc); } } @@ -604,7 +607,7 @@ private void handleDynamicChannel(MeterReading values) { } - private @Nullable List cut(@Nullable IntervalReading[] irs) { + private List splitOnTariffBound(@Nullable IntervalReading[] irs) { List result = new ArrayList(); String currentTarif = ""; int lastIdx = 0; @@ -640,13 +643,17 @@ private synchronized void updateEnergyIndex() { handleDynamicChannel(values); + List irsDaily = splitOnTariffBound(values.baseValue); + List irsWeekly = splitOnTariffBound(values.weekValue); + List irsMonthly = splitOnTariffBound(values.monthValue); + List irsYearly = splitOnTariffBound(values.yearValue); + for (int idx = 0; idx < 10; idx++) { channelName = CHANNEL_CONSUMPTION_SUPPLIER_IDX + idx; updateTimeSeries(LINKY_REMOTE_DAILY_GROUP, channelName, values.baseValue, idx, Units.KILOWATT_HOUR); - List irs = cut(values.baseValue); - for (IntervalReading[] ir : irs) { - if (ir[0].supplierLabel[idx] == null) { + for (IntervalReading[] ir : irsDaily) { + if (ir[0].supplierLabel == null || ir[0].supplierLabel[idx] == null) { continue; } channelName = sanetizeId(ir[0].supplierLabel[idx]); @@ -657,36 +664,92 @@ private synchronized void updateEnergyIndex() { for (int idx = 0; idx < 4; idx++) { channelName = CHANNEL_CONSUMPTION_SUPPLIER_IDX + idx; updateTimeSeries(LINKY_REMOTE_DAILY_GROUP, channelName, values.baseValue, idx, Units.KILOWATT_HOUR); + + for (IntervalReading[] ir : irsDaily) { + if (ir[0].distributorLabel == null || ir[0].distributorLabel[idx] == null) { + continue; + } + channelName = sanetizeId(ir[0].distributorLabel[idx]); + updateTimeSeries(LINKY_REMOTE_DAILY_GROUP, channelName, ir, idx, Units.KILOWATT_HOUR); + } } for (int idx = 0; idx < 10; idx++) { channelName = CHANNEL_CONSUMPTION_SUPPLIER_IDX + idx; updateTimeSeries(LINKY_REMOTE_WEEKLY_GROUP, channelName, values.weekValue, idx, Units.KILOWATT_HOUR); + + for (IntervalReading[] ir : irsWeekly) { + if (ir[0].supplierLabel == null || ir[0].supplierLabel[idx] == null) { + continue; + } + channelName = sanetizeId(ir[0].supplierLabel[idx]); + updateTimeSeries(LINKY_REMOTE_WEEKLY_GROUP, channelName, ir, idx, Units.KILOWATT_HOUR); + } } for (int idx = 0; idx < 4; idx++) { channelName = CHANNEL_CONSUMPTION_SUPPLIER_IDX + idx; updateTimeSeries(LINKY_REMOTE_WEEKLY_GROUP, channelName, values.weekValue, idx, Units.KILOWATT_HOUR); + + for (IntervalReading[] ir : irsWeekly) { + if (ir[0].distributorLabel == null || ir[0].distributorLabel[idx] == null) { + continue; + } + channelName = sanetizeId(ir[0].distributorLabel[idx]); + updateTimeSeries(LINKY_REMOTE_WEEKLY_GROUP, channelName, ir, idx, Units.KILOWATT_HOUR); + } } for (int idx = 0; idx < 10; idx++) { updateTimeSeries(LINKY_REMOTE_MONTHLY_GROUP, CHANNEL_CONSUMPTION_SUPPLIER_IDX + idx, values.monthValue, idx, Units.KILOWATT_HOUR); + + for (IntervalReading[] ir : irsMonthly) { + if (ir[0].supplierLabel == null || ir[0].supplierLabel[idx] == null) { + continue; + } + channelName = sanetizeId(ir[0].supplierLabel[idx]); + updateTimeSeries(LINKY_REMOTE_MONTHLY_GROUP, channelName, ir, idx, Units.KILOWATT_HOUR); + } } for (int idx = 0; idx < 4; idx++) { updateTimeSeries(LINKY_REMOTE_MONTHLY_GROUP, CHANNEL_CONSUMPTION_DISTRIBUTOR_IDX + idx, values.monthValue, idx, Units.KILOWATT_HOUR); + + for (IntervalReading[] ir : irsMonthly) { + if (ir[0].distributorLabel == null || ir[0].distributorLabel[idx] == null) { + continue; + } + channelName = sanetizeId(ir[0].distributorLabel[idx]); + updateTimeSeries(LINKY_REMOTE_MONTHLY_GROUP, channelName, ir, idx, Units.KILOWATT_HOUR); + } } for (int idx = 0; idx < 10; idx++) { updateTimeSeries(LINKY_REMOTE_YEARLY_GROUP, CHANNEL_CONSUMPTION_SUPPLIER_IDX + idx, values.yearValue, idx, Units.KILOWATT_HOUR); + + for (IntervalReading[] ir : irsYearly) { + if (ir[0].supplierLabel == null || ir[0].supplierLabel[idx] == null) { + continue; + } + channelName = sanetizeId(ir[0].supplierLabel[idx]); + updateTimeSeries(LINKY_REMOTE_YEARLY_GROUP, channelName, ir, idx, Units.KILOWATT_HOUR); + } } for (int idx = 0; idx < 4; idx++) { updateTimeSeries(LINKY_REMOTE_YEARLY_GROUP, CHANNEL_CONSUMPTION_DISTRIBUTOR_IDX + idx, values.yearValue, idx, Units.KILOWATT_HOUR); + + for (IntervalReading[] ir : irsYearly) { + if (ir[0].distributorLabel == null || ir[0].distributorLabel[idx] == null) { + continue; + } + channelName = sanetizeId(ir[0].distributorLabel[idx]); + updateTimeSeries(LINKY_REMOTE_YEARLY_GROUP, channelName, ir, idx, Units.KILOWATT_HOUR); + } } }, () -> { }); @@ -911,6 +974,15 @@ public void handleCommand(ChannelUID channelUID, Command command) { } } + private void initIntervalReadingTarif(IntervalReading irs) { + if (irs.valueDistributor == null) { + irs.valueDistributor = new double[4]; + } + if (irs.valueSupplier == null) { + irs.valueSupplier = new double[10]; + } + } + public @Nullable MeterReading getMeterReadingAfterChecks(@Nullable MeterReading meterReading) { try { checkData(meterReading); @@ -992,30 +1064,46 @@ public void handleCommand(ChannelUID channelUID, Command command) { } if (ir.valueSupplier != null) { - if (meterReading.weekValue[idxWeek].valueSupplier == null) { - meterReading.weekValue[idxWeek].valueSupplier = new double[10]; - } - if (meterReading.monthValue[idxMonth].valueSupplier == null) { - meterReading.monthValue[idxMonth].valueSupplier = new double[10]; - } - if (meterReading.yearValue[idxYear].valueSupplier == null) { - meterReading.yearValue[idxYear].valueSupplier = new double[10]; - } + initIntervalReadingTarif(meterReading.weekValue[idxWeek]); + initIntervalReadingTarif(meterReading.monthValue[idxMonth]); + initIntervalReadingTarif(meterReading.yearValue[idxYear]); + for (int idxSupplier = 0; idxSupplier < 10; idxSupplier++) { double valueSupplier = ir.valueSupplier[idxSupplier]; if (idxWeek < weeksNum) { meterReading.weekValue[idxWeek].valueSupplier[idxSupplier] += valueSupplier; + meterReading.weekValue[idxWeek].supplierLabel = ir.supplierLabel; } if (idxMonth < monthsNum) { meterReading.monthValue[idxMonth].valueSupplier[idxSupplier] += valueSupplier; + meterReading.monthValue[idxMonth].supplierLabel = ir.supplierLabel; } if (idxYear < yearsNum) { meterReading.yearValue[idxYear].valueSupplier[idxSupplier] += valueSupplier; + meterReading.yearValue[idxYear].supplierLabel = ir.supplierLabel; + } + } + + for (int idxDistributor = 0; idxDistributor < 4; idxDistributor++) { + double valueDistributor = ir.valueDistributor[idxDistributor]; + + if (idxWeek < weeksNum) { + meterReading.weekValue[idxWeek].valueDistributor[idxDistributor] += valueDistributor; + meterReading.weekValue[idxWeek].distributorLabel = ir.distributorLabel; + } + if (idxMonth < monthsNum) { + meterReading.monthValue[idxMonth].valueDistributor[idxDistributor] += valueDistributor; + meterReading.monthValue[idxMonth].distributorLabel = ir.distributorLabel; } + if (idxYear < yearsNum) { + meterReading.yearValue[idxYear].valueDistributor[idxDistributor] += valueDistributor; + meterReading.yearValue[idxYear].distributorLabel = ir.distributorLabel; + } } + } } } From 98d0b5fc3ed32a2340b7c4cf51800a95f3d3e6a7 Mon Sep 17 00:00:00 2001 From: Laurent ARNAL Date: Fri, 19 Sep 2025 19:33:23 +0200 Subject: [PATCH 18/34] fix for first index, we don't have the necessary value to calc it, so set to 0. Signed-off-by: Laurent ARNAL --- .../linky/internal/dto/MeterReading.java | 20 +++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/MeterReading.java b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/MeterReading.java index 3d86e2c182538..8f0cebfe68e1c 100644 --- a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/MeterReading.java +++ b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/MeterReading.java @@ -89,7 +89,11 @@ public static IntervalReading[] fromAgregat(ConsumptionReport.Aggregate agregat, if (i > 0) { result[i - 1] = new IntervalReading(); - result[i - 1].value = value - lastVal; + if (i == 1) { + result[i - 1].value = 0.0; + } else { + result[i - 1].value = value - lastVal; + } // The index in on nextDay N, but index difference give consumption for day N-1 result[i - 1].date = dataObj.dateDebut.minusDays(1); result[i - 1].valueSupplier = new double[10]; @@ -109,7 +113,11 @@ public static IntervalReading[] fromAgregat(ConsumptionReport.Aggregate agregat, for (int idxSupplier = 0; idxSupplier < dataObj.classesTemporellesSupplier.length; idxSupplier++) { ClassesTemporelles ct = dataObj.classesTemporellesSupplier[idxSupplier]; - result[i - 1].valueSupplier[idxSupplier] = (ct.valeur - lastValueSupplier[idxSupplier]); + if (i == 1) { + result[i - 1].valueSupplier[idxSupplier] = 0.00; + } else { + result[i - 1].valueSupplier[idxSupplier] = (ct.valeur - lastValueSupplier[idxSupplier]); + } result[i - 1].supplierLabel[idxSupplier] = ct.libelle; lastValueSupplier[idxSupplier] = ct.valeur; @@ -120,8 +128,12 @@ public static IntervalReading[] fromAgregat(ConsumptionReport.Aggregate agregat, for (int idxDistributor = 0; idxDistributor < dataObj.classesTemporellesDistributor.length; idxDistributor++) { ClassesTemporelles ct = dataObj.classesTemporellesDistributor[idxDistributor]; - result[i - 1].valueDistributor[idxDistributor] = (ct.valeur - - lastValueDistributor[idxDistributor]); + if (i == 1) { + result[i - 1].valueDistributor[idxDistributor] = 0.0; + } else { + result[i - 1].valueDistributor[idxDistributor] = (ct.valeur + - lastValueDistributor[idxDistributor]); + } result[i - 1].distributorLabel[idxDistributor] = ct.libelle; lastValueDistributor[idxDistributor] = ct.valeur; From 359373def39bcec9e82756e13e40015ad88fa06c Mon Sep 17 00:00:00 2001 From: Laurent ARNAL Date: Sat, 20 Sep 2025 16:35:04 +0200 Subject: [PATCH 19/34] fix condition location add verification for tarif change Signed-off-by: Laurent ARNAL --- .../linky/internal/dto/MeterReading.java | 18 ++++++++++++++++-- .../handler/ThingLinkyRemoteHandler.java | 7 ++++--- 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/MeterReading.java b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/MeterReading.java index 8f0cebfe68e1c..baa1c0f602e04 100644 --- a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/MeterReading.java +++ b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/MeterReading.java @@ -82,10 +82,21 @@ public static IntervalReading[] fromAgregat(ConsumptionReport.Aggregate agregat, double lastVal = 0.0; double[] lastValueSupplier = new double[6]; double[] lastValueDistributor = new double[6]; + String lastCalendrierSupplier = ""; + String lastCalendrierDistributor = ""; for (int i = 0; i < size; i++) { Data dataObj = agregat.datas.get(i); double value = dataObj.valeur; + String calendrierDistributor = ""; + String calendrierSupplier = ""; + + if (dataObj.calendrier == null) { + dataObj.calendrier = agregat.datas.get(i - 1).calendrier; + } + + calendrierDistributor = dataObj.calendrier[0].idCalendrier; + calendrierSupplier = dataObj.calendrier[1].idCalendrier; if (i > 0) { result[i - 1] = new IntervalReading(); @@ -96,6 +107,7 @@ public static IntervalReading[] fromAgregat(ConsumptionReport.Aggregate agregat, } // The index in on nextDay N, but index difference give consumption for day N-1 result[i - 1].date = dataObj.dateDebut.minusDays(1); + result[i - 1].valueSupplier = new double[10]; result[i - 1].valueDistributor = new double[4]; result[i - 1].supplierLabel = new String[10]; @@ -113,7 +125,7 @@ public static IntervalReading[] fromAgregat(ConsumptionReport.Aggregate agregat, for (int idxSupplier = 0; idxSupplier < dataObj.classesTemporellesSupplier.length; idxSupplier++) { ClassesTemporelles ct = dataObj.classesTemporellesSupplier[idxSupplier]; - if (i == 1) { + if (i == 1 || !calendrierSupplier.equals(lastCalendrierSupplier)) { result[i - 1].valueSupplier[idxSupplier] = 0.00; } else { result[i - 1].valueSupplier[idxSupplier] = (ct.valeur - lastValueSupplier[idxSupplier]); @@ -128,7 +140,7 @@ public static IntervalReading[] fromAgregat(ConsumptionReport.Aggregate agregat, for (int idxDistributor = 0; idxDistributor < dataObj.classesTemporellesDistributor.length; idxDistributor++) { ClassesTemporelles ct = dataObj.classesTemporellesDistributor[idxDistributor]; - if (i == 1) { + if (i == 1 || !calendrierDistributor.equals(lastCalendrierDistributor)) { result[i - 1].valueDistributor[idxDistributor] = 0.0; } else { result[i - 1].valueDistributor[idxDistributor] = (ct.valeur @@ -141,6 +153,8 @@ public static IntervalReading[] fromAgregat(ConsumptionReport.Aggregate agregat, } } lastVal = value; + lastCalendrierDistributor = calendrierDistributor; + lastCalendrierSupplier = calendrierSupplier; } } diff --git a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/handler/ThingLinkyRemoteHandler.java b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/handler/ThingLinkyRemoteHandler.java index 78bf5773a843f..30c57e8e3ef25 100644 --- a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/handler/ThingLinkyRemoteHandler.java +++ b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/handler/ThingLinkyRemoteHandler.java @@ -783,14 +783,15 @@ private synchronized > void updateTimeSeries(String groupI Instant timestamp = iv[i].date.atZone(zoneId).toInstant(); - if (Double.isNaN(iv[i].value)) { - continue; - } if (idx != -1) { if (i < iv.length && iv[i] != null) { timeSeries.add(timestamp, new QuantityType<>(iv[i].valueSupplier[idx], unit)); } } else { + if (Double.isNaN(iv[i].value)) { + continue; + } + timeSeries.add(timestamp, new QuantityType<>(iv[i].value, unit)); } } catch (Exception ex) { From 4015928870d8b227d00e289f0e5bd411671f4a93 Mon Sep 17 00:00:00 2001 From: Laurent ARNAL Date: Sat, 20 Sep 2025 17:02:36 +0200 Subject: [PATCH 20/34] code refactoring for simplifications Signed-off-by: Laurent ARNAL --- .../handler/ThingLinkyRemoteHandler.java | 167 ++++++------------ 1 file changed, 55 insertions(+), 112 deletions(-) diff --git a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/handler/ThingLinkyRemoteHandler.java b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/handler/ThingLinkyRemoteHandler.java index 30c57e8e3ef25..352907207bd3b 100644 --- a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/handler/ThingLinkyRemoteHandler.java +++ b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/handler/ThingLinkyRemoteHandler.java @@ -633,124 +633,48 @@ private List splitOnTariffBound(@Nullable IntervalReading[] i return result; } - /** - * Request new daily/weekly data and updates channels - */ - private synchronized void updateEnergyIndex() { - dailyIndex.getValue().ifPresentOrElse(values -> { - int dSize = values.baseValue.length; - String channelName = ""; - - handleDynamicChannel(values); - - List irsDaily = splitOnTariffBound(values.baseValue); - List irsWeekly = splitOnTariffBound(values.weekValue); - List irsMonthly = splitOnTariffBound(values.monthValue); - List irsYearly = splitOnTariffBound(values.yearValue); - - for (int idx = 0; idx < 10; idx++) { - channelName = CHANNEL_CONSUMPTION_SUPPLIER_IDX + idx; - updateTimeSeries(LINKY_REMOTE_DAILY_GROUP, channelName, values.baseValue, idx, Units.KILOWATT_HOUR); - - for (IntervalReading[] ir : irsDaily) { - if (ir[0].supplierLabel == null || ir[0].supplierLabel[idx] == null) { - continue; - } - channelName = sanetizeId(ir[0].supplierLabel[idx]); - updateTimeSeries(LINKY_REMOTE_DAILY_GROUP, channelName, ir, idx, Units.KILOWATT_HOUR); - } - } - - for (int idx = 0; idx < 4; idx++) { - channelName = CHANNEL_CONSUMPTION_SUPPLIER_IDX + idx; - updateTimeSeries(LINKY_REMOTE_DAILY_GROUP, channelName, values.baseValue, idx, Units.KILOWATT_HOUR); - - for (IntervalReading[] ir : irsDaily) { - if (ir[0].distributorLabel == null || ir[0].distributorLabel[idx] == null) { - continue; - } - channelName = sanetizeId(ir[0].distributorLabel[idx]); - updateTimeSeries(LINKY_REMOTE_DAILY_GROUP, channelName, ir, idx, Units.KILOWATT_HOUR); - } - } - - for (int idx = 0; idx < 10; idx++) { - channelName = CHANNEL_CONSUMPTION_SUPPLIER_IDX + idx; - updateTimeSeries(LINKY_REMOTE_WEEKLY_GROUP, channelName, values.weekValue, idx, Units.KILOWATT_HOUR); - - for (IntervalReading[] ir : irsWeekly) { - if (ir[0].supplierLabel == null || ir[0].supplierLabel[idx] == null) { - continue; - } - channelName = sanetizeId(ir[0].supplierLabel[idx]); - updateTimeSeries(LINKY_REMOTE_WEEKLY_GROUP, channelName, ir, idx, Units.KILOWATT_HOUR); - } - } + private void updateEnergyIndex(IntervalReading[] irs, String groupName) { + List lirs = splitOnTariffBound(irs); - for (int idx = 0; idx < 4; idx++) { - channelName = CHANNEL_CONSUMPTION_SUPPLIER_IDX + idx; - updateTimeSeries(LINKY_REMOTE_WEEKLY_GROUP, channelName, values.weekValue, idx, Units.KILOWATT_HOUR); - - for (IntervalReading[] ir : irsWeekly) { - if (ir[0].distributorLabel == null || ir[0].distributorLabel[idx] == null) { - continue; - } - channelName = sanetizeId(ir[0].distributorLabel[idx]); - updateTimeSeries(LINKY_REMOTE_WEEKLY_GROUP, channelName, ir, idx, Units.KILOWATT_HOUR); - } - } - - for (int idx = 0; idx < 10; idx++) { - updateTimeSeries(LINKY_REMOTE_MONTHLY_GROUP, CHANNEL_CONSUMPTION_SUPPLIER_IDX + idx, values.monthValue, - idx, Units.KILOWATT_HOUR); + for (int idx = 0; idx < 10; idx++) { + String channelName = CHANNEL_CONSUMPTION_SUPPLIER_IDX + idx; + updateTimeSeries(groupName, channelName, irs, idx, Units.KILOWATT_HOUR, IndexMode.Supplier); - for (IntervalReading[] ir : irsMonthly) { - if (ir[0].supplierLabel == null || ir[0].supplierLabel[idx] == null) { - continue; - } - channelName = sanetizeId(ir[0].supplierLabel[idx]); - updateTimeSeries(LINKY_REMOTE_MONTHLY_GROUP, channelName, ir, idx, Units.KILOWATT_HOUR); + for (IntervalReading[] ir : lirs) { + if (ir[0].supplierLabel == null || ir[0].supplierLabel[idx] == null) { + continue; } + channelName = sanetizeId(ir[0].supplierLabel[idx]); + updateTimeSeries(groupName, channelName, ir, idx, Units.KILOWATT_HOUR, IndexMode.Supplier); } + } - for (int idx = 0; idx < 4; idx++) { - updateTimeSeries(LINKY_REMOTE_MONTHLY_GROUP, CHANNEL_CONSUMPTION_DISTRIBUTOR_IDX + idx, - values.monthValue, idx, Units.KILOWATT_HOUR); + for (int idx = 0; idx < 4; idx++) { + String channelName = CHANNEL_CONSUMPTION_DISTRIBUTOR_IDX + idx; + updateTimeSeries(groupName, channelName, irs, idx, Units.KILOWATT_HOUR, IndexMode.Distributor); - for (IntervalReading[] ir : irsMonthly) { - if (ir[0].distributorLabel == null || ir[0].distributorLabel[idx] == null) { - continue; - } - channelName = sanetizeId(ir[0].distributorLabel[idx]); - updateTimeSeries(LINKY_REMOTE_MONTHLY_GROUP, channelName, ir, idx, Units.KILOWATT_HOUR); + for (IntervalReading[] ir : lirs) { + if (ir[0].distributorLabel == null || ir[0].distributorLabel[idx] == null) { + continue; } + channelName = sanetizeId(ir[0].distributorLabel[idx]); + updateTimeSeries(groupName, channelName, ir, idx, Units.KILOWATT_HOUR, IndexMode.Distributor); } + } - for (int idx = 0; idx < 10; idx++) { - updateTimeSeries(LINKY_REMOTE_YEARLY_GROUP, CHANNEL_CONSUMPTION_SUPPLIER_IDX + idx, values.yearValue, - idx, Units.KILOWATT_HOUR); - - for (IntervalReading[] ir : irsYearly) { - if (ir[0].supplierLabel == null || ir[0].supplierLabel[idx] == null) { - continue; - } - channelName = sanetizeId(ir[0].supplierLabel[idx]); - updateTimeSeries(LINKY_REMOTE_YEARLY_GROUP, channelName, ir, idx, Units.KILOWATT_HOUR); - } - } + } - for (int idx = 0; idx < 4; idx++) { - updateTimeSeries(LINKY_REMOTE_YEARLY_GROUP, CHANNEL_CONSUMPTION_DISTRIBUTOR_IDX + idx, values.yearValue, - idx, Units.KILOWATT_HOUR); + /** + * Request new daily/weekly data and updates channels + */ + private synchronized void updateEnergyIndex() { + dailyIndex.getValue().ifPresentOrElse(values -> { + handleDynamicChannel(values); - for (IntervalReading[] ir : irsYearly) { - if (ir[0].distributorLabel == null || ir[0].distributorLabel[idx] == null) { - continue; - } - channelName = sanetizeId(ir[0].distributorLabel[idx]); - updateTimeSeries(LINKY_REMOTE_YEARLY_GROUP, channelName, ir, idx, Units.KILOWATT_HOUR); - } - } + updateEnergyIndex(values.baseValue, LINKY_REMOTE_DAILY_GROUP); + updateEnergyIndex(values.weekValue, LINKY_REMOTE_WEEKLY_GROUP); + updateEnergyIndex(values.monthValue, LINKY_REMOTE_MONTHLY_GROUP); + updateEnergyIndex(values.yearValue, LINKY_REMOTE_YEARLY_GROUP); }, () -> { }); } @@ -768,8 +692,19 @@ private synchronized void updateLoadCurveData() { } } + enum IndexMode { + None, + Supplier, + Distributor + } + private synchronized > void updateTimeSeries(String groupId, String channelId, IntervalReading[] iv, int idx, Unit unit) { + updateTimeSeries(groupId, channelId, iv, idx, unit, IndexMode.None); + } + + private synchronized > void updateTimeSeries(String groupId, String channelId, + IntervalReading[] iv, int idx, Unit unit, IndexMode indexMode) { TimeSeries timeSeries = new TimeSeries(Policy.REPLACE); for (int i = 0; i < iv.length; i++) { @@ -783,16 +718,24 @@ private synchronized > void updateTimeSeries(String groupI Instant timestamp = iv[i].date.atZone(zoneId).toInstant(); - if (idx != -1) { - if (i < iv.length && iv[i] != null) { - timeSeries.add(timestamp, new QuantityType<>(iv[i].valueSupplier[idx], unit)); - } - } else { + if (indexMode == IndexMode.None) { if (Double.isNaN(iv[i].value)) { continue; } timeSeries.add(timestamp, new QuantityType<>(iv[i].value, unit)); + } else { + if (i < iv.length && iv[i] != null) { + if (indexMode == IndexMode.Supplier) { + if (iv[i].supplierLabel[idx] != null && !Double.isNaN(iv[i].valueSupplier[idx])) { + timeSeries.add(timestamp, new QuantityType<>(iv[i].valueSupplier[idx], unit)); + } + } else if (indexMode == IndexMode.Distributor && !Double.isNaN(iv[i].valueDistributor[idx])) { + if (iv[i].distributorLabel[idx] != null) { + timeSeries.add(timestamp, new QuantityType<>(iv[i].valueDistributor[idx], unit)); + } + } + } } } catch (Exception ex) { logger.error("error occurs durring updatePowerTimeSeries for {} : {}", config.prmId, ex.getMessage(), From d478c54953c2e0b70c3f9c5dbb5b16cb5b5cc094 Mon Sep 17 00:00:00 2001 From: Laurent ARNAL Date: Sat, 20 Sep 2025 19:02:43 +0200 Subject: [PATCH 21/34] code refactoring to simplify code Signed-off-by: Laurent ARNAL --- .../constants/LinkyBindingConstants.java | 4 +- .../handler/ThingLinkyRemoteHandler.java | 139 +++++++++++------- 2 files changed, 85 insertions(+), 58 deletions(-) diff --git a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/constants/LinkyBindingConstants.java b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/constants/LinkyBindingConstants.java index 044fdbbeafdce..fed4e54f80900 100644 --- a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/constants/LinkyBindingConstants.java +++ b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/constants/LinkyBindingConstants.java @@ -57,10 +57,10 @@ public class LinkyBindingConstants { public static final String LINKY_TEMPO_CALENDAR_GROUP = "tempo-calendar"; public static final String LINKY_REMOTE_LOAD_CURVE_GROUP = "load-curve"; + public static final String CONSUMPTION = "consumption"; + // List of all Channel id's public static final String CHANNEL_CONSUMPTION = "consumption"; - public static final String CHANNEL_CONSUMPTION_SUPPLIER_IDX = "consumptionSupplierIdx"; - public static final String CHANNEL_CONSUMPTION_DISTRIBUTOR_IDX = "consumptionDistributorIdx"; public static final String CHANNEL_MAX_POWER = "max-power"; public static final String CHANNEL_POWER = "power"; public static final String CHANNEL_TIMESTAMP_CHANNEL = "power"; diff --git a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/handler/ThingLinkyRemoteHandler.java b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/handler/ThingLinkyRemoteHandler.java index 352907207bd3b..8ca6998cdb7f4 100644 --- a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/handler/ThingLinkyRemoteHandler.java +++ b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/handler/ThingLinkyRemoteHandler.java @@ -514,18 +514,6 @@ private synchronized void updateEnergyData() { }); } - private void addChannel(List channels, ChannelTypeUID chanTypeUid, String channelGroup, String channelName, - String channelLabel, String channelDesc) { - ChannelUID channelUid = new ChannelUID(this.getThing().getUID(), channelGroup, channelName); - Channel channel = ChannelBuilder.create(channelUid).withType(chanTypeUid).withDescription(channelDesc) - .withLabel(channelLabel).build(); - - if (!channels.contains(channel)) { - channels.add(channel); - } - - } - /** * The methods remove specific local character (like 'é'/'ê','â') so we have a correctly formated UID from a * localize item label @@ -547,15 +535,52 @@ public static String sanetizeId(String label) { return result; } - private void handleDynamicChannel(MeterReading values) { - ChannelTypeUID chanTypeUid = new ChannelTypeUID(LinkyBindingConstants.BINDING_ID, "consumption"); - List channels = new ArrayList(); + private void addChannel(List channels, ChannelTypeUID chanTypeUid, String channelGroup, String channelName, + String channelLabel, String channelDesc) { + ChannelUID channelUid = new ChannelUID(this.getThing().getUID(), channelGroup, channelName); + Channel channel = ChannelBuilder.create(channelUid).withType(chanTypeUid).withDescription(channelDesc) + .withLabel(channelLabel).build(); - for (IntervalReading ir : values.baseValue) { - for (String st : ir.distributorLabel) { + if (getThing().getChannel(channelUid) != null) { + return; + } + + if (channels.contains(channel)) { + return; + } + + channels.add(channel); + } + + private void addDynamicChannelByIdx(List channels, ChannelTypeUID chanTypeUid, IndexMode indexMode) { + int size = getIndexSize(indexMode); + String channelPrefix = CHANNEL_CONSUMPTION + indexMode.toString() + "Idx"; + + for (int idx = 0; idx < size; idx++) { + String channelName = channelPrefix + idx; + String channelLabel = indexMode.toString() + " Consumption " + idx; + String channelDesc = "The " + indexMode.toString() + " Consumption for index " + idx; + + addChannel(channels, chanTypeUid, LINKY_REMOTE_DAILY_GROUP, channelName, channelLabel, channelDesc); + addChannel(channels, chanTypeUid, LINKY_REMOTE_WEEKLY_GROUP, channelName, channelLabel, channelDesc); + addChannel(channels, chanTypeUid, LINKY_REMOTE_MONTHLY_GROUP, channelName, channelLabel, channelDesc); + addChannel(channels, chanTypeUid, LINKY_REMOTE_YEARLY_GROUP, channelName, channelLabel, channelDesc); + } + } + private void addDynamicChannelByLabel(List channels, ChannelTypeUID chanTypeUid, MeterReading values, + IndexMode indexMode) { + for (IntervalReading ir : values.baseValue) { + String[] label; + if (indexMode == IndexMode.Supplier) { + label = ir.supplierLabel; + } else if (indexMode == IndexMode.Distributor) { + label = ir.distributorLabel; + } else { + return; } - for (String st : ir.supplierLabel) { + + for (String st : label) { if (st == null) { continue; } @@ -568,31 +593,21 @@ private void handleDynamicChannel(MeterReading values) { addChannel(channels, chanTypeUid, LINKY_REMOTE_WEEKLY_GROUP, channelName, channelLabel, channelDesc); addChannel(channels, chanTypeUid, LINKY_REMOTE_MONTHLY_GROUP, channelName, channelLabel, channelDesc); addChannel(channels, chanTypeUid, LINKY_REMOTE_YEARLY_GROUP, channelName, channelLabel, channelDesc); - } } - for (int idx = 0; idx < 10; idx++) { - String channelName = CHANNEL_CONSUMPTION_SUPPLIER_IDX + idx; - String channelLabel = "Supplier Consumption " + idx; - String channelDesc = "The Supplier Consumption for index " + idx; + } - addChannel(channels, chanTypeUid, LINKY_REMOTE_DAILY_GROUP, channelName, channelLabel, channelDesc); - addChannel(channels, chanTypeUid, LINKY_REMOTE_WEEKLY_GROUP, channelName, channelLabel, channelDesc); - addChannel(channels, chanTypeUid, LINKY_REMOTE_MONTHLY_GROUP, channelName, channelLabel, channelDesc); - addChannel(channels, chanTypeUid, LINKY_REMOTE_YEARLY_GROUP, channelName, channelLabel, channelDesc); - } + private void handleDynamicChannel(MeterReading values) { + ChannelTypeUID chanTypeUid = new ChannelTypeUID(LinkyBindingConstants.BINDING_ID, + LinkyBindingConstants.CONSUMPTION); + List channels = new ArrayList(); - for (int idx = 0; idx < 4; idx++) { - String channelName = CHANNEL_CONSUMPTION_DISTRIBUTOR_IDX + idx; - String channelLabel = "Distributor Consumption " + idx; - String channelDesc = "The Distributor Consumption for index " + idx; + addDynamicChannelByLabel(channels, chanTypeUid, values, IndexMode.Supplier); + addDynamicChannelByLabel(channels, chanTypeUid, values, IndexMode.Distributor); - addChannel(channels, chanTypeUid, LINKY_REMOTE_DAILY_GROUP, channelName, channelLabel, channelDesc); - addChannel(channels, chanTypeUid, LINKY_REMOTE_WEEKLY_GROUP, channelName, channelLabel, channelDesc); - addChannel(channels, chanTypeUid, LINKY_REMOTE_MONTHLY_GROUP, channelName, channelLabel, channelDesc); - addChannel(channels, chanTypeUid, LINKY_REMOTE_YEARLY_GROUP, channelName, channelLabel, channelDesc); - } + addDynamicChannelByIdx(channels, chanTypeUid, IndexMode.Supplier); + addDynamicChannelByIdx(channels, chanTypeUid, IndexMode.Distributor); if (channels.size() > 0) { Thing thing = this.getThing(); @@ -601,7 +616,6 @@ private void handleDynamicChannel(MeterReading values) { channels.add(chan); } - // channels.add(chanTest); updateThing(editThing().withChannels(channels).build()); } @@ -633,37 +647,50 @@ private List splitOnTariffBound(@Nullable IntervalReading[] i return result; } - private void updateEnergyIndex(IntervalReading[] irs, String groupName) { - List lirs = splitOnTariffBound(irs); + private int getIndexSize(IndexMode indexMode) { + if (indexMode == IndexMode.Supplier) { + return 10; + } else if (indexMode == IndexMode.Distributor) { + return 4; + } + + return 0; + } + + private void updateEnergyIndex(IntervalReading[] irs, String groupName, List lirs, + IndexMode indexMode) { + int size = getIndexSize(indexMode); + String channelPrefix = CHANNEL_CONSUMPTION + indexMode.toString() + "Idx"; - for (int idx = 0; idx < 10; idx++) { - String channelName = CHANNEL_CONSUMPTION_SUPPLIER_IDX + idx; - updateTimeSeries(groupName, channelName, irs, idx, Units.KILOWATT_HOUR, IndexMode.Supplier); + for (int idx = 0; idx < size; idx++) { + updateTimeSeries(groupName, channelPrefix + idx, irs, idx, Units.KILOWATT_HOUR, indexMode); for (IntervalReading[] ir : lirs) { - if (ir[0].supplierLabel == null || ir[0].supplierLabel[idx] == null) { + String[] label; + if (indexMode == IndexMode.Supplier) { + label = ir[0].supplierLabel; + } else if (indexMode == IndexMode.Distributor) { + label = ir[0].distributorLabel; + } else { continue; } - channelName = sanetizeId(ir[0].supplierLabel[idx]); - updateTimeSeries(groupName, channelName, ir, idx, Units.KILOWATT_HOUR, IndexMode.Supplier); - } - } - - for (int idx = 0; idx < 4; idx++) { - String channelName = CHANNEL_CONSUMPTION_DISTRIBUTOR_IDX + idx; - updateTimeSeries(groupName, channelName, irs, idx, Units.KILOWATT_HOUR, IndexMode.Distributor); - for (IntervalReading[] ir : lirs) { - if (ir[0].distributorLabel == null || ir[0].distributorLabel[idx] == null) { + if (label == null || label[idx] == null) { continue; } - channelName = sanetizeId(ir[0].distributorLabel[idx]); - updateTimeSeries(groupName, channelName, ir, idx, Units.KILOWATT_HOUR, IndexMode.Distributor); + + updateTimeSeries(groupName, sanetizeId(label[idx]), ir, idx, Units.KILOWATT_HOUR, indexMode); } } } + private void updateEnergyIndex(IntervalReading[] irs, String groupName) { + List lirs = splitOnTariffBound(irs); + updateEnergyIndex(irs, groupName, lirs, IndexMode.Supplier); + updateEnergyIndex(irs, groupName, lirs, IndexMode.Distributor); + } + /** * Request new daily/weekly data and updates channels */ From fa59f1fb09ef51fbf8726bef3adac396d6a19c72 Mon Sep 17 00:00:00 2001 From: Laurent ARNAL Date: Sun, 21 Sep 2025 10:11:07 +0200 Subject: [PATCH 22/34] code refactoring to simplify code Signed-off-by: Laurent ARNAL --- .../binding/linky/internal/dto/IndexInfo.java | 6 + .../binding/linky/internal/dto/IndexMode.java | 23 ++ .../linky/internal/dto/IntervalReading.java | 16 +- .../linky/internal/dto/MeterReading.java | 99 +++--- .../handler/ThingLinkyRemoteHandler.java | 332 +++++++++++------- 5 files changed, 296 insertions(+), 180 deletions(-) create mode 100644 bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/IndexInfo.java create mode 100644 bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/IndexMode.java diff --git a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/IndexInfo.java b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/IndexInfo.java new file mode 100644 index 0000000000000..cbae5987bc90e --- /dev/null +++ b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/IndexInfo.java @@ -0,0 +1,6 @@ +package org.openhab.binding.linky.internal.dto; + +public class IndexInfo { + public double[] value; + public String[] label; +} diff --git a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/IndexMode.java b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/IndexMode.java new file mode 100644 index 0000000000000..7db0df22dc01d --- /dev/null +++ b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/IndexMode.java @@ -0,0 +1,23 @@ +package org.openhab.binding.linky.internal.dto; + +public enum IndexMode { + None(-1, 0), + Supplier(0, 10), + Distributor(1, 4); + + private final int idx; + private final int size; + + IndexMode(int idx, int size) { + this.idx = idx; + this.size = size; + } + + public int getIdx() { + return idx; + } + + public int getSize() { + return size; + } +} \ No newline at end of file diff --git a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/IntervalReading.java b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/IntervalReading.java index faaa2b983302b..1beca9eda3e5d 100644 --- a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/IntervalReading.java +++ b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/IntervalReading.java @@ -23,9 +23,17 @@ public class IntervalReading { public Double value = 0.0; - public double[] valueSupplier; - public double[] valueDistributor; - public String[] supplierLabel; - public String[] distributorLabel; + public IndexInfo[] indexInfo; public LocalDateTime date; + + public void InitIndexInfo() { + indexInfo = new IndexInfo[2]; + indexInfo[0] = new IndexInfo(); + indexInfo[1] = new IndexInfo(); + + indexInfo[0].label = new String[10]; + indexInfo[0].value = new double[10]; + indexInfo[1].label = new String[4]; + indexInfo[1].value = new double[4]; + } } diff --git a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/MeterReading.java b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/MeterReading.java index baa1c0f602e04..a80bad8998f9a 100644 --- a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/MeterReading.java +++ b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/MeterReading.java @@ -59,6 +59,14 @@ public static MeterReading convertFromComsumptionReport(ConsumptionReport comsum return result; } + /** + * This method will get data from old ConsumptionReport.Aggregate that is the format use by the Web API. + * And will result and IntervalReading[] that is the format of the new Endis API + * + * @param agregat + * @param useIndex : tell if we are reading value from raw consumption or from index value + * @return IntervalReading[] : the data structure of new API + */ public static IntervalReading[] fromAgregat(ConsumptionReport.Aggregate agregat, boolean useIndex) { int size = agregat.datas.size(); IntervalReading[] result = null; @@ -80,78 +88,47 @@ public static IntervalReading[] fromAgregat(ConsumptionReport.Aggregate agregat, } } else { double lastVal = 0.0; - double[] lastValueSupplier = new double[6]; - double[] lastValueDistributor = new double[6]; + double[] lastValueSupplier = new double[10]; + double[] lastValueDistributor = new double[4]; String lastCalendrierSupplier = ""; String lastCalendrierDistributor = ""; - for (int i = 0; i < size; i++) { - Data dataObj = agregat.datas.get(i); + for (int idx = 0; idx < size; idx++) { + Data dataObj = agregat.datas.get(idx); double value = dataObj.valeur; String calendrierDistributor = ""; String calendrierSupplier = ""; if (dataObj.calendrier == null) { - dataObj.calendrier = agregat.datas.get(i - 1).calendrier; + dataObj.calendrier = agregat.datas.get(idx - 1).calendrier; } calendrierDistributor = dataObj.calendrier[0].idCalendrier; calendrierSupplier = dataObj.calendrier[1].idCalendrier; - if (i > 0) { - result[i - 1] = new IntervalReading(); - if (i == 1) { - result[i - 1].value = 0.0; - } else { - result[i - 1].value = value - lastVal; - } + if (idx > 0) { + result[idx - 1] = new IntervalReading(); + result[idx - 1].value = value - lastVal; // The index in on nextDay N, but index difference give consumption for day N-1 - result[i - 1].date = dataObj.dateDebut.minusDays(1); - - result[i - 1].valueSupplier = new double[10]; - result[i - 1].valueDistributor = new double[4]; - result[i - 1].supplierLabel = new String[10]; - result[i - 1].distributorLabel = new String[4]; + result[idx - 1].date = dataObj.dateDebut.minusDays(1); + result[idx - 1].InitIndexInfo(); if (dataObj.classesTemporellesSupplier == null) { - dataObj.classesTemporellesSupplier = agregat.datas.get(i - 1).classesTemporellesSupplier; + dataObj.classesTemporellesSupplier = agregat.datas.get(idx - 1).classesTemporellesSupplier; } if (dataObj.classesTemporellesDistributor == null) { - dataObj.classesTemporellesDistributor = agregat.datas.get(i - 1).classesTemporellesDistributor; - } - - if (dataObj.classesTemporellesSupplier != null) { - for (int idxSupplier = 0; idxSupplier < dataObj.classesTemporellesSupplier.length; idxSupplier++) { - ClassesTemporelles ct = dataObj.classesTemporellesSupplier[idxSupplier]; - - if (i == 1 || !calendrierSupplier.equals(lastCalendrierSupplier)) { - result[i - 1].valueSupplier[idxSupplier] = 0.00; - } else { - result[i - 1].valueSupplier[idxSupplier] = (ct.valeur - lastValueSupplier[idxSupplier]); - } - result[i - 1].supplierLabel[idxSupplier] = ct.libelle; - - lastValueSupplier[idxSupplier] = ct.valeur; - } + dataObj.classesTemporellesDistributor = agregat.datas + .get(idx - 1).classesTemporellesDistributor; } + } - if (dataObj.classesTemporellesDistributor != null) { - for (int idxDistributor = 0; idxDistributor < dataObj.classesTemporellesDistributor.length; idxDistributor++) { - ClassesTemporelles ct = dataObj.classesTemporellesDistributor[idxDistributor]; + InitIndexValue(IndexMode.Supplier, dataObj.classesTemporellesSupplier, result, idx, calendrierSupplier, + lastCalendrierSupplier, lastValueSupplier); - if (i == 1 || !calendrierDistributor.equals(lastCalendrierDistributor)) { - result[i - 1].valueDistributor[idxDistributor] = 0.0; - } else { - result[i - 1].valueDistributor[idxDistributor] = (ct.valeur - - lastValueDistributor[idxDistributor]); - } - result[i - 1].distributorLabel[idxDistributor] = ct.libelle; + InitIndexValue(IndexMode.Distributor, dataObj.classesTemporellesDistributor, result, idx, + calendrierDistributor, lastCalendrierDistributor, lastValueDistributor); - lastValueDistributor[idxDistributor] = ct.valeur; - } - } - } lastVal = value; lastCalendrierDistributor = calendrierDistributor; lastCalendrierSupplier = calendrierSupplier; @@ -161,4 +138,28 @@ public static IntervalReading[] fromAgregat(ConsumptionReport.Aggregate agregat, return result; } + + public static void InitIndexValue(IndexMode indexMode, ClassesTemporelles[] classTp, IntervalReading[] ir, int idx, + String calendrier, String lastCalendrier, double[] lastValue) { + if (classTp != null) { + for (int idxClTp = 0; idxClTp < classTp.length; idxClTp++) { + ClassesTemporelles ct = classTp[idxClTp]; + + if (idx > 0) { + // We check if calendar are the same that previous iteration + // If not, we are not able to reconciliate index, and so set index value to 0 ! + + if (calendrier.equals(lastCalendrier)) { + ir[idx - 1].indexInfo[indexMode.getIdx()].value[idxClTp] = (ct.valeur - lastValue[idxClTp]); + } else { + ir[idx - 1].indexInfo[indexMode.getIdx()].value[idxClTp] = 0; + } + + ir[idx - 1].indexInfo[indexMode.getIdx()].label[idxClTp] = ct.libelle; + } + + lastValue[idxClTp] = ct.valeur; + } + } + } } diff --git a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/handler/ThingLinkyRemoteHandler.java b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/handler/ThingLinkyRemoteHandler.java index 8ca6998cdb7f4..e02fe35966706 100644 --- a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/handler/ThingLinkyRemoteHandler.java +++ b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/handler/ThingLinkyRemoteHandler.java @@ -43,6 +43,8 @@ import org.openhab.binding.linky.internal.dto.Contact; import org.openhab.binding.linky.internal.dto.Contract; import org.openhab.binding.linky.internal.dto.Identity; +import org.openhab.binding.linky.internal.dto.IndexInfo; +import org.openhab.binding.linky.internal.dto.IndexMode; import org.openhab.binding.linky.internal.dto.IntervalReading; import org.openhab.binding.linky.internal.dto.MetaData; import org.openhab.binding.linky.internal.dto.MeterReading; @@ -535,6 +537,16 @@ public static String sanetizeId(String label) { return result; } + /** + * This methods create a new Channel for a consumption index + * + * @param channels + * @param chanTypeUid + * @param channelGroup + * @param channelName + * @param channelLabel + * @param channelDesc + */ private void addChannel(List channels, ChannelTypeUID chanTypeUid, String channelGroup, String channelName, String channelLabel, String channelDesc) { ChannelUID channelUid = new ChannelUID(this.getThing().getUID(), channelGroup, channelName); @@ -552,11 +564,20 @@ private void addChannel(List channels, ChannelTypeUID chanTypeUid, Stri channels.add(channel); } + /** + * This methods create dynamic channels of forms: + * consumptionSupplierIdx0, consumptionSupplierIdx1, ..., consumptionSupplierIdx9 + * consumptionDistributorIdx0, consumptionDistributorIdx1, ..., consumptionDistributorIdx3 + * + * @param channels + * @param chanTypeUid + * @param indexMode + */ + private void addDynamicChannelByIdx(List channels, ChannelTypeUID chanTypeUid, IndexMode indexMode) { - int size = getIndexSize(indexMode); String channelPrefix = CHANNEL_CONSUMPTION + indexMode.toString() + "Idx"; - for (int idx = 0; idx < size; idx++) { + for (int idx = 0; idx < indexMode.getSize(); idx++) { String channelName = channelPrefix + idx; String channelLabel = indexMode.toString() + " Consumption " + idx; String channelDesc = "The " + indexMode.toString() + " Consumption for index " + idx; @@ -568,17 +589,18 @@ private void addDynamicChannelByIdx(List channels, ChannelTypeUID chanT } } + /** + * This methods create dynamic channels labelled by tarif name : + * heuresPleines, heuresCreuses, bleuHeuresCreuses, bleuHeuresPleines, ... + * + * @param channels + * @param chanTypeUid + * @param indexMode + */ private void addDynamicChannelByLabel(List channels, ChannelTypeUID chanTypeUid, MeterReading values, IndexMode indexMode) { for (IntervalReading ir : values.baseValue) { - String[] label; - if (indexMode == IndexMode.Supplier) { - label = ir.supplierLabel; - } else if (indexMode == IndexMode.Distributor) { - label = ir.distributorLabel; - } else { - return; - } + String[] label = ir.indexInfo[indexMode.getIdx()].label; for (String st : label) { if (st == null) { @@ -595,9 +617,29 @@ private void addDynamicChannelByLabel(List channels, ChannelTypeUID cha addChannel(channels, chanTypeUid, LINKY_REMOTE_YEARLY_GROUP, channelName, channelLabel, channelDesc); } } - } + /** + * This method create new channel dynamically at runtime when we read dataset from Enedis. + * We do this because we want to expose Index for each tarif. + * For tempo tarif for exemple, you will have 6 different channel. + * + * But this is not the only available tarif, to listing all possible channel in ressource file will not do it. + * It's far more easy to create them looking at the available tarif in customer dataset. + * + * There will be to set of channel: + * - Enedis channel: + * Supplier0 to Supplier9 : will expose originals Enedis Supplier Index. + * Distributor0 to Distributor 3 : will expose original Enedis Distributor Index. + * + * - Named channel: + * Will enable to have a more speaking channel Name, for exemple you will have + * for Heures Pleines / Heures creuses tarif : channel name will be heuresPleines / heuresCreuses. + * for Tempo tarif : channel name will be + * bleuHeuresCreuses/bleuHeuresPleines/blancHeuresCreuses/blancHeuresPleines/rougeHeuresCreuses/rougeHeuresPleines + * + * @param values : the dataset from enedis. + */ private void handleDynamicChannel(MeterReading values) { ChannelTypeUID chanTypeUid = new ChannelTypeUID(LinkyBindingConstants.BINDING_ID, LinkyBindingConstants.CONSUMPTION); @@ -609,6 +651,7 @@ private void handleDynamicChannel(MeterReading values) { addDynamicChannelByIdx(channels, chanTypeUid, IndexMode.Supplier); addDynamicChannelByIdx(channels, chanTypeUid, IndexMode.Distributor); + // If we have channel change, update the thing if (channels.size() > 0) { Thing thing = this.getThing(); @@ -621,59 +664,65 @@ private void handleDynamicChannel(MeterReading values) { } - private List splitOnTariffBound(@Nullable IntervalReading[] irs) { + /** + * This method take the full dataset, and return a List of subdataset, split on the tariff change. + * This can happen if on your subscription, you're ask your supplier to change tariff. + * For exemple, move from Heures Pleines/Heures Creuses tarif to Tempo tarif. + * + * We do this split because we want to expose tarif to dedicated named channel so it will be more easy to display + * them on a chart. + * + * @param irs + * @return a List of subdataset cut on Tarif change + */ + private List splitOnTariffBound(@Nullable IntervalReading[] irs, IndexMode indexMode) { List result = new ArrayList(); String currentTarif = ""; int lastIdx = 0; - for (int idx = 0; idx < irs.length; idx++) { - String tarif = String.join("#", irs[idx].supplierLabel); - - if ((!tarif.equals(currentTarif) && !currentTarif.equals("")) || (idx == irs.length - 1)) { - logger.debug("tarif change:" + lastIdx + "/" + (idx - 1)); - IntervalReading[] subArray; - if (idx == irs.length - 1) { - subArray = Arrays.copyOfRange(irs, lastIdx, idx + 1); - } else { - subArray = Arrays.copyOfRange(irs, lastIdx, idx); + for (int idx = 0; idx < irs.length; idx++) { + IntervalReading ir = irs[idx]; + if (ir != null) { + String tarif = String.join("#", ir.indexInfo[indexMode.getIdx()].label); + + if ((!tarif.equals(currentTarif) && !currentTarif.equals("")) || (idx == irs.length - 1)) { + IntervalReading[] subArray; + if (idx == irs.length - 1) { + subArray = Arrays.copyOfRange(irs, lastIdx, idx + 1); + } else { + subArray = Arrays.copyOfRange(irs, lastIdx, idx); + } + result.add(subArray); + lastIdx = idx; } - result.add(subArray); - lastIdx = idx; - } - currentTarif = tarif; - logger.debug(""); + currentTarif = tarif; + } } return result; } - private int getIndexSize(IndexMode indexMode) { - if (indexMode == IndexMode.Supplier) { - return 10; - } else if (indexMode == IndexMode.Distributor) { - return 4; - } - - return 0; - } + /** + * updateEnergyIndex methods will update timeSeries for a given energy index. + * There will be 2 timeseries update for each given energy index: + * - The index based time series. + * - The tarif labelled base time series. + * + * @param irs + * @param groupName + * @param indexMode + */ + private void updateEnergyIndex(IntervalReading[] irs, String groupName, IndexMode indexMode) { + List lirs = splitOnTariffBound(irs, indexMode); - private void updateEnergyIndex(IntervalReading[] irs, String groupName, List lirs, - IndexMode indexMode) { - int size = getIndexSize(indexMode); + int size = indexMode.getSize(); String channelPrefix = CHANNEL_CONSUMPTION + indexMode.toString() + "Idx"; for (int idx = 0; idx < size; idx++) { updateTimeSeries(groupName, channelPrefix + idx, irs, idx, Units.KILOWATT_HOUR, indexMode); for (IntervalReading[] ir : lirs) { - String[] label; - if (indexMode == IndexMode.Supplier) { - label = ir[0].supplierLabel; - } else if (indexMode == IndexMode.Distributor) { - label = ir[0].distributorLabel; - } else { - continue; - } + String[] label = ir[0].indexInfo[indexMode.getIdx()].label; if (label == null || label[idx] == null) { continue; @@ -686,9 +735,8 @@ private void updateEnergyIndex(IntervalReading[] irs, String groupName, List lirs = splitOnTariffBound(irs); - updateEnergyIndex(irs, groupName, lirs, IndexMode.Supplier); - updateEnergyIndex(irs, groupName, lirs, IndexMode.Distributor); + updateEnergyIndex(irs, groupName, IndexMode.Supplier); + updateEnergyIndex(irs, groupName, IndexMode.Distributor); } /** @@ -719,12 +767,6 @@ private synchronized void updateLoadCurveData() { } } - enum IndexMode { - None, - Supplier, - Distributor - } - private synchronized > void updateTimeSeries(String groupId, String channelId, IntervalReading[] iv, int idx, Unit unit) { updateTimeSeries(groupId, channelId, iv, idx, unit, IndexMode.None); @@ -753,14 +795,11 @@ private synchronized > void updateTimeSeries(String groupI timeSeries.add(timestamp, new QuantityType<>(iv[i].value, unit)); } else { if (i < iv.length && iv[i] != null) { - if (indexMode == IndexMode.Supplier) { - if (iv[i].supplierLabel[idx] != null && !Double.isNaN(iv[i].valueSupplier[idx])) { - timeSeries.add(timestamp, new QuantityType<>(iv[i].valueSupplier[idx], unit)); - } - } else if (indexMode == IndexMode.Distributor && !Double.isNaN(iv[i].valueDistributor[idx])) { - if (iv[i].distributorLabel[idx] != null) { - timeSeries.add(timestamp, new QuantityType<>(iv[i].valueDistributor[idx], unit)); - } + int indexIdx = indexMode.getIdx(); + + if (iv[i].indexInfo[indexIdx].label[idx] != null + && !Double.isNaN(iv[i].indexInfo[indexIdx].value[idx])) { + timeSeries.add(timestamp, new QuantityType<>(iv[i].indexInfo[indexIdx].value[idx], unit)); } } } @@ -945,15 +984,103 @@ public void handleCommand(ChannelUID channelUID, Command command) { } } - private void initIntervalReadingTarif(IntervalReading irs) { - if (irs.valueDistributor == null) { - irs.valueDistributor = new double[4]; + /** + * This method will init the IndexInfo data structure for an IntervalReading ir + * + * @param ir + */ + private void initIntervalReadingTarif(IntervalReading ir) { + if (ir.indexInfo == null) { + ir.indexInfo = new IndexInfo[2]; + ir.indexInfo[0] = new IndexInfo(); + ir.indexInfo[1] = new IndexInfo(); + + ir.indexInfo[0].value = new double[10]; + ir.indexInfo[0].label = new String[10]; + + ir.indexInfo[1].value = new double[4]; + ir.indexInfo[1].label = new String[4]; } - if (irs.valueSupplier == null) { - irs.valueSupplier = new double[10]; + } + + /** + * This method will sum day index value to respective week, month & year index. + * Will be done for supplier or distributor index in regards to indexMode + * + * + * @param indexMode : the index mode : Supplier or Distributor + * @param meterReading + * @param ir + * @param idxWeek + * @param weeksNum + * @param idxMonth + * @param monthsNum + * @param idxYear + * @param yearsNum + */ + public void sumIndex(IndexMode indexMode, MeterReading meterReading, IntervalReading ir, int idxWeek, int weeksNum, + int idxMonth, int monthsNum, int idxYear, int yearsNum) { + int indexIdx = indexMode.getIdx(); + int size = indexMode.getSize(); + + if (ir.indexInfo[indexIdx].value != null) { + for (int idxIndex = 0; idxIndex < size; idxIndex++) { + double valIndex = ir.indexInfo[indexIdx].value[idxIndex]; + String label = ir.indexInfo[indexIdx].label[idxIndex]; + + // Sums day to week + if (idxWeek < weeksNum) { + meterReading.weekValue[idxWeek].indexInfo[indexIdx].value[idxIndex] += valIndex; + meterReading.weekValue[idxWeek].indexInfo[indexIdx].label[idxIndex] = label; + } + + // Sums day to month + if (idxMonth < monthsNum) { + meterReading.monthValue[idxMonth].indexInfo[indexIdx].value[idxIndex] += valIndex; + meterReading.monthValue[idxMonth].indexInfo[indexIdx].label[idxIndex] = label; + + } + + // Sums day to year + if (idxYear < yearsNum) { + meterReading.yearValue[idxYear].indexInfo[indexIdx].value[idxIndex] += valIndex; + meterReading.yearValue[idxYear].indexInfo[indexIdx].label[idxIndex] = label; + } + } + } + } + + private void checkData(@Nullable MeterReading meterReading) throws LinkyException { + if (meterReading != null) { + if (meterReading.baseValue.length == 0) { + throw new LinkyException("Invalid meterReading data: no day period"); + } + } else { + throw new LinkyException("Invalid meterReading == null"); + } + } + + private boolean isDataLastDayAvailable(@Nullable MeterReading meterReading) { + if (meterReading != null) { + IntervalReading[] iv = meterReading.baseValue; + + logData(iv, "Last day", DateTimeFormatter.ISO_LOCAL_DATE, Target.LAST); + return iv != null && iv.length != 0 && iv[iv.length - 1] != null; } + + return false; } + /** + * This method will do some basic checking on dataset from Enedis. + * And will also calculate the Weekly, Monthly and Yearly agregate. + * + * When data are coming from Enedis, we will only have data day by day. + * To get date for week, month, and year, we need to sum the daily data. + * + * @param meterReading + * @return + */ public @Nullable MeterReading getMeterReadingAfterChecks(@Nullable MeterReading meterReading) { try { checkData(meterReading); @@ -1013,6 +1140,7 @@ private void initIntervalReadingTarif(IntervalReading irs) { int idxWeek = (idxYear * 52) + dtWeek - baseWeek; int month = dt.getMonthValue(); + // Sums day to week if (idxWeek < weeksNum) { meterReading.weekValue[idxWeek].value += value; @@ -1020,6 +1148,8 @@ private void initIntervalReadingTarif(IntervalReading irs) { meterReading.weekValue[idxWeek].date = dt; } } + + // Sums day to month if (idxMonth < monthsNum) { meterReading.monthValue[idxMonth].value += value; if (meterReading.monthValue[idxMonth].date == null) { @@ -1027,6 +1157,7 @@ private void initIntervalReadingTarif(IntervalReading irs) { } } + // Sums day to year if (idxYear < yearsNum) { meterReading.yearValue[idxYear].value += value; if (meterReading.yearValue[idxYear].date == null) { @@ -1034,47 +1165,15 @@ private void initIntervalReadingTarif(IntervalReading irs) { } } - if (ir.valueSupplier != null) { + if (ir.indexInfo != null) { initIntervalReadingTarif(meterReading.weekValue[idxWeek]); initIntervalReadingTarif(meterReading.monthValue[idxMonth]); initIntervalReadingTarif(meterReading.yearValue[idxYear]); - for (int idxSupplier = 0; idxSupplier < 10; idxSupplier++) { - double valueSupplier = ir.valueSupplier[idxSupplier]; - - if (idxWeek < weeksNum) { - meterReading.weekValue[idxWeek].valueSupplier[idxSupplier] += valueSupplier; - meterReading.weekValue[idxWeek].supplierLabel = ir.supplierLabel; - } - if (idxMonth < monthsNum) { - meterReading.monthValue[idxMonth].valueSupplier[idxSupplier] += valueSupplier; - meterReading.monthValue[idxMonth].supplierLabel = ir.supplierLabel; - } - - if (idxYear < yearsNum) { - meterReading.yearValue[idxYear].valueSupplier[idxSupplier] += valueSupplier; - meterReading.yearValue[idxYear].supplierLabel = ir.supplierLabel; - } - } - - for (int idxDistributor = 0; idxDistributor < 4; idxDistributor++) { - double valueDistributor = ir.valueDistributor[idxDistributor]; - - if (idxWeek < weeksNum) { - meterReading.weekValue[idxWeek].valueDistributor[idxDistributor] += valueDistributor; - meterReading.weekValue[idxWeek].distributorLabel = ir.distributorLabel; - } - if (idxMonth < monthsNum) { - meterReading.monthValue[idxMonth].valueDistributor[idxDistributor] += valueDistributor; - meterReading.monthValue[idxMonth].distributorLabel = ir.distributorLabel; - } - - if (idxYear < yearsNum) { - meterReading.yearValue[idxYear].valueDistributor[idxDistributor] += valueDistributor; - meterReading.yearValue[idxYear].distributorLabel = ir.distributorLabel; - } - } - + sumIndex(IndexMode.Supplier, meterReading, ir, idxWeek, weeksNum, idxMonth, monthsNum, idxYear, + yearsNum); + sumIndex(IndexMode.Distributor, meterReading, ir, idxWeek, weeksNum, idxMonth, monthsNum, + idxYear, yearsNum); } } } @@ -1083,27 +1182,6 @@ private void initIntervalReadingTarif(IntervalReading irs) { return meterReading; } - private void checkData(@Nullable MeterReading meterReading) throws LinkyException { - if (meterReading != null) { - if (meterReading.baseValue.length == 0) { - throw new LinkyException("Invalid meterReading data: no day period"); - } - } else { - throw new LinkyException("Invalid meterReading == null"); - } - } - - private boolean isDataLastDayAvailable(@Nullable MeterReading meterReading) { - if (meterReading != null) { - IntervalReading[] iv = meterReading.baseValue; - - logData(iv, "Last day", DateTimeFormatter.ISO_LOCAL_DATE, Target.LAST); - return iv != null && iv.length != 0 && iv[iv.length - 1] != null; - } - - return false; - } - private void logData(IntervalReading[] ivArray, String title, DateTimeFormatter dateTimeFormatter, Target target) { if (logger.isDebugEnabled()) { int size = ivArray.length; From 582f9d5a33ac54549ab85dd1ffd5f7f44075dff8 Mon Sep 17 00:00:00 2001 From: Laurent ARNAL Date: Sun, 21 Sep 2025 10:31:41 +0200 Subject: [PATCH 23/34] spotless:apply Signed-off-by: Laurent ARNAL --- .../org/openhab/binding/linky/internal/dto/IndexMode.java | 2 +- .../linky/internal/handler/ThingLinkyRemoteHandler.java | 4 ---- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/IndexMode.java b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/IndexMode.java index 7db0df22dc01d..ecd28420d754c 100644 --- a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/IndexMode.java +++ b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/IndexMode.java @@ -20,4 +20,4 @@ public int getIdx() { public int getSize() { return size; } -} \ No newline at end of file +} diff --git a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/handler/ThingLinkyRemoteHandler.java b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/handler/ThingLinkyRemoteHandler.java index ede45bd714e24..32f6ef1334b90 100644 --- a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/handler/ThingLinkyRemoteHandler.java +++ b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/handler/ThingLinkyRemoteHandler.java @@ -656,7 +656,6 @@ private void handleDynamicChannel(MeterReading values) { updateThing(editThing().withChannels(channels).build()); } - } /** @@ -726,7 +725,6 @@ private void updateEnergyIndex(IntervalReading[] irs, String groupName, IndexMod updateTimeSeries(groupName, sanetizeId(label[idx]), ir, idx, Units.KILOWATT_HOUR, indexMode); } } - } private void updateEnergyIndex(IntervalReading[] irs, String groupName) { @@ -834,7 +832,6 @@ protected void sendTimeSeries(String groupId, String channelID, TimeSeries timeS * * @return the report as a list of string */ - public synchronized List reportValues(LocalDate startDay, LocalDate endDay, @Nullable String separator) { return buildReport(startDay, endDay, separator); } @@ -1230,5 +1227,4 @@ private boolean isLinkedPowerData() { private boolean isLinked(String groupName, String channelName) { return isLinked(groupName + "#" + channelName); } - } From b2b3775287ce68ef19b7f877c6a9feec132a4eb4 Mon Sep 17 00:00:00 2001 From: Laurent ARNAL Date: Sun, 21 Sep 2025 10:33:53 +0200 Subject: [PATCH 24/34] mvn spotless:apply Signed-off-by: Laurent ARNAL --- .../main/resources/OH-INF/thing/group-linky-remote-daily.xml | 2 +- .../main/resources/OH-INF/thing/group-linky-remote-weekly.xml | 2 +- .../main/resources/OH-INF/thing/group-linky-remote-yearly.xml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/bundles/org.openhab.binding.linky/src/main/resources/OH-INF/thing/group-linky-remote-daily.xml b/bundles/org.openhab.binding.linky/src/main/resources/OH-INF/thing/group-linky-remote-daily.xml index 1f66f743741c2..50f13c3ce95fc 100644 --- a/bundles/org.openhab.binding.linky/src/main/resources/OH-INF/thing/group-linky-remote-daily.xml +++ b/bundles/org.openhab.binding.linky/src/main/resources/OH-INF/thing/group-linky-remote-daily.xml @@ -26,7 +26,7 @@ The energy consumption - + diff --git a/bundles/org.openhab.binding.linky/src/main/resources/OH-INF/thing/group-linky-remote-weekly.xml b/bundles/org.openhab.binding.linky/src/main/resources/OH-INF/thing/group-linky-remote-weekly.xml index 6e8c7af72f4e6..8c06e8ada42d7 100644 --- a/bundles/org.openhab.binding.linky/src/main/resources/OH-INF/thing/group-linky-remote-weekly.xml +++ b/bundles/org.openhab.binding.linky/src/main/resources/OH-INF/thing/group-linky-remote-weekly.xml @@ -23,7 +23,7 @@ The energy consumption - + Maximum power usage value diff --git a/bundles/org.openhab.binding.linky/src/main/resources/OH-INF/thing/group-linky-remote-yearly.xml b/bundles/org.openhab.binding.linky/src/main/resources/OH-INF/thing/group-linky-remote-yearly.xml index 2d02b6a6d2ad0..cccbd8b595ea3 100644 --- a/bundles/org.openhab.binding.linky/src/main/resources/OH-INF/thing/group-linky-remote-yearly.xml +++ b/bundles/org.openhab.binding.linky/src/main/resources/OH-INF/thing/group-linky-remote-yearly.xml @@ -23,7 +23,7 @@ The energy consumption - + Maximum power usage value From c679e332ed8eb710854424d0ae44a378e562a2b2 Mon Sep 17 00:00:00 2001 From: Laurent ARNAL Date: Sun, 21 Sep 2025 10:43:00 +0200 Subject: [PATCH 25/34] fix sat errors Signed-off-by: Laurent ARNAL --- .../binding/linky/internal/dto/IndexInfo.java | 18 ++++++++++++++++++ .../binding/linky/internal/dto/IndexMode.java | 18 ++++++++++++++++++ .../linky/internal/dto/IntervalReading.java | 2 +- .../linky/internal/dto/MeterReading.java | 9 ++++----- .../handler/ThingLinkyRemoteHandler.java | 5 ++--- 5 files changed, 43 insertions(+), 9 deletions(-) diff --git a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/IndexInfo.java b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/IndexInfo.java index cbae5987bc90e..a39400899172c 100644 --- a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/IndexInfo.java +++ b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/IndexInfo.java @@ -1,5 +1,23 @@ +/* + * 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.dto; +/** + * The {@link IndexInfo} Will contains data for an given indexr + * + * @author Laurent Arnal - Initial contribution + */ + public class IndexInfo { public double[] value; public String[] label; diff --git a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/IndexMode.java b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/IndexMode.java index ecd28420d754c..8b8d2bdceb5cc 100644 --- a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/IndexMode.java +++ b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/IndexMode.java @@ -1,5 +1,23 @@ +/* + * 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.dto; +/** + * The {@link IndexMode} represents the index type : Supplier or Distributor + * + * @author Laurent Arnal - Initial contribution + */ + public enum IndexMode { None(-1, 0), Supplier(0, 10), diff --git a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/IntervalReading.java b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/IntervalReading.java index 1beca9eda3e5d..248843c3fc491 100644 --- a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/IntervalReading.java +++ b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/IntervalReading.java @@ -26,7 +26,7 @@ public class IntervalReading { public IndexInfo[] indexInfo; public LocalDateTime date; - public void InitIndexInfo() { + public void initIndexInfo() { indexInfo = new IndexInfo[2]; indexInfo[0] = new IndexInfo(); indexInfo[1] = new IndexInfo(); diff --git a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/MeterReading.java b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/MeterReading.java index a80bad8998f9a..8bc471c76348f 100644 --- a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/MeterReading.java +++ b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/MeterReading.java @@ -111,7 +111,7 @@ public static IntervalReading[] fromAgregat(ConsumptionReport.Aggregate agregat, result[idx - 1].value = value - lastVal; // The index in on nextDay N, but index difference give consumption for day N-1 result[idx - 1].date = dataObj.dateDebut.minusDays(1); - result[idx - 1].InitIndexInfo(); + result[idx - 1].initIndexInfo(); if (dataObj.classesTemporellesSupplier == null) { dataObj.classesTemporellesSupplier = agregat.datas.get(idx - 1).classesTemporellesSupplier; @@ -123,23 +123,22 @@ public static IntervalReading[] fromAgregat(ConsumptionReport.Aggregate agregat, } } - InitIndexValue(IndexMode.Supplier, dataObj.classesTemporellesSupplier, result, idx, calendrierSupplier, + initIndexValue(IndexMode.Supplier, dataObj.classesTemporellesSupplier, result, idx, calendrierSupplier, lastCalendrierSupplier, lastValueSupplier); - InitIndexValue(IndexMode.Distributor, dataObj.classesTemporellesDistributor, result, idx, + initIndexValue(IndexMode.Distributor, dataObj.classesTemporellesDistributor, result, idx, calendrierDistributor, lastCalendrierDistributor, lastValueDistributor); lastVal = value; lastCalendrierDistributor = calendrierDistributor; lastCalendrierSupplier = calendrierSupplier; } - } return result; } - public static void InitIndexValue(IndexMode indexMode, ClassesTemporelles[] classTp, IntervalReading[] ir, int idx, + public static void initIndexValue(IndexMode indexMode, ClassesTemporelles[] classTp, IntervalReading[] ir, int idx, String calendrier, String lastCalendrier, double[] lastValue) { if (classTp != null) { for (int idxClTp = 0; idxClTp < classTp.length; idxClTp++) { diff --git a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/handler/ThingLinkyRemoteHandler.java b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/handler/ThingLinkyRemoteHandler.java index 32f6ef1334b90..499baf7862f59 100644 --- a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/handler/ThingLinkyRemoteHandler.java +++ b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/handler/ThingLinkyRemoteHandler.java @@ -647,7 +647,7 @@ private void handleDynamicChannel(MeterReading values) { addDynamicChannelByIdx(channels, chanTypeUid, IndexMode.Distributor); // If we have channel change, update the thing - if (channels.size() > 0) { + if (!channels.isEmpty()) { Thing thing = this.getThing(); for (Channel chan : thing.getChannels()) { @@ -679,7 +679,7 @@ private List splitOnTariffBound(@Nullable IntervalReading[] i if (ir != null) { String tarif = String.join("#", ir.indexInfo[indexMode.getIdx()].label); - if ((!tarif.equals(currentTarif) && !currentTarif.equals("")) || (idx == irs.length - 1)) { + if ((!tarif.equals(currentTarif) && !"".equals(currentTarif)) || (idx == irs.length - 1)) { IntervalReading[] subArray; if (idx == irs.length - 1) { subArray = Arrays.copyOfRange(irs, lastIdx, idx + 1); @@ -1030,7 +1030,6 @@ public void sumIndex(IndexMode indexMode, MeterReading meterReading, IntervalRea if (idxMonth < monthsNum) { meterReading.monthValue[idxMonth].indexInfo[indexIdx].value[idxIndex] += valIndex; meterReading.monthValue[idxMonth].indexInfo[indexIdx].label[idxIndex] = label; - } // Sums day to year From 38b58e0a5d01d054725060e74fdcc3ef7a3c20c3 Mon Sep 17 00:00:00 2001 From: Laurent ARNAL Date: Sun, 21 Sep 2025 11:10:42 +0200 Subject: [PATCH 26/34] review documentation to add new index Stuff Signed-off-by: Laurent ARNAL --- bundles/org.openhab.binding.linky/README.md | 274 +++++++++++++++++- .../doc/GraphConsoWithTarif.png | Bin 0 -> 98984 bytes 2 files changed, 270 insertions(+), 4 deletions(-) create mode 100644 bundles/org.openhab.binding.linky/doc/GraphConsoWithTarif.png diff --git a/bundles/org.openhab.binding.linky/README.md b/bundles/org.openhab.binding.linky/README.md index 269541263c29c..2f3b62021f6ba 100644 --- a/bundles/org.openhab.binding.linky/README.md +++ b/bundles/org.openhab.binding.linky/README.md @@ -17,9 +17,9 @@ Step are: ``` java Bridge linky:enedis:local "EnedisWebBridge" [ - username="laurent@clae.net", - password="Mnbo32tyu123!", - internalAuthId="eyJhbGciOiJBMTI4S1ciLCJlbmMiOiJBMTI4Q0JDLUhTMjU2In0.u_mxXO7_d4I5bLvJzGtc2MARvpkYv0iM0EsO6a24k-tW9493_Myxwg.LVlfephhGTiCBxii8bRIkA.GOf9Ea8PTGshvkfjl62b6w.hSH97IkmBcEAz2udU-FqQg"] { + username="myUserName@myDomain.com", + password="MyPassword", + internalAuthId="zeJhbGciOiJBMTIdaqzerZiLCJlbmMiOiJBMTIcwxdsq..."] { } ``` @@ -251,6 +251,40 @@ The retrieved information is available in multiple groups. | contactMail | The usage point Contact Mail | | contactPhone | The usage point Contact Phone | +#### Dynamic Thing Channels + +#### Dynamic Thing Channels + +Add-ons now support reading consumption indexes from the Enedis website. +This makes it possible to view consumption for different tariffs such as *heures pleines / heures creuses* or *tempo*. + +To handle this, add-ons will create a new set of channels for daily, weekly, monthly, and yearly groups. + +You will have two different sets of indexes: + +- **Raw consumption indexes:** + These are the default indexes returned by Enedis. The naming uses base indexes, so there is no direct way to know which tariff each index corresponds to. + Channels will be named as follows: + + + + consumptionSupplierIdx0, consumptionSupplierIdx1, ..., consumptionSupplierIdx9 + consumptionDistributorIdx0, consumptionDistributorIdx1, ..., consumptionDistributorIdx3 + + In France, the distributor is most often Enedis — they are responsible for distributing electricity on your network. + The supplier is the commercial company with which you have a contract (EDF, TotalEnergies, etc.). This is where your specific supplier tariff is defined. + +- **Named consumption indexes:** +To make things simpler, the add-ons also expose tariff-named channels. +For example: + + daily#heuresPleines, daily#heuresCreuses, daily#bleuHeuresCreuses, + daily#bleuHeuresPleines, daily#redHeuresCreuses, ... + +⚠️ **Warning:** +Dynamic channels and indexes are currently only supported with the **EnedisWebBridge**. +Support for other bridges will be introduced later, once Enedis provides an API to access this data. + ### Full Example #### Remote Enedis Web Connection @@ -273,7 +307,7 @@ Number:Energy ConsoAnneeDerniere "Conso année dernière [%.0f %unit%]" ### Displaying Information Graph -Using the timeseries channel, you will be able to easily create a calendar graph to display the Tempo calendar. +Using the timeseries channel, you will be able to easily create a chart to show the consumption graph. To do this, you need to enable a timeseries persistence framework. Graph definitions will look like this: @@ -354,6 +388,238 @@ slots: nameLocation: center ``` +### Displaying Information Graph / New version with tarif + +Using the timeseries channel and new version of the addons, you will be able to easily create a chart to show the consumption graph with tarif differenciation. +To do this, you need to enable a timeseries persistence framework. +Graph definitions will look like this: + +![TempoGraph](doc/GraphConsoWithTarif.png) + +Sample code: + +```java +config: + future: false + label: Linky Melody Conso Monthly 2 + order: "9999999" + period: Y + sidebar: true +slots: + dataZoom: + - component: oh-chart-datazoom + config: + type: inside + grid: + - component: oh-chart-grid + config: + containLabel: true + includeLabels: true + show: true + legend: + - component: oh-chart-legend + config: + bottom: 3 + orient: horizontal + show: true + type: scroll + series: + - component: oh-time-series + config: + barGap: -100% + gridIndex: 0 + item: Linky_Melody_Monthly_Conso_Month + label: + formatter: =v=>Number.parseFloat(v.data[1]).toFixed(2) + " Kwh" + position: top + show: true + name: Consumption + noBoundary: true + noItemState: true + service: inmemory + type: bar + xAxisIndex: 0 + yAxisIndex: 0 + - component: oh-time-series + config: + color: "#1010ff" + gridIndex: 0 + item: Linky_Melody_Monthly_Supplier_Conso_Month_Heures_Pleines_Bleue + label: + formatter: =v=>v.data[1]!="0"?Number.parseFloat(v.data[1]).toFixed(2) + " + Kwh":'' + position: inside + show: true + name: Bleue HP + noBoundary: true + noItemState: true + service: inmemory + stack: total + type: bar + xAxisIndex: 0 + yAxisIndex: 0 + - component: oh-time-series + config: + color: "#f0f0f0" + emphasis: + disabled: true + gridIndex: 0 + item: Linky_Melody_Monthly_Supplier_Conso_Month_Heures_Pleines_Blanc + label: + formatter: =v=>v.data[1]!="0"?Number.parseFloat(v.data[1]).toFixed(2) + " + Kwh":'' + position: inside + show: true + name: Blanc HP + noBoundary: true + noItemState: true + service: inmemory + stack: total + type: bar + xAxisIndex: 0 + yAxisIndex: 0 + - component: oh-time-series + config: + color: "#ff7070" + emphasis: + disabled: true + gridIndex: 0 + item: Linky_Melody_Monthly_Supplier_Conso_Month_Heures_Creuses_Rouge + label: + formatter: =v=>v.data[1]!="0"?Number.parseFloat(v.data[1]).toFixed(2) + " + Kwh":'' + position: inside + show: true + name: Rouge HC + noBoundary: true + noItemState: true + service: inmemory + stack: total + type: bar + xAxisIndex: 0 + yAxisIndex: 0 + - component: oh-time-series + config: + color: "#d0d0d0" + emphasis: + disabled: true + gridIndex: 0 + item: Linky_Melody_Monthly_Supplier_Conso_Month_Heures_Creuses_Blanc + label: + formatter: =v=>v.data[1]!="0"?Number.parseFloat(v.data[1]).toFixed(2) + " + Kwh":'' + position: inside + show: true + name: Blanc HC + noBoundary: true + noItemState: true + service: inmemory + stack: total + type: bar + xAxisIndex: 0 + yAxisIndex: 0 + - component: oh-time-series + config: + color: "#7070ff" + emphasis: + disabled: true + gridIndex: 0 + item: Linky_Melody_Monthly_Supplier_Conso_Month_Heures_Creuses_Bleue + label: + formatter: =v=>v.data[1]!="0"?Number.parseFloat(v.data[1]).toFixed(2) + " + Kwh":'' + position: inside + show: true + name: Bleue HC + noBoundary: true + noItemState: true + service: inmemory + stack: total + type: bar + xAxisIndex: 0 + yAxisIndex: 0 + - component: oh-time-series + config: + color: "#ff1010" + emphasis: + disabled: true + gridIndex: 0 + item: Linky_Melody_Monthly_Supplier_Conso_Month_Heures_Pleines_Rouge + label: + formatter: =v=>v.data[1]!="0"?Number.parseFloat(v.data[1]).toFixed(2) + " + Kwh":'' + position: inside + show: true + name: Rouge HP + noBoundary: true + noItemState: true + service: inmemory + stack: total + type: bar + xAxisIndex: 0 + yAxisIndex: 0 + - component: oh-time-series + config: + color: "#00ff00" + emphasis: + disabled: true + gridIndex: 0 + item: Linky_Melody_Monthly_Supplier_Conso_Month_Heures_Pleines + label: + formatter: =v=>v.data[1]!="0"?Number.parseFloat(v.data[1]).toFixed(2) + " + Kwh":'' + position: inside + show: true + name: HP + noBoundary: true + noItemState: true + service: inmemory + stack: total + type: bar + xAxisIndex: 0 + yAxisIndex: 0 + - component: oh-time-series + config: + color: "#80ff80" + emphasis: + disabled: true + gridIndex: 0 + item: Linky_Melody_Monthly_Supplier_Conso_Month_Heures_Creuses + label: + formatter: =v=>v.data[1]!="0"?Number.parseFloat(v.data[1]).toFixed(2) + " + Kwh":'' + position: inside + show: true + name: HC + noBoundary: true + noItemState: true + service: inmemory + stack: total + type: bar + xAxisIndex: 0 + yAxisIndex: 0 + tooltip: + - component: oh-chart-tooltip + config: + confine: true + orient: vertical + show: true + smartFormatter: true + visualMap: [] + xAxis: + - component: oh-time-axis + config: + gridIndex: 0 + nameLocation: center + splitNumber: 10 + yAxis: + - component: oh-value-axis + config: + gridIndex: 0 + name: kWh + nameLocation: center +``` + ## Getting Tempo Calendar Information ### Tempo Thing Channels diff --git a/bundles/org.openhab.binding.linky/doc/GraphConsoWithTarif.png b/bundles/org.openhab.binding.linky/doc/GraphConsoWithTarif.png new file mode 100644 index 0000000000000000000000000000000000000000..1b044d3a47c6f29d1f70af48c97953c2b982a3b6 GIT binary patch literal 98984 zcmeFZby$>bw>OM|C<@#L1|hc!(xB39Q_|fkl0$cojiP{(N+Uf33^gDip(qH_Fdz*A z0yEMK9q+o}uIJtR+sCut?>+WE-#3TB4A-2w&K1A)Tjz6CWmy_(25Jfl3L1I2>*^E~ z`&20?_Jr);2cI0YiLHnKb~&rdUZu!xWSW3Cdo8XgU7?`J4L-PjhZ5c&aFEk;rl6p$ zLjLZmu}{B8L9vx0fBlN4hw-mIUqb?UV{4ZEp&`qI%ZGC{1?lVNcAIy9&U?{S@C4=k9ZU1s2)P85b)-F!f(IV>|qj%8({gw z-WYwb!P7CUX2VUQNF3L>Hoxifn>9(&vtq!HKoB~;3&ynls%&lK`SWLr0@uA9f4$ka z``BMU@7aY+efxF)0R=Ll?N{aH`@8@A`S$OZ|7!HYZwY^XOmT8QtkfT`L;s=qnAv{c zC8H;g_mcYD2gn$2$GTIL-nSR-eDGS5Hu>`9i#dtP$|JS8`L`^? z;#M2f{7 zLv+11mZAK_3By;(uU_4D9lzaHVAZB+WMoT=EP}Fx&d*EX1K+Dit}|PHrnLE*TWgM3 zlgP-FfCG=TA|uUcB__6~@6h2?+E>@qq;u!akk#7luHjkB*&iZNKF_vbmbDWuUId@6 z#B2V7o^vcNJ+?*iA)6~r=!wxtlMXE%qm=G$edqU09@Aw@nwpv;T5CisuE25lCa(K= z4yu!HV}xCYl#v!EXu#gPxv@qLMoT6Z6bSmxd{hWzme9^Omr6u$tquG{wxN=JdFg}P z>r*CN#D>@aFXyyRn+p%clJ#~y=M3<%O4@xygY}%DSl1t8rry85xwlw_OJYs&OHEDm zq{cgw3cuCyq~HSR7F%dPAkAyM{#0E`!!Uwd-CNv8ATBP!QE*G|tsTivXXSIyVXocX9AH=al*y?sYCDf;8uQ*?-8DyK^VexaS2(tU9y! z_b3~&pAMo6bKmB}V`4G~)_?M?tgU&I8Np>a;0i;Jt3ip3|vak!dO zQnDVk-f4o)YDtpiTi*tnUb~{|K*qGq4gQXD-?ik`>DiH5^bAJGa-h8I?AfzfOMH=vc0~q< z4<9xj2C=#?aAlacoQwTX$57t1&TAIFKJ1>*Y*i!U2-?j7d(Hd>y<%yTlsQK4ui?e@ z3>sp0a)e~b6)cggp0uZ%A5i(7AA3QE{SkSBSh+_gbZl@JYAWdrqD?MG&vzJn8=0QQ zsmN?-o-lHG-P+cMx9t(eU4#M9NeOC23)^;l68YX#m~SO>6TdV2S?t zaSSTExSG^medOp-S6C*}WYK`St1F&JBoQc)VX*pcoL>>uMnb=qwXj`6j|2pJgxwW~m`i0-F zrN7LZ+>OFb;5&^wC8j@^+A>Cj%$;+c>eZXB*ou=p9ZcW9Fg;-Mh>C8pB(iEq6{Nyz zX{zrB)k1i9_$nD^--*$}3flKeW|>qNpD^*s^&?|VJY#3`yqE0C$Q=gyg}-<=hUh}c zt0de+ph&-}l2YL2jQ;>?tT9$U|GWmN6Q`r883r>8TE!Q#!j@ZES#`gkb)Nj5do85E zt}l(6mewxM`e(F#dCQxNNqBer*AaY1sc^L9cI}!Z&&O{Wn)to74Ma^?j0JFKH^X^J zdiClzspUb<(NSB%lM{=f#EPFQ^J5d_&Jz=K=r_npo7jJty_r<1xVgDmvbjoHfHS}? zvG#Q%ARu6&-+Ou@HQFy*Iab^=(Ee9*;x(@y&(Y~0J{X!-_gWSrrXqf@KlcXxFkj)- zpr4NG*i z=z_GrzRy5Z*x5Lfv}^I`L05ByzD^UaA5X}{nPr2ygQP(r3)HqIYyN5I)@4>Q7UwN= zyYjM9oTT39=ztzej)-odO~xk7eY|)qPRq$D07P5R?N`?7`aBQ)v172oLns^c%i!ScFQ2b}Yid#% z8oEPl&j{@)YUtURX)2O?$1)jxaBr>`Ci;LPC%f@y8^5yRgM}7KmXjO;S``hNjRyj>M7VQ36 z0u@v0yLV?dHm1>?^g4!#sO*&tt%{#fsAHf)>g#X$Zn%~^Ic@f^?zd8g15Zd%Jdvf3 zSK!wEfSc+o710}2)6z?uMKFk0ecF&tn6P?h&Lx%jt~{b`x!7`$f&Kbq@2j&bkz zgoV`b^eis4?ZLq5A`=PUi2jfFa$mTh8Zt6AmS(bkH*;~S5Buwv4XJANXQSBxT3Yp* zpx6R1wJ366M)BH{r%y2&Y06;Q@P*C9=8vi4!bJ z{Bys>e8vK&QH8R#DQ^WuMG0clS!BZ$C8_AX8fl;1n)iy%GX1%EzM_^G5tY(JoGI#Q zNR&Cb?Ofh|_TmLNS9GY+x^SS@Ifg;CA>+{}12d+l7&3s&TZj!=U z#ChI?1JV?gIhWIALnL4${pLdB$4R&%(1=$_uAN5i-hng+4{Cst4UdnXc8#bO8SqJJ zZdP@jIFbg38GJ5ckNuMV#>Sdr+3r^__)6p=4V|7x@S|m49OrEf*D=nJ_FdK8*!TLi zS_z24!jL};G1k4`Zt=q#_ywkSRtw`Th!b4kv*P5tK3f9|n!h``8bpZPGoY*q%iWP#+in(VWKN4{ zQ=d2!U8)t?nz{|8a-9wT$1Iy;vih2wu3*LSzhk=Oo3!lCu*XV4UYBme$O%H z@JO@;t(ZCD2qgteV~U-(QrP2TxPxoX$;ikUp*LRJ;^NSo#ViGVgz=UnEuFCr@RtIa zx)$oga&j(!?nwDm4k8|y6&ySUQHKPu^W^4hbYLKlTYZb* zU4;XU!&PkR7xm7)NXmZcAj+hmqS6_`YtWu;B=M!%ErO_%%QT6Sih7#B!Okw5Bpa;e z;qhMVx{k4DxQ%f1d8Mf2aBdx9ev@>Y)Y7{4u3ekP*jM_)(=5NE(P%TXF7j**3pOF4 zlF3O^%6qXr;mRXH+it-w8(|ujexD0@U?33362vFo`db8lR{R<$6I7`nawKjw^OZ*$ z#(&7DN|E(j!SeAd`WI%3_~SW#pOD3@ttygkg&+OH%MI+xx0vWKuqR{{^<#SMx5B~C zZ8%etX;kXA+BbcEqO)QX4|XH9s9$^zT}?&9kc>$4WDk{aX`hy&*W#~?GuQZ92s$Qx zzqsh+S10YKTfpRqNUe-$fPfbHtq&`@&Lmt*OicW#+UHoqjI3;dHGzxf^!m9z2+kA~ zOO^O5)&s6yz1m*p?GE~QOXK$K1klncrvy&7g>h;`WtzVSEltnNoCSwdXJc(?i73=j z>}&ygpfu{zsI>3O=y9bOk0QSfk_k6Izs|*rEyVA+N&S9n%rvKcp4(MyxFNlbj@h@B zPM3F%R4)G9yLaz+>2#goy{~M@C=2Zx+PHD+GOpA88|fyb7QwwYj7nTuggOOjaIBo1 zobp(0Uyf*ALr@Adh^9OC+iwY4SZPgl^$@WSdT_={<{Jmbvx*|DdfykE|Ja$kpO&@* zS9T_i#?(= z$N3D?y{A^P`qAhw7i|wyA@VbFn-Yo09S(t~PEAb>a%ZwI8riip1^;cl#cRJYrZ?q!eaN*2fzx?+h zihq9q|3?VOf8G4x*sO=7`Qe?1C@7dpQxqeuKR%@FF0cwUmHSHF zW$sw`Fe!n0K$KM8XXlt>nSa4TgQBsRoXfJs$XBy=+EHFC`!C_4?Y5O4txZ>$l)6o} zvd8<}Eo}4xm;1TfWOoXDJYchO=x>}#ITv|ysoxt{S67FalP@;$WC(97(law}As;tA z<@kF}F;@etLghoxPqeAX2>HhdHc8IEberzadMjX|k3W3w<~RhF?a&}@X2t}6sbyQyegGX-p+LQ$1u!GP4iq*)MOhCL{P-oo!Rd{4 z4 zEISxEHwb~t>5PP1v8@F5z!mF%_9E{#iSNYtt}P-Sn+nR~-A$}&i7P3UlwGR)_AXFi5y%nm0t56{-{i)`Lw=y_uVv z>opeZuG~Wm-Is6P(E2^s6r3#_JYR1UfBYW{y}SI_yG-LU{n61;E`I(_fYllnIY(a% zJ^y?2MO}S3ug_AJGsHdn>%`vIc=6lleiK?1*4n1wS&#a&e~DTNpHa!16w}38_V{%7 z>3;q94vC;lrStnT%epw9h6h*uPD-NHFO_ZZhb z3%+<8NXfL8M8}wD*LHXRJQQ#~%_owrCXKTITkM!3=ucml=vunFt-iNAs{!qN5%J3F z=j0s`oS`;^&0Oz?m@@T0M+4`(zE!@hyG)-HYnj!D#rHpV%bOmJkBf`5JN~D-t!!aC zf4&aF)))Nc7YugJJ4j#;kAX+f(cr&FwafzNUGv~^o@Q_yMx2W47ab2AI@DhIU{5$; zX#g5UPnyp%vD5xj=?eT;Fz(K({X0tf-_envwrfm}w+M$t>R2{b#A%p~?z(Z^rFmpv zAS*px&iPH}`-FtcedOpKf55k@}a^#mQ-A;30@@Wh_ z@M>p1evG;x-JtK-T7j0)(mt)~subAyzRdfDmWBbA#Y9{;#N{KUFMZ3$RA$O2HGO}L zrt@&Z<&d!0*dzdlI9b1YyUKZbKDjI5TJSL9)!=QIkgA$mt>e<4oy`FDZu>px;Z>=b z+a-Q7D`&t#Z6$YZd}eC39(4R5_2+Xw%>G}mg)B7A(S^lV7*%Xcdf>D;7WwnSp{E1G zZJFJQ^T#=a3qybWxEqY^G*oR%c>i8^UG5LV_bU1Qd(r&8F_-poydoA6y?z$2tbXA& zZ5K!^)ha^2&Jsd(=zUax8OC{<$33O1&)a(>z<3S^Nrah8#QGawQavEoRZ>62BbY&l6$*(i%ZB^ zyRVTB;Sp8`r)_SY`pc~p9uRURYI1vgt*?i$;9%A!M7+n29yK+R@SMBX@aEz*w^Aec zlaY}jJ1L~Cs+s`Klb&gu@1h={SPCJF(*rmJV_7l=F9<^S2F|FjU%#pXu7My0B)x5; zCfas50mcw>_2R{g5WE*%eUY5Z3K90?K!piFFVi39gCI=5M`rW}G}DEkr6a^>UCm2 z{JcCRkQvTCF)?A>B5?KwVf(PA$r~HTAxmdgckIjO&)XRU zaL!ye$erKpicsL%d(Ah9ghxiQLx%`>0lr{ppIyWL>&y2xvjgE|b zop90LrB-Cu$149VlE3rW;dA$jdW5W6S$PfL=g5z5Yst!qZ8z6};oiM_<8j_oE#M|k zx@yZSCv=41CZ~N7ikL(8O$#Y^6D>o;nnw3=sw6UG`B;Qfc){a;ulPqk>NE%Mh z#2b2JqLsxqZ5y4+jO&Q!?GON>55P-8?U_kt2{%muZ92d20|0~ATb!>K5!5R2R^07Z zIg{ediid$9fC;I`Q=g}-{00Jy#OojZ2|o5=9Xd0T4s>6@N9A z4GwC_#$pd>3-c#@a4Z-kJ#sWz4#8<7|LSTJw>iDNOeSMHA*ra*|ASo**>*7~V=nz2 z`BCgd+sh2Wva5>*6Z1{M>-gD+Ll`bAlD1VR-$Zj_wT>q?L+ny&YV|KUzQ_XKwb#WY zV*S+C)XW{L;hR>AHWSa;lj*tj(c0UC@c9i zYt<@4g~jUYZy9LxM5|&-CeQT!ln;ihB$E_CT`!$Kb0tbxiHlp}($Y~CxiW7>PR`aR zClopj0e%wk+YP|sxw3Q)K$P-&QkMEdy%xj7-6J4LbgrpAq{=^qOwuBh7SSGJ;Vau% zeBF%jKOp_Ne`h%98LxjfLOw5)l;5P8YZr^TbSY@TkZ;eug-z0givm*<$ywG;8EXzPk%I1xr zMdRym-oW>Mb4RtlPh#RO49UsOH(wK<=|*94dPbc@qfENC-MmV@$4CrSd7`}oll{Rk ze7b^v%w&s8#(#LNzaI=iij#8m`3elz4Yu=KzslD}ly7O0gZ)^2S0`R;v3&XBq@{yh z43aojVDt9fQyPtCM*FX>lXxdWd%7J-TWf1Jc&@2YyM1Mh+p_Btq2JNp@Z(3>LP}In z)fuUUR~Ud6J#B=aY)z<8_m&&geSPIN71og*Zr%_Q=c~=d@k)Aq=4*!$s?$%V<|$2U zvV7|_n%{S>wzf<7(Dqs$-3F>FW%t_jq&_=y?N}Esew_igzpF?kd*mLo^cWK!QlwsM zwv)q zu<4`4w6omC*M|@AY(MR`wUX?itt*uFW>#aozC%SQd z6`M@f+CFR~D<>q*JfcF0ci^{$J0(sM4x=nSI%+ zPp0};%%E@RZKG^w6;Df>se%lmjMuK8<<%&`;Svzt8FSS336avUz6W+WG) z6QrZHnGcw-5{$eu>JCzCde1k^adWCYOER3|Gh&q=qF-w#eG4{2xx@!~&k>eh(6x(@ zd|kJ!W4j5($qBzRm;9h!v9REFX=M5`+50ZtxAa(!Rj6ccE_J5cxPwBT|$9bng>5(7)4bl-KMoK`!=B0+@44;C$KUf zFzznu99q@l+?KWF9dAtF`e18Hz^_gfwF3tNh&?)Sw9Lr#I^UE$*h9!pmpkM9Q@68# zn=7Q$&-+$zLuyIlHMU?`*K7WDrule=%;oSf4$Dm+LQ3*iG3V5gv9!t*#ZeutlxvYD ze6ZWy==I$0$^I;8Z1njv%eh;vuLrkjU-tzLZ2Ow0kgvh$cMtd(K%*F^s(o6#x(WA{ zqFqG!lnJ5iiXkvDl7nH)dc}tJ1N#c!WVb4skobYu6$;vAY%n?wM12G?m^F|<1d(nD z?Hmph>XbI!tnTU|wC)pUw?18U-r=1!iZY$0i~n?Hj^cjdkN72YO5GeqKBP(+HU!rW zuzk27@5jcr>hsg9>!@A3iqmFBT${aGUrMAxNHfo2xTRaS zng)3Nu7npEuxU6|VELLl-Y{j)nTsXMt9+PZ5+COURX-{^u7x4lf;%KJ{T_>$-c4Cr z*J;CCrHW@Ox^ZjM;R7T^a!b@@qd77vufccx>qaC`B--(H?@_;$gra`+&3qIgE3Kk? zWAe(1mKLY|4@IYaH@G~B7d-Ep;+;RqkO!P`t>kM z*8khIh(?gjK=`ZQw+@R~b`7=Yiw*L37vTKG4OdOI*6&30D|p^AX>8K)H}*Xi{wjgC zY<0YX-?rr)i%g2UpP&XwW`Se<;U*&fGnI*}9%C!|7jH+^on6ywQl;-UiW|iqdmFwbv%oR_5JMCF4<1s=Jjy6puvB^8It??nnjYFs24Wj%~ZO4Mb z1VpumtcKf%h}codQ{wHcf3vG5$64qnb$Ty8ke=Po!`8_xn4tSehQZLGUFHOW6Qg@@wJ#Uv}-Ha z`VON37p8kp*lEahuNt*I&dTF=-RT}kvFvzk?E`4C?SyXsNb!Hh zdffT$|F=MZWlx|%>0R%+y>-^$;AWXz$gyL`(o6lEOTEs!i1<&$j*><;f)pl#p4vkO3p2DF>~PGjtX=C>+Wi4YO>11!vY4C zoK@6e!Wd|%t4l0OZ1~Ows?oG$Wh`l7>yj~0F`kHNvazkXjM7(;abm#}CeEkZd^M)j zB8C*xBA$CFoYdOQUgxOR8my-bddx#sfT zOUJRPh1xRZE;+0d4-c+#y?dl2K}~Hi`OhYPmq#G2ZvDu{=K%{CozC|3yt(_w*S%jO z$e&bdT4*I0&e^l#&}%XQoN!BT!)$fi{+RRr=bCJ`8%08w1O$Rx zsJ_(aup!4iWBJ<=rp&d}UQ^qF@DRz%^Cp|YFd==KYuhui_g=hbIoW+L%P2Zi%KK7g zNzbIFa^rPuWPJRTtC62y2?3QokTCnc+B8TQZ3+*2keP!dZNFdmMDpD3P&C@%nGfR< zfRF`?Z8SA5m8C?TSjqWbpN$_87dynbj#j7XZAAX$Rkg8U2j*_!_tMRhcwHe4SRiYu zGa>8s$0;Z*!pBS&(n}<-nwh=DWb#J)cj`|23$=_h` zyS(w4nwB@a>Y{{53q1otMYQ4QpKm=Gq}SEut#c7PR4gk6@KB&Y&d$yPAbLN)?hi$! zGijM9H~bsWkhx^G4##>=F*ocxVtuDPFYi`(c$W5bzmiV0VBA1^GpTF-f%c<_2qxak z&!5XA$ZX|4(nF$&=9~2N^zuh`Q7d!%HQ)jfF$wWqa~*y|YKVv=Mlq1;kIH*F*Gf>+ zQmaSu#kvCa%qDN*izkq9Ck&g5wgtr4mCQ#Vh~3L)d&^tgr;MzSrR76%|Io62v`Cs2 z?EL7+t+1X+L4K6Bt*v}$?$34JuH0cqO3E+#L(i*Tr!_>kjW~rXkasC*cW~gA0*f{= zm^#8!nsa_L0^8t#0|u5#ZFH2ry9lQ_f%Y}5h+lt2&I1G)#L$a45vgeeoYbhTIdrBU z!1pLEd640ofhF26`gKzWRQ%-ZEAp?pudhbRBFDm>#v*KFREa=&qF(Lx?U!QLCEO(W z-~gA(ha5Y3GAS&~j6t|Q`%cv_YU%)dwcyMWotCO9n13TnN_sOhY+1G7g)w_&`C2C} zZF8*ml6RKAoSX@>`o_k)Bw4u-A`zphznTbn>rn`CpIBQ5 zXsX>Q`neN2Mdmy?5tRe;c=s+uyNOFo@B62N6wTU#MNf2`EnWz=pFO)OQi|8!<9|-Y z1ihgfN|Abp5f>1^Bi~v))}Kfu#uEG1)K=E~PtUJI_Tb6W^Xci9&w=W~Mv|tXs&ywW zXwV;*PWk#(QB!U9ZHaq@4p0a+;-%ZzGHQ<-udGZ^(*g36-N>R9c`Mph{-`o&O@uEk{W0(~v}>b0X$Mrzwzi-2QS zj#^cG(33h^f&h#TM}B>ur4wWFRNt@;efN&7BSOb}sXY7rd&X!fm%dRh{O&_VJsGhQ zV_ZC$X}kBBM#WrftsMH=wz+|A&uCLgNva=+ij!h_#bCQefAYJUkkFUa#U3;_j07eE zJ4z54_yAyz?y&W?U2NPzR?}LF$2RpnJvH!dwt2#KOpt%L7!af!4ywx5_PN_s3>J?H z&C(fu)G1zjjzjx8T+gnVBCeM9Vn)(FRahzokZT67QP zACB1eD0lF72W>81UM+q7sJXeI82)VuqL{gcR6jUG)B%|3xWM80POGP z%ae(K(JIA=sws~xSs33lH`nh(2=dgw11^;1HLL^hs;jLj&C-RnEGZq+Q>RWTsBCTC z3VynB>C&YS)w@K69|s^qn5o+C=Qw~u8UUICA`5@2U!F>`YD)!r8wS8yzz<6z_)YAd zFupj!Uzi;&go-%Mqx*YlUpDZ2jhx3Cf&Z=Ps6mfRVrgEfNTIviKHb*mk?)AnU5^75~%hF(G6w{E?JNz*ehV1SxObL!G{ zz~?%?L&6MeP-Lf+jj9Kl9F)$DbFP=Yd0hAeGDR4_l_F_9>WS=Kotbq=} zAYlFy@U8}7hg-+_jWcsiYwGNZxCI0>*x1;#XPSRw?Vfbw)y=sKa9k>0;oP}%*Wed* z_4PJKM1?au>S}9WH<;&7nORTv(*V;BLDg{jsQvVaxf>io;Nk?DuXS}`J3K;js-VlN zr!)YVKh|V%c>Lr^Em(q1BUgeJz<9GIjnFX<55nv-CbM8t#;ktrK>Zkl!62+i>y^1t{oK1xfb^Z|u-{han!_8Ylqp0?M8ueA zVU6WO{8roIk@bSJf(V?la^u!Q{y=XTqjt)n(~{p&Hj00&b32k99^{FLi^~Sm8>;2q zjh(Ug12PU|*$#i~+qZA)8XG%+Etom^65#Gp*YScrJD}9(6t0M$=9P+^C?RFHA;>uJ z1xZzdAXY$lRMFGZgU$(1-5~_nJd9sIB_#!Dm$}mGgxoi4|*b{for?qH-I550`kci5o)GTYY+bpu>}0MT#7@x-@N183f(UuIfo@H}v&@3{UkI z120Y@c*P<@b(>?P%#47|jaO+Z)cTi2eldb_@?yTd0aXWBi>5db?iNTD6u{XaJ?aAf zq1AMMS%-bEh>vCO%pIFO&fgLu3Kk8sJO>|8CEu(l8^|@4g_0}Mz}9Awy1F{#hLXO2 z*Dag*bP(`qq3nG@nF}Xn{63i1zZt^EhI5`je-l}aPyVp39=2VejL!O3%|6;47BOX8 z)B}YQX0~rZF{Q$42QrDZQaeOl`xkjngn}G1Gc!&Q^xE25fcZOMun3*LyzwiD0pV{;Q$dcwdF7aNM2LQM zX$VI*bSv0EVy(N>9S8HN|F)|P${tE)s~MyFE={p)E&G!NboKQyY>E+<@E8K=H^oUI zH5&$|`FVNAPM`K`NAF|s58&-9oz?cDG585|4eKuf^h%9vmkx6w=A*LqKZn=<(_m-i zv4ePQ$#nS|gA8Phzo)2Pj=OWEu$WcI?@jGm zWyfeJgy>20o?h_|rw!|_cZ#MzioT5AG^NT%VCIBCILaZcFvGXJgJEYbKTrY2!2P3X zg&!2I92kRu(srN@_!!aw2#!v1*>s0uFvAJppTm#e5(IDD|52)Z<9^Wdt&tc!Y9Lc?y}TrNBBZ)f@&r;ov4sfd(FSNEwr(0 z{bm)RlI|KWf%mwRu4k7oIX52fyk3t`KnIV4F<_N~9UPT8$b#8bI-d5rGvcd`|76_SyAM0_oorvH$UuyMx<|hbI2^ z1KLbOE4Q5m_Pc;6$Zm?x@fOV2S~brr4-jA)o*`fv#Wlkjg>}oScRlVx`4~MS)`n`8 zyJCe>hqQ|CJR(Apij`TnxD_K|(I}YqiXpDWPKP{Ft36p+7YI0WJs@>CYg3`6Xf*d%ht#xhI7}UpIp_r{cTD-#0<|bB=eqs*9)&j@uhvf1t7m+?R0U#Yo9ww<+~?K%igMm}@K>xpM;Kn!h0xJoeRO zW4)esdjOS=JO0kNLlq76TLIZQk7Ms$GP>#MYnfT>$$NxtLs)TfFy8HK40_8KI~E_A z21s!hDQdW6XdeQ2E_|e7lP@d~Bu(#uyo)tzbgIAaeO>gQ$(G?@Zm=rTj5K%TP4}{I zcRI+582<#`*?Ea0FF${&-RiMtlb?hlGk?C~>j-Qu`d4Tb&EBJZI~H&T0z=eiDX^i(DWy=6dU z(GbFlha^O4$dMIh@#JfrhNec|?{)Y|QRgM&&S6yy`B3;5FP$}MX;`vh)8>t}T6I-l z(Hc2ES5R^@MC@rNuh zaf85#RzVJ)LrSWTC;m7%E(}7}VGvdzHKd@kou46e0&r`#mksX5_Pf!)23P#T`iX#P!c&$v+%n+a5|q~ zok6hU!FM;DB)5D*oi%yzcxwFuIY^0A$mH}7t5t#%`Q64T@HCQGAo%r(XGq`5_-Dpr zak}VGPl0^g;-u^Qa4mtI^Uwgw+2Q6znPVa{f;s{$B`hSW#hhyu|@lA)hcmBV>?8!#*klDaU9vcg}~-wWhVM=OB{bHQ1=u}E@fHJsSa*Ob%A z9m;03d&NpwzERkf_qhITlG}|NS#e1}kfhwoyjEn0)4erb85v4GBQD6-GMB77LH>ZS zIUG!|Te2h2Fmb3{zdqm8-s`!O8ltEH@@(a)(R)6(2U=ZIJF0~;^>cbAqolkG&70zU zZ#|H^wtog<1+Mewt8LakApB`+hP!v~wpXChP%o}#?t*xv3hVR47Rh-wOrvw>!d3o(D2%;dCP=o5N z9mbHpq3yly3Umb$I6wtV)9MPunF}R+=guY7u2pj zyXJU-HKEPaH?wUUof;UxDCQL|WAt+>F$QCi?e^gW-KrH18mcyt{cso&y+~zIuL|F= z_FIymEMLp+TshBsz<1^f;3Zee%FjbY^kxBF|*b{{N#|ye0 zZ~=jyEC@|QMqIP&frgg`W(ABCr!ajl^3n^8FNYm3-X2%vdEN&YEu@;u^@rVAV zpw&mDz_g++nm#r59HmFS$aqRm_n*xun5h_-MWRqatIzspoh>3iR(c>r-C2=|I899WRZeO;J8^T zJL-dif{cMxT^MJF9#4(e{Svs-;VBK{X4-E*QlstE0UAUgNCKn>Ud zuww|0@%{UEQ|exZa&}~|z%RfvBS00{giMF(rluwm#dfV4pw7PQkiUcKsx(DN;y~5e zkKy5l<4|^_uJq=@74OAgvltV&P{Ej`8wjStV*Y^25uqZc4UDRRq`;RC*+WsllJoFg zwhPkmwRCEdS{jFPgOFsvc|^S9k!FB8K?ejP@*Ok`1F0;BUtwWWk-GgbK!hNQC!GRH zXBO@vfOiN`xCW9YwfNl>O5__IDHTP%2@yJ+97B>20ZtkeE61w)oLkq&pVx^!O9vqx zYU{!8Mq-&bJ?X>_->LQ=Pil_8E22E@xzHpQLyu&32jF&5sDxbH1=|o zzn~ax-zJUT=RbG(0srlT{5$dp|NeJ_E(8|(<8|^sTh!Yt9P;+QWsBc9ed(KR$L5|ZB~H2~`94>()_OC( z9K6=x6g~P{b}MGsA_5V`c-8+f)rY1RO&51f_Vj(^Dbi!Ui&+?tw>4i0TOr1|7|jqX zAQseW9y_x87|TtFefO&>OCf;}1wuwy-u-ta*&j^5o$3A8SO4rh^It8V=q$L@xV9me ze#V`LEoh+A%I9Na?jx?^4_}7**duXRPLGz9XWkC=jbexU=5SR@B`$L=%K*HJHXC`~ z1x9ON-eG!ku7EebISNv*%;6oIR_p)X<6nDek=q4=X#jkk)SIinfUxDqtWcf`)-LT)#f}i z@P%KY-3l=*9+#op)AMCYfnx>0trk&frG^A3@`^cy#JB8!a(RYA&(}^jpULNyM_5BD zw-bQBqKgM=4gr`G^f`WIh?iYD*)#e)kX)i1Lfl6e5nq*9Su_yAO3ABtR%LBbT+n0Y zVqcFY#5qX6n_XSh;1uNh-ecThs!- zkcV^YDS`maOJ02^%xSmz?!*Iez#JAMo;UY-Sgo%)5f<}f_a^p^mg4Xd#J%Z!VtOGX z$_nDCwlzCbb1+A0N@HATuu{8a)>R)g?B!^DE1+epnt%jSlbxAXZa2Qp?qh3kTq34G zWI^q>*maIlMg~QGAt7yyF_}FZspz_gN`HC-gE@A)Nb$-QClilE8~5HsBp7CwGz8aK z^b)-RdPld$&~LA#5&83T7^b!G$=AuvR4kgBS0BDRazs58(Y~Vt58WRMmthq!e4O$Ru-dhz`L(b>#C^2uerC69vv!~0Rj*tnQyZ3O zP6Ez7NO^f4%D#krS6x6YNAYe?*avd96obhS-0->s>uZTMRs5N8NVk12hrFzI=w2%BnaQF* zDOvK=>C=1IzX9$o2zD1P6wt?e&i}MpU!G+E1U1g%#{sw(NU(Zaip7IfeV(FcDJESl_{lIR7@ zS(?lHnBm|P&ZH^?Z+4kSsJz8Z$#+uXHotk9fPm39M!DYO&X(E8sJ-*cc!P>142kcD zhl68ooYhNI@WCH3g7OVRwC5^^Ma|6|pe8|ttjM{tr40BK**(>NNYF$C$o}yw~WB((9X_CE9 zf}xgrN`1VHbwp1=KV5L=iq83{(YCvUC_~o($$MqE%*^)sRRQ#;hG z-ok>Lq7$-!WKKREeLAFy%ql&d11eW_q6Do%=o94f zb_Z6Uxe0d-A-_W5$_l$KzZ1)P!iUt30@)myG9?w2H@SVhzhZrqzwBX*DIlZX+mPMh zHvs+)E^AU!I9#R7G{FIvS6^~Ex!4cHqOkGu^OKi8Q{TAC;-mU{V-*3RNWRX)41yv> zwGheUt=V=$Ws8OrtAme#-q=Yk%dkqreQ$RppVmF6Jc7=~yvV|X;nxtR!Hr@@4j(Cv z$>3j%S8Q$I;AA4(30Z`3xXS1iE1{#q8gO_dJConzEtCet4FZ8cY)SI(YDMm@P~V$AvV>z8@arg(?NHr;#B1t~9!k!;AMSos*_ zo;dRS{gUHMOzPT?dM#bubhze47XGF^d>EA-#0kZrkZyAUf5#HW0L_Q6B7z9Pag>OC z9NueGW@{zbv$%acGi)p#-LK!*stc#n+S(ef$RKfF?!cMgpe&aJo?M=Ftb}`q*jP*w z%vc@?+rS}no9gA1UxkoD(6ae=`H;4WFh~v#15|@JPf+v>X;l25%m&2o@L_zgxACr! zkPuR_oL#ZGdOq-zP_Pw7~)`OB9-VL&BYLt={I(!FP$ z-y>c!iK)Zn=BS-k7bmeG6qclptbpKP9i#zc<7`+|`OYi9Q{`3M}afsLO3b?rVb^Q@GBay{e|B=Cw9i zE$mr7&C*ERxm? zCES3>qK)BQC&(UBCr+Lo7=RkrFDGE)OC>)oA3l2I3YFW|NTK5)LC_odn=7OcEf*CulQH_!EZEsepBkDl1E2F$6^pUSUUfzLzu za-t&HcdfszG1i&U$n|>g>CkpONff9lb+yHC!4(xN;4%61%&|lvx2Cw4$H!i^-^HOi z9gT$|v6?2nLV9EITdzJ<9c7aTrVS>uz5{8mvhpNuwxKJZ{hG4H1Ft1JhIbx_Zgyxy z{97LlxMqu$laH^_W>?ywcno}$(7?(4t-ZhOppc7Y>QkD0ZZ@;Gzw$9j_M{rtl{Iw5*%UE^b2Hyq+D6Uu@X;_(yW~S`3Rx-lejjm~ z6v>`5Z*fY4Zjjz=&71o2k-&6tN zu69y7{p;6z^6EgmdiU-r-0-)=Di3Ta>VqUJTO5JOImi^M_q4*X4>lrulPv!3Z@`QRM7gm3kk)`{jGqA%;SCp;yP0#C}m{z;aXdR2^nqV4q^Qp zH|)JAX)z|ut?*t++viuWm<~{PxJBFpfPwVED>dS$s6OtBkx_V|eS9J?jLIHHjgKF8 z5YtUjA*K3fLt6Q5277gydf7Id^GORA>ky;^gU1*Jf4Zl@kq zpX|-l^z6nf$mUT{)ZC{)RGO`Vs;VsI-pA2q{K)17G2`aMqm$pARXlunPiFY<2#xF6 zIF3mCbW_I*Jw@pUAFU-c898DxLadGMM5ODi4GJ9>F(ApK{Z`vH0{_QMKt-`~jv$D!T zAQ2)IYjP>@vGM^g z^&!9;ww>+e2mfi@`u5`5{l&?t+YvpPv=_C!7Mg_fed;n6Vf%1(6b@ zOF)qlK~jFBWVqS(j2uk3g`0pA4vXulU2qLgggj9WEO5Izo zzDjKyDge)F{Oi*G=S=~4*1zduCm)OscG9CPCk>M2Ovx)Zfx`W*KOLltJJBrtn50=& z%9l3&xWvS0FU)Mlbw@}{9o5{yWgEg!nc>{$iq)K*BkuLM>Kw*z#M&PG=Gt!z8`dy1 zoDnXn$x=dSzUoo9LML7Ry-TuL+rD9X) z<*{gdr5~GiQ&Ryq!I_GR+2x-W2u8|27+3nCE5YDUjiai@EA4uqQkN!)bLTZM1H(8t zm;@5(Vn6~^nQ zWghr!&BM<>#CtmeO~!@af&$$QXb{U>7F;=#=|*_^3h;^J_(JzXt)Wq(>R)y^JaDh1 z;EmQGBkn8hgI4@G_to_E3v_P;pOz`Z)-mi5EeN@@I;d`Q`?5ynkD86meZh^fba86h zWYd@9yob-Y_kXxF{FEsq2~s{MMQH*71cNQVww#MM?3u@lo$%R=$$Qn^V|qh|SDqMj zuj6msRZWuiO7S)lUVER>L8=jV7kM6Ytff?FPTO_C?gJSxVu9$rHMV?hbG&xd;EA{O^?`oN3Ag@@ub^|UOFGrFRw8yzBwzme3+DZ@Zr+p&hweq{0pq`(HC+0a{2ZN? zd!_eHDSs3VVtYRRKSDhYyBMx;hpECyO_qYs<4++sG{PF%psPXy z^50Nuu54zciUT#>iSDnE&27QgE zLYi)y0`Gfn{%`p^8psEm$p#(k5F#k>n}IB(>jWy}_U(*NN4RH>{#RsjzS|f+8bNZuzYL6&y zUrZxSFt(%UU;-tYi-b!Sm^IE!=GgQidQlh{)(@+HNq zG^fc(0W*b+(fF~dRVk_aeL;_vb+TJ4biy_}FYlp-_Iyd@>V>LB#DsVs3XEFQ zx*Loq=H=;AQ+27|nI*ECy8??;Q4xDoEKt1T=C*TjbT_}E@Gbf}sE9!Hby8U!brAnz zhhIazzkq{B0zSXeCLs$r$SReeEyKrGp11z2&@b{tmM=~IDHe^QVrb^>;OV(JhJmtq z%(fRQgM*sT1L-6@RG*dbHOEanu_SCj*Om;R`JuX|W(0D_`7T$YsPSGr=dG&bS?)89 z0yg-__c)%TTPZ{$v{Yh*DBkGRtDDx6rD9sIWi86+2EAREsi=h&>K|o4GI+!bi@nr8 zN0WPCh}0Cg{l?MMUL68Hat^Z;hcLc%JmtUK0P(J=d)L%~dyE)S4YRY-2O>!JI_G^< z_fT)VjL5b)4=N<=`b~o*KUfFkwP%bJ3znaUT?`4PO9hsAu2;Zi!zm47`tr*LwGo0r zEMR4G9{Wio+`XcHfAK0fvSW2(D#soX*!s=i^TTe~qT$%sP8zTUD^vR+75iEsJdYpW z(Y$FA^XYaf_->=t`x-YRp-ngiLS#OwW=HMaK;%la6ZrpdQab0jUo2@SXlU7LnVE&a z`oVJL`1_w7a$|2#|0#AFsN=6kckj>lRKp1wO1ybf-tkBE7h&tzN%u2nx{>!Fba_5z z(c;}_*)FS&@R53#u0c_O^NPFH!GQ?@_$u&A+p9*on7EZ(0woz@M{Ce@h}0tclubnP zRoM2?8ch_yA*pb4h^L_VaSPmJxD_DaKTnW&C+_Hp6ei3cme|z6VaNtXc%e~B-q!Lh zV5L3M&%aF{8lgG}Ec+s;PSDVLdApHG`vn_2?zJuRWGt)^Dw?4U+<@aOQG3f#!zjBN zDq7mk6=xo4=_%_IFHe?{T73RN0&0DU>{h1op|xOVc2{F^zQ#-?9$+%qfh+@uDx~nu zcfNV8?q5y3w9`xRU51w)OJBP8yIkjgP9-SIgk~3lS@7=pDToA0J~tp7`-fjPbBh`+ z56EBt8{RG5^N&J83S^L%Pc$rF(@Ed}Aev3D7`*I!ZLOF02QOOF%}~pNK#!G}_zxAS zXhESywSJ~nacP@F>=!7Vu_zrLK@?x_pxr7c9w3e%ec~Du2xSkHuH34qICJ%Cd?A!o zASZFE{wWqUiE=Ul!M*!UKA?%ZiX3|D4J2H8fO7x5QxgLC+@(nggVw-NYTAoRP__^~ zzOotrIgy>;HjIEU<+VF}-jJtWeCxokDMYx(kFJOEz(SIcx=@nv__26A2)e85gCp`i z+GS>i+5u29*tqRyFI>1Cg;Wecmr*sSU@2ba0dJf9yeyobp*JAi@O%W|sP-oB=$SI~ zex`tT+KgwNh3lth0P*|ydMxNvKSTm^b{>TwD&j664TSk={E-X6Dr+63rB)n4EB^_yxR z1Yw8=DkoQSEh@1Gqxd`@KK^16gu2V$pTlYl4tQ9R+=9<_UiDIh*v9g_?Af!oi#~j? zc3Sl+)+EGV%$lC=(K>bOZ*5D|tJkjwLn4@2uOKS&{$kK>-_w^?Ytz(uxo2OuuqNt) z11$1Dbwm6X8uNwfD;B^x%)|K&Y@q?Z1Y!@scD#5oid4ja-GT@#2puS9!kPgEDH^W zbsQX^G#N4I%Y&|4sz;@0^xwwHI9sh4kJfoi1bL85EICOMXLKrKvfWlipJF z@gtU^5=TBneMl@!tRkA_<{1E|p#G83@KdjHeaPqFi-d&GB(#>htS;x}OBmM!vxHu7 z-mW$Ysi}e3arw_+t;@!Vt4Q#%zMho_YSYR}_~@=>kwa*vtYyQ9&juE%3n9xHkajxao2pWFQ8eVcAg!-i%85qgK} zt1n&%e~53q7E?NeyD~T!0OHcWyOlfi@+N~wnCMb99o*c~C-WL^p0XFwf;Zv+0F}t~ zK_$Ssxe^7%Tsc|y9z^?0U!QfBKt5EwRdLHZ`LoAHdX3}nfZ>t0_}ZeiuO$OpMZL;iHTCew?U`+4%XRJxaX&e~)sH zBfn=$+CgxrmAnG$3Zz;L34xoN?Llv!fe$~|n3xJi z9L}cZQM^#1E%FO!? z5h-Z@+XJx3{|b4j#x5*qEE7e+8Pj@pQ=?qx(xuYwKqkw%qlYo5nMn7;brBj3DZFI; z-3d8{aAVUze7#N&HyIKVF1}j#83DTBAbj|srN?86xLx40A=OwQm*5#f0jRa(v1&?0 z&rIP|ln=IFGj0e8d71Odth#~;z@wKB9kPWOo(s`>z0uE$&tacK#`lSLwVNB_XH*h=9s14oS!?S zpOEr3P#55FbBnwCBMy=6&ZXaf%C?T8XoUQZff8i}8#U)h^SA64%o$IrMfXob^D#I+ zR!#sFS(nYH#Qv*XO%JzO`JeK%%R_mTGT`T-Q!5~zV`Lh~0&*2}VuX+(+^Zf8%JSHf2NSyY|P_TvHIP>A|0IkNemk z%y};6d{`6#;KIzpN4==wEhMW7WPi4yTjKq?0B@rugSV2UPV11Ry0XjuFVm_xV3O3!)K!_-g_^AYyo*JG#1ztLk^#^mJ2j*pQjzX zJh#@zDy|yVYGu5Bt+DjbZR^v{N=+GO<+-b#`6Jyjm*E{nun3$o!>)!3S z;R@ob2K<>eQ{SCz+`YWYdeLgAX%yWtv?<8!<%|0>Q}rJh#Wl7~TFZIQD4zHARo@SZ z_*!AreXZ_!jlQmd);)uisoJWnZ*#c=bLzRBnYnImaL0P^1|c8~mJt#}oF(fRDN{aE zQQ_`1ExYW7UOUkQ^dtPj%N{Ru*0x-xFQNy>A(Ft<$4|zEPK}?!$$x=1Irn|AToISA1*SO9|Qh!_H z6*eZ$d-y0VBIbfKii&(i1f|$m7S8HXFR7^A3jMFV9@S9S%U6%NcAJ{Ih7ZUBq_o*9?XII}FC79xA54uiBn^s{8Ww0iIeHh;y#PHuya@E6!2#{gnIl{pGE) z(dU{s)1t<7GY4>G(d(Spfj*iZ3RTa@#9+Y8o1DveM(1Bwzh2MrRrum?86Ip!)KzWA zdDWKW7TlePwwA8m;}j&tC2S85al`3( zAI?zdH_kxv;06)tuqQtL)$zL99ah7ZU)#dtqn;Vtln%xZELco>{isHM5_k9R)hA&Z zu)jFNK@yX4a%O~hYP}@_#T!R{t>~@a=td6zsu>wx%_I>rZ~KlpR#ocx-l{kE0sY|4cke z^u6avK3*nI#4cYMpJ|1zNtNPEAiD%V;{;U5pO9}yM@gf_nTVjK=a+akB;wA?+} zy?(!$#!#g+AZGOrVeze_mdVqdP>tUJDV@JNKfXw!rd8aj?dV#8R558+etMC+52Ag^ zr=RLX$SRwx*cMtD98n<9rTmG)S_L|3kWEIT#-NPQ^EiA&QXx?eWz|R$7s+)O>Xf!D zA^B?{2x~onzaWvkS>rWT=Z2(dHEw;{bG9k$F5X?%TM~wz<^u?0{f89HjZ#Sx*Mtg5 zL(`-4po#=Vs4*X8%ongSV-WrZQc>F-FATgEbV7QeZlmWDQqG5@xSUcU@292?Ymor7Mk8>>W^>R7#DzFEDvXTYd<4#?-3$R93%f<2W^in_HQW~zNcUn7#JEr@4W$pkKQdPoUWuB zI(o!%#6bFpq?eSakxH)lKyg6}O|S4GP!1qn9C=!sCf4D>9>qy=}?af zxr@e6uvT>>39CS4fgS*ww{IiNizQB(-%lVtiI6B@C`ozcF1@&84mHUZp#H$f7`jTA zT6i0{VIUDlRM4YjrH{xd2?@PknZvrQ8!1JwYw;_Gx2-m`wY8xM&xE^?zW&5)98VP0 z$&Nb}TeN`*@=E zM_?w9+!k6%Wd``FChjDi?biWv^VZJj=;-cx=eei!mxZK*URC^NXJdN~jSQYj?_SP3 zlojRSxu&geA22M64C!%dXL7MP*!w*c)DE3nkl9uUgM*wr4QLok&>nIaTET)UxvP4j zJZENp=djFW$6n?0&eSg>st+o7)bzGr+d@Xt)J9VS-4>LFFpIV}`vsS3#D+09@-Fg} zIq>Q76%~xGX@@GWv}HO~R4lJK8afV*4Y`%7S`h2qR_1l&%Dta47l1#TPK zTc+irIaERT4&hEi6OT`}8Ri7U?j~WLFGa62>MNdl%6fL>k(| zazjxO+uhwAX&Df{s{`uX)dtn{k3<~C5sT>{zU)lVN1)3nH~wY z%sHzi>oRh`eOiSIOk5J=8s%FPu;Sa_NBy8h?-!8bjG^-sC3}lEyeY8He90{?R(5j} z1B&L8nNdW%9|`?4DB zeR4AMgy+s={~6zAV_7{thHqVQ>;@FMIXO*GOa)|yQv{|^H*ix|m)qK*3i^-OV-m+* z4@7zdi@L49ve4OwT}QUOp+VYXvrqXAl(zUr3e3S|sut+B8(=pNr6d$TWV{bNbPBab ztVL#YkM2Q81cwX#FLphbBvE?|@gx!$iP#Q4!E+)o}N+y7tGH}99&#wY%17< z8|Js-x^>GhHT9H9ZQ;+i?d@OEf|y%?YKG<4j6wy6 zhrfgRlL+`R%!$B+E4RaVH&P*CVOF>`cYO4v*=nv5siAL&BPSyRllGjUBZJ!J_I8W0 z0Cd(tL5UuX;nvnx@YAR5F!YFfKs8zNNrlz0q5Vq&6e3zssUdG~e;!2hJhHaDG^aT^ zG1&!gm|Zvtr63c|?PQ3|k@vm*MMO$UdM#ApRwi7ABnT5mfb^eXYu=uW`RH^&bAQxo zl49zq(Sf9bZnBW3_a}MQs;ix!w2X{cpzrR1dNMUzr)McrhIp=Q>F-sH%a z^<#yR4gx%5g3{gtNsDrF`ab2~E8=x+-fTo%zW+O3eBAo2@!?$aGM0xl(W^`^6JAm(i&@3wA{15pGm?f zdTLk2cKGuGv(=~`DQG`THymvLYU}?0Xx_S z8W(tV%uuEBG1dwwiK4D+r{I0BVyH#s$>*1|W%kcP9hCsAAQ1^kdUiIh%>LqyfGuz) zuAMs{vg`mgs0uaLk0e$>!7vQby9Sh?(0|R4dUMsK$aqT^Xz^H!Fd*YexTpwB79J`o z;lQ`V#l;0o+lZ_`H!#7R7SYF#AL|#1HZ(V1+FNkwfGh3kQcl@G{(uuAbgaef=_6&d}4{kkZ=Tm{J@Wld?MBFOz;&8TzJFelQRxP1Sw=5Pl@M{PDSa!@bDLCsP77XvVR24Sx~hw@%0XLzG)E@ z1rF#ml!_t8xg(lWvG=|J-5N*?KAg=q@VA~NCWcXRsv571Rfu~05`eDa!O(LC29i-# zS69p7cKKrBk})uZLTi!K+Eor-t<2ybVwweh@*h*A*!dn+=&{cmYi;uNuRqU`_!Qv( z1_cixTrp-h&lK6eF6>k}8r*zfMY}MyAkBi)uinVPM~R$gH~;^~h!o+^)9Apd9s!3a zXS$_=@ZG(#X3Gl0!7hrh3FP76X@9<%=}-V+#)5jyT7fQR6flGF!>q~gRp=t>1NtH1 zPo8o3)+&K(^rQTae{A@pkep>VytqN3RtzzlGb!AD?sv z(qq~%at)u4D8Q%JuIm8jj1 zLvG?dyIruP8-{c0N6k;kdF{Q-$3s1O`^6R8aiUR}G$iaN}zg6<0$ zGI$!9y!`bCJ?LY^SJWyt3R8^5|T1R5AVhEdWLu}2w0!1RYcUnJirYiUtyFaRnf z;)D;x4n#ymmphxNi%bOQ?0(KFGJ>&3JQqYCLTp878Yon>ySuvvO~lZa7V7w4b^g`L z3SU|2;9&`g@TQ$fzmh}6vM=!Ozd!>oWi_>rITauc>;eL0AbC0^#wYmo-<%T90;vU8 z>GIX96oW|<22hgAEVDO58elr#n?jmDNRVK&SNINu@+Z`qOzVB#rGe7R%zwM!CFFwm zgoKKBW>+jMMJ~q#OAdj^tENbY2N2Q{RDd&72eSwfHM21CGt7xto5gdJ9F#Q~nZSXh zA81Hy7$6`w{{(z4s8}i4()RSMx8Ny(XFOkZKyM{+{#ahv*lH^2QlIHbN!9-f*ge@- z7ftzs(|*m{aE^#--4*-;WUHX!;3@dAga#}9Wbjxc~0!QHszzf)@L}(y`XU$7PSgy+ljjw!R zN?fbig&D9x6h%-9ar_#7*}Jy3R#JXhNE58xw4EquJ01fzio~1)Jn|}!QcYw@;8p}} zn@-3sd`0dQn1@hrjSaDZyWG5ZwP9-oG7oI}31@N>^nK8eIC#9_1)2=;EEjaH*i!;OSwnqBBJxr z(Sp{IVfY&?oFkqtK3w&@Kbk-Jv!XgUZdF<|iPy=z<{8;Vui^Nr`~SoXAMDatOxAld z?-tMf@`xROjri}8?UoN7;Gz_im9NDFa}Z-vog4o2Db(D1L+=&OrxNSM`u@A9CKl4h zCyfUyt1W-V@4cJll(`+A+9W%(*U)ec4{r?u=E9Z<|4x}{rz#O<94;YpBM#Z~bVP_# zqE)CH09CUj1_lQG@^&x`_el%+*?D28(f9H3K|$vyTv&uRvjVQeP2b=sLQvJr{#5cY z=ul9HNZ|5VJy+qz4N_=02hztBI`A4YXu%2Z!UuoVp;LJ<&zO~W&c)ViDl=WFd#$^jMP?sl`DT!of^U1XKLNZnSz))QA4 zhU^>Z#)TpSw*neD%=UU82tr?rrx0_3D?m>NUzt=8x=okrnhT3=&8zd& zC^rS$1la!FUb1VI(B!`}B$3pO$j&2R#!L)5k?eokp+vk_NOb`fUnc@b95J#eoi}!@VM^?9+vlW`{u>eWImPTr_w- zS;EI*9*UIjb-lrUy8qn-tn!`1(7U1zO1MqRIgQR=`4kit*~G;Owzi0M%EYzbysZ5r zYEO7B9&eGxa4JI2)s4n>W^NRAZ+@1>F#4&UD`JNtIls^te#Ia0m9rI-&_vYp$l50- zfdt&tgF%wL@1=#J4&T;kl&TN6Jom2dW3}ivoOtkH)v@~D%vfJ|027zvyB@M(lQq<| zOBu_MpQt<3+M3L)Q5>nC=;}>|6^hlXyEDJ^WM0>eoeP8Yo%pp z({kQ8AeQ;vVTpx}Lon{?xvcjYPKV74=7+W6`u39HuDtzq>Y>j*V7zRByM&BR}Xu=823`N?Hjf*U>I{T>Xh{& zAXFPLg*PV1NW!Yf>K{=A2O$FPG7mEoBnzf3t3m{Sh=J-O`whB*z~Brj_0Q#}09Y}% zwzlHoN1A>yl_9^ECM3+Tws#O?QZ}mCIzI_9k_wiD+S{xeOht!vB3qJa8beh-Kj~jP zwe73r3D>{No(yN=N)Ba$6U>B;r@wZZeIui0UDN4;7ac+ZZ3VYai`QvcMARG-pkFBMU(uk(!dJ&ulea>UHU3cclYt5GPz( zDC7sbR8;i*l}=c5;|)qnM1Fpan#Opa+DR}^X}C9okBHpSD>B>IM=s;5^e@jYm0;Sb zoz2Zz+9h*4s8V0Mwnh1?yX=Ob=wFu$K&|`XCkPB~>gaHJWqM&S5DuAs6c7G(&u^1V z=puu^$;Q7e>qT)KE=Nu9Dt=bQd#84sQMeBRp!S=*&;H+k2{f7G;L9l&QYh@BA8Nj| z3D@7bdq``fQjKZCD0lT=zxxSvV2vfz@Ij-&W?#%hzZkC_g8mox6N3;p@2f9BwKvt( z*^V4=@;)p9*|c+t)dJMed_oQ!0HPZf{by~He5aqqd4$)sP=7xFtE~&L^!uJ3C1+<5 z=rEHyyqp=-WOjk|;ze4Rs41sXJj@O$RM9FJ$>|t@%$K*j4!ASvn!?dMKwzmA&IieSIYJTPpT0-BzD@TrqP{6-A2FN~S7;bT`Ks%E}wULMH z_JPEZCJ}=66v~A#yz2}6NY2&%*P4m(>4A|7D_Rm}NxVX>P|bWT#0dw~4MGNJB=gZi z=%ZzYTK*t>j(?v8l>rO;%oPU*L2#AeYa51m$6etHLK9= zo16@XQMwcWoTmelUKl!ocHWm@=BUA-9i``6}_+zgRF!tmy3C9Bt$f! zPmulQ!c7Ece%3jcF*IN)42B85z)|=(F~M8F$-!|4oHUfr=B8t8_dNiT`rJ@RXbVH# z9bH@q=kJ^bU@;f*HrVoN`z?g%=;&mj(~0B#>k`i2Wi>RYAbIoZ8u~Oj`kDBkBO}9W z#lsn|USWX_zTt0|4(kU~)&K*4#!P}jWaV{P*-z+X$p)=(YQg{}1A)2)kR41hh9Fj= zKr4T|q7-gsZ+Ez{4VnilTnfLM`MB8_nlH$BGv6ya86F-E@Smvnfh5WpS~H=1AgpEJ zen9|2t5apmgd~3KSLZvxgM~;Hkw+A``f_%50)XGX47^hxE&dkg&MhRY$g10vk zfGG{mJrqOMldn#{YViVnxa&*+?V2CUo~+}PD@@kD>*(x!XX9IU(|f`B`FX-&N9XZ3 z4bX6fR@}7fp5Xe%250#S9a{AncppJ~?|DG5OyMmcTR~(mK>$;0YZ(rIryQP@Vsx)( zeo((f0}>MiM+zxH6$M|LT0$IpA&2?;o?sn`U7$z3pL=LMUP%mHhbW25Zd|{v;No)e zItS&_($ZS}swXKO{w???Y!|Qf7E)4*clZZ-6$`-2vMq?yq36QQTn(M-hi5dnUs%)w ztavq(0{muE99@80Ei34uB}+yNHAI9+S^_{1knEBAM;BqFGI2&9m-V{}=M`Nb#y#m_ zxMpL+hh!)Kh{j7g*Hd)Td}NNu8X5}w?*Jy=;vm*L45uPjBfZf9k!`kkkK-j+e*XM< zld-_`LSKfQdC;KcJIL?_LYjitpoR*h*%h)-rQU~L)??-Phriloocqf!>Yh@zly0g$<^RC zk%nr>7XsP}8GR7uGDAQM&xJVIFir2w(ij&HPb(z%N?{7|&F0W(SP~41O!X&#N(zaH z%tCH6I3$FAM=}=tS=mM)K`q+H%VIb-otmJSh;T4LO4;+SRSiRG?JaaB@N-%oG67%l7f5(S zU?7f3WpIaC3&c3~<2F^7iif{69xOM4)i9~hRdi4>^!TZWg^OznA7VQ^apn5;w~%{* zZbfVpG7z~o{+V1QV`L4iRij8Ww`qsdJTk;xf)oz=v? z^g+w>0s<&lDo7TaESEyt@jhYn!@hR_U5)H|@tq5CaMwTgSG|qs3Zx@vmOu-Gvn&Ot z0}B7cp^?L%UmZ8RE_^F^4lz36@XC)LN=N{l`4V_cppr@+&RzXFKcA76g>(P@eSw5- z#CsOFu%W;o13|ji7#0_OI1co?YWV{hU4U(9A%6rmr3K(P|J3x%Ol%t)8@MGO)ol4& zw?4(T&NzuPHEblU%4UCJh4dB#%q`a`K&u0Ywm06m&k(^Z#qHPt_7GIADI^xm0-M9}AIr0M|+4i1k0O2g69Z~PAsJFa<+z(syp z-EG|z!xC0uF?B%L&AtB6{}BD7jevdsI9!!pJQ!Z3nvlgZAbp_lwN4Z=TL`RUo&>bKp=rnkUf#&y#JWPUiS1pegg%QlUPUX=f3DhZZaorjP zq5I&VD1$|yY(^=Do%rze!q|%I_pX!H`yaHQosPK{b28Oc6bGDF_|ZoBQA+APH73=2#exg4{34b=cg4EDXH4&_jorDM29Z>pW#hLbT z=cn@uDl{FFH-5p}{^bdNoumVgp-(tXFrn{$*`HO3glM%E%Nv))J?kLQM zn=9X&*vPfHu-ba;41lGEU{UY@^Z>Mirt%PWT-Uyc6HA989kAH4?+C->#$MNNceM?e9F zD29HN7R9r9P;AF{v;wdOKhc>FtE+{>rY;VGh6Zoc@B}H$E-ad0xLExt2!3+uQxhu8 zS%Rdhik zC+xS2OGFu=C0lS9E`Oo3b63h|~IC_oi)C1ew5w+in5){luzAQeK zQM!4DP2{3Tx|5xnJKjqr#c+C|)m>i8dgde%2In)J^52#TVY9L~-FZX96kc@rcRS4A zfn`466Qc57U7;4&dexgfv>bUVn)Mt%N%#8g%Ut|qlMZG=znZsilZOxb(N#GL#O(3qz;ES}ffGv!kGz(rc@D`-E?Fr&Vn2}Q=l z#Am=3FGQsFJ_912C&oT(-_D*0X)|j{s}x}kvpYkqs4q|CVnrW~Uz?Edb9T4^Ck%Rm z5{Zk5h_JzqXvog$>z$+&yr&$pywzS4p1n?jp97I|? zmX98#xvxqcD+w(XV#j-eE&KbWAdUVf)`X>`MkHPM1ul%6)d}aac&(jyJ~*fbZBJuo zItHVD@20y>Ne=96;rmUOQ4y3Aldwr`u9H$n%gaq;o;q{dOhdpin@On7-&mAQ>y)g(%q*y&{(0$=+F7dfo@6_3?-|#m(XcII;#kfY!X$+s z;-PKqD;T@0Ssc9KA#zdrBycfd7O&vP{{AB|2RAqM2kS?DAAY%m`{Ao8Ziu-l zl2RpilW>M`-5B|IB<=gqP)dx@ug}Q*fBjAy&&)?oMCQi)&n7yfSS4(%>!MB2_29ud zL`nRSl6EO7nY;}LT;06Or+LY1$QQOda@(1zH^D`72c5%3)RPvXp2*$eK-In9-xH8n z2%UKwG(JxLeK{1Hl{MXzSc@mZ$=T3;`5Y%ma#c-D2^)t}4OW!&OY;#Cqy!>FGYGm# z7U2<^LEfi;j@#BTE*KeozjlROF5m@=2P$cl=vd)`nk;&wsRah1{G^~7BxMaSlrW;? zZEW7tvorR^b?fXt^*+VUYd!J%G>U9?K4`L|L$rU2pI#(|k>WAf9i>{Piu(Fr#gVsf z^PxpMl7fOw@~}ULi#7EMDRF?CMeskVB?Do!cWKxrj3+iVhz(*hQ6mn=;O57X;ISz~ z-u-r&3Fr7QMG1-mF8O!zxxyTj1Ww|0HFm7)vqh3kQf#MC%TWzw)JKX4uT2r zd@8GGVO5@|rCr9yzXTE?ZX@#Q+K(&fz)nG7v-l2T@PxW=Gm_+Nxzi~Jwk>zv8`#=n zl@ih@SB!xb1H&1~Odn!ubSNJ6^WPBnV%8a`MgA89ox+ zF`Aj z&SnoDrI#Mr1B_R9i?`n#;b z3uDMTFACLz-b!RNGdDK{9t;H7rbWX819%lMgaf|!)V-Sdi^i>6>;?1RzO|TLxI%hz zgSqG!8>@3+HZnBi7mxVoa+0`^`K&PkFr*G9ZiqTAkU?@E2Zbbm11csa^dE_+exFLrA1I|GhWHc$43lcL;4p8nznyTh63>{4RQw%oV*J?*T5x?gEI8qciP<9 zxz=fVA?`XCKYtrULM0I9EM7Q1rl6Pb;BYv$s&i2x=oXT6qoVSMU=9)`CM(m;en^B1 z-IOx334o^o8L1N6NpUP}>?cD=Bpns?7#fYEnr7dH0_MWEBnP(xPdvw?6jI4q0`BN~bRU0rqFqyWT)!CE#N7R`BM7W#y_LQ{mk z(a|Ts-9u@amMHAcBsbKy%s9_fkZS?1IgN z_u;+(9Z~wpGha$|3td+#hSw%ny<7&qQ)&ZQnhYZzns||+kgyJV)y@FdlBUK80aO5y zsv))m);GehF@{E4pd29zMGzujmH`VcGCn@!#}8ZBGoXCQr>R{&C;!iBiw4M@e6j@a zCN($+b>7fMo0Cx-;4~Cn#r{i2k|)uCeZat?QW(VpxNCC(A*v3CGdvw^_-)%vfK%ofip4cUX%ezrc1 z#f1f*))`r71?>3b@>+8oe^)qE^8orX4Nr)pR3ipBuOibKt$b?eE|T#r1A#-K?-+xV zhl&a@z`SAz+J=LY++E19u(HBL0qkVc#aG)_8wB{Y7%YCD$f7hL)jz!R|KP1&^8msKL3PdKe7N~OJ9n@_pcY1n!A zE#cYP>Q#t!|I>Ay1TN^E zqIFFFgrff_wf`HtG#^M|bptEZGOEieJBdO9#fj#zoM} zkk0-mUl1S+sIW?c2&;qy1K?z)IAk!G3-Db^Kt~<%A&JmZU36~P9LC<0FTq()-$;TJ z0x*kWua>&{V^FZLS$(C)w{npi?Jp=qzXzHxVDb_NjqT*r)Ms@6O`!n@iQxV)U`LJ% z=$_E*4-eGx?4Y*MJHVv^p0%Egc!K~+Xk878nf3g6a>&WzC>5ot=OT##(BCuF^T`Qn zr(h0h1e}xapBq^aKJ+nv&QTuiLMRA50r(k#J+lx0pg@uptbldV-qhqnW&zwcHfV}B zum~TCLJp-HR#R!L=!lK}@cEDV_ry(H(pt5155Tx6rbFhDTt3-zv z(m*fxPp~gTwF9KAUpii~+x~tVVXVNxX%U1G6L@-$#i|$`JP#38_Si18oMc$`a%2JB zr^b17G~+Fgwph|%kjs7^`0sD~Gp`}A7z^E^#qSG<6$8G4!hrCsxal06LZEW~d}DA1 zpzX$kt>RMa(F^co1WqrmV*53t!W;(x3Y^&5w9${JK~}(=`SBd z(tM{BZ}?a##2@oYCEwaqO~QZyFml_gpP#mg5Hu|5$Mvh}a(gF` z0bvxv69kh4GF!SWx%R_qMmYYhu%Zg`3ovR84mvR$Zoq9lRv&%FRBCw+{{+S<^-$f2js%YR6#(9#a_I0?Kzk$LW_bM5$C0+A!(TX;wuX(`fyTVz zJpJg-9X9A3ucocdX+QxqO5pR)fc%!e7PcR!n)6>RqN1&x=V60(toS^wcpvdvDO7=gNE?{!DKzhu-c?mFhMgl@Ya&S37 z(x_-?W&!&@P6VQmlvRJ{8>fRkH%BL@aDofqoY)=i@0bF$8~GhCFKOWN$XF&6m%{WI z7=!ZyTs37-&wyeS4zMuk7Kmm-B~$+>Nb5Hypf*Bj#6K*#CxJ{D-%? zgvd|Hx z7^GVm2oQVW$#5kYb%q0@3YB4?qJ>SJ)BF}Ru*5=vJGPUv+%h*YG^F+R-MbkmEK+cF z75(txLtEnSa>z>D>`m>nuv_@Z`hBc|5WK!O_W+%RL?JM%0QEe5`gB;02sl$J$^Q>C z64Aasktj&D;zFu#GsnpCwGJ;VX*-nROdOp00%cPcHa65#4B<+!Q*wn`^7(^z00l-o z#4x3d1V3z;%Z1R(YUnKlF(zJj_s1+dtp`-ERXvI!vL5|7{O z_(7a#0Z}~?%HUv*xX}W60|Wl=&i26qga0)pmo5x+dQhh`kRSoWN>M1b=qN5EBsBf` zaR0O77Gxz5VmAr~0+3TsJO-`WcOFn^$lU<)jq-tc50I5=xl0FjKa|KwLP$@)u4q+QcsD1v*-NN5-WC#u&?}w^mGVi`=j5uFP)&giiJGF zzUtct+l#YZaqZ8Zmovr1>6+cMOjFZOem7&=%@lpbZ+n|v=gpet`?(+TzKhn7O#|ye z=TM1{g7HyukhOx;7@*kwca2Xoc0h_+J(tl1=L)zzNR11(jy*I$359wbQ-l|656chb z2jE~fU4F(M(|J=<^GqFB5{eq##CfX<-5d>Wm|$2t0eLFG!%rOZKi9cPg0Sa%8~qm+ za1|<&lj+~SE$}ZcCOSBn363x%iO&7760e~>3=9U3M_s9{lVh%3_n6`nTv*TEm)P5% ze84H31du%J^{g*$!!HSk{zXT`v9(*9zGP9M3u6$bA$ub?ACwQvuc4`VDSr?Hg?Iyc zu82564rtsr9H#y;!GxAJNPa0Mu|r1I!otGm-JNC_D)ba|KcufHk8<3QQK5)1fYBKr z&wQ~x;v&X3pfMjnl$7s3kRdYqZ0zsPOJ><)Qzs|a@T=KPO-<6eQgvh{%YInD>hPf` z@51VXrjO5VkEC~2E5_DV{2W=C{J2OE`K;t7p22mF@P)deq1QK84XS7b<5UTDJ}p1! zVx5O1J7oFfAl0bBP5u1p8SpBRBnAXXUjVj~hZu}Z$v-HF9ZF>uuuk?u*$OTMD=%Nt zq-lZ2@xc4fxOSo^1_~0v`C5f?K$*qW9{vIO^=Um z^#1k6O5MwgHTcQx6R5SV7pxrj&bYfqW54rcD9Tt{CT4N)* zmKEXNK07(NJ*oR6E0YStAFuETY8dgbaN5|U56BVPKW0Lq-kTxE$mh@f!x=8J0RaZA z!L^sXz#nPOxqTM8|AN+o1Lv=v`xdfz&<~Q=pPrA856YNiA=X)_zPLPQS_4~W)Vj`Btd9)UqYL79r_x&3{8CxTjife^Y#5jpKAXb{UldkU=F zjdLT<;u+8K-H5-HEn)wsKo&A^Ny{aycc%vSJ>SsRnfa>Jt3U>Bn`NH;&J^!|lYu+7 zeD7Hz#D7l+g@tmk+;{sRsVm@5IDhDKLx%4Ke{=6DO;^g{VY9I-iP7&5S|+sxLqivI zc)Nq2dSjXQNQ!*Zy)#QY6WD6vYj{X?uiO1{`J9&CIBQpec34(c)cN74XqP7#OhK)W z+uFim7oI$OX6Es0hF(GWoU;DJobNnGG!DltYO~i^SlI8_Po2U5g@P5p#^C&jc6N5% zSY&zaXZwvW5faG(joxn-;t2u>cKDf^qP`o?B%_5O>wm!Y+feVyTs3^|I5dg4&hgpQ zJ=qfU?jjh2Hp0Ae(dTO{(}edJfLPr#bTa#&iQgot@@f#Xt#( z*edAg;K0316jetBGTkd9AtqjqjLo5$T6Vl~NPa3~HTc z06qXx1CoSAToR}NJaS+Ntsq88x5Hv@RyH^11lR<=P}9I^Fg5fBngSOeACnXD+2Kd? za|Ujt4~`=S_zy&8LLtDhff^inQmNAha+;ddFjOgGZO!hz*@XgtZy?454U|(@sHm$; z9~H$<%2N+xY(I{U;={QF@;LCK<#RO%tiXiB>5Y;=fsK6DVFq7dYGoZ9%1U0w#s&ie z0TTs7l7RO#iNgi@9&Q(T2;iqU{ssJDA==P&5R&(U>r}H_fs!6>p5jRZ4iDUc>r!s^ z+DCa(eAXw+=aHg6#<3x6P*Yt?i-W*0vG?@;=U06eNC8qEjD~Lc1M|cr*Wa_d!UW!D z(b0b3r_MqYclpMR_9BDYQ_(8>YX&5dv|pekqXkMb!sWk8(FVQ0Ar0MqtZzvv9Q-WA z?2ppER{zNtm*tHj68xtx2g`r$Kq^Ko9)l+Ztoc`WF9Cra0!M+ESEZ%oP*H_K-Hf$A z137n{*Jk)&2Bi;vnS#&-4?z>KR1Iz+*?>29RHc?0gf&w0=J|M&WXi|rPKC+>T#x#k>mj4?rD33-ns z;BUQseF>egL5N7_prE{*ne#W z!ES+l@j>FO1>7D;K7h-Eas}EMaza9q1@{{& zanB0vSN%_(t!->2EI+0KAaq~8^e?>(tlZz^(3UC0)604|HHdPB3jl|ujv7edh3R+A zn(vr5vzX6IK*0xGlEej4SgW96{;cvvBBh{Nz&xhREqJ!Jy4ojJ<}w=_F#44?Z3qCq{n9&K=AxS2+y@jvJ1zzv@5`w~lIa5a$jM!ZCngVmAJbWQ58SG9EYv1A0g0p8>fHhot5~~^xpo|L0%^kze<-u8&86jmwNv)AmP-I2fToD9}?1KAfn)4 zOw=)i)I$p34~R1aOv#HOI^c7GgNsX2_!!=PYgd;S0Qu9?$Vvs-546Bb*>oT=^yB$feXzb z6F3YQSIOum5AvY*D)emu3Rk@lc^RnwX8hEk{(H+xg1c}~!C1UAsRk{dx49{F56+Ca zWI2??2oAjL)5nwp(z?1g;iXm3nqT5DfMe7kG_)l0uTxZNAcFNmHKuHXX6naIFQLW! zQaT}1>OAdE_tA7k(+SHT?5vTFfO)PORGf75+Jon?UTjBWsgErj`Gpxo{K(elpj7Bm znK>@8=&*p6%d;bH&eoGFM={RV`6UbvA!ur90$Qqsj4agG<8oS%OxpgQ{;)ik)e=~c zwrJa(-(FHD#C!fcIw+c}J)AE!cD{`b@$q?&bO`=kmJU@T)R}%fZ6FP^N7XF|=BTi2ZH)!Bb#`K=fsi)M8VI=431b#Egf^SBEr1dN z=6>1gb2+1{xfdrq|J355jJ&+x(qO@_C6HRRwzi^x&IOvy#Psw)uKRFyOcnms2D8Ed z9+9g$Ygj0>rRi0^Ji=t>WH5&l0T$ueJV{-aF%3!9u%ePpm0aUKFDOeDCuO0cg+<*8 z8y8CpB36c{uJWKOkwGGssG9H*hkWz*-4lBNf; zDngAtjfV+g`I6wqvwB_5yl#rcLC&8&mLFi>A2~0u^@hSwNPM6b!@*jUDDC$uHWr}+ z!Vsc!jEqnEXW_v~G}!)e-5@A#ZRC{&K&_8*g+otke}x>v89KzvOP4Oe`FcgV15GS6F!lS0Ia$-Qj>xrSQ14c z5X2zTxVfPNUmds498P(9VPSO~6G+;nBxjJu<3+#!3|93YxXasaw&cNPxnis+_UW?3 zJLs+c8(QV}CiFvfd=pyVa3`U4%);lm?Z@2Hr$(NbEDrP*g@1LgUJ{wuW4AIFmS8G<`AxS8H#x`l<$E} zH{2Zu0U_k$zbDPMa<1*VnMv6lmd^3RxUu~+?mlKMTM1Z)LaJC#G@xPprJjflfG-bI zT<+~-4TaQL{?jeJQuV{*76~~3a2(-?$jwGR{)=ez=x3#*j@t@H`J#`{g}-jp;V<9# zPh`a88=bnDk6TQ4LHd=uJ}K#gvkB6#>OHZ151Ru=Kah~NldUIxI*)h&e7J2hy7BM@ zG@b>!btJzR%R(tc&`X_37zw|ux7SF=RptJtKl2Zpjr7I3W~#cYD(mq#f$%8GM>v*P zFQXdE8AL`4zxEKM6_Gw{JpAq`~orQP1wT!}BuKJF91qYj9@j^{5V$qH! z7TPwrVtJ=XZpr))zX0Stwgu=iyRuCpCL&XUhdl+WGe^yj1khO=QPJUIaR(r*)I`Jg zKtzWQN+m!eQrYRflmZP8?bRna6m!CtbiA5zcXZjhiMVrnM)~=U;odo>smW?8=E|{d7xN6 zg7P1_>pD z2)Y+RGv^RqFv8L=R*;vkDlbQ|kof^n0sRfcgaFboX&BP^camL9jUZEn7a zKqydBqG2BnsU9H_ji3hrQS{6(tHAI!v^aPXMJ>cGoE!kt5mf|aMCIQsk;?)#)*^Js zK^chkUmMm(JH~44@gY~U20A{(?iFS=w_ii^9%`DqQ|gG%Nq6Z6&$Nyk^a6xoWE?p2 zxMt3GOLeIa{G&15whAd*0%Yib`+{#lWsvh4gN_hKz;z60(V$*6{E`q)IwGyp+x{Vs zBu>9T^9`Gwo~~i*Xc>=Md~8(oKpb9#0?C~q}+x!48xeCq^%C6DH}hb?J#!!DLK zj^?Ym5QdQyy6y8B8^ba1e!?VUQ8`D+yI8CbDK=1#A?rr^a|uP&8q9ySpnu?lCiMvo zb#?lH!}6S|vmb~WWy3H*`c%ADr2H!VW|e8I6Xno>CXut&h6_j=Nr^Gw2LISx|G_8(B$XMylhTbCB^dhYV;dfhGP zY6U?_0zZB&`R<5K*$xe|@**z(iC5eu?BdIJuf2HDqmoHmc314hSuT@cR{Y*xL3V?z zk*Fb+6R7;L!0m8zd7Q{dLM{6MpZ6KaDuAvUvTCKIPUIT>JTIbqdNT9*q}>qsXaz&E zDMA3ve*o2K5DEk=U0|pnIKO?#;C!SIfJ-3HsYd#i*OmYdgj7yeULNCm;cWypijEdu zxOwK!5{+~|#*S84*=YHFQc`4D)a)~?aC`5qr(~FxM|KuSG}$>?S#^Y|Qq79q{zq1L z>(fPd8VBbwHV`tViP1?&<;TGhbet(JyNj8I9A%0&uQg2n7g1G&0Vnm}oFKY4A5R91 zk$tv*6{CCm|6xo`-SpRAJ|VhKHpe_QJF9?hzTXlX@nOM0 zm3Bm0=zJPy)dD=v)tDcryu*a4t#_h)*rZIxH!AxgjWOxQ}KX2)rJ0q zZvTVPjHsS!U!#iIPKPna=u=?bN~?tH8!_nQxdeV|{kUUVbt=b-Nay;uCSUDI|5KX6;t7P%2{x?0mK;pI&6y-av3j`@MjcRtqKis}31aS8dn6&_8_( z89J^D0is$3g6ubA*Ykv;>KJEGTF(t`E}9 zX7OdCSWb-OoE(B{*Ou=v-)E2DHZU0!xm<4MMT7Fl`63bUB|A5C>gkYm|ri$N&YvfgT8Aa2BBD%)TP&Itl`ZlYkiS?v`$O z1`cXo1w^mki;1GZ88CJ{Y3R3C8+BETP>5TtVYD-nWIgnrn9z`w>lM{Ic}Uc@;(?5( z>vQuN>p0MKf_UwrL=OiaJ*`lz;uxau2GzH?N)NdjlhaS0!}VPF1@QRF76mY5`YF+* z+g`3WE2YwNzU>QOseoQ9-V5;rpb=VM`^nqqjg5c#_^b1p*x;f5Zdvr?Hb9dA>703E zQcT+hmzo-1J(aG*86#z#L)zIXH?{sO;WiTQ245n=ThR56kK45$QqtP2Eq^mm%~0)t z1~X>woAPfNZpNe5nxaOU6o9{NWNLtuPBxdg+O7IZS9k$949MsHK9=L&Y~8Dfk2U^h zKeAJ7Bt%R0rQqOb-KuSbIDI9`N@L6+3qaacYjVEgfYf5sHGk z7+@bP=z|4^K0oQ?1d4C#BSW2UJ7!pTcBf%|sDo8%0Y64fmKm0>{unHvnzPIyEkE?) z8~x9^HO-vWGPHHmNXD>bwwck7&*zM?Zk>)5zuV4*gBN)g?C&0Q}9)-sp=2H&@BHzM4r5#NpB%akY17%4sD^7i;+<{nR`!OD4)f{Da1}k3ReP_& z?p4W7K?h(RPnrz6bNG|FId2*GZCPPRMM8Hfldx8`-Yi*j6b^JqE-)I`ZLhLHcL{|; zM?a4|7~td{9vASr)KyhCAj#3Rz&u|_df35dlaK>=aQe_+3fVRRqqA2eZ#LpZ3$8mQ zG*nxQ7YV_WwzV}Z@9)Up(Ma=vB~oC?Cr?~-KI9`F=R>#bxlr|3k7_9%N-BILbiq=` zA@IK@E`Azb+6X+DtSlaiPUkvaTTc9uL^t^^E_?<|Sokn$G$69v%^JrT;1Y~b1A+q> z5m4832I>dakKpV>oSO|hy#29yhUp<*gpp(aDB&VhD|vr9X;P10H-x+jR%En_%qKFi zVGLuUzd9eEa_<5ws{%h8 zCW^?<7uFeAk2N>9lT{GAES#~nT~~N9SP0sZU@`>g#=i&@`JdfNQ%cItzK_EJDJ)_Q z1g%i`oa@UY=8V{Se9fM)d-m}K3IV8x0S5=|C&;~gm&`U})c!`Ekt{gT{8+yR;o>*B zw2Tacz_x|*J!mok1m6nM+rRK_bQA!B;}!yd26$MGjb#+a^xJml0V+M4mIt3YcahSWFb3)}U(3A(u<5hpe$6Cv0{Hf%SeK z2~q3%U;6{^Ob~926(*cYIXMDIt3Q`lhQed|U3V%Hd8~8pQrs8lQZ=02(kd5Rq!+0* z>PmQjQ9d7r2a`2Uv}uL+&R_57{N>xVX1^ZZA}_@q^G_OEj3VBLQkpT_R25j%a}N=N!+ZFUORU{@e~CVl!OXG;fsH6#29VJ*P8lJ6#f$LTL3xV%3Q zo-rQ;Kmsoj(TTgZe%0R z&&yoLWPvSDNVvVVRK){ZhAa&YI!}ZJjfU(sDktZsi`>~i>=uOo}FLO z1nDm9hkudxOV-X+%(2MLbLwuJqybZm%B(JB1k2#;V4mzKJ zzMod7ofLNo@WK0?cjiH7N&(d~B25RP8#;pq!f|ZBCk3oDV9>sM@}kHvzbH=#YVfIr zg<7D^Cfs2@bbK0TgyooG;-qj777NH->F?tZ6U#h%_NlcEID;OnAz)tsWI3JKda3*( zXQ&;O9>q$;?tKq01JO^|xh}x%1Z+dh-6W5>oVxET+0y71qELT|}+LKGrb(``= z)3Bv4S{K&I81M14?EqE*wJ6X(Dg35POgm4%*GTPBCcmI^q1NeHfh~d zP>|8kh-qy@ZnBn^*o4^@u$Im-Fkl?D%n~D*95&`&sI%4q86x4w^is^CBeq~n^ZYvO zQbMSnf!cz>FmT!0+8S9~=|Z8|tX!ZmCbM>s{fxstNC2Qjs@&dzO^5D-R4cW-k-A_)16G;GJG zPqAS;8aAF~MnTdNq#*he*3qJG?)F{gui9U@F;Jho$Os76lgi4_U~?+X{>oj7k8db@ z+VRuwX3uM*=4e6#Lm}=3J_#wS#{xDB)Pv4D#yV72Z3ZsfMLdCYIe+F$-pLq$w&?UX zb5mjxxCSpX^Ky^S3#r98`B5C`Y>R%2SNys9>2?o|rjjN-F6X^jt;avVK1nn#g-&lKW-T}lLkk#@ z6vIXUBR&k2UQ;9{3ULE~ZH%yq0Rb2RVyvZ59`O68u8%{gf$YZ7=?T)+XJL`ts`&`J z^90s)2JW?$oBojYv2*Ksb@q-vzh%F-wwAOtqZ~)KUJ?Lk^pB2g3-_G`@0OhaB4 zFui&&&jC>{QC}&wGjX%s5Q!N~NX)3T9a7i!cLnYcK~agCN}ejAvPjGLGt3iG=YYZ0 z9Ov@T;LGMFF?e>Kws*+MREZ>^{3HcA8A6-MvOdq<;iSe#a!U;4^0GH>z%=C;sDgAk z6QHdFv(Jn4`V2It2mdDJdJvuRkM90&oZJ>$V4))0AN49U)C;U6hCUdDWOEnoIhZhvz12oQFo|Y|Wj9T3@jl zvGH>2k`*2pKKoib--uA03qv7eF3^D-xnpgpawk1~EKz_7^5!kKK}aJqGKgS}z$ix< zsp`D8uIJm{E|7%`47k9#fd~D|mju_cRa}sDD@RXqX-rEr%juiRM+ud-NfU z@ySo<9G%=p;GRSNZDmsyARo4m>vDiI&FJHULV;C{D{yv!Eqm-E0=*xiE6VBAdG6z? z0F^?fQJEm=?J=JR{TFz#U~>$(pKC|QeV|e%!OueT3E-d*#17b?mn(uBlB3a*&HKQ( zxnvbmG>c`P_nfD;Xj27lPTQa}1(f?IG5K_Kz;c+)CR|WoF7oDs${kl%W}V!m)m=yt zPUF2@DmiQftpncqjS%Q4u-+=~tizTH=gUwrco6jR;fs48N`s)zoSCseSa9KDq%?ad z01xgfUqO<3vp!=(>8u+UD|PLa^Yb(_04x~9)d2KYr|a{Bl+=bZ5@87m&K_Q@=-2gP zx3)dc@!O^^5#o~EGEGZMt2{vLh$AB-uXquqtC>Rk?GZrK@E)t+Ivk>k)TB10jU~Y3 z{zGuqxdp@nP!0_t2rn@0LFi%&F8*c{jc0X~5alSJBJ=&qM(BUYQ8coKFv=32$yX~v zjwu$nx8SVIMM>G^CnzH!`NMPDzocYwEVr)||($#E7-MO8yOQv5m|$hYwC&$OV}p`8eCDTUI(bwHcXV~kGH->i+ZN1L zW7E@Lg1ZU3=lk}5aF_o7xZ6~>kQrfg=+bydqmWFazSRX{yM3(P~HQh*Yr4HQJ&c7McFc4p#wI;2kCrh79N=w zq4GvV2;dO3MtUBo=|#^$=QrPnxIt-LJfC>uaBm+zq0f5}xMo0llE=AO`=h^~z~3q! zvDblyw@KcR#goHq8)&<@0xmC*qSuW|>-GN|(ZNR!yd z{QJThsAj3KAPQZe*my)jl7HkYpK$;37vL*HqtU0?vao$a4KCJk8?kjAB03uQu8DM{ zf$R&Ij^9@pduC)7D~}KNWp~!o(UpJlSVWoO-8(a=mc=G zr=+BqPkzex5=`&206t~Hr%!k}IXNoX1|&c=Y-??Gg;xM?!{0#!@SX0zcM#-6;3CDq zP-WZ-QUu^KUh-gpuF|P4D?u8>@QYquF|`xn)ac72Y~>e=%?WGGS1ZBIbAKhQOx+wy z9@rv?qiL9y^TQra7O3=~hC-%)z+z}M|7toU3LQ21bRaT!LoE5T<0EsUvu@SmQ38F* zwUtWvC$gN1F zQWY3@D7ISHxODEAEd9?Z0JA&CbZP#5(-?cIxF|34wsD553K0<;l9>-h#taD+GE=Qk z#J{lQVYJ8a@-+hD)7|`)BY7SERS23BRF27%c*M#EIPq zqXiF-Ex{n`56$8qEy0MwmU$P)B)Y{CA!{u^2Wl;$*n#@R2jppQ3FfF00?Dix?Ck;| zGD4U^o_lmzKX~V zIUF{OP++Gh2*53bcEk=D*l_rD1?d5GFwq4#%kYfea>=iehyTaPbWsYFTsd$1$l7u~ zbUq^iDLd$XreCWqG$Xj+e=s@}ISmmJ4wc7V?6;2q>Nl-+kG`n9ii=}2qM4YBGW^U-Qt#1IYQA}nNbH=6S+>i;H{r^w0srEAF9^JS86zx}In z-|tm=DG#88-5cGfQ?D#?DV2t(_tfvV;scWNu$P8tr>dBmB!L%&N3>SM<|pyL<9hnM z3C;L;xM3^4l+kZo|GSI-pK{fv=sxT=LqwgfbZvz3wDE?2hS(-FxUAM=msw)V>xSG0 z81HQs>;;*wjg{C)YUfeuu+LTW7eqme$G!gDHOo~K$9Y4zhoZY?gruZ+@pn?R0fFs$ zjspldoFqd*GKWoGarJ213AX_gYHO#-lD)l}vL}o7Y$yYxejfln6}zqUiziX2!x-DL zt@&FYGj8PlNEC#sx7HbF8$$id&UD=eD^Mh708d4p%jH1Khg^pN3NL|YeJnPU0^Q)Q z7(lM&NCFTf!bCmxn!};F0@6jqhL7VhlKO#hDk&)`0)4lAf%F0f9+5h4H)uTqi#p`^ zMhHOw=tAy*4XBsw?c24-B!Az9s8_Gb1umOp=jCCb5|fgE_7&oV25n6R+&hCiJNi8b z?eTED#r9Ks^qFq@(>-GsCyAOtJ!$$aTD;+X7Vcssmnn*F*J;Li_?Iu*cY}WmXR)W_ zPaDxzf0&#@ZaCgr4V`9eVbB?U*Yi*EsLUrCA8&nE&aH%IALa$5W4L!OoM)E)1ZET2 zzDt4Q+JijG969s-VY*__@mKI>o|7t0&}}( zuA{8y%=1Jw2i|hOGZT>kswL2AK{}ED$Y+G$4jlFH2{^uID|r45fBXhg=ZcnbbuPISpdJxBLe6tLn$|{+MY2tu;G0uOE^=e(Oh<16YcP zSvt&NI<(+}FMq<9WD0n9cuh(?!Zz$`vuxeIso*!q#=;>nhRK|+ac*xfAPN1=u{z1s z6(?HkfD~13;E0W4*KhVx9PwCZ)s`l zkVv?bkd}t@x0F9XbC4o}$%5yiMJ#AB5G4g9fpB!eP!!Fc(gv6o1$q~tI32S2Nnw|? zPFK^p+&zMk5&wOx3Vle4)6xh**MM^U3p1{nUnL2i1j8`9)6Ze7$?<<=@P80VAz$AV zJ^nlLJywNT@u$XDtDvFn(iY5732cZjUr?x%z%&pO6FAQhhE|@k&191IjdG;$7CCAP z)LxgS&qBLAsZd5;K6G8VLW)THi7vsK8D2H*{{H@-6tiRib88jhgdRVF-r(N2E-nr-#{C>RgUyYu zN5B>si3J51AR`Z^E}r$1NT~e$Fg5+lx_Xdv-BvmBu|4|?z#K@A6D+GwFXI?jjausZHA ze{F65N=#Je+DZ@9zVTas7vFpMM!8V%9WowZg?WYnd|IV*4H`suEC=?#L-!6s3;>oV z0Kn^RmXk9z6+ zpf$qR%erml4|uMmq$qTbQWnO|R9XR3@25j30ky>d8{dLiAV_M3d~h4bh#d$)H&F%x ziyzs8_>w)4vj9))UmDp|0H?$929AVpXlOa4}@%DE?`7? zgx-CJy5`qk`WG%PJsjRteaEyB+~;qJh7N|K;OeJkwtM{1sxH~cGMO4NeRnn((HL#LIA_vt-QQ+MrCYYIi6VuH7TjJZr*#}W-!N80W$o;20U;-p z6{0wGaBxE{f!Jpy3_+owfL^S*2B6d>{5S)fJ?Ug@_)W^T4J~kNNE$3Mk=Nsl0r5=+ zomdCp%2y6a(9jScmUH@fFjE+^#y{-d9YMlKDBteNZV7Ya6em57Ag7-!p zi00uHSaL=g*p8V^2m^|nZ}E4#rM&32Zqd{>>Kw58DD0F#unbf)XxLUgGbS1I_Ii(j z!op?)6zZC7XlZF7+a62zj{*gWlin_4hU1+!&Y^2TK0d<7qp>14L=UL>U7Gj8N=20o z+_sgwW8T%>7}8>!o(@}r!HK6D!fc>4go;W8>I|3N$CoNAIi=K$k7z^0aQHSxWTDYH zW?I}IZtYSEeD&&$V#Rlr@s$gp&-a4E7rKy*QF(;CY9CT8s}Ik3ML2FfI(yMw4rHaI z`rYAYK)sJT2Yh?1f7Pvr>MxJT+mZx-usG1TfCPy)!iBY`+CMqtQ`k?|)ZDTi3$TCp zK?_~IP|Vja%51dy638s^;2#oKHM(P6_E(7SM^84z8pDuJZhwabsX&K)yMdJ1pyd4g z8`-6fcI1I{2ihGXz^0XL)NL34aHYD^Gb1q4Vf5MOr7EK?9qoAB;NZ=dfhC%7GC(JZ z0#-$$aRG4Y!f!r68ngye=RLgMBYko*`38?F8n~bKN5GyzFC{IOs@jW=4khF@nVY+3 z_LXgs_S9k9ZjC4*26yG}D%7TYJ4GH#QlFFMlNwEtimkr8`V6=ixo zuyE3YaAz3o#0q;}0U!fL+&b6boQEXKpYrp9nM!$hh4@gb#f%lM~o2 zG}h^w8Dxc0pSvt2_EyC00;VXH8{m%JxY1|6zuQnL{1#G9I7DH@3l=?ny+qv&`G!)C z$pSEV&N(zYG>H7Ff~vw4(qhfPC-0$MOxsutszTmrml{bs z$q0yYlfHZrr}(@;oxboltq)FcMdh$7l!HX5BTauBPx8(8Hqzaf z|DdJ+EnuW?UwQX1tCTonPsa-)^F$+6IkpRND9D0pVp}-)UX)!Knc3>&)qRHzda3g} zV!xLwhCVDj?*5_6TI#`fVCy8BmzRywZ$V$Y-`CdJ<@ffAQ08M9;b(?2L#Ep+tU4vB zvwA6=O~8!9p%x$&c;?g;i9a>FTH}VPx3xq3Si79OFPG83aACD&wpKVDemH@v=4`s~ zuGtk%E{9C(*?EJfxTG2%V5i_bM-A7BmF~Kq?biR4R)UEv)`KnLq$L*QZ`s+-#aoY9 zcujfZOGQr%ZCxkc-SckyIUU3lS4<;zV0SW(P%pHbeJv{#l`lJwV{C#HYp^M#HfVu9=xF$@1uv z)UWCGQf}d#KhGfMuD*a$*NfnnzTSH-jF_Ic%WHS{0oX!kBwVBq&)=#zpUWm4lS{vMY1B_^_v_0nL%pug*Ptn}f;bBjcIb&KBU~CQ=O6W5U=kJOE*Btq{ew+zqY>v@s5%J|3%a?~J@Q#gm zt3ex*3-*q-ZfLzqbe}0&dECM0^0YkXiv%fg-^>-noa|awFt8~Be7Ur?zU`}*Ez84G zztrO5HC)dW<++oceIg-;=OpYpS%oqWSJ#&1g`%)Lba{oc69tYi4^(5{*t%%%V)2IR zY1oeOor7+HfvD=*UhOZkCP(R!)N$7$`kA3=h1kK_^Hy$KJ1(rfyjM8eiHZiWKPWWl z@D>f3JhR=h$RAnYcc;f877ag75SI7Tv8|U9G@bE^8f>?vr7hdqNL*#;j7bEQXJ#Z$ zlJohPI%i>PXzXvzYE{9N*BPmlXOGyGF}{Kby5U3l$Q971SoP`jLl&(;7%2kpA;Won zB~-aNVF5%t5I4?qWywrk{FsVg;j-(kICY|6Q}Yz7X7R&tp_PfKb@+wx4gT)t&|eKj zFme_(=(xHPzq4GZiNcAwqLxRALcv5z309lo^dft>S9Vh+Kj*yXj86UjT}y0sbZZR$ zjMYe`5vB-$RYM452xwo?jl~@hka!gyK!{_H+A|v#XCR_Bkh09WnY{s*AIvO+kel-D zmn4X>zYmxr8wX~W21J%wFkZcSZEHP4|MA34Dowj&Gz|^DvC*0+oxCBBdmmU^gy7v= zGs~0=cR5DZmFqwuUEGA`b5ZLL@LfjXFL*{&*tl*W0zuLY`9(bPRa&+PyN-eH?_%mJ zWQRx^hqV~B+W(uNBbK+=BjPh(^P+vlnn!r9G(`9E_LjP{C~9lF<%J9#=I;BYxZj68 zh7U_Ajcp{^34zwmadx?7eRu*f zar>~#pt0}3x|B2aGbVPmWUU2HwCYwBA5vG$YI22M4VDLxuzTlsYvSZ)@a_E-v^S>} z2Fi9~KmAi#v-=V8;wG{4v|AMm3r$ab>BV$!WA>(K)zmbay}j$LsQ0|WM_BjDpH-di z{?daf-}l%K#K7VoU^r40{au+N3-bS}xzT3*D31zG7yPV;Od99U6aS;j==0tmWIVv} z1rw$e6V9hVCPzdJKoJJcS5oswC}|FL4$@YvD8NNV9J|A2ouE?j_-)$_2!J~r(1SvY z1HGyj!R+@v-QSgq6!-KIsC&$cQ_9u@dQg8cK9K!_f%a#$W&stn$7*ly_vQ5rI`BWz zAd>9ugCY0WuE6Z#+Hsi#8`Tb|oYIB>GXW|< zXkkZ+I>)&pdc<`@s7j9#DdB7dRx48E!*g4ezJaVoxmzuqOCJN$FN6{Y%vZ-4w*lK-VlM<5%Lxi$~2%KhxSvR z#bB#^&(!0lJ|v4i{@G*5`6jJ<+;=06;x_Es9LW)Dd%nYAqppt! zs#W*qF4L4t7Tnuhtp7K_xwJGc>Eg4oE_vIhaY*yc$3JIyZu;X+e`!`W-NDek-Oc_> z7diqh5EV?j};XRiVM8%mWvP zOqqBy@x}Ic|SdgM$ z7(d}W)PmjgGc0FQGp}Sr(;Il#mA>ws-;pBGOE&?=v2;2$mjq3 z;i^XADbwOFXFNR#OP?I-@_c+)@7LEq27<-Ky^;0WS{j3=KQks%7n63EgSunBu1b|S zaX#x9uvfKQrKJ{93S@JdCJ`y_UKnWEF3o!MDNV!g_wTe)KBbO6wx%WrR6!6f?j<0k z8t(~laGb)xB9L})U?8Gntx+y3BvaPNp}l$)+kCEvjE0P)hUt0yNyW0^!txJs=m$G4 z%FxE?k0t!Z$5j}qtQhQ@n1~LC`^ERSg#0TjvEIDV9S&7Xmr_(j10z;;%ZU>mU0#`X zt0(V2VBD#MZATm#NeaW$T$y6?DZ(_KtJ`>-CgUAmuG`eE@l_Q3%qVZ+I9tc*;>>vW zZtwCphOorXpI4c1JB!@D44&=Z47&g$eZ?J$M7)pA&J-6)-Z?>k+C7^4_Z!P6nnFm3 z%3fBu<2$Z)=E(=rKO7KD4G&mHxvti-1=6?2Q`?Z9Vx_dH$~> z?57&R=BfsFxl;`{7NX`^0>W! z??Ds;p_QDD94vBJ*9_CyjYB z=Yvmy?)iy7rFGCE#xkrOrv_`8+s#h03UMASqEUXb<0rJ|THxB8d`Vq5ToD3O_dX7> z-nbPrjPLIAoBZ&mpTt3F9TZMops-nva0zcUcXe~a%FYpBX4|yM9-rVH`;Ki>*Kn$W9#e0Rf!80=q}q061uvM_!YzA)B;~|J3Fte?_ywUR>WVk!}0VQ z`xbnQ`AU(7&HI!8_cwx# z5tfSHd7{XD%gsf}$#Du2ygfriEUp&i{_$QAinG{!Op1YpVeEnzk;u!nH6L0m0^mCf z4&zJWdM4&C*CzGkUhUkhm6Mjf%*ok{LSbSC;CLo}tA7f!AGz@Gs-svx{a|8S^q_q` z=T40E7@qM2dK161IySh0BOe{6IXN`fAO4)@D6`}=>apfGf5gXY8tdN_#gvqjqhw>$ z)A36Exy*HDZfZDa0$Rl4yy2h5R$C@@cDFu|;1ONtu~w+uH$KV|3UUU%42?%1AmpZ}b!?Tm8PTMAT8`zc;OL z3CB~NfYOSTNr95;7~Gw~cB^=iKk8dkN?hSBQKhJ|RY+UeurIsI7}oe>H9h~)mxUAx z2m+_cO4EfTV3#)Y4ZV5INvo{2$NT=~Pq)+zXEhh%n$QqSI@W^fom$^szB3D%I^Ek{ z%Bz+Z3qQ(MSG86=UFsQE*BsCLYC(e{5&!m$8(7btk*;sbpBoZIbIlu^aokG(`jxx? zdEW)_F+PlN_V(c6_7TS$JO1M(9IKV!O_j=#ld#}D-Q4UOV>JltNJjX07>9ceXI}-k zd2J3-0pfCDe3I|HNQuZqo`=C$7w_Mn zXpD){$8M{5%?QCkp-z%`z6xF`=7um`|oS%|EL8;Vv+M;oFadDr5?KGB&}&xp=9S2{Tkc-S^Ksox zr^i=F(b8kv-NY8h`zT(~h~VO~o+rV6T!)@ff;ApfIP_ko$v&Sp)#a^;LaDaXbo~`7o)st+#*EIKXx}#ItUBlk4|_<4Urhs@WV)pW64w3f$TY zV8JvPaN#^(xA>u<-HLa9$jO_ zm{_Fu_W_4BdyS?;8-kfz>4pkN&w;G?gnOUT2$QBJW|x0Vk`I4qVJj!5bD*fdal^pm zX@y^zif3j0_SW<&{t}G-F~ zy_aWF`|hapXtx}WHA>`%$xJh$!3W>6ENXsCw$-qf6(UQ^O*Ru-yaXO6+$;VQajAcwrvPAHGNNT6Oh#wdH%t~F) zh7|6L$v_=5E74Bqp+idIJ`8T|NS+8K>*(eiPCnQtEIn`+K(5J}+68NCP=9lvOL`Gt zK|0JxFCs#SP2BZd0HAZYKNAwgoSgd2wA?(5NC;LCHs@e1n#*5!lR5e1NzTk|e|R$R z3S7YFA`|jo@NoGwQm)sMUR(V#01OoU?%aK?)3PD(K;>{}pB95%ts!??t`aV)3h~eb zegQhR{YPga)l>`T2&SLWBDIww%YRNrQo=r_$abzm1PqQDz(a+@ZU>7&duJAXN@wM& z7iY-5IgERCKC5uxJ$dqUV~(g05Ap5s@fr0SD|E#2EzC$?pdhwPD8K04a*S3DqOmlu z()!RB!e&smYfW?U7C~>wPPPAzh7sH2-^?!7dmJne`->(c0v5B!yos|)??$`&`6?6# zx|b^PKRtQT=@&n(izBh7vw(bAJqP5UaB2fVb4*d4x;C%W8fSGPK=OL+4dLGgXW#_v z?lwpA`u8_KRz(YtCw#j6v&U^pT%d1>-g`+ka~OZFC)f?JcbEpt1BQ6xD2`k)c*xoS zp^+WJpNe2Y!_!m9bB1rw_B86wLxGdT!tAuDrSUTAikKP<|Fl@OZUx+gec9I#@YV+w z=_(|N*T7CE)^6j1^PYSgu57uFBGb}N`*VjCfvnvyre(SoPLVfn)|84jUrhA`V!QAN zoW064nlxAIUKc0mc3^GeSY6kUk)4$5JI~3;pGh*b!9SVuL7eHiPr3XbRT20LN~_)d=T2?Sg_NfD9XRPQ?5|&nZF$>KVcfG5wp9??{9W{=68t9qei|-u zv)mV^&6nh4-AOZ+nlhIY-i&RDt?n!o(`6uos!tjg^b9Nl$S+ zyHCbiw#$ivf^1=B?*R1UvlfT@Ik?H(6^`-ZB?;EAUsWMg1lIW3JUVc0n~sR;^A}o3 zk+!oFgt82&zmPLKBCC$*(>)QIeNz`9>PwgAVPww-+Q`(;EQD~lgJF~SwQCHNRHrfx zmGM?qsTA*=8{E3YM$V@eM5j>x^~q>O!ph2hZXTe8w@nzkf>=TLj3_@U;OebirUM?j zprE|*mnjUT5QgrW2F?xS(V^J&ta6=GPoOT_0Q3=eK$@0j4C2%(;!-g+7yi;zyOq>c^&1++12xEp%9j0EAz(%JO-kDPod8hO_ z@7AMRv)>^yj}g9Uit>&x7A8~>EdX1uU+mclNM9T=7@L;}-)zd<5mcg|LjgiE)Ya$TZu}~gW^tw-S z9`)y+g0$*~X>`AtWQ_CJ(ZM`QcYJ^|n>5C0y=-r{03)v1Jh6?1TU}MAw;=JJ#Av0{ zSWF+pDSLMT!>~(txLL>$LR&IzVCu(9Xb`(oKFI-P%NFNjakMEB;^#=RPEK9vH;U)I z^*&(dKEc*X%j&gV`9sluP z6P#w>+1`8hN~YeReU_3s{a0Qz^_f3VBsnQ-`o#3@E&cD-FGfe%HJz-f@bIbTej-0= zg?2QY5w2U!8`xoh|A2J`b#n4AWG9*K?{2^4n5+VRE}Ko&6gtoio+Strez zEPdt2qt(}uZ-fh2q61K3js5NOLu>?Qf$GF6TtAXy>>N^iSHyc}rf?8suiQZ3odDQ&h(Ftyu2ge}|6x{=NOqy$~NLIZ)!TU!Y4Q{qP|rl!ag5 zq5Y|B&a%%;)-DXI|{xw5*RC4Cf)9f8& zl~dFsKa7#f*I~+^xLl}H#jbAsK*Yi_N>Qc9_05~-pC57|&jZzRp(Q2?_N)Xw`Ps8f z0j||2)y8UL#QCYw1*fyks5zpQm5@0&3c_hiZ*{6}k3y&(PRsL}fy>%xwYPp6zODyBMVL(QyVqj+&DJz4va%Inj^u(|o82ffS3)$4_d; zuFb%|=-lKNsKttZ&+rC*1S_CPzNCT3h@ZZ{8f<`Gen5bN;_n-6mDwVv2#}GVhz74_Jnq8l_yI7QU$Fttg0ty0!3p3{c z+_C9b^jlf6g7t;urf@zWSYG7_n!z(X^q68r^rRR9)aIZACaj8iX_Osw$ZVOOFVgy8 z)BYd!-a0DE_G=pkQBf%olm-C>1tdhJLqRZ*5EPV>P*h^5p-WLfQbk$@0i{#AW>8v6 zq;u#Rx|?rb>iyim=Y78V^ZV9%XD!8mne#eh?_(ePIQOQfs#T6ue~sFnMR{|c{nEew zZ3{wa1mu4DL=3dZQ!}OV4Bhj!%_u04!b$Y0RLAxcvkb=LasAiT+=PTrtaf~u!mo?y zd~<>J!_za26~+R+(dN8s+Ojg}av)6BLOT6dUhl9%ArT3M zfallG(XOHe;XK8rH(nK$B&w=1klY-#0Gxfx-na51;LSPQJ`u)gnxi3_AVX(=(v7dN z@#jkXFFIMEttLPsHfH(~`f_vWTYkVD>guZc;hA6u zV&D3`JYXgFIe|+ptm?9DoM`1lqJtOau1&Z|$`Zre|{aU#rIr}BM-Y5~_fjUI3^Eu4>K>paPZ zndjMQ`VjKP06~1`k~*mbUjI)A%mOAS_nMFUQK2nK<;2;0K(6#!M7?s~squq*@zaN! z!QX@NN=iS8&&uxZo%`|EhQsz0&&`{`Bc^ZhQ*dvkyfH67B`2-#bW}l+b;Br>ET7NY z$To^v^}f&^UQd+e?hXyKdR#Lbqh=QF1aexi7UYR-y%Y#t90@Y&$*q9e#~-jqF4vGX zfw2K==ZbS*^iz=_-v!QeL%tsjFg4K65Ein>JTgZXBuM(0`Q6O#gHiu7XZ zyCjgx`BxHKV*xW4{GM4ov`farJlzTBRojG3X9$Q3N8i(U@S&cI3SH(q6!Y;6N8Ivt z!NIr`9tMHZw|=m;t742xvtaqYo+14YQmDU#6eHV_l7+)IFV2v-Kj zqk@K`?Y(+(E;;u`J8lfQB5c^pLq;m8vtL+!ZYudd`V+#}6r$(`i;$B2pTmtu=kFvMZF{_HhEew;Q zam$>-IVXWhoi<=a(jyQM#@0I~G>P9jEpbjU=#-q}Rc!jjr>Z4o;h|yn2Y~L|N>-|& zRwp}+CXjok=B{2@ehEV!e5Z1V5$j0e< zjE~PbX$Ji9qS^eLPDxzOqWm`JxGx{n1ded7Z2cJixXSqPyGWD?zp!#$+4S(0 z?xL^8Hv{z)8#RZDGaOpC^Lx3byaR#khH@!1tMgh0)v2xK&S=dH7j65I>R5;M{%>n$ zpMuS6-bZwM3Ah*C#C@?AHx~%udcF&@IMd5LV11%%1Dr*Lmw%>r4ut>^5|Gx+Yyyp3%;vx;p}f393q7V0;iB1UIDfkB<==M{g$vT= zy*4k_$j1l{iC(c_9_N)gd2%=$nCz{?vS8y7Mz~4_wo{eJHWzNU1Sb}puW8>9iALO^ zt{n$>C~b4|Be?xt!G_3tvn7}*i(WFKAN8%Pi%FU3WJPRIF&A0~sH!HM-_U zUyCyQU0R8EiO719I@q-H;FN8T z{Xudp;*2SyU3CbE$TljY(nfYTZgTb+=ny7{J-s>n`y z{u^m3w5+%fW(1ryH6-CaH+4cN_Pr&ZB+DspeE#k+g#B?-olZP zGlaO9UhuRpv}GnFZ+|}}>HuVM5@+7BPaj^V3*>lrpg9+(&KyI$N{2s2G>?HIpUXj|0UV6C#+tt;?IiKjWemY=dzB=k`yZ4CEa6i12pC zPkargr8gdB?P)Cm*hY5zTl~C`Ook-TcNy3%dZNLN#nXFZ`qgMh8#9RIko;B{FP|Bx z>E^0GU8OE-0Gw#JIQ)_FF%s$ln46mYOG<$ukl4-2Nro&~kPMbo0Qd3avVq6>>l9kc z&nLDAXa@cln(Ks2i0N<2qefAilXb^<$ZBWTA1e{#G8K1o3saU5Cu|42EotdE)X z+&0PKcY3s&BV;&w)CC*=U%7{r`JUlLuHV^*s%<+1{dypyI%pC*>+Geq7f^Q`EbseD z6{$41B(a_qurCcPRsEAVslZCi*Q7!}h66+jfy~dEImAsIG+!r?mAO*?>>Y|pcIX$F z)_3AzTOB*J$uOm|eap3JMzXuq(TA1iI{F5}P5gn~#KI=ZvbS!Pum*ksImF6}U0GAU zc`_EY)z#*(me3Nf4KAXbMnxyBVydZE8}>)x{vHV+gLv&|XujCC-IP#fdxa23+9AV4 zIsY7DSTzAdbx%O55B2rls_zQ}v$0obUTPCRyboahs=E3$??6AmzYw@ybN^TXfF(+V z>Z%)d^5A)3^VYHU$$Hp0j+1~f{r!W5ya%C!q>S{o+3B91okmF@q4nQQx#U70*!;KrMfo2!GXvrKXP0efb(z|e4t&mTTF4PcbYe3~a` zhsZ?)Y>EtY`F_79)U>sEhDSmSVbt{5DgmbeXR!03ChGmKjMnm*NhA&eENkf?>rMF< zat!al5U5BmQVG3>G$KqAbfct1kjZZ%L2JCBJ07<=COUj49L`$8y5ri+bG#6M?%CVR zfD;Ku_cqVp_ojk#h8#8h-+V>OAz4>+Yo^;HwmeIo^z^$dt04gk_&e-sC|_O6)M_OS9k3bI;n60y^ahS1 zY*+WidF{tzW62O&8m>%2orzaRYlt1#=$e{QZES}rU&nLz;EK%UAJtN>03a3@|iz;-NQhsHOZv8O4)R z9p8!!nKX)ffPIG;`za~KM-G=-$``2_DM*S68;gC?ctnEU{6AC}s4W5AVs$EkFJ1?Q z-S>y-V(jR*>RE5`4IDfWw;Y>%{u}1YYj}e*SKzX z2U739l}XZfqU;V5WtnxKL$Tj<_5NHcdB+}$46-GzB`KBZw}c7Uok3H58UtA`eO1*= z{HbZ*A5=EuGsHwQy72`86aDcsI*ap4LWK~upluY@>=x&&B4(jD~TwA zYT3iCpjcl5LO#tIpWf<--x)XPgEND-zbxMRfF7bK^1nokD-ACB{8hX|!GD9y!^N_W zj*Q55f*l615Y%Cw#N83^WAHwIu=85>SL;uEdx$LMFRhkdNw&CE1JD@4;*-!)*943U zL~OeDs$fOZ@t=#p2s^BGXK$GY0xxL%B!6nC41fcY^~8!;DUZeZMXnYjY5^daCkqL; z{*{Oe5*a=R2BqqJEGUvJ4K8&;Su0LC0u#WFjM{w*r6C^ zyCw6YYv6y!$Sd|Dzgl~Z20j<8pT#srM7VC4H)I3Wi;dWCd5}|EfXCa7aW|flb|2tH+}| zPl&C)n}-}sRaf?K3AcJen*jviP{i>Yq(W=lODpYd zcJn$fs%bMpIjcZLN5_;s%A1-)`+MthEgj>JwpQev+`)1{$^hWpg})(lYoa&WaQ6HZvz9smgx@;3t9bLZ}q<--6Nm4-namN{qDx~Eh>HP!ZjyVyB$DoSnjfL*yQ-WNl2P#Xed)41r~#Jy zDx{JS{0UU60<6Sgav#H*z^fiR<2SXnW!(ivUC;?EEGETBUMd<9I-;EuVKpI%-ihiv zMdyudA0h9)ndg`~=AKrarh$wty3}AJ#Cy4&gg8GyBp=C%m`Q+}uR@wO!U!u#sQf!o z!Cfr$x{c~Egl}c*dN0bHgVpYZAcpIEXUne#^v{zLwU(8#y&UdVXk2*k-V@|>eb)6M zPT}YCsGc~i39663y@zbSvh$qr6l^YnS_O`6KSTco8zhkF{>?@}sM6Or&OZ^+W^990c$JlY(gzK+mdaij<4Idd~MB@L=Ap6EE$zT22nJz-yo1i*Bnotd10nw#}Hu z$0Y6Fk3N3r5C!L)TR5pp~fJj==uCObq`%Qbr8iHu+&#UD@ZKJsYu!#Q{XCQo`SSHg(EVnSl0IGGTRdV1rYX{pg!kVMf*KU>?FTB-&m(Yc#Eqy+2VO#T#9vD`}(qP6YBYQFD{9OCY8BPf?~g)?Dy| zudeYQJI1I60qSMbiz~h75&PNmP^e8)b6IKH3n78SFQ+orhmK9oe?JeSM4S<{2?~zS zY4V4-&24|%0KTCISZk0_jgICbWH=2Mb4_(X1;l6>INV*ecPU{P1XMcl>8U^`1Hj#} zxXsNpYXonWroAo%#!{m6a%;Eb>Z!oUjJ>kO>W*+0+O?(WS(a71ph#(etz|rq_!2uj zl~6YNZ+s^E{wu2l2>0RFGm5zAC7@4{gh+gRs-y1+5uz?uP++ns8oSl+Xn^pzpd4)* zO8=k*+zlidGi2nch~ObSjsHaT6&{!WNYO@j)V-N9fjUst6d^+3I8=2Lpp=Nx{6k`X zZcTvs4GAI1HrIacEy@p=2N?8kz9-{XzNUEIb~Q!*`>!s=p+H>zKjbHd2?fbASRpz2 zHvNBHPHDQh7^ z>$^2@LM-GDoJjI!Oa;^gi*nMf)D6c`xt%5XXY zOsZ?d<9dbeoWJldNlsA_AQmFiq+qHPc?LPIEYr@@QkNc@K<4AGO7*00Oqb!T05FB* z5R?Di-O*nZ+0uViTmtE$QPW31)q5d*21gPtMPI?#Jm@PVB9@gH7|j0xLD-?sZI&v) z>I2_kyOFe$jKt4GS(Za4U^Ec;ahSq)GLU|?<0cw?H!+%O_bM3&Q7)y?v7X*f_U4IQKu3Ta*zH6Sx{Q#prhrt>bq5L@sr8;@srt>ygmI7F1uZ z|LVBJ$<`D{=fTst#`j}P?B3d5&V)p8LI0Jb;6Jbi*amliFXiZ}ulD_y7eAnQ`5wHd zvB4uA5Sb)k22l>Ym;FPE^uaC-toKq1W0GRifu(4*%75DOR4K}i9>14QufW?O>Fjf1 zb*)Yt^+x0-%n7%U?c~kRiHeBMAVQd%2oc750LukiKq=bk;M?d6kjKlqwon2x*`(x~ zZO_Q!Ie9rpKF?)=b{4E*8%}0V%hrj4u`N)@=;6vD!vBb}a&SyT`HFuQXEiPZtJcz9 zI_Hx)6xFo!sObe*dWb@XfAC6B#(x>_mNUB(?ew|6z!EI1Q+Ngecz~%hW3)_T6OSDO4FiGtC`1}Sweum&QsSQ~7M_BwsD!KrX?8v*t8|3rPze;)sw9j6t2Hjs0# zyVdy?oGc_I?yb%_?fu_|)_&A_ceQ3ku{}|R3d%$?@8I`&vCAh+FdIMO4#-SwjVe>FOFZYxiz$T0DK)MF%&s=8_zi)_y>mX|_TyA;@Y(ld4$n)p7 zfRK%xD}1pS@L@0G@8ybUg}2elJbRF5=Yqr*=!SwfA^p5o)}11lsl8L$=3Yc+lKpdCCajo7Zg(HFx=yt@;Pk+Q zUc9J0J>R-BBBSS@pFel~5o{k;*W6=>xCTkYEn4hz#qIrkstfXeTz)C`9uc6?+&oJ|Us97gJszo23 z4hV##<#4|}>2_#0PKG*OQG`I1Nk@C>(a%qIL<{H-U4~_ctbxQvlgpw4q@~^G(+;Jmokb+=fYNK5EDnuI=fjYJfxtyY{@-L{vT+vv(Ohwbzai_NP#5Sj zMy8iU9uIOTVV>E??H~RJdy zFDVp()nk(?I-V2;!LC>KlJ}b*zY%awkIEY)a9}l8>~* zydXiiC^NJ}zXB z<#5%L!>N$}Hx78e2VLw}RRRfim+zi#DZ2Mp^lr9zH3Pf!NAxc2&N$?f`&;$S-35Zy zg8h7A_pWlyM+{?q?{=4ik zoAV|jyZs-;^Zz@o`@xt!%~LztG^`&(3_S$5?%nHu zZ6xw8Pyy>7D1rRp220G)MjBLXgt<>oz42MO!R8>&?%ecA`?~P~lW*d%WLiK?LJ>Y)+d7YbsIL)c*17 z&&;dxI&}^a6+%(Yx0ui$1^=EVpCoAgaxAM>TU&!LuW@k4Vs-p3%k5#ZxBg@;f_D5FYTEPA;6?^IanjqYgA^5%&FI~Fp zC$;&}(Z%9GM+U0_T<;mvYx61SxYQlI4XPKq&YjDIi>>ZB7a~o!Dnwt6E0a0e`@^dX ztfMC0lD3{MuP4~KZ5~gI-Cwu?Z(4t}aAPHy5#;N20`xCB#t$MNl1r= z{ebWO=3tUkQS+z@YhVWr$qc*!Hzwtqf5}KZzw=Jr!L$oQ=+Ez+tvCmqIHfZ$P_pTL1=idzp z5er7G`zWJqJhhO})caS^pQPEv!e!@u42@rR7bJzf@IoqLn5dNtD=0P0)XME9WWIHH zL<+tPleKapU#MEoAYbTyf6F=6)mhP-wgbo=lGy@x8l2-Q8rgYSmA6w1GhYzIH|Lh(3 z`p>1KU!fP=kD@aAAF05-o;Ww32{c0VRIr!&Y;}b$r8`XF+~eJY7@`BbStH+!em;Y@ zwzX($XQ^10(LF%4@onA2sP^qQr-83y0tUZ6^9mL{dY^rG^-2iW>MDg3WBpD1Gq`XB zw;i(W@ouLT9%{@Dcz%)DKl@O-mxxGsZw_S}kv(y;-$e?GCTMs}_3_T+UK1Iy{j-T< zTfK@^qFWnF%^&9PoCTFTu440b(}jCiph~$QYs~1o1uTlgE)xQ+iR4Q7V4j=Pr zN*C$0kTo|Ce|)y-qjK>=>A^AO0h{1#S(1wLiWg_jko*xV*_Tk31h`r?u*<=1rA z(C|nG!4O>+wZ=Ei`Q{C~ z6Fyz_ULKWt;=rrWj{D4_szw6WTt}bw3mN;<2i6PVkY=_YUX{#!Y$EtcufRAB;1Th? zHgIe1_n91Uk1ag)Q3N$0>C8dBid`QI2r=E;;&DC|m*EG!%pvZk5jxFI3}?yXx}SdK z76sGdH}A(Azq*7rg7$X=UR8K!oA)Me3|3rJ+WHap?qh>jdH3^Y&#rcd9-~!mLI$7a zXj2y08^oK>&dW;^uke_7sogPzpCDpZ=|f#1Xgv9dPp6PpF|cju*KPg-#wOj%0(=S< zb5F^~-d;$&^QA5fCpkdpURQT>V=(@LxLyIch5gNf*IGCO&VkJeNY#tn7eIxvhlUA@ z7<~UCJ_pywVoP0kzbXb)-qXJTH~iT?!OGYVI5K%rGWfrGb zcBZ|*3<+`nCW6~U&kU52LZi8=5KXs3Z#!T3g_Zj7VkHOc2dG|Oqrc3PDkt$Nc}i5z zeWbw2QCx!3BLIc^{*&F)C_i4j#7$$WqoczsX0N7MA~$5J;Zi~Fqus55jZ|D@AG_51 z;9%7Un{irz8>Dv9lmJq?&*$MkEa|_Z0#=j17aAL%|C&aEuV;Se_?{z z%XT}lA8;8p1a?WsefRRl&a%Kt6g6@v`-H@5nM67$y_&AwwLSs&s{kmGhm@=jR!|j8 zt~d;q;J;<+pTDZxpB?_#XKZXHIU7Sf2+P=&d>&h|7%*d?v zZ>k1^{K_=5=*N0)fm6wLkEyuyNBIc_Q&XnBl|+>Vm1VbAjxuh{;JJ!f&xq@r_ZK6~ z3pHRr!Dqitc9o!T^NqzBh`Rt!XS;h}Uw__|s0JVjTYah*k*V)^tve=n?<6e#6BC`FF38X3fw;sWI zRRIT^*~5H0K`|smtQ&Lo%H3k&fOJRCEVlKlV74RXB!y(2NCSSr$~uluu^EB!ZS$hC zqJPEu$c^N2M*~AKC2#*m#+=r9_&qb;^hcg1!S>#&wms_b+sVN)#8V@vuKZi1ax=+w z5&a;rCs-#!lK;fY%B6uLbBDAGEIQG@-m-fN!9?U+^v`K0r0&Fo)B8q*+CFKx(uyov zgFiJ((|I@;)yM(e87+z#LN?fJ%nX#UrAPj~l9#^*Jni9865a>~P6zC~9C-fUn{4bh zMf*)#=@f7~;1G>QYn5a>p82#0vEA0~7lKFGY&tJ8Ao?1Ai(oL#7;kAUJ-)k~vtPo8-3Gflf(qoo39*QUs&R7jgQBqxNrk?iLj z7(B4PR2@+D!8G$!rf-KxSB?=Q?2lvI6Gm_)IH|9Ud%TfI+<9ERk_b}tZ8+otoj?GK zQKo{)%`&OGw#%yyYY~pv(&&hYq3!XyK$aNjK|m1^!GIWC0BmYZ(ytV;>;05?5h00N zU?Wt)e0=_VhSIsyuOaZ{sW9YQ8kqs-2UGF#x&0tWjA>n_Q0<;cs?MIewcs`{XOF-fR}%5LqMPJL5~D6oyU2?k5hcMXxtE$6>KA*n)%P@Wp7J@!bt8 znB^%Ow=_acQ${ZVyjxygJ4PvyUF@dhBV(Dum-Nx^eDiQ;$IFZc9E6-df@5VX{onn_5!tKpJbZ3#@OGpvmFcaF z>0o(KU?16v4a#`Ds`&W-nuu!)n`4yl`i(a^EBVuc;G8tn~}-kH7) zxJ+BZR^wN<8j3H4=^?WDv$qcK{v;u!W53zHq2ghgiCPG9nJ*ESLhu3%+BG1qr$W$+ z^FFGDxEv12_;Ed{r?)rn?^eG_&mky&0dz56H{XHSlzw}Br(9l3L zuu4DeJK}TzNFvVZN%ax^ZUhrZoK03)o&3RzgIYf$4vS&9Uu+yG6spEZ2JToU zpZC~t7>7+fq$!YD4bV&Ix$!CNQiy<&?6m`d%*ho5RN6>QsIzJsRVdYs8?xtUzvr9B z4D-?m)Fi1rVEOf!5R-2NHfHN^J$P8SddDhCgxze+TsE>XiG}V=^JV+gSIoEK`MGPR z1BVv(*Qo(CQr2!L-@aYtenI=qD$|uXLxSB-rt=&$wz}(`d-t&qSjO9A(mPLYdnaas z9aq(IID1inyao&qf&)F6f5=fRq6Z%ZU@RC#Ecp1#MmhW$f;|xc4vxXYg~JTSvo>tY zSiI#^s>|Q^l(xW4-Jf&Es_KfdabP*BbC)#}mrWVtx@OX_vyCGhKaBp2c`3kGdV=J{ z38<#Gdg+(Y|1AX-spr%B|L6dnKKnqUIM{qKMmx5_VoA)xI0P`4F;>& z$AMjHo|XFiz^%}B_+;326Y(^2rZgAMW?~|Gd|%7_pt!4D|7U6Ds3-}>FqPvh=VILT zj@B0QxLS(4t#8|Btrg(psyb#1H-@czaurt;BjYpt@L^cLsn$exd!;~>w(XP1G4FKE zu(YahA+fEUPdx?M)9It5pDD*Y*#zbvn7rrT@o|~&E-7GSbJGY!;rH&~#jbv-!ALn< zHR|i1l_AH0sLV`$`OTBh>&3O}Az74pbL{oZ_+;Zt3p?##kn!B2uvrJpkQ6i;eGWS9YmEZ$H05?I@ne+=L3TXX}e;M$xfM+@3bVAZW#a zrbmc!=sH3P{Cye2)$f1)jC}py^z8qpr2l_J!~b)nT9QAS*)O_P88D0Y94OB=iFx-~ zm1t!7mt{*ht@Ej9PVIsN^Ej$Ym%DA6bd)JAw`a>gU0q}LmFIwlMg`n4A+o1Eh)E82 z1yA`2c%<-rN*wOB@}TDQ*l}^DM90R)5;rzBu8j(hi&8>YDQE=zq`7#IMTXL>_wc1l zUGSFH7cCd7puEZ&^8q544fX|@eUcRoR(s~b+xiFQit90Q(Otgj4y5y+h^4|7ZQgns z(i$D{>dM)wG}$0Gctw4^4TDxVaO5=EgFHwCEG< zv&XxxrMabptAh5nQ0;)4{IJl|pQlBertB>+VT-$4gz}R49(CvHa3N6Vlb$)#Wm5}1 z`F0LbNI8n&1sxV1K~u4wV3nzbp^A-fRBj$(HsgN1Vy zCR!k%om-r~5)!A!q38tkfizkgtt*7Cp;ZF^SKWg`m}&Cep<<{Zua);>kz4Nl9-HA7s!#1xu$3avjI zD0S2-vi^dKj6P-8g9c=S?G#rb8idICshL?WH;K>>!n283R>KMN$Iu}$T*$}^6*E)1 zJm^U^T={Ipt8`g;9*rR?_o7&%y&7UFjGc9{<%fZtf6`a=V;z~>J0ktIPadxhej6Z2 z5__n~917*jPCOvA^yAt5w%BoAtU%^IxQVFN>Htcswq!>g4zM6N& zCTi2omIvzn{@KOz*mAlx<3Syyic*~DZ(WACc}Z$eZ?KcbrDHP^^xI8A#cD)E+WBVR zLN919MB#T1XO=GA^7bp{5!I0+!%+rUXg1Y?|h_^YtVTCd#CWwaV(X89-6j8 zQ)G+vdgnU&$D%~O*4 z(EoAW(G3<4%K3XQ=EUqO)od18jTGbX6LEWe`Hh&kXsT86rlrz{Z*DOwtcOaeYiCtq zY}^nR}hHOlHgI5~DNI{$|Yno14L0o8qUu zsMrGG_O`)b`3vmh2B}B<0Zrtqayn6wQDjGW_ zJJ2>CYV)orUKb~nNX{nB9L#m+JAj^<2Ud6b3-{|(+~Nq`FGxrYVdGg{CRevt%oXc60N9J!qKa`(QElu`v4rmUwe z98$)0o>bC4WzOrVT{I77cIl_~suJJs zW@(S^?xxGB`px^u9)dPf(2&6KdjHS66^9#gyga&a?lMSo)C<;=ad4diSuSrXwrcm? zPWLHxK|S}~RSTI1yFI3vL+=z>TSP2b9Y?A8wDZZ4Q~UX5@InmiL8tCK)6U_9D2r*q z+&b6wes5?gt4yVDAoHvgCSqq&QRbH%hgnbVQRr^CHI89f;|!8~2_@S${VfJpUsqQv zG=vM0Am$VL#&t|J_Jr_iLV+ABao0@j)^n%^l(S~Yf(#6W-Q>Db7kkxhEhom{+si}I z18wdz*)=P7V!=$0AV+e|UP!667)>aidF3P0A21I`b#uw|@#8Pps~uecr+?jr&a9^h zotZUIelPp6AN_za9L{s+&wtxmC8#{WD2rt1JC2=%wq$5Vy;7kOr=jn!9X7|pgxq!< zkS(9{q;ZL&^+QBNclr)F5%BMP7lnmai-#<=+(|}lm$!TJOkJM|IxZQ*$8_kW>8nS# zBG1*4uDu$J&j%Ys79_cJB)`_Ca^8J^TOA{Fx>*V)MZAelK#w^`DPr^LZj_LbLLweD zm8nsL5i&ZAUCYa?0>cxf5`y>=7|%u{$}OIdu)meLKfN+u7O^&s|Nfe6wrVS{X_~<{ zUbig-n`W}L?3h>bxC9s7%>U{upzl95a3)KwM5~#2kRd^pFbHFyyPcigmAxUhJz}D_ zQBi(s@d`sfW98_jT|}3bmIT3dwYt4umB4RORtK>Kfh8E8PR*=_bziAk06XSVKj1`2 zccS+ks_>34EG*QnFzZrTrC=644%?badSj5J|E8gru9lWhLPCOD>kGp`JoZ^D7t$&c z+r&<<@?Aiu@TdKdp0;-HSOL}Pk@;x*ffl+w;U^8{d%N2bER6C2_{_a@eH?3dcQ^KN z1JhA9mxUK7hj@q1=nXICD^DRsfw<;{0F(>T+P%#R8ZrAq=gEU4q?9WZ4Dr9FvTIlg z9d}~qD)SsiD5@#G?a5ZFsO2B9R%u50c=5{$mtL*bUy@20pMT;$(KsRxNuwc zg!FF3FunPhk9iTGZ*Wcl@j z3}~sylGA{(kiKU()Ai!(*Naf{3SVyAJ@vff&K2R=b{q_^61D~*4fE!nX4h<7Q{}^g z0z$;{SZU9_?z!fP9I{xXZcx>4;nK8-2Djpa2N3|$8lps0@Je%2vFF5doYyU!e;ram z%cD<;tWm62`O$HTiFNCxk!+7Q_M&jE_bZgbMNWu2_ms*^@?qP5ULd36uY}E+4YQAb zD>(5!!fto!N$@kkP!4c{RPWxk{z3U3^HY6OcPyv?@79ZHM@2-lB~)17$b-K!X^T%a z5X@<~lt9l(17>LE!EOg&g;$Vz2xr+ciBvJ_ua#;=@qr(kf8xst_t{k>o7xjV3LgjC$*r3D12!1eoL3Le*=VwQBsYF+I7 zz1?@WR_R48PrEGz#|-|uEl?FJ=PhVhd4yTiQWoFvu)sn$tb#212cRz7nuaF&kq`C> zSy?|tbQ-`|FF+MFd+|Vb_k@8*S@;1p5$nd-x+~^AufQfCUJ}X{wuv}Wu}S;Ce}AGZ zgT);&d&5Ig&4P#4I%clE8T8&nx+l3^^l^?^BZ1i+uy}a4nx;o$Bp0y@~qu#8NqHw!c zz}857OfCMAs5|CfG6M~R4)~qAARfPQ*aI>|oG^!oc^aDl^NCqwN?t5aGcg>ylcU>q zPcnw!|XMt*efeM*!q$_({N5`}y;^6_B z+5L!ph2RTIQGdkH785e$o;=~oD47%YXOm&w+}w21A;3<^apEZY^huWL>Sd#9@|^{5 znKl*El&EW+bwRaJi1}dKTkv+0L4Hifb+w%Z8}ds(!pPemZH9mXXAAHhDlB;L zAzf!=o=TW@@afnzUUrg_*n83#`MLxmo+F#ze?@QHmupByF_dnONA*w7$obnI^R7tb z^Q1c#qGAt+E-}WfXWhz#zM{zX-a0v0qveU_8pH+{GcYhXC52;}=E0WMK(_&2{eCcD z1;bzn=0v8RQ&OxfR8WmXm|&(BKD%gIx$&$OnShO95hpJuV7KQGe6ixD6pD`4RTzob zH0QLvsHCR3nVXsm7PByOrVri>Dn&Cji(=W=GYbo~sK^r5ww3VDr%Y2Y-MP=yy-T0v z`xQgQSl#BIn?o+Vx4|?M4Hjk)dT0Rd6klAIk*><MG=CSx2%-bloKqM0SbY%S#+OF@5n^ugsy$#m+Tj z!1-j1kmTctYrVpsXpj1F_(1vOyG68t8@6QS?x%wJ*DhncYahro1?h9SR|{>j1Kiko z>oLEUi#J2BsXQ!mLgN^JWlG@Bi)&99GVKCaxAp;Z*+Ec3#iePugsuOIRh-n9&ecSx zaSokVLzDd?!(4KucUm+r{@iA%gr9)P+@<(~kzJR!-qkXP8mr6+xjWa>pP?K)Kdp1@ z@Cz{;2o<>{m>f0?oTp#cTQQ&E;sWuIk|6y|`@(hzMCOFdg2c6tLPj3a^pSAc=9!bI z?B$t9l2nTyFqpvH+)K{ZK@4D_SReX$FrjApl^2uMXV23WzTSua2eJ!TTu ziudj}@6hbkVcLu?zI)?dDbo|&oP0FgEWcM138x1&(F#>0OrV9MsTR zw>P-?*6HKrKr_tLtyIy;-aGkIeQ{Pa&W1|{kCD@T`t+%+jZKc!roULAC$yc{oBcSI z8OTPOZ%)GKWLeobYCJgXcio-1YGKH`e1C`Pr@CXGm4|@q(ZPc2#pb;E!z`tqhgWo} zo3B#(s`<0i8#YhV#0&|)qE?_)bM9a`dfJ1}cy;UeQ)Kf0Q?&_TlaHD0|4jVzkG}1{ z%JRXtBH#TV>S6en|L1bWe}3%G?|e;uexZfK0;HBig~Qx~<%b4EDdSqsG1t8F6C+}l z*VR-JPR=7HrSv$bbMWJ79g*jJXA2J1S{lm)7CaLT%;vRT*U{W*^(*lpq`0bYlNMUz znQLC89^x~jv(rxqyHLp;R&NFMx16qk`5<0LV>p+nS? zZ4D93<7P5O_Zi|J)k16S@1T(Z~xlA@-@f7Tc_!TXA;x( z@}nDpJ!hs*wFePc5`FYJ6Kpp{!ZdMJSm%1j?c)I{f@Qf^*5yoig=4A)RWz5iG&T-v zzB0;=CNIQiXz5K5XGfCJ5eu{Zc*a9le7xTGV*Ip$=D1P3a!|1cU(m3FMzK%&Qw3Tv zVpL!c_LkYZnCq|Si}^o#G}sVlU(Ev*gPK9IV7!_m>ro%kzp#}Y>q#c(duFF;U2X95 z^`9CuFOyl)GA zcS7B_zb{l-HB?=A9{BvT&$B=)Jvw!Hox{we?~-a!8EMNJ{pXjW?um{zC9m9zQqN-< z{8A=;B>QUSUe4sqCr+K}Xg&$U;3qu{^{M!dO}vCP9kJ`4$n<&52J}adE*m3#beCEL zek_th)@lGDWQftNQ)tPtg#zeqDRbNpQ2`f7=`4SkrEP^6D50y{9^M zjV0)u`9(`j=AZ}eqSou}Vgjd|$)2CAJ+~l4RdwUx^)Tv_g*&*W{fzeeYw2bU!V?nJ zfk_nd0YB6)jGCs_l@1;>HqsfVp_4s*y7k`YkOCo2^2;~J49f1feqE!~*f;lz7tWR8 z@ZSh-$?(*8&?u>)$y)rtf5k<>bSqvR)s&Pqif>60_4d8F#5|uwV95>&q>;VENWFZ3 zrQWND-Iax}Vyzvm&?~#F>z(3xVmq;iBL(BF#2CWtBwT&TrdVqrNkHRFeO6iVw{!c) zw?{>Z83NylTI)9u+;Q}+D}fGU67;H_yh5En4VY376f+aPRI1sE?mJH zS7uj<)0Vl9D-pCbii@0`^Ia@X7e2!`IonF6sD6?~Yg0$*#qy&d_*@|$pm@mzd(~Q)!I@y8ecn$a4u^R;f z)7B482b6v(FmJo#D|&GM>lGHSA{McVkZsmf{uAz7VV%as$&PzFCLaaTSx#(Ln#xrL z+M`%>z3WD3(CM!G%_Be7GPank3+{<0-$~*#PPA`(>RqeVzHZDMU30yVWB1?dj_!9;yjYF33*BC%p-a}EC~_bXzg2}IN8%l znFQOy>5Y{seWjC|jrC8&%i=D&8gD3DmeRS|k z_XBlOqa(FT^JjB4JyCDHT&hwm9(zXu5e81GS0qPHZi*AtYgO7 z_e&?L`vme{#x=`TOKuj3>dd>CPlqQ~?iwU;eUQpvjZs7B86%iMV_$GmQQQVR`Lc^wQFx5-q0l#w~pKDb##|Y=HXM@b?UBRB-*Mj+IPtjhFp1?oc?OA-Z{(?uq+H__ zFn$~xARl++a23S^(yvzB6Ujc*y(c1ct0S6H^hO=IIpz;ErS=h_7nxo&p{X@Qx@?Ix z4HVveV&ZvTqGwd~f={XCTwIHt=nIchExG_3wNEM^{ z^ZiUd!-s&#q*TVE&t0>X%8yQ(r;r`75km)SH>#ii!g%$0iJj5IS#wQs%bQgmh zwx>^imMy>fSt<_ohgV#rH#^HZ$8V5 zF8eo|qEP&q-~AQhIreM6rwmt1Dr7eJ;Gb}EouH0>o@WP5x%MrDaHzfR{=~CP_Kc;sBcfQ}I*HB50Q(?Hyb;KGG2)g8XMpaz|D&O6k7xROnbIWa? z-_HN%bnd8<8gpJKml~^h~LTyS-Z9Q3tc}_U&~q?#$R!M%ysKQMKjD z=IZlZ#NE(Jn;g+0{H<)DFwyZdNe)S8h*8SdY#zVqtQ)P}haU}YyYeI6633*-rBVm5n7}Yf-#yOGkvW z8fp3Z@qv0)#T(2`~{5L)W9F+1YztSL1i)>u#zZI(rk(ZSqJUdwS zk+M8jM;1(OJo&RfeN)e`?H1uQIcDFN^7~?@!bU>zBTGV*aj+m^_`CJ#mJOoH-&nvGu4vGkqK$;=I=p_b+|6+uG1|5i6VYu|I4 z9Mp?qDkfq5Z)~9Vn3+m+a4NwOUHpgqVW>J}uH>!*>N9rbX}yD-ahv;)*MNNJb?|;%(@%No@ev)O);Q(fJE2#;JXgIvXYy=5 zvN*4N(q+f<-N!WJDk~^He~F(VCMj!7W~BRkN`%wyZv{S&o0@SruFa>+nE_i<*WSL* z8E^b|4O_P6P}1laaCYYyqY6>Uz^&+)2g4g@OOiUICnZQA1n(d0<^Vgwn(gL#_c#l> zjH8vuFu8FuTl= z7i3E+r|}%=RVDwUHXvxgWXCN=za0pj#v|qvFPY&i&;1ac3~u08_N32ryJS&H@tagkEcu{cu@mK9*=a zho@s@(n-QImYG83m`V$&OT<;6Iix_s+|Bw*+E~#VavA73&6ZLxWqXT4SM2Onp; zKd=^=@7?dwu3aBy)4*suR1gYFKEONQ8+7$!!k+o@DW z4PR}|?QlF}(4^sVu;_ud+3_wp3y4RV{paDwufW>2Rno+YQN!!*-MKvm)vfi+t`+Da zge%_g4l?b`102w3-#)tcsA5>O)Zv=fgOr~Wcfnx2>8B{PS_|tV>~-0{J>tLYHPkD` z6A7PEmF@gv&fbIH5(ebW1Zx;2q+EN1@vr~4rZJ_@g%jh@?gML@f7?eN#hmcRRZwN5 z{;d%r4n80|cH5_Q$(fFKd_v%v0fbKHL%1rfx+y|vqQlG{$s+SqlNEkC^~?ITV9qlV zu|dBuJGrynA9Af?rtPgW2LG&gzoz!Pvd5?|v%)5<@Ad_hu(oQRJKQ`qW`+M?Fn{C4 z+YY*^)HJ4s@)E4_@$4yWy|)@rtgSMC*J#6ex%~&Mj}H^Q)I^`$EhnVv_MZ7kKRKo5 z*Z+P-`4an_?>iS*zo$0CjOJ$RYa;8w_tkchR&Qqfd-iT0f?0pt_H1EiMugiz8RO@W zXuApVMrSqVVDPcd!26JM6}Y2V&QE$Z+m%~X*zrl2%(R~K)fmp)a35%f?tBWjep^Nv zN7FiUb6;u8(6&=QYzuu}!CU_0w&rY)*m8S367lcI37J)8*c9URi`FP8Hqjxl$4PTZ znYMj@OXJA0&5|LpX+s=>#2!s}sp)H(bL@A>F@95X*T0}>YvvX-Grp*g#T|G2O#L4M zyL&YocBpzrraXn_Y#uC%XvChtOxC0>*Ha&q^FKwPVCj0SRh}m9K?TuvS_vM69-dSF ztlmQE0U2GfOTrgJ3&e2j_56iu#?)77vQxh(--_0ZBWMrrX60Ht(oB$SgLTLrMY&w>jGPfzuoD4Mg7 z&p%*0V-&NbJNLj3R-qAob-tOT*&VGrAU|?w<3hIn&X)w-(VV5iDNiH&G-~iP-u1oY zR)Vu{lnK8*jrL`)gqYC&_!X?2-eYUXZN(=t{R(h)`iaGFj;|SMxxYhxytWo0q(oiL zlY2w-EG>8Uyyte}kyvjwBJh^J8~bl6#P@Z_hkhw{g{s^z%0nO#J&j#{(+wz7u7gMkrn>D0C64)CN(G3Fq{ z;50vjT7RsQL_$PkPc4_|i5bkpww&yeZlK-=lxB5~M4iW56fQUQD5x)J@Q06Sn_Yyw zT<>g-8r0Ute0w!7mc?a;_a~ka#htyz*;i~)wA?%z0jCpu;M};XX8%|C*V=VM%R3y; zw@65~8TfKmUV}1`d{h|LADNu2${wa@U}$q@=%&-j4WuCVvXxbiMe%F8V8mx2;x-7J zeU@jiybU(%!qu<7pQ?G*mL8tTRv+W!f}^fiUCB~yn9AYCop&jX4|2jYzE7<_y}*^M zaVTm!Qi!R@4cvq4ImG3QgNYupr1GAOkn4ys!c&&#i&NS- z={iFb1B^Xg9j*01@b1I9A(%9&h1yjQr8vUj<`1V~FfTOCzcqcu0VPJ}9KvmTTR<_Se^-B_^)&mW4dTcKCyDUK zexOR6z?lEQl=Zoq4VO-NDHNgT-_SYsBHj_E`;H=}_S5-vInVYZTYmj*L)`@YUoYt% zFc*1{p*$qCCQQE0x@{i73}IV*B&onJfGXXs+mrl(@~zL+v3+36jy8BKYnn#XU{HL< zfM@jG^-h$fUzh01%~W72_#MVjdl&hc9d!H#-9rp zg=XdH>`U;EB%_&MU(T7DCMsLBL9x&5JpSU9bPF+GOeedaIUXWXR+v(nt1Hh&1N8UA zr#>w7|5P7bEPMR;@s8ZzlmD1aQA>q-Va()j>J~HNBbidxU3?WA^hTUZhAedrPuq zos`o$dvT4*XkwI(&CvpPdr(e_Z6=|YaM`R_Jz|%Aas(_AD#ErC3p?s|xAaKs(V7Zp z2ZxmpWJ6fLAla^A1$I}X?_yhhj;9K~+JKR3r5dS;Je%J&Piii1Hz1~Md7UAwtr1v* zRnC6u9@p>M@8pH?eEowQJhhJeEUJF9fU&;88K=rzGnVk0cCs^u!2||+irWgs|1);C zQ(?$au8YoX>oJH}{fZeI3#>N7raxWX=G;Bl;O)qi8?1&G(4XD&Wy<@WEU8aA&PW$8 zp#=kUE#;&SLgN>?uu$M*n5N<&Udd1QKUBW`o1ALlfP;Mh%fa0?xQ@v6 zf%S#S-=-G@60k81vuTfFY)OytOf}EMcgpk6J3sN?eDHFH2?R4@db*h3ULu>H86!%N zcVJt5#SUXKvQEjL zvbf>Q|0NMrWzpUjdtZV_ENT1C^8;n8IDUpH!Y0J=4MXj5z25a;bl-)N%uSzP<1Nqpyqc9!I7NVEZE5l|JH%!-JjWD88cYn1nlUF*s50EcEF(rUX(}3T7oPtj z5Ji{0Zaq)d&Oi+s#Y7^Vakw{>aYqc*a?>If)88`Wi^zfAG2O=R+UFRt(u zRdg)cEK}FgrwGxM?}rePhXm-$JKXU)JljLOVhe z2$FGI<0DOua+S_9TrpskGMF9V)33koHh}Y2`e^C8q*eDe#%SGGerf`*aWP|S4ehV3cR&g7~KrMrk^J7{iFUR-g+1lfM?ZTaOH9lb3hc$%0 zpi!8(7o7>!Qd+ryDd3QP|B|;|RmHHCY#ci}k6y%^SA-5gXes~x=EPMMJY&Uq^3@*TVfIrIWsIYI?*b#0MkBF>U$EzC5; zH!V(p0p8IMb%B{IwU+p_eH~{+hRPIV(1S*75d{W$^U~YTN`Q9YuIkg8lt*bvy(>*A z(Z5WHvCFLu_Sls~C8!DTaY?%9b!Yp7^2cwVq}xKBr}xl1E<1121(;7Ai(xzK}rGFH>*N{O?<)g5_=dYa{}Jq^O(9m6Kg-59^CKkuauSS0nwP}M>ez?6p&n(yC8^uDRI&+ z^q=C+zy7b3JpG%z+p|DcDRWWX&Y4547p2-a{qrfVIP@>%=!M&zq^M0~X}X`<2uKGt z7&WIC#=gq9aP9+Wb)^{^2@_*&k9-zg=zMq7f~Yac+zM>Z@nHKM|I9zN*LnuIk)t>9 zS8xbzw|_^r-zT0?$dn7!js-1xqb%@xlT0(w*5k_J#LPwbX6hLKi0T>sx;@pe;s|Sd%npOj$BD5 z%AQr3ibPu!A8~9dNz2+L+H11b*=%ej^Vif|txji>^%*~5@;BcyfjV^Zanl)aMbYFN zg_i|r=0ui~L*tTGtbLJC=9>ICG<9}UgV$+%WxBi9`hT)g`yC#h`u3NXOFBw%%1dp1 zWTb+Zvu3X(j$*2Qs4qOzdw^v4AIA1)YlX`G#hD#2McXx5_ zH{N1m$>xhj&tuATO!uw)j4^OboMZezjm>ApVVWpYXvL!K)O{-QwKT1(3YprKH2yaQ z14`Ei=1ezIj?wukQ$YfXU$$iq6W^^SPbe^b6q^WdPYrWbln3BW8rV$^zCp)~l z`mg?6?QU`u-}6;`YI`vY(ech7u9D1Ov?wcGv#EGz{HAOtByFD(0=K&?(#Rm**oA|{ zqt+j3|EQ~u+2x6f_|>7DOCEbWX27VBOD{d^)#ki$iul@jB3pQ@^6LGMxm*!%Tc5F>vn zE{%01{eaj&;)|2=Y4t8VplCk>=lvXACCzVdm}|b}n-iBAZa?aT8k|Wl>8S*may3;% z+xchznlb0pMr0Cl#_bv+=Z@Xp$eAzysRg*w10M!1o=ww?JGOqp>(RdYm>Nos^MQ(- zGGBiNdPR_0XYVyoR_z063p6~Eg+XmBQ?5%i;Lr^<4tA2X*OaHwezVC3+VYyBxY)D9 z0Qt0P`GLQEt~Eeo!yZq8Ouu<3Fi#Cf(@1mo+(&GZ!gA(r!c!`DFdi+6c94gsxGy)Z zX6-??`_>=mFn-rMgX(3TcLK ztgo?BHE?;0m_$;v>JwIUe1Lg8uM%PUTm*z|uOWuTm>F|tulY<@~uS|tCbeJp+ zD72bRM!41*t>7Lv(P!eQu&#ZC9<7htwl#p=$R!S(>g^X=Wl_IQ+n!UaIj|1EHjEd6 zHf=zkZSATuB_|=$iaX{pcC;7sz1)OgzcPE2X9Ju4#|P#cPw0*SA+!{Qnz48dPso$i z8dEDf%IdGGn7|_LcXKbUAuhf6$H#aC3BFO+hdiq-k|p_u z2tDmRZDFQLruMD_=~GKNaW*k)-#mm)@V@$8t!y%(_bkM6nP=_bDR+PA%{8fxTMfbm3asqzlxhyw*@n z*`_H~yEovJ-XSDlU_aIJ`HRBVAe*gc_MnxfeU|i19RE?|e=LG2xZE(IXB?G5Gj*0X zniyBa6p(+dDySw>`l5aSfEX?Gmy3{lMO;N_JcJ}fk zcXJfz>dmBJP;vYV`=eErKvLIm}E_%@0 zY{u@hPQeYC?MLq1vp!K3N3Pyu%x&~vPDT9(&gJtK*qRYwVGKc1r7H(%e<4oBq zlRp!d_h)SX^Tlq+Ya)W>A0GS>>HzzGk-q-Pt`|Wv>nu=bRy$f;T6>W&tAOh$&yx zzLSx*C#bzzuM3aFwmq67@ui{Q1Ap5eFkAM{zM1pYV9Oh0>{@IWoUF5ND&2;Sjb3x1 zn-=LkGS8rrx6Nd9%8gy7*Ka^>NJ1>83DH~*zH&hhyU1}{f+Z_NB-H8p#=3TG7sZy> z*}>{zzi*em4|sl4e%=4ob$f^)6yV`F&b-5 z2V{>JVX8@nD+@P;ciI<|jNg$4de$m*VSQ_Qi4f&Zxnp?1CEq+*^sDdwOGncldO7}i zugi7*UwI>(TY8st$qJq-)?BF}?1`Tz)#A!0 zz1u4z%tL4%_9rrL4{$|$v9}g7h;eX1BS|uixKpURkA{33@fHm(t;< zncmiiRVZi@;eumxUR9vV?Og7&_&@PsXvN#YdX%LI!;NPw6}d~6_vK_|)sU*4S!_XAQnKZIm!3;UdQ z-t%AX6>Hjg{yXqd2}wCg=yF(HLD;?yzUpNej0G=uhG$;FFY?#n`5e6EP>ixXH1TTO zJ=j-pc#iBNL+5Y;^nEJ``!Y6rawF3J8xyR1E}lYpM7=v^CNR?hcs5AM$y>V}4ou{G znTL_iaY*N$2?!s^3JuX-Y-%Fqeq+*ewPi}0qjgSVJZ=X}r0eDI9yo@cukq9lVq)Z# zW>&N6Rc~)~5Wy_?cqnxBX|v^3x=vreHjq~fw5>L8K7OH;twii}K<2~==c|eseY;Bc z3wIoxAery6e)loOs#mc7&8>x!a_{u8hCmXR9?V%y9F`Vo_uEyZvh@Q1%^hROOCNh< zyODEWC&K zMiSCgmoaCpwA7pD3W8&V_9xcsKW_J3b{$9~C6#wJL+v3d$xG)G_?C}A*`@+V*a4}S z6uxTmr>)_W+Pb*3nYxDgU>J>r>qhrCk@0IAs$=H?1`!fUb1qk$k6XrcM87TlFD*2& dQ-O>U0HO;b^}4Ia!)rx^ Date: Sun, 21 Sep 2025 19:11:39 +0200 Subject: [PATCH 27/34] fixes for copilot review of 21/09 Signed-off-by: Laurent ARNAL --- bundles/org.openhab.binding.linky/README.md | 5 ++--- .../internal/constants/LinkyBindingConstants.java | 2 +- .../openhab/binding/linky/internal/dto/IndexInfo.java | 2 +- .../binding/linky/internal/dto/MeterReading.java | 2 +- .../internal/handler/ThingLinkyRemoteHandler.java | 10 ++++++---- 5 files changed, 11 insertions(+), 10 deletions(-) diff --git a/bundles/org.openhab.binding.linky/README.md b/bundles/org.openhab.binding.linky/README.md index 2f3b62021f6ba..2ebf29b11242e 100644 --- a/bundles/org.openhab.binding.linky/README.md +++ b/bundles/org.openhab.binding.linky/README.md @@ -253,8 +253,6 @@ The retrieved information is available in multiple groups. #### Dynamic Thing Channels -#### Dynamic Thing Channels - Add-ons now support reading consumption indexes from the Enedis website. This makes it possible to view consumption for different tariffs such as *heures pleines / heures creuses* or *tempo*. @@ -390,7 +388,8 @@ slots: ### Displaying Information Graph / New version with tarif -Using the timeseries channel and new version of the addons, you will be able to easily create a chart to show the consumption graph with tarif differenciation. +Using the timeseries channel and new version of the addons, you will be able to easily create a chart to show the consumption graph with tariff differentiation. + To do this, you need to enable a timeseries persistence framework. Graph definitions will look like this: diff --git a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/constants/LinkyBindingConstants.java b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/constants/LinkyBindingConstants.java index fed4e54f80900..c2e9b2832f2c9 100644 --- a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/constants/LinkyBindingConstants.java +++ b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/constants/LinkyBindingConstants.java @@ -57,7 +57,7 @@ public class LinkyBindingConstants { public static final String LINKY_TEMPO_CALENDAR_GROUP = "tempo-calendar"; public static final String LINKY_REMOTE_LOAD_CURVE_GROUP = "load-curve"; - public static final String CONSUMPTION = "consumption"; + public static final String CHANNEL_TYPE_CONSUMPTION = "consumption"; // List of all Channel id's public static final String CHANNEL_CONSUMPTION = "consumption"; diff --git a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/IndexInfo.java b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/IndexInfo.java index a39400899172c..879c29d3d91b4 100644 --- a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/IndexInfo.java +++ b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/IndexInfo.java @@ -13,7 +13,7 @@ package org.openhab.binding.linky.internal.dto; /** - * The {@link IndexInfo} Will contains data for an given indexr + * The {@link IndexInfo} contains data for a given index * * @author Laurent Arnal - Initial contribution */ diff --git a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/MeterReading.java b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/MeterReading.java index 8bc471c76348f..436a102a1f33f 100644 --- a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/MeterReading.java +++ b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/MeterReading.java @@ -99,7 +99,7 @@ public static IntervalReading[] fromAgregat(ConsumptionReport.Aggregate agregat, String calendrierDistributor = ""; String calendrierSupplier = ""; - if (dataObj.calendrier == null) { + if (dataObj.calendrier == null && idx > 0) { dataObj.calendrier = agregat.datas.get(idx - 1).calendrier; } diff --git a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/handler/ThingLinkyRemoteHandler.java b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/handler/ThingLinkyRemoteHandler.java index 499baf7862f59..90c2dd790b25f 100644 --- a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/handler/ThingLinkyRemoteHandler.java +++ b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/handler/ThingLinkyRemoteHandler.java @@ -527,7 +527,9 @@ public static String sanetizeId(String label) { } result = result.replaceAll("[^a-zA-Z0-9_]", ""); - result = result.substring(0, 1).toLowerCase() + result.substring(1); + if (!result.isEmpty()) { + result = result.substring(0, 1).toLowerCase() + result.substring(1); + } return result; } @@ -637,7 +639,7 @@ private void addDynamicChannelByLabel(List channels, ChannelTypeUID cha */ private void handleDynamicChannel(MeterReading values) { ChannelTypeUID chanTypeUid = new ChannelTypeUID(LinkyBindingConstants.BINDING_ID, - LinkyBindingConstants.CONSUMPTION); + LinkyBindingConstants.CHANNEL_TYPE_CONSUMPTION); List channels = new ArrayList(); addDynamicChannelByLabel(channels, chanTypeUid, values, IndexMode.Supplier); @@ -669,7 +671,7 @@ private void handleDynamicChannel(MeterReading values) { * @param irs * @return a List of subdataset cut on Tarif change */ - private List splitOnTariffBound(@Nullable IntervalReading[] irs, IndexMode indexMode) { + private List splitOnTariffBound(IntervalReading[] irs, IndexMode indexMode) { List result = new ArrayList(); String currentTarif = ""; int lastIdx = 0; @@ -797,7 +799,7 @@ private synchronized > void updateTimeSeries(String groupI } } } catch (Exception ex) { - logger.error("error occurs durring updatePowerTimeSeries for {} : {}", config.prmId, ex.getMessage(), + logger.error("error occurs during updatePowerTimeSeries for {} : {}", config.prmId, ex.getMessage(), ex); } } From 84c731d7b36204ebe69dadc58e01838f37dcc0a2 Mon Sep 17 00:00:00 2001 From: Laurent ARNAL Date: Sun, 21 Sep 2025 19:13:35 +0200 Subject: [PATCH 28/34] fixes for copilot review of 21/09 Signed-off-by: Laurent ARNAL --- bundles/org.openhab.binding.linky/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bundles/org.openhab.binding.linky/README.md b/bundles/org.openhab.binding.linky/README.md index 2ebf29b11242e..e097240320e64 100644 --- a/bundles/org.openhab.binding.linky/README.md +++ b/bundles/org.openhab.binding.linky/README.md @@ -386,7 +386,7 @@ slots: nameLocation: center ``` -### Displaying Information Graph / New version with tarif +### Displaying Information Graph / New version with tariff Using the timeseries channel and new version of the addons, you will be able to easily create a chart to show the consumption graph with tariff differentiation. From 0aecb2fe11eaa4102919539b2cbccece1a70d768 Mon Sep 17 00:00:00 2001 From: Laurent ARNAL Date: Sun, 12 Oct 2025 14:40:11 +0200 Subject: [PATCH 29/34] @lolodomo review of 12/10 Signed-off-by: Laurent ARNAL --- bundles/org.openhab.binding.linky/README.md | 46 +++++++++++-------- .../linky/internal/dto/Calendrier.java | 5 +- .../binding/linky/internal/dto/IndexMode.java | 6 +-- .../linky/internal/dto/MeterReading.java | 14 ++++-- .../handler/ThingLinkyRemoteHandler.java | 20 ++++---- .../OH-INF/thing/group-linky-remote-daily.xml | 11 ----- .../thing/group-linky-remote-load-curve.xml | 1 - .../thing/group-linky-remote-monthly.xml | 5 -- .../thing/group-linky-remote-weekly.xml | 1 - .../thing/group-linky-remote-yearly.xml | 1 - 10 files changed, 52 insertions(+), 58 deletions(-) diff --git a/bundles/org.openhab.binding.linky/README.md b/bundles/org.openhab.binding.linky/README.md index e097240320e64..36cb6605ecc21 100644 --- a/bundles/org.openhab.binding.linky/README.md +++ b/bundles/org.openhab.binding.linky/README.md @@ -27,7 +27,7 @@ Step are: 1. Link your old thing to the new created bridge thing: ```java - Thing linky:linky:linkremotemelody "Linky Melody" (linky:enedis:local) + Thing linky:linky:linkremotexxxx "Linky xxxx" (linky:enedis:local) ``` 1. Start using the new channels added by the enhanced binding.. @@ -253,10 +253,10 @@ The retrieved information is available in multiple groups. #### Dynamic Thing Channels -Add-ons now support reading consumption indexes from the Enedis website. +The binding (as of openHAB 5.1.0) supports reading consumption indexes from the Enedis website. This makes it possible to view consumption for different tariffs such as *heures pleines / heures creuses* or *tempo*. -To handle this, add-ons will create a new set of channels for daily, weekly, monthly, and yearly groups. +To handle this, binding will create a new set of channels for daily, weekly, monthly, and yearly groups. You will have two different sets of indexes: @@ -265,7 +265,6 @@ You will have two different sets of indexes: Channels will be named as follows: - consumptionSupplierIdx0, consumptionSupplierIdx1, ..., consumptionSupplierIdx9 consumptionDistributorIdx0, consumptionDistributorIdx1, ..., consumptionDistributorIdx3 @@ -273,7 +272,7 @@ You will have two different sets of indexes: The supplier is the commercial company with which you have a contract (EDF, TotalEnergies, etc.). This is where your specific supplier tariff is defined. - **Named consumption indexes:** -To make things simpler, the add-ons also expose tariff-named channels. +To make things simpler, the binding also expose tariff-named channels. For example: daily#heuresPleines, daily#heuresCreuses, daily#bleuHeuresCreuses, @@ -301,11 +300,22 @@ Number:Energy ConsoMoisEnCours "Conso ce mois [%.0f %unit%]" { channel= Number:Energy ConsoMoisDernier "Conso mois dernier [%.0f %unit%]" { channel="linky:linky:linkyremotexxxx:monthly#lastMonth" } Number:Energy ConsoAnneeEnCours "Conso cette année [%.0f %unit%]" { channel="linky:linky:linkyremotexxxx:yearly#thisYear" } Number:Energy ConsoAnneeDerniere "Conso année dernière [%.0f %unit%]" { channel="linky:linky:linkyremotexxxx:yearly#lastYear" } + +Number:Energy ConsoDay "Linky Conso Day -x Histo [%d]" { channel="linky:linky:linkyremotexxxx:daily#consumption" } +Number:Energy ConsoMonth "Linky Conso Month -x Histo [%d]" { channel="linky:linky:linkyremotexxxx:monthly#consumption" } + +Number:Energy ConsoMonthHeuresCreusesBlanc "Linky Conso Month Heures Creuses Bleue -x Histo [%d]" { channel="linky:linky:linkyremotexxxx:monthly#blancHeuresCreuses" } +Number:Energy ConsoMonthHeuresCreusesBleue "Linky Conso Month Heures Creuses Blanc -x Histo [%d]" { channel="linky:linky:linkyremotexxxx:monthly#bleueHeuresCreuses" } +Number:Energy ConsoMonthHeuresCreusesRouge "Linky Conso Month Heures Rouge -x Histo [%d]" { channel="linky:linky:linkyremotexxxx:monthly#rougeHeuresCreuses" } + +Number:Energy ConsoMonthHeuresPleinesBlanc "Linky Conso Month Heures Pleines Blanc -x Histo [%d]" { channel="linky:linky:linkyremotexxxx:monthly#blancHeuresPleines" } +Number:Energy ConsoMonthHeuresPleinesBleue "Linky Conso Month Heures Pleines Bleue -x Histo [%d]" { channel="linky:linky:linkyremotexxxx:monthly#bleueHeuresPleines" } +Number:Energy ConsoMonthHeuresPleinesRouge "Linky Conso Month Heures Pleines Rouge -x Histo [%d]" { channel="linky:linky:linkyremotexxxx:monthly#rougeHeuresPleines" } ``` ### Displaying Information Graph -Using the timeseries channel, you will be able to easily create a chart to show the consumption graph. +Using the timeseries channel and the binding version in openHAB 5.1.0, you will be able to easily create a chart to show the consumption graph. To do this, you need to enable a timeseries persistence framework. Graph definitions will look like this: @@ -316,7 +326,7 @@ Sample code: ```java config: future: false - label: Linky Melody Conso Journalière + label: Conso Day order: "110" period: 2W sidebar: true @@ -342,7 +352,7 @@ slots: areaStyle: opacity: 0.2 gridIndex: 0 - item: Linky_Melody_Daily_Conso_Day + item: ConsoDay label: formatter: =v=>Number.parseFloat(v.data[1]).toFixed(2) + " Kwh" position: inside @@ -400,7 +410,7 @@ Sample code: ```java config: future: false - label: Linky Melody Conso Monthly 2 + label: ConsoMonth order: "9999999" period: Y sidebar: true @@ -427,7 +437,7 @@ slots: config: barGap: -100% gridIndex: 0 - item: Linky_Melody_Monthly_Conso_Month + item: ConsoMonth label: formatter: =v=>Number.parseFloat(v.data[1]).toFixed(2) + " Kwh" position: top @@ -443,7 +453,7 @@ slots: config: color: "#1010ff" gridIndex: 0 - item: Linky_Melody_Monthly_Supplier_Conso_Month_Heures_Pleines_Bleue + item: ConsoMonthHeuresPleinesBleue label: formatter: =v=>v.data[1]!="0"?Number.parseFloat(v.data[1]).toFixed(2) + " Kwh":'' @@ -463,7 +473,7 @@ slots: emphasis: disabled: true gridIndex: 0 - item: Linky_Melody_Monthly_Supplier_Conso_Month_Heures_Pleines_Blanc + item: ConsoMonthHeuresPleinesBlanc label: formatter: =v=>v.data[1]!="0"?Number.parseFloat(v.data[1]).toFixed(2) + " Kwh":'' @@ -483,7 +493,7 @@ slots: emphasis: disabled: true gridIndex: 0 - item: Linky_Melody_Monthly_Supplier_Conso_Month_Heures_Creuses_Rouge + item: ConsoMonthHeuresCreusesRouge label: formatter: =v=>v.data[1]!="0"?Number.parseFloat(v.data[1]).toFixed(2) + " Kwh":'' @@ -503,7 +513,7 @@ slots: emphasis: disabled: true gridIndex: 0 - item: Linky_Melody_Monthly_Supplier_Conso_Month_Heures_Creuses_Blanc + item: ConsoMonthHeuresCreusesBlanc label: formatter: =v=>v.data[1]!="0"?Number.parseFloat(v.data[1]).toFixed(2) + " Kwh":'' @@ -523,7 +533,7 @@ slots: emphasis: disabled: true gridIndex: 0 - item: Linky_Melody_Monthly_Supplier_Conso_Month_Heures_Creuses_Bleue + item: ConsoMonthHeuresCreusesBleue label: formatter: =v=>v.data[1]!="0"?Number.parseFloat(v.data[1]).toFixed(2) + " Kwh":'' @@ -543,7 +553,7 @@ slots: emphasis: disabled: true gridIndex: 0 - item: Linky_Melody_Monthly_Supplier_Conso_Month_Heures_Pleines_Rouge + item: ConsoMonthHeuresPleinesRouge label: formatter: =v=>v.data[1]!="0"?Number.parseFloat(v.data[1]).toFixed(2) + " Kwh":'' @@ -563,7 +573,7 @@ slots: emphasis: disabled: true gridIndex: 0 - item: Linky_Melody_Monthly_Supplier_Conso_Month_Heures_Pleines + item: ConsoMonthHeuresPleines label: formatter: =v=>v.data[1]!="0"?Number.parseFloat(v.data[1]).toFixed(2) + " Kwh":'' @@ -583,7 +593,7 @@ slots: emphasis: disabled: true gridIndex: 0 - item: Linky_Melody_Monthly_Supplier_Conso_Month_Heures_Creuses + item: ConsoMonthHeuresPleinesCreuses label: formatter: =v=>v.data[1]!="0"?Number.parseFloat(v.data[1]).toFixed(2) + " Kwh":'' diff --git a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/Calendrier.java b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/Calendrier.java index 49316391f3235..91861b6bf28a7 100644 --- a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/Calendrier.java +++ b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/Calendrier.java @@ -13,10 +13,9 @@ package org.openhab.binding.linky.internal.dto; /** - * The {@link Calendrier} holds informations about energy consumption + * The {@link Calendrier} holds informations about the available energy calendar * - * @author Gaël L'hopital - Initial contribution - * @author Laurent Arnal - Rewrite addon to use official dataconect API + * @author Laurent Arnal - Initial contribution */ public class Calendrier { diff --git a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/IndexMode.java b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/IndexMode.java index 8b8d2bdceb5cc..c5b22a256e9db 100644 --- a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/IndexMode.java +++ b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/IndexMode.java @@ -19,9 +19,9 @@ */ public enum IndexMode { - None(-1, 0), - Supplier(0, 10), - Distributor(1, 4); + NONE(-1, 0), + SUPPLIER(0, 10), + DISTRIBUTOR(1, 4); private final int idx; private final int size; diff --git a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/MeterReading.java b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/MeterReading.java index 436a102a1f33f..0caeaf0162f60 100644 --- a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/MeterReading.java +++ b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/MeterReading.java @@ -61,7 +61,7 @@ public static MeterReading convertFromComsumptionReport(ConsumptionReport comsum /** * This method will get data from old ConsumptionReport.Aggregate that is the format use by the Web API. - * And will result and IntervalReading[] that is the format of the new Endis API + * And will result to IntervalReading[] that is the format of the new Enedis API * * @param agregat * @param useIndex : tell if we are reading value from raw consumption or from index value @@ -103,8 +103,12 @@ public static IntervalReading[] fromAgregat(ConsumptionReport.Aggregate agregat, dataObj.calendrier = agregat.datas.get(idx - 1).calendrier; } - calendrierDistributor = dataObj.calendrier[0].idCalendrier; - calendrierSupplier = dataObj.calendrier[1].idCalendrier; + if (dataObj.calendrier[0] != null) { + calendrierDistributor = dataObj.calendrier[0].idCalendrier; + } + if (dataObj.calendrier[1] != null) { + calendrierSupplier = dataObj.calendrier[1].idCalendrier; + } if (idx > 0) { result[idx - 1] = new IntervalReading(); @@ -123,10 +127,10 @@ public static IntervalReading[] fromAgregat(ConsumptionReport.Aggregate agregat, } } - initIndexValue(IndexMode.Supplier, dataObj.classesTemporellesSupplier, result, idx, calendrierSupplier, + initIndexValue(IndexMode.SUPPLIER, dataObj.classesTemporellesSupplier, result, idx, calendrierSupplier, lastCalendrierSupplier, lastValueSupplier); - initIndexValue(IndexMode.Distributor, dataObj.classesTemporellesDistributor, result, idx, + initIndexValue(IndexMode.DISTRIBUTOR, dataObj.classesTemporellesDistributor, result, idx, calendrierDistributor, lastCalendrierDistributor, lastValueDistributor); lastVal = value; diff --git a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/handler/ThingLinkyRemoteHandler.java b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/handler/ThingLinkyRemoteHandler.java index 90c2dd790b25f..779cbf6dc904f 100644 --- a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/handler/ThingLinkyRemoteHandler.java +++ b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/handler/ThingLinkyRemoteHandler.java @@ -642,11 +642,11 @@ private void handleDynamicChannel(MeterReading values) { LinkyBindingConstants.CHANNEL_TYPE_CONSUMPTION); List channels = new ArrayList(); - addDynamicChannelByLabel(channels, chanTypeUid, values, IndexMode.Supplier); - addDynamicChannelByLabel(channels, chanTypeUid, values, IndexMode.Distributor); + addDynamicChannelByLabel(channels, chanTypeUid, values, IndexMode.SUPPLIER); + addDynamicChannelByLabel(channels, chanTypeUid, values, IndexMode.DISTRIBUTOR); - addDynamicChannelByIdx(channels, chanTypeUid, IndexMode.Supplier); - addDynamicChannelByIdx(channels, chanTypeUid, IndexMode.Distributor); + addDynamicChannelByIdx(channels, chanTypeUid, IndexMode.SUPPLIER); + addDynamicChannelByIdx(channels, chanTypeUid, IndexMode.DISTRIBUTOR); // If we have channel change, update the thing if (!channels.isEmpty()) { @@ -730,8 +730,8 @@ private void updateEnergyIndex(IntervalReading[] irs, String groupName, IndexMod } private void updateEnergyIndex(IntervalReading[] irs, String groupName) { - updateEnergyIndex(irs, groupName, IndexMode.Supplier); - updateEnergyIndex(irs, groupName, IndexMode.Distributor); + updateEnergyIndex(irs, groupName, IndexMode.SUPPLIER); + updateEnergyIndex(irs, groupName, IndexMode.DISTRIBUTOR); } /** @@ -764,7 +764,7 @@ private synchronized void updateLoadCurveData() { private synchronized > void updateTimeSeries(String groupId, String channelId, IntervalReading[] iv, int idx, Unit unit) { - updateTimeSeries(groupId, channelId, iv, idx, unit, IndexMode.None); + updateTimeSeries(groupId, channelId, iv, idx, unit, IndexMode.NONE); } private synchronized > void updateTimeSeries(String groupId, String channelId, @@ -782,7 +782,7 @@ private synchronized > void updateTimeSeries(String groupI Instant timestamp = iv[i].date.atZone(zoneId).toInstant(); - if (indexMode == IndexMode.None) { + if (indexMode == IndexMode.NONE) { if (Double.isNaN(iv[i].value)) { continue; } @@ -1163,9 +1163,9 @@ private boolean isDataLastDayAvailable(@Nullable MeterReading meterReading) { initIntervalReadingTarif(meterReading.monthValue[idxMonth]); initIntervalReadingTarif(meterReading.yearValue[idxYear]); - sumIndex(IndexMode.Supplier, meterReading, ir, idxWeek, weeksNum, idxMonth, monthsNum, idxYear, + sumIndex(IndexMode.SUPPLIER, meterReading, ir, idxWeek, weeksNum, idxMonth, monthsNum, idxYear, yearsNum); - sumIndex(IndexMode.Distributor, meterReading, ir, idxWeek, weeksNum, idxMonth, monthsNum, + sumIndex(IndexMode.DISTRIBUTOR, meterReading, ir, idxWeek, weeksNum, idxMonth, monthsNum, idxYear, yearsNum); } } diff --git a/bundles/org.openhab.binding.linky/src/main/resources/OH-INF/thing/group-linky-remote-daily.xml b/bundles/org.openhab.binding.linky/src/main/resources/OH-INF/thing/group-linky-remote-daily.xml index 50f13c3ce95fc..a3fd9822b6590 100644 --- a/bundles/org.openhab.binding.linky/src/main/resources/OH-INF/thing/group-linky-remote-daily.xml +++ b/bundles/org.openhab.binding.linky/src/main/resources/OH-INF/thing/group-linky-remote-daily.xml @@ -11,53 +11,42 @@ The energy consumption for previous day - The energy consumption for day -2 - The energy consumption for day -3 - The energy consumption - - Maximum power usage value - Maximum power usage value for Yesterday - Maximum power usage timestamp for Yesterday - Maximum power usage value for Day-2 - Maximum power usage timestamp for Day-2 - Maximum power usage value for Day-3 - Maximum power usage timestamp for Day-3 diff --git a/bundles/org.openhab.binding.linky/src/main/resources/OH-INF/thing/group-linky-remote-load-curve.xml b/bundles/org.openhab.binding.linky/src/main/resources/OH-INF/thing/group-linky-remote-load-curve.xml index 4f2dcc1a80b66..9f6bf4441909c 100644 --- a/bundles/org.openhab.binding.linky/src/main/resources/OH-INF/thing/group-linky-remote-load-curve.xml +++ b/bundles/org.openhab.binding.linky/src/main/resources/OH-INF/thing/group-linky-remote-load-curve.xml @@ -12,5 +12,4 @@ - diff --git a/bundles/org.openhab.binding.linky/src/main/resources/OH-INF/thing/group-linky-remote-monthly.xml b/bundles/org.openhab.binding.linky/src/main/resources/OH-INF/thing/group-linky-remote-monthly.xml index 761fb22d209a1..243182c811de1 100644 --- a/bundles/org.openhab.binding.linky/src/main/resources/OH-INF/thing/group-linky-remote-monthly.xml +++ b/bundles/org.openhab.binding.linky/src/main/resources/OH-INF/thing/group-linky-remote-monthly.xml @@ -11,23 +11,18 @@ The energy consumption for the current Month - The energy consumption for the previous Month - The energy consumption for the Month -2 - The energy consumption - - Maximum power usage value diff --git a/bundles/org.openhab.binding.linky/src/main/resources/OH-INF/thing/group-linky-remote-weekly.xml b/bundles/org.openhab.binding.linky/src/main/resources/OH-INF/thing/group-linky-remote-weekly.xml index 8c06e8ada42d7..3b2e59ca14187 100644 --- a/bundles/org.openhab.binding.linky/src/main/resources/OH-INF/thing/group-linky-remote-weekly.xml +++ b/bundles/org.openhab.binding.linky/src/main/resources/OH-INF/thing/group-linky-remote-weekly.xml @@ -23,7 +23,6 @@ The energy consumption - Maximum power usage value diff --git a/bundles/org.openhab.binding.linky/src/main/resources/OH-INF/thing/group-linky-remote-yearly.xml b/bundles/org.openhab.binding.linky/src/main/resources/OH-INF/thing/group-linky-remote-yearly.xml index cccbd8b595ea3..9ad761e6eaddf 100644 --- a/bundles/org.openhab.binding.linky/src/main/resources/OH-INF/thing/group-linky-remote-yearly.xml +++ b/bundles/org.openhab.binding.linky/src/main/resources/OH-INF/thing/group-linky-remote-yearly.xml @@ -23,7 +23,6 @@ The energy consumption - Maximum power usage value From 68f68d4477eeee8ad1cbc80335b395df7d82f3ec Mon Sep 17 00:00:00 2001 From: Laurent ARNAL Date: Sat, 18 Oct 2025 13:09:33 +0200 Subject: [PATCH 30/34] fixes for @lolodomo review of 18/10/2025 Signed-off-by: Laurent ARNAL --- bundles/org.openhab.binding.linky/README.md | 11 +- .../binding/linky/internal/dto/IndexMode.java | 14 +- .../linky/internal/dto/MeterReading.java | 4 +- .../handler/ThingLinkyRemoteHandler.java | 244 ++++++++++-------- 4 files changed, 153 insertions(+), 120 deletions(-) diff --git a/bundles/org.openhab.binding.linky/README.md b/bundles/org.openhab.binding.linky/README.md index 36cb6605ecc21..e4f87f45390ef 100644 --- a/bundles/org.openhab.binding.linky/README.md +++ b/bundles/org.openhab.binding.linky/README.md @@ -272,7 +272,7 @@ You will have two different sets of indexes: The supplier is the commercial company with which you have a contract (EDF, TotalEnergies, etc.). This is where your specific supplier tariff is defined. - **Named consumption indexes:** -To make things simpler, the binding also expose tariff-named channels. +To make things simpler, the binding also exposes tariff-named channels. For example: daily#heuresPleines, daily#heuresCreuses, daily#bleuHeuresCreuses, @@ -304,6 +304,9 @@ Number:Energy ConsoAnneeDerniere "Conso année dernière [%.0f %unit%]" Number:Energy ConsoDay "Linky Conso Day -x Histo [%d]" { channel="linky:linky:linkyremotexxxx:daily#consumption" } Number:Energy ConsoMonth "Linky Conso Month -x Histo [%d]" { channel="linky:linky:linkyremotexxxx:monthly#consumption" } +Number:Energy ConsoMonthHeuresPleines "Linky Conso Month Heures Pleines -x Histo [%d]" { channel="linky:linky:linkyremotexxxx:monthly#heuresPleines" } +Number:Energy ConsoMonthHeuresCreuses "Linky Conso Month Heures Creuses -x Histo [%d]" { channel="linky:linky:linkyremotexxxx:monthly#heuresCreuses" } + Number:Energy ConsoMonthHeuresCreusesBlanc "Linky Conso Month Heures Creuses Bleue -x Histo [%d]" { channel="linky:linky:linkyremotexxxx:monthly#blancHeuresCreuses" } Number:Energy ConsoMonthHeuresCreusesBleue "Linky Conso Month Heures Creuses Blanc -x Histo [%d]" { channel="linky:linky:linkyremotexxxx:monthly#bleueHeuresCreuses" } Number:Energy ConsoMonthHeuresCreusesRouge "Linky Conso Month Heures Rouge -x Histo [%d]" { channel="linky:linky:linkyremotexxxx:monthly#rougeHeuresCreuses" } @@ -311,6 +314,8 @@ Number:Energy ConsoMonthHeuresCreusesRouge "Linky Conso Month Heures Rouge -x Hi Number:Energy ConsoMonthHeuresPleinesBlanc "Linky Conso Month Heures Pleines Blanc -x Histo [%d]" { channel="linky:linky:linkyremotexxxx:monthly#blancHeuresPleines" } Number:Energy ConsoMonthHeuresPleinesBleue "Linky Conso Month Heures Pleines Bleue -x Histo [%d]" { channel="linky:linky:linkyremotexxxx:monthly#bleueHeuresPleines" } Number:Energy ConsoMonthHeuresPleinesRouge "Linky Conso Month Heures Pleines Rouge -x Histo [%d]" { channel="linky:linky:linkyremotexxxx:monthly#rougeHeuresPleines" } + +Number Linky_Tempo "Linky Tempo Day [%s]" channel="linky:tempo-calendar:local:tempo-calendar#tempo-info-timeseries" } ``` ### Displaying Information Graph @@ -593,7 +598,7 @@ slots: emphasis: disabled: true gridIndex: 0 - item: ConsoMonthHeuresPleinesCreuses + item: ConsoMonthHeuresCreuses label: formatter: =v=>v.data[1]!="0"?Number.parseFloat(v.data[1]).toFixed(2) + " Kwh":'' @@ -697,7 +702,7 @@ slots: aggregationFunction: average calendarIndex: 0 coordinateSystem: calendar - item: Linky_Melody_Tempo + item: Linky_Tempo label: formatter: =v=> JSON.stringify(v.data[0]).substring(1,11) show: true diff --git a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/IndexMode.java b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/IndexMode.java index c5b22a256e9db..51ddbdb1b9813 100644 --- a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/IndexMode.java +++ b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/IndexMode.java @@ -19,16 +19,18 @@ */ public enum IndexMode { - NONE(-1, 0), - SUPPLIER(0, 10), - DISTRIBUTOR(1, 4); + NONE(-1, 0, "None"), + SUPPLIER(0, 10, "Supplier"), + DISTRIBUTOR(1, 4, "Distributor"); private final int idx; private final int size; + private final String label; - IndexMode(int idx, int size) { + IndexMode(int idx, int size, String label) { this.idx = idx; this.size = size; + this.label = label; } public int getIdx() { @@ -38,4 +40,8 @@ public int getIdx() { public int getSize() { return size; } + + public String getLabel() { + return label; + } } diff --git a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/MeterReading.java b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/MeterReading.java index 0caeaf0162f60..04a8b72a464c9 100644 --- a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/MeterReading.java +++ b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/MeterReading.java @@ -103,10 +103,10 @@ public static IntervalReading[] fromAgregat(ConsumptionReport.Aggregate agregat, dataObj.calendrier = agregat.datas.get(idx - 1).calendrier; } - if (dataObj.calendrier[0] != null) { + if (dataObj.calendrier.length >= 1 && dataObj.calendrier[0] != null) { calendrierDistributor = dataObj.calendrier[0].idCalendrier; } - if (dataObj.calendrier[1] != null) { + if (dataObj.calendrier.length >= 2 && dataObj.calendrier[1] != null) { calendrierSupplier = dataObj.calendrier[1].idCalendrier; } diff --git a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/handler/ThingLinkyRemoteHandler.java b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/handler/ThingLinkyRemoteHandler.java index 779cbf6dc904f..5f6415764e0a4 100644 --- a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/handler/ThingLinkyRemoteHandler.java +++ b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/handler/ThingLinkyRemoteHandler.java @@ -34,6 +34,7 @@ import javax.measure.Quantity; import javax.measure.Unit; +import org.apache.commons.lang3.StringUtils; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.openhab.binding.linky.internal.api.EnedisHttpApi; @@ -91,8 +92,7 @@ public class ThingLinkyRemoteHandler extends ThingBaseRemoteHandler { private static final int REFRESH_HOUR_OF_DAY = 1; private static final int REFRESH_MINUTE_OF_DAY = RANDOM_NUMBERS.nextInt(60); private static final int REFRESH_INTERVAL_IN_MIN = 120; - // private static final int NUMBER_OF_DATA_DAY = 1095; - private static final int NUMBER_OF_DATA_DAY = 90; + private static final int NUMBER_OF_DATA_DAY = 1095; private final TimeZoneProvider timeZoneProvider; private final Logger logger = LoggerFactory.getLogger(ThingLinkyRemoteHandler.class); @@ -152,11 +152,10 @@ public ThingLinkyRemoteHandler(Thing thing, LocaleProvider localeProvider, TimeZ return meterReading; }); - // We request data for yesterday and the day before yesterday - // even if the data for the day before yesterday - // 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 + // We request data for yesterday and the day before yesterday. + // This is a workaround for an API bug: if the data for yesterday is not yet available, + // the API returns INTERNAL_SERVER_ERROR instead of the expected NaN value. + // By requesting both days, the API does not fail, and you get the expected NaN for yesterday // when the data is not yet available. this.dailyConsumptionMaxPower = new ExpiringDayCache<>("dailyConsumptionMaxPower", REFRESH_HOUR_OF_DAY, REFRESH_MINUTE_OF_DAY, () -> { @@ -377,7 +376,7 @@ private void addProps(Map props, String key, @Nullable String va } /** - * Request new data and updates channels + * Requests new data and updates the channels. */ private synchronized void updateData() { // If one of the cache is expired, force also a metaData refresh to prevent 500 error from Enedis servers ! @@ -448,7 +447,7 @@ private synchronized void updatePowerData() { } /** - * Request new daily/weekly data and updates channels + * Requests new daily or weekly data and updates the corresponding channels. */ private synchronized void updateEnergyData() { dailyConsumption.getValue().ifPresentOrElse(values -> { @@ -512,13 +511,12 @@ private synchronized void updateEnergyData() { } /** - * The methods remove specific local character (like 'é'/'ê','â') so we have a correctly formated UID from a - * localize item label + * This method removes specific localized characters (such as 'é', 'ê', 'â') + * to produce a correctly formatted UID from a localized item label. * - * @param label - * @return the label without invalid character + * @return the label without invalid characters */ - public static String sanetizeId(String label) { + private static String sanetizeId(String label) { String result = label; if (!Normalizer.isNormalized(label, Normalizer.Form.NFKD)) { @@ -535,105 +533,126 @@ public static String sanetizeId(String label) { } /** - * This methods create a new Channel for a consumption index + * This method creates a new channel for a consumption index. * - * @param channels - * @param chanTypeUid - * @param channelGroup - * @param channelName - * @param channelLabel - * @param channelDesc + * @param channels the resulting list of channels */ private void addChannel(List channels, ChannelTypeUID chanTypeUid, String channelGroup, String channelName, - String channelLabel, String channelDesc) { + String channelLabel) { + addChannel(channels, chanTypeUid, channelGroup, channelName, channelLabel, null); + } + + /** + * This method creates a new channel for a consumption index. + * + * @param channels the resulting list of channels + */ + private void addChannel(List channels, ChannelTypeUID chanTypeUid, String channelGroup, String channelName, + String channelLabel, @Nullable String channelDesc) { ChannelUID channelUid = new ChannelUID(this.getThing().getUID(), channelGroup, channelName); - Channel channel = ChannelBuilder.create(channelUid).withType(chanTypeUid).withDescription(channelDesc) - .withLabel(channelLabel).build(); + ChannelBuilder builder = ChannelBuilder.create(channelUid).withType(chanTypeUid).withLabel(channelLabel); - if (getThing().getChannel(channelUid) != null) { - return; + if (channelDesc != null) { + builder = builder.withDescription(channelDesc); } + Channel channel = builder.build(); + if (channels.contains(channel)) { return; } + if (getThing().getChannel(channelUid) != null) { + return; + } + channels.add(channel); } /** - * This methods create dynamic channels of forms: + * This method creates dynamic channels in the following formats: * consumptionSupplierIdx0, consumptionSupplierIdx1, ..., consumptionSupplierIdx9 * consumptionDistributorIdx0, consumptionDistributorIdx1, ..., consumptionDistributorIdx3 * - * @param channels - * @param chanTypeUid - * @param indexMode + * @param channels the resulting list of channels + * @param indexMode indicates whether this is Supplier or Distributor index mode */ - private void addDynamicChannelByIdx(List channels, ChannelTypeUID chanTypeUid, IndexMode indexMode) { - String channelPrefix = CHANNEL_CONSUMPTION + indexMode.toString() + "Idx"; + String channelPrefix = CHANNEL_CONSUMPTION + indexMode.getLabel() + "Idx"; for (int idx = 0; idx < indexMode.getSize(); idx++) { String channelName = channelPrefix + idx; - String channelLabel = indexMode.toString() + " Consumption " + idx; - String channelDesc = "The " + indexMode.toString() + " Consumption for index " + idx; - - addChannel(channels, chanTypeUid, LINKY_REMOTE_DAILY_GROUP, channelName, channelLabel, channelDesc); - addChannel(channels, chanTypeUid, LINKY_REMOTE_WEEKLY_GROUP, channelName, channelLabel, channelDesc); - addChannel(channels, chanTypeUid, LINKY_REMOTE_MONTHLY_GROUP, channelName, channelLabel, channelDesc); - addChannel(channels, chanTypeUid, LINKY_REMOTE_YEARLY_GROUP, channelName, channelLabel, channelDesc); + String channelLabel = indexMode.getLabel() + " Consumption " + idx; + + addChannel(channels, chanTypeUid, LINKY_REMOTE_DAILY_GROUP, channelName, + StringUtils.capitalize(LINKY_REMOTE_DAILY_GROUP) + " " + channelLabel); + addChannel(channels, chanTypeUid, LINKY_REMOTE_WEEKLY_GROUP, channelName, + StringUtils.capitalize(LINKY_REMOTE_WEEKLY_GROUP) + " " + channelLabel); + addChannel(channels, chanTypeUid, LINKY_REMOTE_MONTHLY_GROUP, channelName, + StringUtils.capitalize(LINKY_REMOTE_MONTHLY_GROUP) + " " + channelLabel); + addChannel(channels, chanTypeUid, LINKY_REMOTE_YEARLY_GROUP, channelName, + StringUtils.capitalize(LINKY_REMOTE_YEARLY_GROUP) + " " + channelLabel); } } /** - * This methods create dynamic channels labelled by tarif name : + * This method creates dynamic channels labeled by tariff name: * heuresPleines, heuresCreuses, bleuHeuresCreuses, bleuHeuresPleines, ... * - * @param channels - * @param chanTypeUid - * @param indexMode + * @param channels the resulting list of channels + * @param indexMode indicates whether this is Supplier or Distributor index mode */ private void addDynamicChannelByLabel(List channels, ChannelTypeUID chanTypeUid, MeterReading values, IndexMode indexMode) { + + if (indexMode.getIdx() < 0) { + logger.error( + "We only support indexMode values of Supplier or Distributor. Your incoming data seems corrupted—please check! !"); + return; + } + for (IntervalReading ir : values.baseValue) { - String[] label = ir.indexInfo[indexMode.getIdx()].label; + String[] labels = ir.indexInfo[indexMode.getIdx()].label; - for (String st : label) { + for (String st : labels) { if (st == null) { continue; } String channelName = sanetizeId(st); String channelLabel = st; - String channelDesc = "The " + st; - addChannel(channels, chanTypeUid, LINKY_REMOTE_DAILY_GROUP, channelName, channelLabel, channelDesc); - addChannel(channels, chanTypeUid, LINKY_REMOTE_WEEKLY_GROUP, channelName, channelLabel, channelDesc); - addChannel(channels, chanTypeUid, LINKY_REMOTE_MONTHLY_GROUP, channelName, channelLabel, channelDesc); - addChannel(channels, chanTypeUid, LINKY_REMOTE_YEARLY_GROUP, channelName, channelLabel, channelDesc); + addChannel(channels, chanTypeUid, LINKY_REMOTE_DAILY_GROUP, channelName, + StringUtils.capitalize(LINKY_REMOTE_DAILY_GROUP) + " " + channelLabel); + addChannel(channels, chanTypeUid, LINKY_REMOTE_WEEKLY_GROUP, channelName, + StringUtils.capitalize(LINKY_REMOTE_WEEKLY_GROUP) + " " + channelLabel); + addChannel(channels, chanTypeUid, LINKY_REMOTE_MONTHLY_GROUP, channelName, + StringUtils.capitalize(LINKY_REMOTE_MONTHLY_GROUP) + " " + channelLabel); + addChannel(channels, chanTypeUid, LINKY_REMOTE_YEARLY_GROUP, channelName, + StringUtils.capitalize(LINKY_REMOTE_YEARLY_GROUP) + " " + channelLabel); } } } /** - * This method create new channel dynamically at runtime when we read dataset from Enedis. - * We do this because we want to expose Index for each tarif. - * For tempo tarif for exemple, you will have 6 different channel. + * This method dynamically creates new channels at runtime when we read datasets from Enedis. + * We do this because we want to expose an index for each tariff. + * For the Tempo tariff for example, you will have 6 different channels. * - * But this is not the only available tarif, to listing all possible channel in ressource file will not do it. - * It's far more easy to create them looking at the available tarif in customer dataset. + * But this is not the only available tariff, so listing all possible channels in a resource file will not work. + * It's much easier to create them by looking at the available tariffs in the customer dataset. * - * There will be to set of channel: - * - Enedis channel: - * Supplier0 to Supplier9 : will expose originals Enedis Supplier Index. - * Distributor0 to Distributor 3 : will expose original Enedis Distributor Index. + * There will be two sets of channels: + * - Enedis channels: + * Supplier0 to Supplier9: will expose original Enedis Supplier indexes. + * Distributor0 to Distributor3: will expose original Enedis Distributor indexes. * - * - Named channel: - * Will enable to have a more speaking channel Name, for exemple you will have - * for Heures Pleines / Heures creuses tarif : channel name will be heuresPleines / heuresCreuses. - * for Tempo tarif : channel name will be - * bleuHeuresCreuses/bleuHeuresPleines/blancHeuresCreuses/blancHeuresPleines/rougeHeuresCreuses/rougeHeuresPleines + * - Named channels: + * Will enable having more meaningful channel names. For example: + * - For “Heures Pleines / Heures Creuses” tariff: channel names will be heuresPleines / heuresCreuses. + * - For Tempo tariff: channel names will be + * bleuHeuresCreuses / bleuHeuresPleines / blancHeuresCreuses / blancHeuresPleines / rougeHeuresCreuses / + * rougeHeuresPleines. * * @param values : the dataset from enedis. */ @@ -661,27 +680,34 @@ private void handleDynamicChannel(MeterReading values) { } /** - * This method take the full dataset, and return a List of subdataset, split on the tariff change. - * This can happen if on your subscription, you're ask your supplier to change tariff. - * For exemple, move from Heures Pleines/Heures Creuses tarif to Tempo tarif. + * This method takes the full dataset and returns a list of subdatasets, + * split on tariff changes. + * This can happen if, in your subscription, you ask your supplier to change the tariff. + * For example, moving from the Heures Pleines / Heures Creuses tariff to the Tempo tariff. * - * We do this split because we want to expose tarif to dedicated named channel so it will be more easy to display - * them on a chart. + * We perform this split because we want to expose tariffs on dedicated named channels, + * making it easier to display them on a chart. * - * @param irs - * @return a List of subdataset cut on Tarif change + * @param irs the incoming data in the form of a single IntervalReading + * @return a list of subdatasets split on tariff change */ private List splitOnTariffBound(IntervalReading[] irs, IndexMode indexMode) { List result = new ArrayList(); String currentTarif = ""; int lastIdx = 0; + if (indexMode.getIdx() < 0) { + logger.error( + "We only support indexMode values of Supplier or Distributor. Your incoming data seems corrupted—please check! !"); + return result; + } + for (int idx = 0; idx < irs.length; idx++) { IntervalReading ir = irs[idx]; if (ir != null) { String tarif = String.join("#", ir.indexInfo[indexMode.getIdx()].label); - if ((!tarif.equals(currentTarif) && !"".equals(currentTarif)) || (idx == irs.length - 1)) { + if ((!tarif.equals(currentTarif) && !currentTarif.isEmpty()) || (idx == irs.length - 1)) { IntervalReading[] subArray; if (idx == irs.length - 1) { subArray = Arrays.copyOfRange(irs, lastIdx, idx + 1); @@ -699,20 +725,25 @@ private List splitOnTariffBound(IntervalReading[] irs, IndexM } /** - * updateEnergyIndex methods will update timeSeries for a given energy index. - * There will be 2 timeseries update for each given energy index: - * - The index based time series. - * - The tarif labelled base time series. + * The updateEnergyIndex method updates time series for a given energy index. + * Two time series are updated for each energy index: + * - The index-based time series. + * - The tariff-labeled base time series. * - * @param irs - * @param groupName - * @param indexMode + * @param irs the incoming data in the form of a single IntervalReading + * @param indexMode indicates whether this is Supplier or Distributor index mode */ private void updateEnergyIndex(IntervalReading[] irs, String groupName, IndexMode indexMode) { List lirs = splitOnTariffBound(irs, indexMode); + if (indexMode.getIdx() < 0) { + logger.error( + "We only support indexMode values of Supplier or Distributor. Your incoming data seems corrupted—please check! !"); + return; + } + int size = indexMode.getSize(); - String channelPrefix = CHANNEL_CONSUMPTION + indexMode.toString() + "Idx"; + String channelPrefix = CHANNEL_CONSUMPTION + indexMode.getLabel() + "Idx"; for (int idx = 0; idx < size; idx++) { updateTimeSeries(groupName, channelPrefix + idx, irs, idx, Units.KILOWATT_HOUR, indexMode); @@ -735,7 +766,7 @@ private void updateEnergyIndex(IntervalReading[] irs, String groupName) { } /** - * Request new daily/weekly data and updates channels + * Requests new daily or weekly data and updates the channels. */ private synchronized void updateEnergyIndex() { dailyIndex.getValue().ifPresentOrElse(values -> { @@ -750,7 +781,7 @@ private synchronized void updateEnergyIndex() { } /** - * Request new loadCurve data and updates channels + * Requests new load curve data and updates the channels. */ private synchronized void updateLoadCurveData() { if (isLinked(LINKY_REMOTE_LOAD_CURVE_GROUP, CHANNEL_POWER)) { @@ -789,13 +820,11 @@ private synchronized > void updateTimeSeries(String groupI timeSeries.add(timestamp, new QuantityType<>(iv[i].value, unit)); } else { - if (i < iv.length && iv[i] != null) { - int indexIdx = indexMode.getIdx(); + int indexIdx = indexMode.getIdx(); - if (iv[i].indexInfo[indexIdx].label[idx] != null - && !Double.isNaN(iv[i].indexInfo[indexIdx].value[idx])) { - timeSeries.add(timestamp, new QuantityType<>(iv[i].indexInfo[indexIdx].value[idx], unit)); - } + if (iv[i].indexInfo[indexIdx].label[idx] != null + && !Double.isNaN(iv[i].indexInfo[indexIdx].value[idx])) { + timeSeries.add(timestamp, new QuantityType<>(iv[i].indexInfo[indexIdx].value[idx], unit)); } } } catch (Exception ex) { @@ -826,13 +855,13 @@ protected void sendTimeSeries(String groupId, String channelID, TimeSeries timeS } /** - * Produce a report of all daily values between two dates + * Produces a report of all daily values between two dates. * * @param startDay the start day of the report * @param endDay the end day of the report - * @param separator the separator to be used betwwen the date and the value + * @param separator the separator to be used between the date and the value * - * @return the report as a list of string + * @return the report as a list of strings */ public synchronized List reportValues(LocalDate startDay, LocalDate endDay, @Nullable String separator) { return buildReport(startDay, endDay, separator); @@ -979,9 +1008,9 @@ public void handleCommand(ChannelUID channelUID, Command command) { } /** - * This method will init the IndexInfo data structure for an IntervalReading ir + * This method initializes the IndexInfo data structure for a given IntervalReading. * - * @param ir + * @param ir the IntervalReading to initialize */ private void initIntervalReadingTarif(IntervalReading ir) { if (ir.indexInfo == null) { @@ -998,19 +1027,12 @@ private void initIntervalReadingTarif(IntervalReading ir) { } /** - * This method will sum day index value to respective week, month & year index. - * Will be done for supplier or distributor index in regards to indexMode - * + * This method sums the daily index value into the respective week, month, and year indexes. + * This is done for Supplier or Distributor indexes according to the specified indexMode. * - * @param indexMode : the index mode : Supplier or Distributor - * @param meterReading - * @param ir - * @param idxWeek - * @param weeksNum - * @param idxMonth - * @param monthsNum - * @param idxYear - * @param yearsNum + * @param indexMode the index mode: Supplier or Distributor + * @param meterReading the incoming meter reading data + * @param ir the incoming IntervalReading */ public void sumIndex(IndexMode indexMode, MeterReading meterReading, IntervalReading ir, int idxWeek, int weeksNum, int idxMonth, int monthsNum, int idxYear, int yearsNum) { @@ -1065,14 +1087,14 @@ private boolean isDataLastDayAvailable(@Nullable MeterReading meterReading) { } /** - * This method will do some basic checking on dataset from Enedis. - * And will also calculate the Weekly, Monthly and Yearly agregate. + * This method performs basic checks on a dataset from Enedis + * and calculates the weekly, monthly, and yearly aggregates. * - * When data are coming from Enedis, we will only have data day by day. - * To get date for week, month, and year, we need to sum the daily data. + * When data comes from Enedis, it is available only day by day. + * To get values for a week, month, or year, we need to sum the daily data. * - * @param meterReading - * @return + * @param meterReading the incoming data to check + * @return the resulting data after checks */ public @Nullable MeterReading getMeterReadingAfterChecks(@Nullable MeterReading meterReading) { try { From eef3259b6ff091cfb552e0ed9badaa6310efc2e0 Mon Sep 17 00:00:00 2001 From: Laurent ARNAL Date: Sun, 19 Oct 2025 15:49:48 +0200 Subject: [PATCH 31/34] fixes for @lolodomo review of 19/10, part 1 Signed-off-by: Laurent ARNAL --- .../internal/factory/LinkyHandlerFactory.java | 9 +- .../handler/ThingLinkyRemoteHandler.java | 82 +++++++++++-------- .../resources/OH-INF/i18n/linky.properties | 2 + .../resources/OH-INF/i18n/linky_fr.properties | 3 + .../handler/ThingLinkyRemoteHandlerTest.java | 14 ++-- 5 files changed, 68 insertions(+), 42 deletions(-) diff --git a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/factory/LinkyHandlerFactory.java b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/factory/LinkyHandlerFactory.java index e8a0555b21de5..d582a1cdfba5a 100644 --- a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/factory/LinkyHandlerFactory.java +++ b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/factory/LinkyHandlerFactory.java @@ -33,6 +33,7 @@ import org.openhab.core.auth.client.oauth2.OAuthFactory; import org.openhab.core.i18n.LocaleProvider; import org.openhab.core.i18n.TimeZoneProvider; +import org.openhab.core.i18n.TranslationProvider; import org.openhab.core.io.net.http.HttpClientFactory; import org.openhab.core.thing.Bridge; import org.openhab.core.thing.Thing; @@ -70,6 +71,7 @@ public class LinkyHandlerFactory extends BaseThingHandlerFactory { private final HttpService httpService; private final ComponentContext componentContext; private final TimeZoneProvider timeZoneProvider; + private final TranslationProvider translationProvider; private final Gson gson = new GsonBuilder() .registerTypeAdapter(ZonedDateTime.class, @@ -96,13 +98,15 @@ public class LinkyHandlerFactory extends BaseThingHandlerFactory { public LinkyHandlerFactory(final @Reference LocaleProvider localeProvider, final @Reference HttpClientFactory httpClientFactory, final @Reference OAuthFactory oAuthFactory, final @Reference HttpService httpService, ComponentContext componentContext, - final @Reference TimeZoneProvider timeZoneProvider) { + final @Reference TimeZoneProvider timeZoneProvider, + final @Reference TranslationProvider translationProvider) { this.localeProvider = localeProvider; this.timeZoneProvider = timeZoneProvider; this.httpClientFactory = httpClientFactory; this.oAuthFactory = oAuthFactory; this.httpService = httpService; this.componentContext = componentContext; + this.translationProvider = translationProvider; } @Override @@ -130,7 +134,8 @@ public boolean supportsThingType(ThingTypeUID thingTypeUID) { this.httpClientFactory, this.oAuthFactory, this.httpService, componentContext, gson); return handler; } else if (THING_TYPE_LINKY.equals(thing.getThingTypeUID())) { - ThingLinkyRemoteHandler handler = new ThingLinkyRemoteHandler(thing, localeProvider, timeZoneProvider); + ThingLinkyRemoteHandler handler = new ThingLinkyRemoteHandler(thing, localeProvider, timeZoneProvider, + translationProvider); return handler; } else if (THING_TYPE_TEMPO_CALENDAR.equals(thing.getThingTypeUID())) { ThingHandler handler = new ThingTempoCalendarHandler(thing); diff --git a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/handler/ThingLinkyRemoteHandler.java b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/handler/ThingLinkyRemoteHandler.java index 5f6415764e0a4..5f1aaec58b13d 100644 --- a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/handler/ThingLinkyRemoteHandler.java +++ b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/handler/ThingLinkyRemoteHandler.java @@ -34,7 +34,6 @@ import javax.measure.Quantity; import javax.measure.Unit; -import org.apache.commons.lang3.StringUtils; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.openhab.binding.linky.internal.api.EnedisHttpApi; @@ -57,6 +56,7 @@ import org.openhab.core.config.core.Configuration; import org.openhab.core.i18n.LocaleProvider; import org.openhab.core.i18n.TimeZoneProvider; +import org.openhab.core.i18n.TranslationProvider; import org.openhab.core.library.types.DateTimeType; import org.openhab.core.library.types.QuantityType; import org.openhab.core.library.unit.MetricPrefix; @@ -75,6 +75,8 @@ import org.openhab.core.types.TimeSeries; import org.openhab.core.types.TimeSeries.Policy; import org.openhab.core.types.UnDefType; +import org.osgi.framework.Bundle; +import org.osgi.framework.FrameworkUtil; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -95,6 +97,7 @@ public class ThingLinkyRemoteHandler extends ThingBaseRemoteHandler { private static final int NUMBER_OF_DATA_DAY = 1095; private final TimeZoneProvider timeZoneProvider; + private final TranslationProvider translationProvider; private final Logger logger = LoggerFactory.getLogger(ThingLinkyRemoteHandler.class); private final ExpiringDayCache metaData; @@ -119,10 +122,12 @@ private enum Target { ALL } - public ThingLinkyRemoteHandler(Thing thing, LocaleProvider localeProvider, TimeZoneProvider timeZoneProvider) { + public ThingLinkyRemoteHandler(Thing thing, LocaleProvider localeProvider, TimeZoneProvider timeZoneProvider, + TranslationProvider translationProvider) { super(thing); this.timeZoneProvider = timeZoneProvider; + this.translationProvider = translationProvider; this.metaData = new ExpiringDayCache<>("metaData", REFRESH_HOUR_OF_DAY, REFRESH_MINUTE_OF_DAY, () -> { MetaData metaData = getMetaData(); @@ -535,21 +540,21 @@ private static String sanetizeId(String label) { /** * This method creates a new channel for a consumption index. * - * @param channels the resulting list of channels - */ - private void addChannel(List channels, ChannelTypeUID chanTypeUid, String channelGroup, String channelName, - String channelLabel) { - addChannel(channels, chanTypeUid, channelGroup, channelName, channelLabel, null); - } - - /** - * This method creates a new channel for a consumption index. - * - * @param channels the resulting list of channels + * @param channels : the resulting list of channels + * @param channelTypeUid : the uid of the ChannelType associated to this channel + * @param channelGroup : the group associated to this channel + * @param channelName : the name of the channel + * @param channelLabel : the label of the channel + * @param channelDesc : the description of the channel */ private void addChannel(List channels, ChannelTypeUID chanTypeUid, String channelGroup, String channelName, String channelLabel, @Nullable String channelDesc) { ChannelUID channelUid = new ChannelUID(this.getThing().getUID(), channelGroup, channelName); + + if (getThing().getChannel(channelUid) != null) { + return; + } + ChannelBuilder builder = ChannelBuilder.create(channelUid).withType(chanTypeUid).withLabel(channelLabel); if (channelDesc != null) { @@ -562,10 +567,6 @@ private void addChannel(List channels, ChannelTypeUID chanTypeUid, Stri return; } - if (getThing().getChannel(channelUid) != null) { - return; - } - channels.add(channel); } @@ -575,6 +576,7 @@ private void addChannel(List channels, ChannelTypeUID chanTypeUid, Stri * consumptionDistributorIdx0, consumptionDistributorIdx1, ..., consumptionDistributorIdx3 * * @param channels the resulting list of channels + * @param channelTypeUid : the uid of the ChannelType associated to this channel * @param indexMode indicates whether this is Supplier or Distributor index mode */ private void addDynamicChannelByIdx(List channels, ChannelTypeUID chanTypeUid, IndexMode indexMode) { @@ -582,16 +584,16 @@ private void addDynamicChannelByIdx(List channels, ChannelTypeUID chanT for (int idx = 0; idx < indexMode.getSize(); idx++) { String channelName = channelPrefix + idx; - String channelLabel = indexMode.getLabel() + " Consumption " + idx; - - addChannel(channels, chanTypeUid, LINKY_REMOTE_DAILY_GROUP, channelName, - StringUtils.capitalize(LINKY_REMOTE_DAILY_GROUP) + " " + channelLabel); - addChannel(channels, chanTypeUid, LINKY_REMOTE_WEEKLY_GROUP, channelName, - StringUtils.capitalize(LINKY_REMOTE_WEEKLY_GROUP) + " " + channelLabel); - addChannel(channels, chanTypeUid, LINKY_REMOTE_MONTHLY_GROUP, channelName, - StringUtils.capitalize(LINKY_REMOTE_MONTHLY_GROUP) + " " + channelLabel); - addChannel(channels, chanTypeUid, LINKY_REMOTE_YEARLY_GROUP, channelName, - StringUtils.capitalize(LINKY_REMOTE_YEARLY_GROUP) + " " + channelLabel); + String channelLabel = " Consumption " + idx; + Bundle bundle = FrameworkUtil.getBundle(this.getClass()); + + String channelDesc = translationProvider.getText(bundle, "consumptionindex.description", "", null) + " \"" + + indexMode.getLabel() + " Consumption " + idx + "\""; + + addChannel(channels, chanTypeUid, LINKY_REMOTE_DAILY_GROUP, channelName, channelLabel, channelDesc); + addChannel(channels, chanTypeUid, LINKY_REMOTE_WEEKLY_GROUP, channelName, channelLabel, channelDesc); + addChannel(channels, chanTypeUid, LINKY_REMOTE_MONTHLY_GROUP, channelName, channelLabel, channelDesc); + addChannel(channels, chanTypeUid, LINKY_REMOTE_YEARLY_GROUP, channelName, channelLabel, channelDesc); } } @@ -600,6 +602,8 @@ private void addDynamicChannelByIdx(List channels, ChannelTypeUID chanT * heuresPleines, heuresCreuses, bleuHeuresCreuses, bleuHeuresPleines, ... * * @param channels the resulting list of channels + * @param channelTypeUid : the uid of the ChannelType associated to this channel + * @param values : the dataset from enedis. * @param indexMode indicates whether this is Supplier or Distributor index mode */ private void addDynamicChannelByLabel(List channels, ChannelTypeUID chanTypeUid, MeterReading values, @@ -621,15 +625,15 @@ private void addDynamicChannelByLabel(List channels, ChannelTypeUID cha String channelName = sanetizeId(st); String channelLabel = st; + Bundle bundle = FrameworkUtil.getBundle(this.getClass()); - addChannel(channels, chanTypeUid, LINKY_REMOTE_DAILY_GROUP, channelName, - StringUtils.capitalize(LINKY_REMOTE_DAILY_GROUP) + " " + channelLabel); - addChannel(channels, chanTypeUid, LINKY_REMOTE_WEEKLY_GROUP, channelName, - StringUtils.capitalize(LINKY_REMOTE_WEEKLY_GROUP) + " " + channelLabel); - addChannel(channels, chanTypeUid, LINKY_REMOTE_MONTHLY_GROUP, channelName, - StringUtils.capitalize(LINKY_REMOTE_MONTHLY_GROUP) + " " + channelLabel); - addChannel(channels, chanTypeUid, LINKY_REMOTE_YEARLY_GROUP, channelName, - StringUtils.capitalize(LINKY_REMOTE_YEARLY_GROUP) + " " + channelLabel); + String channelDesc = translationProvider.getText(bundle, "consumptionindex.description", "", null) + + " \"" + st + "\""; + + addChannel(channels, chanTypeUid, LINKY_REMOTE_DAILY_GROUP, channelName, channelLabel, channelDesc); + addChannel(channels, chanTypeUid, LINKY_REMOTE_WEEKLY_GROUP, channelName, channelLabel, channelDesc); + addChannel(channels, chanTypeUid, LINKY_REMOTE_MONTHLY_GROUP, channelName, channelLabel, channelDesc); + addChannel(channels, chanTypeUid, LINKY_REMOTE_YEARLY_GROUP, channelName, channelLabel, channelDesc); } } } @@ -689,6 +693,7 @@ private void handleDynamicChannel(MeterReading values) { * making it easier to display them on a chart. * * @param irs the incoming data in the form of a single IntervalReading + * @param indexMode indicates whether this is Supplier or Distributor index mode * @return a list of subdatasets split on tariff change */ private List splitOnTariffBound(IntervalReading[] irs, IndexMode indexMode) { @@ -731,6 +736,7 @@ private List splitOnTariffBound(IntervalReading[] irs, IndexM * - The tariff-labeled base time series. * * @param irs the incoming data in the form of a single IntervalReading + * @param groupName the group name associate to this channel * @param indexMode indicates whether this is Supplier or Distributor index mode */ private void updateEnergyIndex(IntervalReading[] irs, String groupName, IndexMode indexMode) { @@ -1033,6 +1039,12 @@ private void initIntervalReadingTarif(IntervalReading ir) { * @param indexMode the index mode: Supplier or Distributor * @param meterReading the incoming meter reading data * @param ir the incoming IntervalReading + * @param idxWeek : the index of the Week we wants to sum + * @param weeksNum : the maximum index for the Week + * @param idxMonth : the index of the Month we wants to sum + * @param monthsNum : the maximum index for the Month + * @param idxYear : the index of the Year we wants to sum + * @param yearsNum : the maximum index for the Year */ public void sumIndex(IndexMode indexMode, MeterReading meterReading, IntervalReading ir, int idxWeek, int weeksNum, int idxMonth, int monthsNum, int idxYear, int yearsNum) { 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 0da0c67a429ff..c916bd0a298bb 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 @@ -114,3 +114,5 @@ channel-type.linky.tempo-value.state.option.0 = Blue channel-type.linky.tempo-value.state.option.1 = White channel-type.linky.tempo-value.state.option.2 = Red channel-type.linky.timestamp.label = Timestamp + +consumptionindex.description = Consumption at given time interval for index diff --git a/bundles/org.openhab.binding.linky/src/main/resources/OH-INF/i18n/linky_fr.properties b/bundles/org.openhab.binding.linky/src/main/resources/OH-INF/i18n/linky_fr.properties index a62d52db76e83..7ce4a56539dd0 100644 --- a/bundles/org.openhab.binding.linky/src/main/resources/OH-INF/i18n/linky_fr.properties +++ b/bundles/org.openhab.binding.linky/src/main/resources/OH-INF/i18n/linky_fr.properties @@ -37,6 +37,7 @@ channel-group-type.linky.yearly.channel.thisYear.label = Consommation Année Act channel-type.linky.consumption.label = Consommation Totale channel-type.linky.consumption.description = Consommation pour un intervalle de temps donné + channel-type.linky.power.label = Pic Consommation Hier channel-type.linky.power.description = Pic maximum de consommation d'énergie hier channel-type.linky.timestamp.label = Horodatage @@ -44,3 +45,5 @@ channel-type.linky.timestamp.label = Horodatage # Thing status descriptions offline.config-error-mandatory-settings = Le nom d'utilisateur, le mot de passe et l'ID d'authentification sont obligatoires. + +consumptionindex.description = Consommation pour un intervalle de temps donné pour l'index \ No newline at end of file diff --git a/bundles/org.openhab.binding.linky/src/test/java/org/openhab/binding/linky/internal/handler/ThingLinkyRemoteHandlerTest.java b/bundles/org.openhab.binding.linky/src/test/java/org/openhab/binding/linky/internal/handler/ThingLinkyRemoteHandlerTest.java index 3933b20b4050c..a14461e00788b 100644 --- a/bundles/org.openhab.binding.linky/src/test/java/org/openhab/binding/linky/internal/handler/ThingLinkyRemoteHandlerTest.java +++ b/bundles/org.openhab.binding.linky/src/test/java/org/openhab/binding/linky/internal/handler/ThingLinkyRemoteHandlerTest.java @@ -28,6 +28,7 @@ import org.openhab.binding.linky.internal.dto.MeterReading; import org.openhab.core.i18n.LocaleProvider; import org.openhab.core.i18n.TimeZoneProvider; +import org.openhab.core.i18n.TranslationProvider; import org.openhab.core.thing.Thing; /** @@ -49,6 +50,9 @@ public class ThingLinkyRemoteHandlerTest { @Mock TimeZoneProvider tzProvider; + @Mock + TranslationProvider translationProvider; + @Mock LinkyThingRemoteConfiguration config; @@ -71,7 +75,7 @@ public void setUp() { @Test public void testBase() { - handler = new ThingLinkyRemoteHandler(thing, localProvider, tzProvider); + handler = new ThingLinkyRemoteHandler(thing, localProvider, tzProvider, translationProvider); MeterReading mr = getMeterReadingAfterChecks(handler, null); assertEquals(mr, null); @@ -79,7 +83,7 @@ public void testBase() { @Test public void testValidRange1() { - handler = new ThingLinkyRemoteHandler(thing, localProvider, tzProvider); + handler = new ThingLinkyRemoteHandler(thing, localProvider, tzProvider, translationProvider); MeterReading mr = new MeterReading(); mr.baseValue = new IntervalReading[75]; @@ -129,7 +133,7 @@ public void testValidRange1() { @Test public void testValidRange2() { - handler = new ThingLinkyRemoteHandler(thing, localProvider, tzProvider); + handler = new ThingLinkyRemoteHandler(thing, localProvider, tzProvider, translationProvider); MeterReading mr = new MeterReading(); mr.baseValue = new IntervalReading[128]; @@ -195,7 +199,7 @@ public void testValidRange2() { @Test public void testValidRange3() { - handler = new ThingLinkyRemoteHandler(thing, localProvider, tzProvider); + handler = new ThingLinkyRemoteHandler(thing, localProvider, tzProvider, translationProvider); MeterReading mr = new MeterReading(); mr.baseValue = new IntervalReading[716]; @@ -280,7 +284,7 @@ public void testValidRange3() { @Test public void testValidRange4() { - handler = new ThingLinkyRemoteHandler(thing, localProvider, tzProvider); + handler = new ThingLinkyRemoteHandler(thing, localProvider, tzProvider, translationProvider); MeterReading mr = new MeterReading(); mr.baseValue = new IntervalReading[35]; From b607e8843554e1be81ac95f0c232394ff7fd7b40 Mon Sep 17 00:00:00 2001 From: Laurent ARNAL Date: Sun, 19 Oct 2025 15:53:13 +0200 Subject: [PATCH 32/34] fix sat errors Signed-off-by: Laurent ARNAL --- .../src/main/resources/OH-INF/i18n/linky_fr.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bundles/org.openhab.binding.linky/src/main/resources/OH-INF/i18n/linky_fr.properties b/bundles/org.openhab.binding.linky/src/main/resources/OH-INF/i18n/linky_fr.properties index 7ce4a56539dd0..25f83cf5f3102 100644 --- a/bundles/org.openhab.binding.linky/src/main/resources/OH-INF/i18n/linky_fr.properties +++ b/bundles/org.openhab.binding.linky/src/main/resources/OH-INF/i18n/linky_fr.properties @@ -46,4 +46,4 @@ channel-type.linky.timestamp.label = Horodatage offline.config-error-mandatory-settings = Le nom d'utilisateur, le mot de passe et l'ID d'authentification sont obligatoires. -consumptionindex.description = Consommation pour un intervalle de temps donné pour l'index \ No newline at end of file +consumptionindex.description = Consommation pour un intervalle de temps donné pour l'index From a566206febd3d75c48b1ff4a3664427118896581 Mon Sep 17 00:00:00 2001 From: Laurent ARNAL Date: Mon, 20 Oct 2025 11:20:33 +0200 Subject: [PATCH 33/34] fixes after @lolodomo review of 19/10. We don't need anymore this modification, as was already fixed by commit * review data array bound when using index (925585e2465a0af9418083b9b07c064e4ac36159 ) on MeterReading. Signed-off-by: Laurent ARNAL --- .../linky/internal/handler/ThingLinkyRemoteHandler.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/handler/ThingLinkyRemoteHandler.java b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/handler/ThingLinkyRemoteHandler.java index 5f1aaec58b13d..eb294a247d7a6 100644 --- a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/handler/ThingLinkyRemoteHandler.java +++ b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/handler/ThingLinkyRemoteHandler.java @@ -1092,7 +1092,7 @@ private boolean isDataLastDayAvailable(@Nullable MeterReading meterReading) { IntervalReading[] iv = meterReading.baseValue; logData(iv, "Last day", DateTimeFormatter.ISO_LOCAL_DATE, Target.LAST); - return iv != null && iv.length != 0 && iv[iv.length - 1] != null; + return iv != null && iv.length != 0 && iv[iv.length - 1] != null && !iv[iv.length - 1].value.isNaN(); } return false; @@ -1118,7 +1118,7 @@ private boolean isDataLastDayAvailable(@Nullable MeterReading meterReading) { if (!isDataLastDayAvailable(meterReading)) { logger.debug("Data including yesterday are not yet available"); - return meterReading; + return null; } if (meterReading != null) { From 5c20fdb1a4f8577c38f90cff3fcb95fc54ce0979 Mon Sep 17 00:00:00 2001 From: Laurent ARNAL Date: Mon, 20 Oct 2025 12:18:49 +0200 Subject: [PATCH 34/34] add a test to prevent error on gateway that use api and don't currently support index Signed-off-by: Laurent ARNAL --- .../linky/internal/handler/ThingLinkyRemoteHandler.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/handler/ThingLinkyRemoteHandler.java b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/handler/ThingLinkyRemoteHandler.java index eb294a247d7a6..d23464815d833 100644 --- a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/handler/ThingLinkyRemoteHandler.java +++ b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/handler/ThingLinkyRemoteHandler.java @@ -775,6 +775,9 @@ private void updateEnergyIndex(IntervalReading[] irs, String groupName) { * Requests new daily or weekly data and updates the channels. */ private synchronized void updateEnergyIndex() { + if (!(getBridge() instanceof BridgeRemoteEnedisWebHandler)) { + return; + } dailyIndex.getValue().ifPresentOrElse(values -> { handleDynamicChannel(values);