]> git.basschouten.com Git - openhab-addons.git/blob
f2badd66e6f3f66aec529becf881df87de220712
[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 = 1;
69     private static final int REFRESH_INTERVAL_IN_MIN = 120;
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             Consumption consumption = getConsumptionData(today.minusDays(15), today);
95             if (consumption != null) {
96                 logData(consumption.aggregats.days, "Day", false, DateTimeFormatter.ISO_LOCAL_DATE, false);
97                 logData(consumption.aggregats.weeks, "Week", true, DateTimeFormatter.ISO_LOCAL_DATE_TIME, false);
98                 consumption = getConsumptionAfterChecks(consumption);
99             }
100             return consumption;
101         });
102
103         this.cachedPowerData = new ExpiringDayCache<>("power cache", REFRESH_FIRST_HOUR_OF_DAY, () -> {
104             LocalDate to = LocalDate.now().plusDays(1);
105             LocalDate from = to.minusDays(2);
106             Consumption consumption = getPowerData(from, to);
107             if (consumption != null) {
108                 try {
109                     checkData(consumption);
110                 } catch (LinkyException e) {
111                     logger.debug("Power data: {}", e.getMessage());
112                     return null;
113                 }
114             }
115             return consumption;
116         });
117
118         this.cachedMonthlyData = new ExpiringDayCache<>("monthly cache", REFRESH_FIRST_HOUR_OF_DAY, () -> {
119             LocalDate today = LocalDate.now();
120             Consumption consumption = getConsumptionData(today.withDayOfMonth(1).minusMonths(1), today);
121             if (consumption != null) {
122                 logData(consumption.aggregats.months, "Month", true, DateTimeFormatter.ISO_LOCAL_DATE_TIME, false);
123                 consumption = getConsumptionAfterChecks(consumption);
124             }
125             return consumption;
126         });
127
128         this.cachedYearlyData = new ExpiringDayCache<>("yearly cache", REFRESH_FIRST_HOUR_OF_DAY, () -> {
129             LocalDate today = LocalDate.now();
130             Consumption consumption = getConsumptionData(LocalDate.of(today.getYear() - 1, 1, 1), today);
131             if (consumption != null) {
132                 logData(consumption.aggregats.years, "Year", true, DateTimeFormatter.ISO_LOCAL_DATE_TIME, false);
133                 consumption = getConsumptionAfterChecks(consumption);
134             }
135             return consumption;
136         });
137     }
138
139     @Override
140     public void initialize() {
141         logger.debug("Initializing Linky handler.");
142         updateStatus(ThingStatus.UNKNOWN);
143
144         LinkyConfiguration config = getConfigAs(LinkyConfiguration.class);
145         enedisApi = new EnedisHttpApi(config, gson, httpClient);
146
147         scheduler.submit(() -> {
148             try {
149                 EnedisHttpApi api = this.enedisApi;
150                 if (api != null) {
151                     api.initialize();
152                     updateStatus(ThingStatus.ONLINE);
153
154                     if (thing.getProperties().isEmpty()) {
155                         Map<String, String> properties = new HashMap<>();
156                         PrmInfo prmInfo = api.getPrmInfo();
157                         UserInfo userInfo = api.getUserInfo();
158                         properties.put(USER_ID, userInfo.userProperties.internId);
159                         properties.put(PUISSANCE, prmInfo.puissanceSouscrite + " kVA");
160                         properties.put(PRM_ID, prmInfo.prmId);
161                         updateProperties(properties);
162                     }
163
164                     prmId = thing.getProperties().get(PRM_ID);
165                     userId = thing.getProperties().get(USER_ID);
166
167                     updateData();
168
169                     disconnect();
170
171                     final LocalDateTime now = LocalDateTime.now();
172                     final LocalDateTime nextDayFirstTimeUpdate = now.plusDays(1).withHour(REFRESH_FIRST_HOUR_OF_DAY)
173                             .truncatedTo(ChronoUnit.HOURS);
174
175                     refreshJob = scheduler.scheduleWithFixedDelay(this::updateData,
176                             ChronoUnit.MINUTES.between(now, nextDayFirstTimeUpdate) % REFRESH_INTERVAL_IN_MIN + 1,
177                             REFRESH_INTERVAL_IN_MIN, TimeUnit.MINUTES);
178                 } else {
179                     throw new LinkyException("Enedis Api is not initialized");
180                 }
181             } catch (LinkyException e) {
182                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
183             }
184         });
185     }
186
187     /**
188      * Request new data and updates channels
189      */
190     private synchronized void updateData() {
191         boolean connectedBefore = isConnected();
192         updatePowerData();
193         updateDailyData();
194         updateWeeklyData();
195         updateMonthlyData();
196         updateYearlyData();
197         if (!connectedBefore && isConnected()) {
198             disconnect();
199         }
200     }
201
202     private synchronized void updatePowerData() {
203         if (isLinked(PEAK_POWER) || isLinked(PEAK_TIMESTAMP)) {
204             cachedPowerData.getValue().ifPresent(values -> {
205                 Aggregate days = values.aggregats.days;
206                 updateVAChannel(PEAK_POWER, days.datas.get(0));
207                 updateState(PEAK_TIMESTAMP, new DateTimeType(days.periodes.get(0).dateDebut));
208             });
209         }
210     }
211
212     /**
213      * Request new dayly/weekly data and updates channels
214      */
215     private synchronized void updateDailyData() {
216         if (isLinked(YESTERDAY)) {
217             cachedDailyData.getValue().ifPresent(values -> {
218                 Aggregate days = values.aggregats.days;
219                 updateKwhChannel(YESTERDAY, days.datas.get(days.datas.size() - 1));
220             });
221         }
222     }
223
224     /**
225      * Request new weekly data and updates channels
226      */
227     private synchronized void updateWeeklyData() {
228         if (isLinked(LAST_WEEK) || isLinked(THIS_WEEK)) {
229             cachedDailyData.getValue().ifPresent(values -> {
230                 Aggregate days = values.aggregats.days;
231                 int idxLast = days.periodes.get(days.periodes.size() - 1).dateDebut.get(weekFields.dayOfWeek()) == 7 ? 2
232                         : 1;
233                 Aggregate weeks = values.aggregats.weeks;
234                 if (weeks.datas.size() > idxLast) {
235                     updateKwhChannel(LAST_WEEK, weeks.datas.get(idxLast));
236                 }
237                 if (weeks.datas.size() > (idxLast + 1)) {
238                     updateKwhChannel(THIS_WEEK, weeks.datas.get(idxLast + 1));
239                 } else {
240                     updateKwhChannel(THIS_WEEK, 0.0);
241                 }
242             });
243         }
244     }
245
246     /**
247      * Request new monthly data and updates channels
248      */
249     private synchronized void updateMonthlyData() {
250         if (isLinked(LAST_MONTH) || isLinked(THIS_MONTH)) {
251             cachedMonthlyData.getValue().ifPresent(values -> {
252                 Aggregate months = values.aggregats.months;
253                 updateKwhChannel(LAST_MONTH, months.datas.get(0));
254                 if (months.datas.size() > 1) {
255                     updateKwhChannel(THIS_MONTH, months.datas.get(1));
256                 } else {
257                     updateKwhChannel(THIS_MONTH, 0.0);
258                 }
259             });
260         }
261     }
262
263     /**
264      * Request new yearly data and updates channels
265      */
266     private synchronized void updateYearlyData() {
267         if (isLinked(LAST_YEAR) || isLinked(THIS_YEAR)) {
268             cachedYearlyData.getValue().ifPresent(values -> {
269                 Aggregate years = values.aggregats.years;
270                 updateKwhChannel(LAST_YEAR, years.datas.get(0));
271                 if (years.datas.size() > 1) {
272                     updateKwhChannel(THIS_YEAR, years.datas.get(1));
273                 } else {
274                     updateKwhChannel(THIS_YEAR, 0.0);
275                 }
276             });
277         }
278     }
279
280     private void updateKwhChannel(String channelId, double consumption) {
281         logger.debug("Update channel {} with {}", channelId, consumption);
282         updateState(channelId,
283                 Double.isNaN(consumption) ? UnDefType.UNDEF : new QuantityType<>(consumption, Units.KILOWATT_HOUR));
284     }
285
286     private void updateVAChannel(String channelId, double power) {
287         logger.debug("Update channel {} with {}", channelId, power);
288         updateState(channelId, Double.isNaN(power) ? UnDefType.UNDEF : new QuantityType<>(power, Units.VOLT_AMPERE));
289     }
290
291     /**
292      * Produce a report of all daily values between two dates
293      *
294      * @param startDay the start day of the report
295      * @param endDay the end day of the report
296      * @param separator the separator to be used betwwen the date and the value
297      *
298      * @return the report as a list of string
299      */
300     public synchronized List<String> reportValues(LocalDate startDay, LocalDate endDay, @Nullable String separator) {
301         List<String> report = buildReport(startDay, endDay, separator);
302         disconnect();
303         return report;
304     }
305
306     private List<String> buildReport(LocalDate startDay, LocalDate endDay, @Nullable String separator) {
307         List<String> report = new ArrayList<>();
308         if (startDay.getYear() == endDay.getYear() && startDay.getMonthValue() == endDay.getMonthValue()) {
309             // All values in the same month
310             Consumption result = getConsumptionData(startDay, endDay.plusDays(1));
311             if (result != null) {
312                 Aggregate days = result.aggregats.days;
313                 int size = (days.datas == null || days.periodes == null) ? 0
314                         : (days.datas.size() <= days.periodes.size() ? days.datas.size() : days.periodes.size());
315                 for (int i = 0; i < size; i++) {
316                     double consumption = days.datas.get(i);
317                     String line = days.periodes.get(i).dateDebut.format(DateTimeFormatter.ISO_LOCAL_DATE) + separator;
318                     if (consumption >= 0) {
319                         line += String.valueOf(consumption);
320                     }
321                     report.add(line);
322                 }
323             } else {
324                 LocalDate currentDay = startDay;
325                 while (!currentDay.isAfter(endDay)) {
326                     report.add(currentDay.format(DateTimeFormatter.ISO_LOCAL_DATE) + separator);
327                     currentDay = currentDay.plusDays(1);
328                 }
329             }
330         } else {
331             // Concatenate the report produced for each month between the two dates
332             LocalDate first = startDay;
333             do {
334                 LocalDate last = first.withDayOfMonth(first.lengthOfMonth());
335                 if (last.isAfter(endDay)) {
336                     last = endDay;
337                 }
338                 report.addAll(buildReport(first, last, separator));
339                 first = last.plusDays(1);
340             } while (!first.isAfter(endDay));
341         }
342         return report;
343     }
344
345     private @Nullable Consumption getConsumptionData(LocalDate from, LocalDate to) {
346         logger.debug("getConsumptionData from {} to {}", from.format(DateTimeFormatter.ISO_LOCAL_DATE),
347                 to.format(DateTimeFormatter.ISO_LOCAL_DATE));
348         EnedisHttpApi api = this.enedisApi;
349         if (api != null) {
350             try {
351                 Consumption consumption = api.getEnergyData(userId, prmId, from, to);
352                 updateStatus(ThingStatus.ONLINE);
353                 return consumption;
354             } catch (LinkyException e) {
355                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, e.getMessage());
356             }
357         }
358         return null;
359     }
360
361     private @Nullable Consumption getPowerData(LocalDate from, LocalDate to) {
362         logger.debug("getPowerData from {} to {}", from.format(DateTimeFormatter.ISO_LOCAL_DATE),
363                 to.format(DateTimeFormatter.ISO_LOCAL_DATE));
364         EnedisHttpApi api = this.enedisApi;
365         if (api != null) {
366             try {
367                 Consumption consumption = api.getPowerData(userId, prmId, from, to);
368                 updateStatus(ThingStatus.ONLINE);
369                 return consumption;
370             } catch (LinkyException e) {
371                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, e.getMessage());
372             }
373         }
374         return null;
375     }
376
377     private boolean isConnected() {
378         EnedisHttpApi api = this.enedisApi;
379         return api == null ? false : api.isConnected();
380     }
381
382     private void disconnect() {
383         EnedisHttpApi api = this.enedisApi;
384         if (api != null) {
385             try {
386                 api.dispose();
387             } catch (LinkyException ignore) {
388             }
389         }
390     }
391
392     @Override
393     public void dispose() {
394         logger.debug("Disposing the Linky handler.");
395         ScheduledFuture<?> job = this.refreshJob;
396         if (job != null && !job.isCancelled()) {
397             job.cancel(true);
398             refreshJob = null;
399         }
400         disconnect();
401         enedisApi = null;
402     }
403
404     @Override
405     public synchronized void handleCommand(ChannelUID channelUID, Command command) {
406         if (command instanceof RefreshType) {
407             logger.debug("Refreshing channel {}", channelUID.getId());
408             boolean connectedBefore = isConnected();
409             switch (channelUID.getId()) {
410                 case YESTERDAY:
411                     updateDailyData();
412                     break;
413                 case LAST_WEEK:
414                 case THIS_WEEK:
415                     updateWeeklyData();
416                     break;
417                 case LAST_MONTH:
418                 case THIS_MONTH:
419                     updateMonthlyData();
420                     break;
421                 case LAST_YEAR:
422                 case THIS_YEAR:
423                     updateYearlyData();
424                     break;
425                 case PEAK_POWER:
426                 case PEAK_TIMESTAMP:
427                     updatePowerData();
428                     break;
429                 default:
430                     break;
431             }
432             if (!connectedBefore && isConnected()) {
433                 disconnect();
434             }
435         } else {
436             logger.debug("The Linky binding is read-only and can not handle command {}", command);
437         }
438     }
439
440     private @Nullable Consumption getConsumptionAfterChecks(Consumption consumption) {
441         try {
442             checkData(consumption);
443         } catch (LinkyException e) {
444             logger.debug("Consumption data: {}", e.getMessage());
445             return null;
446         }
447         if (!isDataLastDayAvailable(consumption)) {
448             logger.debug("Data including yesterday are not yet available");
449             return null;
450         }
451         return consumption;
452     }
453
454     public void checkData(Consumption consumption) throws LinkyException {
455         if (consumption.aggregats.days.periodes.size() == 0) {
456             throw new LinkyException("invalid consumptions data: no day period");
457         }
458         if (consumption.aggregats.days.periodes.size() != consumption.aggregats.days.datas.size()) {
459             throw new LinkyException("invalid consumptions data: not one data for each day period");
460         }
461         if (consumption.aggregats.weeks.periodes.size() == 0) {
462             throw new LinkyException("invalid consumptions data: no week period");
463         }
464         if (consumption.aggregats.weeks.periodes.size() != consumption.aggregats.weeks.datas.size()) {
465             throw new LinkyException("invalid consumptions data: not one data for each week period");
466         }
467         if (consumption.aggregats.months.periodes.size() == 0) {
468             throw new LinkyException("invalid consumptions data: no month period");
469         }
470         if (consumption.aggregats.months.periodes.size() != consumption.aggregats.months.datas.size()) {
471             throw new LinkyException("invalid consumptions data: not one data for each month period");
472         }
473         if (consumption.aggregats.years.periodes.size() == 0) {
474             throw new LinkyException("invalid consumptions data: no year period");
475         }
476         if (consumption.aggregats.years.periodes.size() != consumption.aggregats.years.datas.size()) {
477             throw new LinkyException("invalid consumptions data: not one data for each year period");
478         }
479     }
480
481     private boolean isDataLastDayAvailable(Consumption consumption) {
482         Aggregate days = consumption.aggregats.days;
483         logData(days, "Last day", false, DateTimeFormatter.ISO_LOCAL_DATE, true);
484         return days.datas != null && days.datas.size() > 0 && !days.datas.get(days.datas.size() - 1).isNaN();
485     }
486
487     private void logData(Aggregate aggregate, String title, boolean withDateFin, DateTimeFormatter dateTimeFormatter,
488             boolean onlyLast) {
489         if (logger.isDebugEnabled()) {
490             int size = (aggregate.datas == null || aggregate.periodes == null) ? 0
491                     : (aggregate.datas.size() <= aggregate.periodes.size() ? aggregate.datas.size()
492                             : aggregate.periodes.size());
493             if (onlyLast) {
494                 if (size > 0) {
495                     logData(aggregate, size - 1, title, withDateFin, dateTimeFormatter);
496                 }
497             } else {
498                 for (int i = 0; i < size; i++) {
499                     logData(aggregate, i, title, withDateFin, dateTimeFormatter);
500                 }
501             }
502         }
503     }
504
505     private void logData(Aggregate aggregate, int index, String title, boolean withDateFin,
506             DateTimeFormatter dateTimeFormatter) {
507         if (withDateFin) {
508             logger.debug("{} {} {} value {}", title, aggregate.periodes.get(index).dateDebut.format(dateTimeFormatter),
509                     aggregate.periodes.get(index).dateFin.format(dateTimeFormatter), aggregate.datas.get(index));
510         } else {
511             logger.debug("{} {} value {}", title, aggregate.periodes.get(index).dateDebut.format(dateTimeFormatter),
512                     aggregate.datas.get(index));
513         }
514     }
515 }