]> git.basschouten.com Git - openhab-addons.git/blob
912e93a9e2b4d6cf9d4268fdc289a5ac1f4f2d1e
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2023 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.ecowatt.internal.handler;
14
15 import static org.openhab.binding.ecowatt.internal.EcowattBindingConstants.*;
16
17 import java.time.Duration;
18 import java.time.LocalDateTime;
19 import java.time.ZonedDateTime;
20 import java.time.format.DateTimeFormatter;
21 import java.time.temporal.ChronoUnit;
22 import java.util.concurrent.ScheduledFuture;
23 import java.util.concurrent.TimeUnit;
24
25 import org.eclipse.jdt.annotation.NonNullByDefault;
26 import org.eclipse.jdt.annotation.Nullable;
27 import org.eclipse.jetty.client.HttpClient;
28 import org.openhab.binding.ecowatt.internal.configuration.EcowattConfiguration;
29 import org.openhab.binding.ecowatt.internal.exception.EcowattApiLimitException;
30 import org.openhab.binding.ecowatt.internal.restapi.EcowattApiResponse;
31 import org.openhab.binding.ecowatt.internal.restapi.EcowattDaySignals;
32 import org.openhab.binding.ecowatt.internal.restapi.EcowattRestApi;
33 import org.openhab.core.auth.client.oauth2.OAuthFactory;
34 import org.openhab.core.cache.ExpiringCache;
35 import org.openhab.core.i18n.CommunicationException;
36 import org.openhab.core.i18n.TimeZoneProvider;
37 import org.openhab.core.i18n.TranslationProvider;
38 import org.openhab.core.library.types.DecimalType;
39 import org.openhab.core.thing.ChannelUID;
40 import org.openhab.core.thing.Thing;
41 import org.openhab.core.thing.ThingStatus;
42 import org.openhab.core.thing.ThingStatusDetail;
43 import org.openhab.core.thing.binding.BaseThingHandler;
44 import org.openhab.core.types.Command;
45 import org.openhab.core.types.RefreshType;
46 import org.openhab.core.types.State;
47 import org.openhab.core.types.UnDefType;
48 import org.osgi.framework.Bundle;
49 import org.osgi.framework.FrameworkUtil;
50 import org.slf4j.Logger;
51 import org.slf4j.LoggerFactory;
52
53 /**
54  * The {@link EcowattHandler} is responsible for updating the state of the channels
55  *
56  * @author Laurent Garnier - Initial contribution
57  * @author Laurent Garnier - Add support for API version 5
58  */
59 @NonNullByDefault
60 public class EcowattHandler extends BaseThingHandler {
61
62     private final Logger logger = LoggerFactory.getLogger(EcowattHandler.class);
63
64     private final OAuthFactory oAuthFactory;
65     private final HttpClient httpClient;
66     private final TranslationProvider i18nProvider;
67     private final TimeZoneProvider timeZoneProvider;
68     private final Bundle bundle;
69
70     private @Nullable EcowattRestApi api;
71     private ExpiringCache<EcowattApiResponse> cachedApiResponse = new ExpiringCache<>(Duration.ofHours(4),
72             this::getApiResponse); // cache the API response during 4 hours
73
74     private @Nullable ScheduledFuture<?> updateJob;
75
76     public EcowattHandler(Thing thing, OAuthFactory oAuthFactory, HttpClient httpClient,
77             TranslationProvider i18nProvider, TimeZoneProvider timeZoneProvider) {
78         super(thing);
79         this.oAuthFactory = oAuthFactory;
80         this.httpClient = httpClient;
81         this.i18nProvider = i18nProvider;
82         this.timeZoneProvider = timeZoneProvider;
83         this.bundle = FrameworkUtil.getBundle(this.getClass());
84     }
85
86     @Override
87     public void handleCommand(ChannelUID channelUID, Command command) {
88         if (command == RefreshType.REFRESH) {
89             updateChannel(channelUID.getId());
90         }
91     }
92
93     @Override
94     public void initialize() {
95         EcowattConfiguration config = getConfigAs(EcowattConfiguration.class);
96
97         final String idClient = config.idClient;
98         final String idSecret = config.idSecret;
99
100         if (idClient.isBlank() || idSecret.isBlank()) {
101             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
102                     "@text/offline.config-error-unset-parameters");
103         } else {
104             api = new EcowattRestApi(oAuthFactory, httpClient, thing.getUID().getAsString(), idClient, idSecret,
105                     config.apiVersion);
106             updateStatus(ThingStatus.UNKNOWN);
107             scheduleNextUpdate(0, true);
108         }
109     }
110
111     @Override
112     public void dispose() {
113         stopScheduledJob();
114         EcowattRestApi localApi = api;
115         if (localApi != null) {
116             localApi.dispose();
117             api = null;
118         }
119     }
120
121     @Override
122     public void handleRemoval() {
123         oAuthFactory.deleteServiceAndAccessToken(thing.getUID().getAsString());
124         super.handleRemoval();
125     }
126
127     /**
128      * Schedule the next update of channels.
129      *
130      * After this update is run, a new update will be rescheduled, either just after the API is reachable again or at
131      * the beginning of the following hour.
132      *
133      * @param delayInSeconds the delay in seconds before running the next update
134      * @param retryIfApiLimitReached true if a retry is expected when the update fails due to reached API limit
135      */
136     private void scheduleNextUpdate(long delayInSeconds, boolean retryIfApiLimitReached) {
137         logger.debug("scheduleNextUpdate delay={}s retryIfLimitReached={}", delayInSeconds, retryIfApiLimitReached);
138         updateJob = scheduler.schedule(() -> {
139             int retryDelay = updateChannels(retryIfApiLimitReached);
140             long delayNextUpdate;
141             if (retryDelay > 0) {
142                 // Schedule a new update just after the API is reachable again
143                 logger.debug("retryDelay {}", retryDelay);
144                 delayNextUpdate = retryDelay;
145             } else {
146                 // Schedule a new update at the beginning of the following hour
147                 final LocalDateTime now = LocalDateTime.now();
148                 final LocalDateTime beginningNextHour = now.plusHours(1).truncatedTo(ChronoUnit.HOURS);
149                 delayNextUpdate = ChronoUnit.SECONDS.between(now, beginningNextHour);
150             }
151             // Add 3s of additional delay for security...
152             delayNextUpdate += 3;
153             scheduleNextUpdate(delayNextUpdate, retryDelay == 0);
154         }, delayInSeconds, TimeUnit.SECONDS);
155     }
156
157     private void stopScheduledJob() {
158         ScheduledFuture<?> job = updateJob;
159         if (job != null) {
160             job.cancel(true);
161             updateJob = null;
162         }
163     }
164
165     private EcowattApiResponse getApiResponse() {
166         EcowattRestApi localApi = api;
167         if (localApi == null) {
168             return new EcowattApiResponse();
169         }
170
171         EcowattApiResponse response;
172         try {
173             response = localApi.getSignals();
174         } catch (CommunicationException e) {
175             Throwable cause = e.getCause();
176             if (cause != null) {
177                 logger.warn("{}: {}", e.getMessage(bundle, i18nProvider), cause.getMessage());
178             } else {
179                 logger.warn("{}", e.getMessage(bundle, i18nProvider));
180             }
181             response = new EcowattApiResponse(e);
182         }
183         return response;
184     }
185
186     private int updateChannels(boolean retryIfApiLimitReached) {
187         return updateChannel(null, retryIfApiLimitReached);
188     }
189
190     private void updateChannel(String channelId) {
191         updateChannel(channelId, false);
192     }
193
194     private synchronized int updateChannel(@Nullable String channelId, boolean retryIfApiLimitReached) {
195         logger.debug("updateChannel channelId={}, retryIfApiLimitReached={}", channelId, retryIfApiLimitReached);
196         int retryDelay = 0;
197         EcowattApiResponse response = cachedApiResponse.getValue();
198         if (response == null || !response.succeeded()) {
199             CommunicationException exception = response == null ? null : response.getException();
200             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
201                     exception == null ? null : exception.getRawMessage());
202
203             // Invalidate the cache to be sure the next request will trigger the API
204             cachedApiResponse.invalidateValue();
205
206             if (retryIfApiLimitReached && exception instanceof EcowattApiLimitException limitException
207                     && limitException.getRetryAfter() > 0) {
208                 // Will retry when the API is available again (just after the limit expired)
209                 retryDelay = limitException.getRetryAfter();
210             }
211         } else {
212             updateStatus(ThingStatus.ONLINE);
213         }
214
215         ZonedDateTime now = ZonedDateTime.now(timeZoneProvider.getTimeZone());
216         logger.debug("now {}", now.format(DateTimeFormatter.ISO_ZONED_DATE_TIME));
217         if ((channelId == null || CHANNEL_TODAY_SIGNAL.equals(channelId)) && isLinked(CHANNEL_TODAY_SIGNAL)) {
218             updateState(CHANNEL_TODAY_SIGNAL, getDaySignalState(response, now));
219         }
220         if ((channelId == null || CHANNEL_TOMORROW_SIGNAL.equals(channelId)) && isLinked(CHANNEL_TOMORROW_SIGNAL)) {
221             updateState(CHANNEL_TOMORROW_SIGNAL, getDaySignalState(response, now.plusDays(1)));
222         }
223         if ((channelId == null || CHANNEL_IN_TWO_DAYS_SIGNAL.equals(channelId))
224                 && isLinked(CHANNEL_IN_TWO_DAYS_SIGNAL)) {
225             updateState(CHANNEL_IN_TWO_DAYS_SIGNAL, getDaySignalState(response, now.plusDays(2)));
226         }
227         if ((channelId == null || CHANNEL_IN_THREE_DAYS_SIGNAL.equals(channelId))
228                 && isLinked(CHANNEL_IN_THREE_DAYS_SIGNAL)) {
229             updateState(CHANNEL_IN_THREE_DAYS_SIGNAL, getDaySignalState(response, now.plusDays(3)));
230         }
231         if ((channelId == null || CHANNEL_CURRENT_HOUR_SIGNAL.equals(channelId))
232                 && isLinked(CHANNEL_CURRENT_HOUR_SIGNAL)) {
233             updateState(CHANNEL_CURRENT_HOUR_SIGNAL, getHourSignalState(response, now));
234         }
235
236         return retryDelay;
237     }
238
239     /**
240      * Get the signal applicable for a given day from the API response
241      *
242      * @param response the API response
243      * @param dateTime the date and time to consider
244      * @return the found valid signal as a channel state or UndefType.UNDEF if not found
245      */
246     public static State getDaySignalState(@Nullable EcowattApiResponse response, ZonedDateTime dateTime) {
247         EcowattDaySignals signals = response == null ? null : response.getDaySignals(dateTime);
248         return signals != null && signals.getDaySignal() >= 1 && signals.getDaySignal() <= 3
249                 ? new DecimalType(signals.getDaySignal())
250                 : UnDefType.UNDEF;
251     }
252
253     /**
254      * Get the signal applicable for a given day and hour from the API response
255      *
256      * @param response the API response
257      * @param dateTime the date and time to consider
258      * @return the found valid signal as a channel state or UndefType.UNDEF if not found
259      */
260     public static State getHourSignalState(@Nullable EcowattApiResponse response, ZonedDateTime dateTime) {
261         EcowattDaySignals signals = response == null ? null : response.getDaySignals(dateTime);
262         ZonedDateTime day = signals == null ? null : signals.getDay();
263         if (signals != null && day != null) {
264             // Move the current time to the same offset as the data returned by the API to get and use the right current
265             // hour index in these data
266             int hour = dateTime.withZoneSameInstant(day.getZone()).getHour();
267             int value = signals.getHourSignal(hour);
268             LoggerFactory.getLogger(EcowattHandler.class).debug("hour {} value {}", hour, value);
269             if (value >= 0 && value <= 3) {
270                 return new DecimalType(value);
271             }
272         }
273         return UnDefType.UNDEF;
274     }
275 }