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 = discoverAttributes();
131 updateProperties(properties);
134 prmId = thing.getProperties().get(PRM_ID);
135 userId = thing.getProperties().get(USER_ID);
137 final LocalDateTime now = LocalDateTime.now();
138 final LocalDateTime nextDayFirstTimeUpdate = now.plusDays(1).withHour(REFRESH_FIRST_HOUR_OF_DAY)
139 .truncatedTo(ChronoUnit.HOURS);
143 refreshJob = scheduler.scheduleWithFixedDelay(this::updateData,
144 ChronoUnit.MINUTES.between(now, nextDayFirstTimeUpdate) % REFRESH_INTERVAL_IN_MIN + 1,
145 REFRESH_INTERVAL_IN_MIN, TimeUnit.MINUTES);
147 throw new LinkyException("Enedis Api is not initialized");
149 } catch (LinkyException e) {
150 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
155 private Map<String, String> discoverAttributes() throws LinkyException {
156 Map<String, String> properties = new HashMap<>();
157 EnedisHttpApi api = this.enedisApi;
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);
170 * Request new data and updates channels
172 private void updateData() {
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));
190 * Request new dayly/weekly data and updates channels
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;
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;
211 updateKwhChannel(YESTERDAY, yesterday);
212 updateKwhChannel(THIS_WEEK, thisWeek);
218 * Request new weekly data and updates channels
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));
232 * Request new monthly data and updates channels
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));
242 updateKwhChannel(THIS_MONTH, Double.NaN);
249 * Request new yearly data and updates channels
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));
259 updateKwhChannel(THIS_YEAR, Double.NaN);
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));
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));
277 * Produce a report of all daily values between two dates
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
283 * @return the report as a string
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);
301 LocalDate currentDay = startDay;
302 while (!currentDay.isAfter(endDay)) {
303 report.add(currentDay.format(DateTimeFormatter.ISO_LOCAL_DATE) + separator);
304 currentDay = currentDay.plusDays(1);
308 // Concatenate the report produced for each month between the two dates
309 LocalDate first = startDay;
311 LocalDate last = first.withDayOfMonth(first.lengthOfMonth());
312 if (last.isAfter(endDay)) {
315 report.addAll(reportValues(first, last, separator));
316 first = last.plusDays(1);
317 } while (!first.isAfter(endDay));
322 private @Nullable Consumption getConsumptionData(LocalDate from, LocalDate to) {
323 EnedisHttpApi api = this.enedisApi;
326 return api.getEnergyData(userId, prmId, from, to);
327 } catch (LinkyException e) {
328 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, e.getMessage());
334 private @Nullable Consumption getPowerData(LocalDate from, LocalDate to) {
335 EnedisHttpApi api = this.enedisApi;
338 return api.getPowerData(userId, prmId, from, to);
339 } catch (LinkyException e) {
340 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, e.getMessage());
347 public void dispose() {
348 logger.debug("Disposing the Linky handler.");
349 ScheduledFuture<?> job = this.refreshJob;
350 if (job != null && !job.isCancelled()) {
354 EnedisHttpApi api = this.enedisApi;
359 } catch (LinkyException ignore) {
365 public void handleCommand(ChannelUID channelUID, Command command) {
366 if (command instanceof RefreshType) {
367 logger.debug("Refreshing channel {}", channelUID.getId());
368 switch (channelUID.getId()) {
386 logger.debug("The Linky binding is read-only and can not handle command {}", command);