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.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;
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;
74 private @Nullable ScheduledFuture<?> refreshJob;
75 private @Nullable EnedisHttpApi enedisApi;
76 private final WeekFields weekFields;
78 private final ExpiringDayCache<Consumption> cachedDaylyData;
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;
91 this.weekFields = WeekFields.of(localeProvider.getLocale());
93 this.cachedDaylyData = new ExpiringDayCache<>("daily cache", REFRESH_FIRST_HOUR_OF_DAY, () -> {
94 LocalDate today = LocalDate.now();
95 return getConsumptionData(today.minusDays(13), today);
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);
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);
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);
116 public void initialize() {
117 logger.debug("Initializing Linky handler.");
118 updateStatus(ThingStatus.UNKNOWN);
120 LinkyConfiguration config = getConfigAs(LinkyConfiguration.class);
121 enedisApi = new EnedisHttpApi(config, gson, httpClient);
124 enedisApi.initialize();
125 updateStatus(ThingStatus.ONLINE);
127 if (thing.getProperties().isEmpty()) {
128 Map<String, String> properties = discoverAttributes();
129 updateProperties(properties);
132 prmId = thing.getProperties().get(PRM_ID);
133 userId = thing.getProperties().get(USER_ID);
135 final LocalDateTime now = LocalDateTime.now();
136 final LocalDateTime nextDayFirstTimeUpdate = now.plusDays(1).withHour(REFRESH_FIRST_HOUR_OF_DAY)
137 .truncatedTo(ChronoUnit.HOURS);
141 refreshJob = scheduler.scheduleWithFixedDelay(this::updateData,
142 ChronoUnit.MINUTES.between(now, nextDayFirstTimeUpdate) % REFRESH_INTERVAL_IN_MIN + 1,
143 REFRESH_INTERVAL_IN_MIN, TimeUnit.MINUTES);
145 } catch (LinkyException e) {
146 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
150 private Map<String, String> discoverAttributes() throws LinkyException {
151 Map<String, String> properties = new HashMap<>();
152 EnedisHttpApi api = this.enedisApi;
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);
165 * Request new data and updates channels
167 private void updateData() {
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));
185 * Request new dayly/weekly data and updates channels
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;
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;
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);
210 updateKwhChannel(YESTERDAY, yesterday);
211 updateKwhChannel(THIS_WEEK, thisWeek);
212 updateKwhChannel(LAST_WEEK, lastWeek);
218 * Request new monthly data and updates channels
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);
229 updateKwhChannel(LAST_MONTH, months.datas.get(0));
230 updateKwhChannel(THIS_MONTH, months.datas.get(1));
236 * Request new yearly data and updates channels
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));
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)
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);
263 * Produce a report of all daily values between two dates
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
269 * @return the report as a string
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);
287 LocalDate currentDay = startDay;
288 while (!currentDay.isAfter(endDay)) {
289 report.add(currentDay.format(DateTimeFormatter.ISO_LOCAL_DATE) + separator);
290 currentDay = currentDay.plusDays(1);
294 // Concatenate the report produced for each month between the two dates
295 LocalDate first = startDay;
297 LocalDate last = first.withDayOfMonth(first.lengthOfMonth());
298 if (last.isAfter(endDay)) {
301 report.addAll(reportValues(first, last, separator));
302 first = last.plusDays(1);
303 } while (!first.isAfter(endDay));
308 private @Nullable Consumption getConsumptionData(LocalDate from, LocalDate to) {
309 EnedisHttpApi api = this.enedisApi;
312 return api.getEnergyData(userId, prmId, from, to);
313 } catch (LinkyException e) {
314 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, e.getMessage());
320 private @Nullable Consumption getPowerData(LocalDate from, LocalDate to) {
321 EnedisHttpApi api = this.enedisApi;
324 return api.getPowerData(userId, prmId, from, to);
325 } catch (LinkyException e) {
326 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, e.getMessage());
333 public void dispose() {
334 logger.debug("Disposing the Linky handler.");
335 ScheduledFuture<?> job = this.refreshJob;
336 if (job != null && !job.isCancelled()) {
340 EnedisHttpApi api = this.enedisApi;
345 } catch (LinkyException ignore) {
351 public void handleCommand(ChannelUID channelUID, Command command) {
352 if (command instanceof RefreshType) {
353 logger.debug("Refreshing channel {}", channelUID.getId());
354 switch (channelUID.getId()) {
372 logger.debug("The Linky binding is read-only and can not handle command {}", command);