]> git.basschouten.com Git - openhab-addons.git/blob
adbf9d7d9137924083a940cef813874d41334c24
[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.gardena.internal.handler;
14
15 import java.time.Duration;
16 import java.time.Instant;
17 import java.time.LocalDate;
18 import java.time.ZoneId;
19 import java.time.ZonedDateTime;
20 import java.time.format.DateTimeFormatter;
21 import java.time.format.DateTimeParseException;
22 import java.time.format.FormatStyle;
23 import java.util.Collection;
24 import java.util.Collections;
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.http.HttpStatus;
32 import org.openhab.binding.gardena.internal.GardenaBindingConstants;
33 import org.openhab.binding.gardena.internal.GardenaSmart;
34 import org.openhab.binding.gardena.internal.GardenaSmartEventListener;
35 import org.openhab.binding.gardena.internal.GardenaSmartImpl;
36 import org.openhab.binding.gardena.internal.config.GardenaConfig;
37 import org.openhab.binding.gardena.internal.discovery.GardenaDeviceDiscoveryService;
38 import org.openhab.binding.gardena.internal.exception.GardenaException;
39 import org.openhab.binding.gardena.internal.model.dto.Device;
40 import org.openhab.binding.gardena.internal.util.UidUtils;
41 import org.openhab.core.i18n.TimeZoneProvider;
42 import org.openhab.core.io.net.http.HttpClientFactory;
43 import org.openhab.core.io.net.http.WebSocketFactory;
44 import org.openhab.core.thing.Bridge;
45 import org.openhab.core.thing.Channel;
46 import org.openhab.core.thing.ChannelUID;
47 import org.openhab.core.thing.Thing;
48 import org.openhab.core.thing.ThingStatus;
49 import org.openhab.core.thing.ThingStatusDetail;
50 import org.openhab.core.thing.ThingUID;
51 import org.openhab.core.thing.binding.BaseBridgeHandler;
52 import org.openhab.core.thing.binding.ThingHandler;
53 import org.openhab.core.thing.binding.ThingHandlerService;
54 import org.openhab.core.types.Command;
55 import org.slf4j.Logger;
56 import org.slf4j.LoggerFactory;
57
58 /**
59  * The {@link GardenaAccountHandler} is the handler for a Gardena smart system access and connects it to the framework.
60  *
61  * @author Gerhard Riegler - Initial contribution
62  */
63 @NonNullByDefault
64 public class GardenaAccountHandler extends BaseBridgeHandler implements GardenaSmartEventListener {
65     private final Logger logger = LoggerFactory.getLogger(GardenaAccountHandler.class);
66
67     // timing constants
68     private static final Duration REINITIALIZE_DELAY_SECONDS = Duration.ofSeconds(120);
69     private static final Duration REINITIALIZE_DELAY_MINUTES_BACK_OFF = Duration.ofMinutes(15).plusSeconds(10);
70     private static final Duration REINITIALIZE_DELAY_HOURS_LIMIT_EXCEEDED = Duration.ofHours(24).plusSeconds(10);
71
72     // assets
73     private @Nullable GardenaDeviceDiscoveryService discoveryService;
74     private @Nullable GardenaSmart gardenaSmart;
75     private final HttpClientFactory httpClientFactory;
76     private final WebSocketFactory webSocketFactory;
77     private final TimeZoneProvider timeZoneProvider;
78
79     // re- initialisation stuff
80     private final Object reInitializationCodeLock = new Object();
81     private @Nullable ScheduledFuture<?> reInitializationTask;
82     private @Nullable Instant apiCallSuppressionUntil;
83
84     public GardenaAccountHandler(Bridge bridge, HttpClientFactory httpClientFactory, WebSocketFactory webSocketFactory,
85             TimeZoneProvider timeZoneProvider) {
86         super(bridge);
87         this.httpClientFactory = httpClientFactory;
88         this.webSocketFactory = webSocketFactory;
89         this.timeZoneProvider = timeZoneProvider;
90     }
91
92     /**
93      * Load the api call suppression until property.
94      */
95     private void loadApiCallSuppressionUntil() {
96         try {
97             Map<String, String> properties = getThing().getProperties();
98             apiCallSuppressionUntil = Instant
99                     .parse(properties.getOrDefault(GardenaBindingConstants.API_CALL_SUPPRESSION_UNTIL, ""));
100         } catch (DateTimeParseException e) {
101             apiCallSuppressionUntil = null;
102         }
103     }
104
105     /**
106      * Get the duration remaining until the end of the api call suppression window, or Duration.ZERO if we are outside
107      * the call suppression window.
108      *
109      * @return the duration until the end of the suppression window, or zero.
110      */
111     private Duration apiCallSuppressionDelay() {
112         Instant now = Instant.now();
113         Instant until = apiCallSuppressionUntil;
114         return (until != null) && now.isBefore(until) ? Duration.between(now, until) : Duration.ZERO;
115     }
116
117     /**
118      * Updates the time when api call suppression ends to now() plus the given delay. If delay is zero or negative, the
119      * suppression time is nulled. Saves the value as a property to ensure consistent behaviour across restarts.
120      *
121      * @param delay the delay until the end of the suppression window.
122      */
123     private void apiCallSuppressionUpdate(Duration delay) {
124         Instant until = (delay.isZero() || delay.isNegative()) ? null : Instant.now().plus(delay);
125         getThing().setProperty(GardenaBindingConstants.API_CALL_SUPPRESSION_UNTIL,
126                 until == null ? null : until.toString());
127         apiCallSuppressionUntil = until;
128     }
129
130     @Override
131     public void initialize() {
132         logger.debug("Initializing Gardena account '{}'", getThing().getUID().getId());
133         loadApiCallSuppressionUntil();
134         Duration delay = apiCallSuppressionDelay();
135         if (delay.isZero()) {
136             // do immediate initialisation
137             scheduler.submit(() -> initializeGardena());
138         } else {
139             // delay the initialisation
140             scheduleReinitialize(delay);
141             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, getUiText());
142         }
143     }
144
145     public void setDiscoveryService(GardenaDeviceDiscoveryService discoveryService) {
146         this.discoveryService = discoveryService;
147     }
148
149     /**
150      * Format a localized explanatory description regarding active call suppression.
151      *
152      * @return the localized description text, or null if call suppression is not active.
153      */
154     private @Nullable String getUiText() {
155         Instant until = apiCallSuppressionUntil;
156         if (until != null) {
157             ZoneId zone = timeZoneProvider.getTimeZone();
158             boolean isToday = LocalDate.now(zone).equals(LocalDate.ofInstant(until, zone));
159             DateTimeFormatter formatter = isToday ? DateTimeFormatter.ofLocalizedTime(FormatStyle.MEDIUM)
160                     : DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM);
161             return "@text/accounthandler.waiting-until-to-reconnect [\""
162                     + formatter.format(ZonedDateTime.ofInstant(until, zone)) + "\"]";
163         }
164         return null;
165     }
166
167     /**
168      * Initializes the GardenaSmart account.
169      * This method is called on a background thread.
170      */
171     private synchronized void initializeGardena() {
172         try {
173             GardenaConfig gardenaConfig = getThing().getConfiguration().as(GardenaConfig.class);
174             logger.debug("{}", gardenaConfig);
175
176             gardenaSmart = new GardenaSmartImpl(getThing().getUID(), gardenaConfig, this, scheduler, httpClientFactory,
177                     webSocketFactory);
178             final GardenaDeviceDiscoveryService discoveryService = this.discoveryService;
179             if (discoveryService != null) {
180                 discoveryService.startScan(null);
181                 discoveryService.waitForScanFinishing();
182             }
183             apiCallSuppressionUpdate(Duration.ZERO);
184             updateStatus(ThingStatus.ONLINE);
185         } catch (GardenaException ex) {
186             logger.warn("{}", ex.getMessage());
187             synchronized (reInitializationCodeLock) {
188                 Duration delay;
189                 int status = ex.getStatus();
190                 if (status <= 0) {
191                     delay = REINITIALIZE_DELAY_SECONDS;
192                 } else if (status == HttpStatus.TOO_MANY_REQUESTS_429) {
193                     delay = REINITIALIZE_DELAY_HOURS_LIMIT_EXCEEDED;
194                 } else {
195                     delay = apiCallSuppressionDelay().plus(REINITIALIZE_DELAY_MINUTES_BACK_OFF);
196                 }
197                 scheduleReinitialize(delay);
198                 apiCallSuppressionUpdate(delay);
199             }
200             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, getUiText());
201             disposeGardena();
202         }
203     }
204
205     /**
206      * Re-initializes the GardenaSmart account.
207      * This method is called on a background thread.
208      */
209     private synchronized void reIninitializeGardena() {
210         if (getThing().getStatus() != ThingStatus.UNINITIALIZED) {
211             initializeGardena();
212         }
213     }
214
215     /**
216      * Schedules a reinitialization, if Gardena smart system account is not reachable.
217      */
218     private void scheduleReinitialize(Duration delay) {
219         ScheduledFuture<?> reInitializationTask = this.reInitializationTask;
220         if (reInitializationTask != null) {
221             reInitializationTask.cancel(false);
222         }
223         this.reInitializationTask = scheduler.schedule(() -> reIninitializeGardena(), delay.getSeconds(),
224                 TimeUnit.SECONDS);
225     }
226
227     @Override
228     public void dispose() {
229         super.dispose();
230         synchronized (reInitializationCodeLock) {
231             ScheduledFuture<?> reInitializeTask = this.reInitializationTask;
232             if (reInitializeTask != null) {
233                 reInitializeTask.cancel(true);
234             }
235             this.reInitializationTask = null;
236         }
237         disposeGardena();
238     }
239
240     /**
241      * Disposes the GardenaSmart account.
242      */
243     private void disposeGardena() {
244         logger.debug("Disposing Gardena account '{}'", getThing().getUID().getId());
245         final GardenaDeviceDiscoveryService discoveryService = this.discoveryService;
246         if (discoveryService != null) {
247             discoveryService.stopScan();
248         }
249         final GardenaSmart gardenaSmart = this.gardenaSmart;
250         if (gardenaSmart != null) {
251             gardenaSmart.dispose();
252         }
253         this.gardenaSmart = null;
254     }
255
256     /**
257      * Returns the Gardena smart system implementation.
258      */
259     public @Nullable GardenaSmart getGardenaSmart() {
260         return gardenaSmart;
261     }
262
263     @Override
264     public Collection<Class<? extends ThingHandlerService>> getServices() {
265         return Collections.singleton(GardenaDeviceDiscoveryService.class);
266     }
267
268     @Override
269     public void handleCommand(ChannelUID channelUID, Command command) {
270         // nothing to do here because the thing has no channels
271     }
272
273     @Override
274     public void onDeviceUpdated(Device device) {
275         for (ThingUID thingUID : UidUtils.getThingUIDs(device, getThing())) {
276             final Thing gardenaThing = getThing().getThing(thingUID);
277             if (gardenaThing == null) {
278                 logger.debug("No thing exists for thingUID:{}", thingUID);
279                 continue;
280             }
281             final ThingHandler thingHandler = gardenaThing.getHandler();
282             if (!(thingHandler instanceof GardenaThingHandler)) {
283                 logger.debug("Handler for thingUID:{} is not a 'GardenaThingHandler' ({})", thingUID, thingHandler);
284                 continue;
285             }
286             final GardenaThingHandler gardenaThingHandler = (GardenaThingHandler) thingHandler;
287             try {
288                 gardenaThingHandler.updateProperties(device);
289                 for (Channel channel : gardenaThing.getChannels()) {
290                     gardenaThingHandler.updateChannel(channel.getUID());
291                 }
292                 gardenaThingHandler.updateStatus(device);
293             } catch (GardenaException ex) {
294                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, ex.getMessage());
295             } catch (AccountHandlerNotAvailableException ignore) {
296             }
297         }
298     }
299
300     @Override
301     public void onNewDevice(Device device) {
302         final GardenaDeviceDiscoveryService discoveryService = this.discoveryService;
303         if (discoveryService != null) {
304             discoveryService.deviceDiscovered(device);
305         }
306         onDeviceUpdated(device);
307     }
308
309     @Override
310     public void onError() {
311         Duration delay = REINITIALIZE_DELAY_SECONDS;
312         synchronized (reInitializationCodeLock) {
313             scheduleReinitialize(delay);
314         }
315         apiCallSuppressionUpdate(delay);
316         updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, getUiText());
317         disposeGardena();
318     }
319 }