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