]> git.basschouten.com Git - openhab-addons.git/blob
eb3782d7ead68c68be288b1d505cfcca2696d667
[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 = discoverAttributes();
131                         updateProperties(properties);
132                     }
133
134                     prmId = thing.getProperties().get(PRM_ID);
135                     userId = thing.getProperties().get(USER_ID);
136
137                     final LocalDateTime now = LocalDateTime.now();
138                     final LocalDateTime nextDayFirstTimeUpdate = now.plusDays(1).withHour(REFRESH_FIRST_HOUR_OF_DAY)
139                             .truncatedTo(ChronoUnit.HOURS);
140
141                     updateData();
142
143                     refreshJob = scheduler.scheduleWithFixedDelay(this::updateData,
144                             ChronoUnit.MINUTES.between(now, nextDayFirstTimeUpdate) % REFRESH_INTERVAL_IN_MIN + 1,
145                             REFRESH_INTERVAL_IN_MIN, TimeUnit.MINUTES);
146                 } else {
147                     throw new LinkyException("Enedis Api is not initialized");
148                 }
149             } catch (LinkyException e) {
150                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
151             }
152         });
153     }
154
155     private Map<String, String> discoverAttributes() throws LinkyException {
156         Map<String, String> properties = new HashMap<>();
157         EnedisHttpApi api = this.enedisApi;
158         if (api != null) {
159             PrmInfo prmInfo = api.getPrmInfo();
160             UserInfo userInfo = api.getUserInfo();
161             properties.put(USER_ID, userInfo.userProperties.internId);
162             properties.put(PUISSANCE, prmInfo.puissanceSouscrite + " kVA");
163             properties.put(PRM_ID, prmInfo.prmId);
164         }
165
166         return properties;
167     }
168
169     /**
170      * Request new data and updates channels
171      */
172     private void updateData() {
173         updatePowerData();
174         updateDailyData();
175         updateWeeklyData();
176         updateMonthlyData();
177         updateYearlyData();
178     }
179
180     private synchronized void updatePowerData() {
181         if (isLinked(PEAK_POWER) || isLinked(PEAK_TIMESTAMP)) {
182             cachedPowerData.getValue().ifPresent(values -> {
183                 updateVAChannel(PEAK_POWER, values.aggregats.days.datas.get(0));
184                 updateState(PEAK_TIMESTAMP, new DateTimeType(values.aggregats.days.periodes.get(0).dateDebut));
185             });
186         }
187     }
188
189     /**
190      * Request new dayly/weekly data and updates channels
191      */
192     private synchronized void updateDailyData() {
193         if (isLinked(YESTERDAY) || isLinked(THIS_WEEK)) {
194             cachedDailyData.getValue().ifPresent(values -> {
195                 Aggregate days = values.aggregats.days;
196                 int maxValue = days.periodes.size() - 1;
197                 int thisWeekNumber = days.periodes.get(maxValue).dateDebut.get(weekFields.weekOfWeekBasedYear());
198                 double yesterday = days.datas.get(maxValue);
199                 double thisWeek = 0.00;
200
201                 for (int i = maxValue; i >= 0; i--) {
202                     int weekNumber = days.periodes.get(i).dateDebut.get(weekFields.weekOfWeekBasedYear());
203                     if (weekNumber == thisWeekNumber) {
204                         Double value = days.datas.get(i);
205                         thisWeek += !value.isNaN() ? value : 0;
206                     } else {
207                         break;
208                     }
209                 }
210
211                 updateKwhChannel(YESTERDAY, yesterday);
212                 updateKwhChannel(THIS_WEEK, thisWeek);
213             });
214         }
215     }
216
217     /**
218      * Request new weekly data and updates channels
219      */
220     private synchronized void updateWeeklyData() {
221         if (isLinked(LAST_WEEK)) {
222             cachedDailyData.getValue().ifPresent(values -> {
223                 Aggregate weeks = values.aggregats.weeks;
224                 if (weeks.datas.size() > 1) {
225                     updateKwhChannel(LAST_WEEK, weeks.datas.get(1));
226                 }
227             });
228         }
229     }
230
231     /**
232      * Request new monthly data and updates channels
233      */
234     private synchronized void updateMonthlyData() {
235         if (isLinked(LAST_MONTH) || isLinked(THIS_MONTH)) {
236             cachedMonthlyData.getValue().ifPresent(values -> {
237                 Aggregate months = values.aggregats.months;
238                 updateKwhChannel(LAST_MONTH, months.datas.get(0));
239                 if (months.datas.size() > 1) {
240                     updateKwhChannel(THIS_MONTH, months.datas.get(1));
241                 } else {
242                     updateKwhChannel(THIS_MONTH, Double.NaN);
243                 }
244             });
245         }
246     }
247
248     /**
249      * Request new yearly data and updates channels
250      */
251     private synchronized void updateYearlyData() {
252         if (isLinked(LAST_YEAR) || isLinked(THIS_YEAR)) {
253             cachedYearlyData.getValue().ifPresent(values -> {
254                 Aggregate years = values.aggregats.years;
255                 updateKwhChannel(LAST_YEAR, years.datas.get(0));
256                 if (years.datas.size() > 1) {
257                     updateKwhChannel(THIS_YEAR, years.datas.get(1));
258                 } else {
259                     updateKwhChannel(THIS_YEAR, Double.NaN);
260                 }
261             });
262         }
263     }
264
265     private void updateKwhChannel(String channelId, double consumption) {
266         logger.debug("Update channel {} with {}", channelId, consumption);
267         updateState(channelId,
268                 Double.isNaN(consumption) ? UnDefType.UNDEF : new QuantityType<>(consumption, Units.KILOWATT_HOUR));
269     }
270
271     private void updateVAChannel(String channelId, double power) {
272         logger.debug("Update channel {} with {}", channelId, power);
273         updateState(channelId, Double.isNaN(power) ? UnDefType.UNDEF : new QuantityType<>(power, Units.VOLT_AMPERE));
274     }
275
276     /**
277      * Produce a report of all daily values between two dates
278      *
279      * @param startDay the start day of the report
280      * @param endDay the end day of the report
281      * @param separator the separator to be used betwwen the date and the value
282      *
283      * @return the report as a string
284      */
285     public List<String> reportValues(LocalDate startDay, LocalDate endDay, @Nullable String separator) {
286         List<String> report = new ArrayList<>();
287         if (startDay.getYear() == endDay.getYear() && startDay.getMonthValue() == endDay.getMonthValue()) {
288             // All values in the same month
289             Consumption result = getConsumptionData(startDay, endDay);
290             if (result != null) {
291                 Aggregate days = result.aggregats.days;
292                 for (int i = 0; i < days.datas.size(); i++) {
293                     double consumption = days.datas.get(i);
294                     String line = days.periodes.get(i).dateDebut.format(DateTimeFormatter.ISO_LOCAL_DATE) + separator;
295                     if (consumption >= 0) {
296                         line += String.valueOf(consumption);
297                     }
298                     report.add(line);
299                 }
300             } else {
301                 LocalDate currentDay = startDay;
302                 while (!currentDay.isAfter(endDay)) {
303                     report.add(currentDay.format(DateTimeFormatter.ISO_LOCAL_DATE) + separator);
304                     currentDay = currentDay.plusDays(1);
305                 }
306             }
307         } else {
308             // Concatenate the report produced for each month between the two dates
309             LocalDate first = startDay;
310             do {
311                 LocalDate last = first.withDayOfMonth(first.lengthOfMonth());
312                 if (last.isAfter(endDay)) {
313                     last = endDay;
314                 }
315                 report.addAll(reportValues(first, last, separator));
316                 first = last.plusDays(1);
317             } while (!first.isAfter(endDay));
318         }
319         return report;
320     }
321
322     private @Nullable Consumption getConsumptionData(LocalDate from, LocalDate to) {
323         EnedisHttpApi api = this.enedisApi;
324         if (api != null) {
325             try {
326                 return api.getEnergyData(userId, prmId, from, to);
327             } catch (LinkyException e) {
328                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, e.getMessage());
329             }
330         }
331         return null;
332     }
333
334     private @Nullable Consumption getPowerData(LocalDate from, LocalDate to) {
335         EnedisHttpApi api = this.enedisApi;
336         if (api != null) {
337             try {
338                 return api.getPowerData(userId, prmId, from, to);
339             } catch (LinkyException e) {
340                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, e.getMessage());
341             }
342         }
343         return null;
344     }
345
346     @Override
347     public void dispose() {
348         logger.debug("Disposing the Linky handler.");
349         ScheduledFuture<?> job = this.refreshJob;
350         if (job != null && !job.isCancelled()) {
351             job.cancel(true);
352             refreshJob = null;
353         }
354         EnedisHttpApi api = this.enedisApi;
355         if (api != null) {
356             try {
357                 api.dispose();
358                 enedisApi = null;
359             } catch (LinkyException ignore) {
360             }
361         }
362     }
363
364     @Override
365     public void handleCommand(ChannelUID channelUID, Command command) {
366         if (command instanceof RefreshType) {
367             logger.debug("Refreshing channel {}", channelUID.getId());
368             switch (channelUID.getId()) {
369                 case YESTERDAY:
370                 case LAST_WEEK:
371                 case THIS_WEEK:
372                     updateDailyData();
373                     break;
374                 case LAST_MONTH:
375                 case THIS_MONTH:
376                     updateMonthlyData();
377                     break;
378                 case LAST_YEAR:
379                 case THIS_YEAR:
380                     updateYearlyData();
381                     break;
382                 default:
383                     break;
384             }
385         } else {
386             logger.debug("The Linky binding is read-only and can not handle command {}", command);
387         }
388     }
389 }