]> git.basschouten.com Git - openhab-addons.git/blob
2da73b75370aaa87b31e278e9183f30539cc7cce
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2020 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
7  * This program and the accompanying materials are made available under the
8  * terms of the Eclipse Public License 2.0 which is available at
9  * http://www.eclipse.org/legal/epl-2.0
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.binding.linky.internal.handler;
14
15 import static org.openhab.binding.linky.internal.LinkyBindingConstants.*;
16
17 import java.time.LocalDate;
18 import java.time.LocalDateTime;
19 import java.time.format.DateTimeFormatter;
20 import java.time.temporal.ChronoUnit;
21 import java.time.temporal.WeekFields;
22 import java.util.ArrayList;
23 import java.util.HashMap;
24 import java.util.List;
25 import java.util.Map;
26 import java.util.concurrent.ScheduledFuture;
27 import java.util.concurrent.TimeUnit;
28
29 import org.eclipse.jdt.annotation.NonNullByDefault;
30 import org.eclipse.jdt.annotation.Nullable;
31 import org.eclipse.jetty.client.HttpClient;
32 import org.openhab.binding.linky.internal.LinkyConfiguration;
33 import org.openhab.binding.linky.internal.LinkyException;
34 import org.openhab.binding.linky.internal.api.EnedisHttpApi;
35 import org.openhab.binding.linky.internal.api.ExpiringDayCache;
36 import org.openhab.binding.linky.internal.dto.ConsumptionReport.Aggregate;
37 import org.openhab.binding.linky.internal.dto.ConsumptionReport.Consumption;
38 import org.openhab.binding.linky.internal.dto.PrmInfo;
39 import org.openhab.binding.linky.internal.dto.UserInfo;
40 import org.openhab.core.i18n.LocaleProvider;
41 import org.openhab.core.library.types.DateTimeType;
42 import org.openhab.core.library.types.QuantityType;
43 import org.openhab.core.library.unit.Units;
44 import org.openhab.core.thing.ChannelUID;
45 import org.openhab.core.thing.Thing;
46 import org.openhab.core.thing.ThingStatus;
47 import org.openhab.core.thing.ThingStatusDetail;
48 import org.openhab.core.thing.binding.BaseThingHandler;
49 import org.openhab.core.types.Command;
50 import org.openhab.core.types.RefreshType;
51 import org.openhab.core.types.UnDefType;
52 import org.slf4j.Logger;
53 import org.slf4j.LoggerFactory;
54
55 import com.google.gson.Gson;
56
57 /**
58  * The {@link LinkyHandler} is responsible for handling commands, which are
59  * sent to one of the channels.
60  *
61  * @author GaĆ«l L'hopital - Initial contribution
62  */
63
64 @NonNullByDefault
65 public class LinkyHandler extends BaseThingHandler {
66     private final Logger logger = LoggerFactory.getLogger(LinkyHandler.class);
67
68     private static final int REFRESH_FIRST_HOUR_OF_DAY = 5;
69     private static final int REFRESH_INTERVAL_IN_MIN = 360;
70
71     private final HttpClient httpClient;
72     private final Gson gson;
73     private final WeekFields weekFields;
74
75     private @Nullable ScheduledFuture<?> refreshJob;
76     private @Nullable EnedisHttpApi enedisApi;
77
78     private final ExpiringDayCache<Consumption> cachedDailyData;
79     private final ExpiringDayCache<Consumption> cachedPowerData;
80     private final ExpiringDayCache<Consumption> cachedMonthlyData;
81     private final ExpiringDayCache<Consumption> cachedYearlyData;
82
83     private @NonNullByDefault({}) String prmId;
84     private @NonNullByDefault({}) String userId;
85
86     public LinkyHandler(Thing thing, LocaleProvider localeProvider, Gson gson, HttpClient httpClient) {
87         super(thing);
88         this.gson = gson;
89         this.httpClient = httpClient;
90         this.weekFields = WeekFields.of(localeProvider.getLocale());
91
92         this.cachedDailyData = new ExpiringDayCache<>("daily cache", REFRESH_FIRST_HOUR_OF_DAY, () -> {
93             LocalDate today = LocalDate.now();
94             return getConsumptionData(today.minusDays(15), today);
95         });
96
97         this.cachedPowerData = new ExpiringDayCache<>("power cache", REFRESH_FIRST_HOUR_OF_DAY, () -> {
98             LocalDate to = LocalDate.now().plusDays(1);
99             LocalDate from = to.minusDays(2);
100             return getPowerData(from, to);
101         });
102
103         this.cachedMonthlyData = new ExpiringDayCache<>("monthly cache", REFRESH_FIRST_HOUR_OF_DAY, () -> {
104             LocalDate today = LocalDate.now();
105             return getConsumptionData(today.withDayOfMonth(1).minusMonths(1), today);
106         });
107
108         this.cachedYearlyData = new ExpiringDayCache<>("yearly cache", REFRESH_FIRST_HOUR_OF_DAY, () -> {
109             LocalDate today = LocalDate.now();
110             return getConsumptionData(LocalDate.of(today.getYear() - 1, 1, 1), today);
111         });
112     }
113
114     @Override
115     public void initialize() {
116         logger.debug("Initializing Linky handler.");
117         updateStatus(ThingStatus.UNKNOWN);
118
119         LinkyConfiguration config = getConfigAs(LinkyConfiguration.class);
120         enedisApi = new EnedisHttpApi(config, gson, httpClient);
121
122         scheduler.submit(() -> {
123             try {
124                 EnedisHttpApi api = this.enedisApi;
125                 if (api != null) {
126                     api.initialize();
127                     updateStatus(ThingStatus.ONLINE);
128
129                     if (thing.getProperties().isEmpty()) {
130                         Map<String, String> properties = new HashMap<>();
131                         PrmInfo prmInfo = api.getPrmInfo();
132                         UserInfo userInfo = api.getUserInfo();
133                         properties.put(USER_ID, userInfo.userProperties.internId);
134                         properties.put(PUISSANCE, prmInfo.puissanceSouscrite + " kVA");
135                         properties.put(PRM_ID, prmInfo.prmId);
136                         updateProperties(properties);
137                     }
138
139                     prmId = thing.getProperties().get(PRM_ID);
140                     userId = thing.getProperties().get(USER_ID);
141
142                     updateData();
143
144                     disconnect();
145
146                     final LocalDateTime now = LocalDateTime.now();
147                     final LocalDateTime nextDayFirstTimeUpdate = now.plusDays(1).withHour(REFRESH_FIRST_HOUR_OF_DAY)
148                             .truncatedTo(ChronoUnit.HOURS);
149
150                     refreshJob = scheduler.scheduleWithFixedDelay(this::updateData,
151                             ChronoUnit.MINUTES.between(now, nextDayFirstTimeUpdate) % REFRESH_INTERVAL_IN_MIN + 1,
152                             REFRESH_INTERVAL_IN_MIN, TimeUnit.MINUTES);
153                 } else {
154                     throw new LinkyException("Enedis Api is not initialized");
155                 }
156             } catch (LinkyException e) {
157                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
158             }
159         });
160     }
161
162     /**
163      * Request new data and updates channels
164      */
165     private synchronized void updateData() {
166         boolean connectedBefore = isConnected();
167         updatePowerData();
168         updateDailyData();
169         updateWeeklyData();
170         updateMonthlyData();
171         updateYearlyData();
172         if (!connectedBefore && isConnected()) {
173             disconnect();
174         }
175     }
176
177     private synchronized void updatePowerData() {
178         if (isLinked(PEAK_POWER) || isLinked(PEAK_TIMESTAMP)) {
179             cachedPowerData.getValue().ifPresent(values -> {
180                 Aggregate days = values.aggregats.days;
181                 if (days.datas.size() == 0 || days.periodes.size() == 0) {
182                     logger.debug("Daily power data are without any period/data");
183                 } else {
184                     updateVAChannel(PEAK_POWER, days.datas.get(0));
185                     updateState(PEAK_TIMESTAMP, new DateTimeType(days.periodes.get(0).dateDebut));
186                 }
187             });
188         }
189     }
190
191     /**
192      * Request new dayly/weekly data and updates channels
193      */
194     private synchronized void updateDailyData() {
195         if (isLinked(YESTERDAY) || isLinked(THIS_WEEK)) {
196             cachedDailyData.getValue().ifPresent(values -> {
197                 Aggregate days = values.aggregats.days;
198                 if (days.periodes.size() > days.datas.size()) {
199                     logger.debug("Daily data are invalid: not a data for each period");
200                     return;
201                 }
202                 int maxValue = days.periodes.size() - 1;
203                 int thisWeekNumber = days.periodes.get(maxValue).dateDebut.get(weekFields.weekOfWeekBasedYear());
204                 double yesterday = days.datas.get(maxValue);
205                 double thisWeek = 0.00;
206
207                 for (int i = maxValue; i >= 0; i--) {
208                     int weekNumber = days.periodes.get(i).dateDebut.get(weekFields.weekOfWeekBasedYear());
209                     if (weekNumber == thisWeekNumber) {
210                         Double value = days.datas.get(i);
211                         thisWeek += !value.isNaN() ? value : 0;
212                     } else {
213                         break;
214                     }
215                 }
216
217                 updateKwhChannel(YESTERDAY, yesterday);
218                 updateKwhChannel(THIS_WEEK, thisWeek);
219             });
220         }
221     }
222
223     /**
224      * Request new weekly data and updates channels
225      */
226     private synchronized void updateWeeklyData() {
227         if (isLinked(LAST_WEEK)) {
228             cachedDailyData.getValue().ifPresent(values -> {
229                 Aggregate weeks = values.aggregats.weeks;
230                 if (weeks.datas.size() > 1) {
231                     updateKwhChannel(LAST_WEEK, weeks.datas.get(1));
232                 } else {
233                     logger.debug("Weekly data are without last week data");
234                     updateKwhChannel(LAST_WEEK, Double.NaN);
235                 }
236             });
237         }
238     }
239
240     /**
241      * Request new monthly data and updates channels
242      */
243     private synchronized void updateMonthlyData() {
244         if (isLinked(LAST_MONTH) || isLinked(THIS_MONTH)) {
245             cachedMonthlyData.getValue().ifPresent(values -> {
246                 Aggregate months = values.aggregats.months;
247                 if (months.datas.size() == 0) {
248                     logger.debug("Monthly data are without any data");
249                     updateKwhChannel(LAST_MONTH, Double.NaN);
250                     updateKwhChannel(THIS_MONTH, Double.NaN);
251                 } else {
252                     updateKwhChannel(LAST_MONTH, months.datas.get(0));
253                     if (months.datas.size() > 1) {
254                         updateKwhChannel(THIS_MONTH, months.datas.get(1));
255                     } else {
256                         logger.debug("Monthly data are without current month data");
257                         updateKwhChannel(THIS_MONTH, Double.NaN);
258                     }
259                 }
260             });
261         }
262     }
263
264     /**
265      * Request new yearly data and updates channels
266      */
267     private synchronized void updateYearlyData() {
268         if (isLinked(LAST_YEAR) || isLinked(THIS_YEAR)) {
269             cachedYearlyData.getValue().ifPresent(values -> {
270                 Aggregate years = values.aggregats.years;
271                 if (years.datas.size() == 0) {
272                     logger.debug("Yearly data are without any data");
273                     updateKwhChannel(LAST_YEAR, Double.NaN);
274                     updateKwhChannel(THIS_YEAR, Double.NaN);
275                 } else {
276                     updateKwhChannel(LAST_YEAR, years.datas.get(0));
277                     if (years.datas.size() > 1) {
278                         updateKwhChannel(THIS_YEAR, years.datas.get(1));
279                     } else {
280                         logger.debug("Yearly data are without current year data");
281                         updateKwhChannel(THIS_YEAR, Double.NaN);
282                     }
283                 }
284             });
285         }
286     }
287
288     private void updateKwhChannel(String channelId, double consumption) {
289         logger.debug("Update channel {} with {}", channelId, consumption);
290         updateState(channelId,
291                 Double.isNaN(consumption) ? UnDefType.UNDEF : new QuantityType<>(consumption, Units.KILOWATT_HOUR));
292     }
293
294     private void updateVAChannel(String channelId, double power) {
295         logger.debug("Update channel {} with {}", channelId, power);
296         updateState(channelId, Double.isNaN(power) ? UnDefType.UNDEF : new QuantityType<>(power, Units.VOLT_AMPERE));
297     }
298
299     /**
300      * Produce a report of all daily values between two dates
301      *
302      * @param startDay the start day of the report
303      * @param endDay the end day of the report
304      * @param separator the separator to be used betwwen the date and the value
305      *
306      * @return the report as a list of string
307      */
308     public synchronized List<String> reportValues(LocalDate startDay, LocalDate endDay, @Nullable String separator) {
309         List<String> report = buildReport(startDay, endDay, separator);
310         disconnect();
311         return report;
312     }
313
314     private List<String> buildReport(LocalDate startDay, LocalDate endDay, @Nullable String separator) {
315         List<String> report = new ArrayList<>();
316         if (startDay.getYear() == endDay.getYear() && startDay.getMonthValue() == endDay.getMonthValue()) {
317             // All values in the same month
318             Consumption result = getConsumptionData(startDay, endDay);
319             if (result != null) {
320                 Aggregate days = result.aggregats.days;
321                 for (int i = 0; i < days.datas.size(); i++) {
322                     double consumption = days.datas.get(i);
323                     String line = days.periodes.get(i).dateDebut.format(DateTimeFormatter.ISO_LOCAL_DATE) + separator;
324                     if (consumption >= 0) {
325                         line += String.valueOf(consumption);
326                     }
327                     report.add(line);
328                 }
329             } else {
330                 LocalDate currentDay = startDay;
331                 while (!currentDay.isAfter(endDay)) {
332                     report.add(currentDay.format(DateTimeFormatter.ISO_LOCAL_DATE) + separator);
333                     currentDay = currentDay.plusDays(1);
334                 }
335             }
336         } else {
337             // Concatenate the report produced for each month between the two dates
338             LocalDate first = startDay;
339             do {
340                 LocalDate last = first.withDayOfMonth(first.lengthOfMonth());
341                 if (last.isAfter(endDay)) {
342                     last = endDay;
343                 }
344                 report.addAll(buildReport(first, last, separator));
345                 first = last.plusDays(1);
346             } while (!first.isAfter(endDay));
347         }
348         return report;
349     }
350
351     private @Nullable Consumption getConsumptionData(LocalDate from, LocalDate to) {
352         logger.debug("getConsumptionData from {} to {}", from.format(DateTimeFormatter.ISO_LOCAL_DATE),
353                 to.format(DateTimeFormatter.ISO_LOCAL_DATE));
354         EnedisHttpApi api = this.enedisApi;
355         if (api != null) {
356             try {
357                 return api.getEnergyData(userId, prmId, from, to);
358             } catch (LinkyException e) {
359                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, e.getMessage());
360             }
361         }
362         return null;
363     }
364
365     private @Nullable Consumption getPowerData(LocalDate from, LocalDate to) {
366         logger.debug("getPowerData from {} to {}", from.format(DateTimeFormatter.ISO_LOCAL_DATE),
367                 to.format(DateTimeFormatter.ISO_LOCAL_DATE));
368         EnedisHttpApi api = this.enedisApi;
369         if (api != null) {
370             try {
371                 return api.getPowerData(userId, prmId, from, to);
372             } catch (LinkyException e) {
373                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, e.getMessage());
374             }
375         }
376         return null;
377     }
378
379     private boolean isConnected() {
380         EnedisHttpApi api = this.enedisApi;
381         return api == null ? false : api.isConnected();
382     }
383
384     private void disconnect() {
385         EnedisHttpApi api = this.enedisApi;
386         if (api != null) {
387             try {
388                 api.dispose();
389             } catch (LinkyException ignore) {
390             }
391         }
392     }
393
394     @Override
395     public void dispose() {
396         logger.debug("Disposing the Linky handler.");
397         ScheduledFuture<?> job = this.refreshJob;
398         if (job != null && !job.isCancelled()) {
399             job.cancel(true);
400             refreshJob = null;
401         }
402         disconnect();
403         enedisApi = null;
404     }
405
406     @Override
407     public synchronized void handleCommand(ChannelUID channelUID, Command command) {
408         if (command instanceof RefreshType) {
409             logger.debug("Refreshing channel {}", channelUID.getId());
410             boolean connectedBefore = isConnected();
411             switch (channelUID.getId()) {
412                 case YESTERDAY:
413                 case THIS_WEEK:
414                     updateDailyData();
415                     break;
416                 case LAST_WEEK:
417                     updateWeeklyData();
418                     break;
419                 case LAST_MONTH:
420                 case THIS_MONTH:
421                     updateMonthlyData();
422                     break;
423                 case LAST_YEAR:
424                 case THIS_YEAR:
425                     updateYearlyData();
426                     break;
427                 case PEAK_POWER:
428                 case PEAK_TIMESTAMP:
429                     updatePowerData();
430                     break;
431                 default:
432                     break;
433             }
434             if (!connectedBefore && isConnected()) {
435                 disconnect();
436             }
437         } else {
438             logger.debug("The Linky binding is read-only and can not handle command {}", command);
439         }
440     }
441 }