2 * Copyright (c) 2010-2020 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
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
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.linky.internal.handler;
15 import static org.openhab.binding.linky.internal.LinkyBindingConstants.*;
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;
26 import java.util.concurrent.ScheduledFuture;
27 import java.util.concurrent.TimeUnit;
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;
55 import com.google.gson.Gson;
58 * The {@link LinkyHandler} is responsible for handling commands, which are
59 * sent to one of the channels.
61 * @author Gaƫl L'hopital - Initial contribution
65 public class LinkyHandler extends BaseThingHandler {
66 private final Logger logger = LoggerFactory.getLogger(LinkyHandler.class);
68 private static final int REFRESH_FIRST_HOUR_OF_DAY = 5;
69 private static final int REFRESH_INTERVAL_IN_MIN = 360;
71 private final HttpClient httpClient;
72 private final Gson gson;
73 private final WeekFields weekFields;
75 private @Nullable ScheduledFuture<?> refreshJob;
76 private @Nullable EnedisHttpApi enedisApi;
78 private final ExpiringDayCache<Consumption> cachedDailyData;
79 private final ExpiringDayCache<Consumption> cachedPowerData;
80 private final ExpiringDayCache<Consumption> cachedMonthlyData;
81 private final ExpiringDayCache<Consumption> cachedYearlyData;
83 private @NonNullByDefault({}) String prmId;
84 private @NonNullByDefault({}) String userId;
86 public LinkyHandler(Thing thing, LocaleProvider localeProvider, Gson gson, HttpClient httpClient) {
89 this.httpClient = httpClient;
90 this.weekFields = WeekFields.of(localeProvider.getLocale());
92 this.cachedDailyData = new ExpiringDayCache<>("daily cache", REFRESH_FIRST_HOUR_OF_DAY, () -> {
93 LocalDate today = LocalDate.now();
94 return getConsumptionData(today.minusDays(15), today);
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);
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);
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);
115 public void initialize() {
116 logger.debug("Initializing Linky handler.");
117 updateStatus(ThingStatus.UNKNOWN);
119 LinkyConfiguration config = getConfigAs(LinkyConfiguration.class);
120 enedisApi = new EnedisHttpApi(config, gson, httpClient);
122 scheduler.submit(() -> {
124 EnedisHttpApi api = this.enedisApi;
127 updateStatus(ThingStatus.ONLINE);
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);
139 prmId = thing.getProperties().get(PRM_ID);
140 userId = thing.getProperties().get(USER_ID);
146 final LocalDateTime now = LocalDateTime.now();
147 final LocalDateTime nextDayFirstTimeUpdate = now.plusDays(1).withHour(REFRESH_FIRST_HOUR_OF_DAY)
148 .truncatedTo(ChronoUnit.HOURS);
150 refreshJob = scheduler.scheduleWithFixedDelay(this::updateData,
151 ChronoUnit.MINUTES.between(now, nextDayFirstTimeUpdate) % REFRESH_INTERVAL_IN_MIN + 1,
152 REFRESH_INTERVAL_IN_MIN, TimeUnit.MINUTES);
154 throw new LinkyException("Enedis Api is not initialized");
156 } catch (LinkyException e) {
157 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
163 * Request new data and updates channels
165 private synchronized void updateData() {
166 boolean connectedBefore = isConnected();
172 if (!connectedBefore && isConnected()) {
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");
184 updateVAChannel(PEAK_POWER, days.datas.get(0));
185 updateState(PEAK_TIMESTAMP, new DateTimeType(days.periodes.get(0).dateDebut));
192 * Request new dayly/weekly data and updates channels
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");
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;
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;
217 updateKwhChannel(YESTERDAY, yesterday);
218 updateKwhChannel(THIS_WEEK, thisWeek);
224 * Request new weekly data and updates channels
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));
233 logger.debug("Weekly data are without last week data");
234 updateKwhChannel(LAST_WEEK, Double.NaN);
241 * Request new monthly data and updates channels
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);
252 updateKwhChannel(LAST_MONTH, months.datas.get(0));
253 if (months.datas.size() > 1) {
254 updateKwhChannel(THIS_MONTH, months.datas.get(1));
256 logger.debug("Monthly data are without current month data");
257 updateKwhChannel(THIS_MONTH, Double.NaN);
265 * Request new yearly data and updates channels
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);
276 updateKwhChannel(LAST_YEAR, years.datas.get(0));
277 if (years.datas.size() > 1) {
278 updateKwhChannel(THIS_YEAR, years.datas.get(1));
280 logger.debug("Yearly data are without current year data");
281 updateKwhChannel(THIS_YEAR, Double.NaN);
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));
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));
300 * Produce a report of all daily values between two dates
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
306 * @return the report as a list of string
308 public synchronized List<String> reportValues(LocalDate startDay, LocalDate endDay, @Nullable String separator) {
309 List<String> report = buildReport(startDay, endDay, separator);
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);
330 LocalDate currentDay = startDay;
331 while (!currentDay.isAfter(endDay)) {
332 report.add(currentDay.format(DateTimeFormatter.ISO_LOCAL_DATE) + separator);
333 currentDay = currentDay.plusDays(1);
337 // Concatenate the report produced for each month between the two dates
338 LocalDate first = startDay;
340 LocalDate last = first.withDayOfMonth(first.lengthOfMonth());
341 if (last.isAfter(endDay)) {
344 report.addAll(buildReport(first, last, separator));
345 first = last.plusDays(1);
346 } while (!first.isAfter(endDay));
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;
357 return api.getEnergyData(userId, prmId, from, to);
358 } catch (LinkyException e) {
359 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, e.getMessage());
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;
371 return api.getPowerData(userId, prmId, from, to);
372 } catch (LinkyException e) {
373 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, e.getMessage());
379 private boolean isConnected() {
380 EnedisHttpApi api = this.enedisApi;
381 return api == null ? false : api.isConnected();
384 private void disconnect() {
385 EnedisHttpApi api = this.enedisApi;
389 } catch (LinkyException ignore) {
395 public void dispose() {
396 logger.debug("Disposing the Linky handler.");
397 ScheduledFuture<?> job = this.refreshJob;
398 if (job != null && !job.isCancelled()) {
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()) {
434 if (!connectedBefore && isConnected()) {
438 logger.debug("The Linky binding is read-only and can not handle command {}", command);