]> git.basschouten.com Git - openhab-addons.git/blob
d164e8aa1e298122c2641965c7186523070f438a
[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                 updateKwhChannel(LAST_MONTH, months.datas.get(0));
226                 updateKwhChannel(THIS_MONTH, months.datas.get(1));
227             }
228         }
229     }
230
231     /**
232      * Request new yearly data and updates channels
233      */
234     private synchronized void updateYearlyData() {
235         if (isLinked(LAST_YEAR) || isLinked(THIS_YEAR)) {
236             Consumption result = cachedYearlyData.getValue();
237             if (result != null) {
238                 Aggregate years = result.aggregats.years;
239                 updateKwhChannel(LAST_YEAR, years.datas.get(0));
240                 updateKwhChannel(THIS_YEAR, years.datas.get(1));
241             }
242         }
243     }
244
245     private void updateKwhChannel(String channelId, double consumption) {
246         logger.debug("Update channel {} with {}", channelId, consumption);
247         updateState(channelId,
248                 !Double.isNaN(consumption) ? new QuantityType<>(consumption, SmartHomeUnits.KILOWATT_HOUR)
249                         : UnDefType.UNDEF);
250     }
251
252     private void updateVAChannel(String channelId, double power) {
253         logger.debug("Update channel {} with {}", channelId, power);
254         updateState(channelId,
255                 !Double.isNaN(power) ? new QuantityType<>(power, SmartHomeUnits.VOLT_AMPERE) : UnDefType.UNDEF);
256     }
257
258     /**
259      * Produce a report of all daily values between two dates
260      *
261      * @param startDay the start day of the report
262      * @param endDay the end day of the report
263      * @param separator the separator to be used betwwen the date and the value
264      *
265      * @return the report as a string
266      */
267     public List<String> reportValues(LocalDate startDay, LocalDate endDay, @Nullable String separator) {
268         List<String> report = new ArrayList<>();
269         if (startDay.getYear() == endDay.getYear() && startDay.getMonthValue() == endDay.getMonthValue()) {
270             // All values in the same month
271             Consumption result = getConsumptionData(startDay, endDay);
272             if (result != null) {
273                 Aggregate days = result.aggregats.days;
274                 for (int i = 0; i < days.datas.size(); i++) {
275                     double consumption = days.datas.get(i);
276                     String line = days.periodes.get(i).dateDebut.format(DateTimeFormatter.ISO_LOCAL_DATE) + separator;
277                     if (consumption >= 0) {
278                         line += String.valueOf(consumption);
279                     }
280                     report.add(line);
281                 }
282             } else {
283                 LocalDate currentDay = startDay;
284                 while (!currentDay.isAfter(endDay)) {
285                     report.add(currentDay.format(DateTimeFormatter.ISO_LOCAL_DATE) + separator);
286                     currentDay = currentDay.plusDays(1);
287                 }
288             }
289         } else {
290             // Concatenate the report produced for each month between the two dates
291             LocalDate first = startDay;
292             do {
293                 LocalDate last = first.withDayOfMonth(first.lengthOfMonth());
294                 if (last.isAfter(endDay)) {
295                     last = endDay;
296                 }
297                 report.addAll(reportValues(first, last, separator));
298                 first = last.plusDays(1);
299             } while (!first.isAfter(endDay));
300         }
301         return report;
302     }
303
304     private @Nullable Consumption getConsumptionData(LocalDate from, LocalDate to) {
305         EnedisHttpApi api = this.enedisApi;
306         if (api != null) {
307             try {
308                 return api.getEnergyData(userId, prmId, from, to);
309             } catch (LinkyException e) {
310                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, e.getMessage());
311             }
312         }
313         return null;
314     }
315
316     private @Nullable Consumption getPowerData(LocalDate from, LocalDate to) {
317         EnedisHttpApi api = this.enedisApi;
318         if (api != null) {
319             try {
320                 return api.getPowerData(userId, prmId, from, to);
321             } catch (LinkyException e) {
322                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, e.getMessage());
323             }
324         }
325         return null;
326     }
327
328     @Override
329     public void dispose() {
330         logger.debug("Disposing the Linky handler.");
331         ScheduledFuture<?> job = this.refreshJob;
332         if (job != null && !job.isCancelled()) {
333             job.cancel(true);
334             refreshJob = null;
335         }
336         EnedisHttpApi api = this.enedisApi;
337         if (api != null) {
338             try {
339                 api.dispose();
340                 enedisApi = null;
341             } catch (LinkyException ignore) {
342             }
343         }
344     }
345
346     @Override
347     public void handleCommand(ChannelUID channelUID, Command command) {
348         if (command instanceof RefreshType) {
349             logger.debug("Refreshing channel {}", channelUID.getId());
350             switch (channelUID.getId()) {
351                 case YESTERDAY:
352                 case LAST_WEEK:
353                 case THIS_WEEK:
354                     updateDailyData();
355                     break;
356                 case LAST_MONTH:
357                 case THIS_MONTH:
358                     updateMonthlyData();
359                     break;
360                 case LAST_YEAR:
361                 case THIS_YEAR:
362                     updateYearlyData();
363                     break;
364                 default:
365                     break;
366             }
367         } else {
368             logger.debug("The Linky binding is read-only and can not handle command {}", command);
369         }
370     }
371 }