]> git.basschouten.com Git - openhab-addons.git/blob
5b80ec1c1c5dab2cff629402e79a7628f562b7ea
[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             String id = getThing().getUID().getId();
177             gardenaSmart = new GardenaSmartImpl(id, gardenaConfig, this, scheduler, httpClientFactory,
178                     webSocketFactory);
179             final GardenaDeviceDiscoveryService discoveryService = this.discoveryService;
180             if (discoveryService != null) {
181                 discoveryService.startScan(null);
182                 discoveryService.waitForScanFinishing();
183             }
184             apiCallSuppressionUpdate(Duration.ZERO);
185             updateStatus(ThingStatus.ONLINE);
186         } catch (GardenaException ex) {
187             logger.warn("{}", ex.getMessage());
188             synchronized (reInitializationCodeLock) {
189                 Duration delay;
190                 int status = ex.getStatus();
191                 if (status <= 0) {
192                     delay = REINITIALIZE_DELAY_SECONDS;
193                 } else if (status == HttpStatus.TOO_MANY_REQUESTS_429) {
194                     delay = REINITIALIZE_DELAY_HOURS_LIMIT_EXCEEDED;
195                 } else {
196                     delay = apiCallSuppressionDelay().plus(REINITIALIZE_DELAY_MINUTES_BACK_OFF);
197                 }
198                 scheduleReinitialize(delay);
199                 apiCallSuppressionUpdate(delay);
200             }
201             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, getUiText());
202             disposeGardena();
203         }
204     }
205
206     /**
207      * Re-initializes the GardenaSmart account.
208      * This method is called on a background thread.
209      */
210     private synchronized void reIninitializeGardena() {
211         if (getThing().getStatus() != ThingStatus.UNINITIALIZED) {
212             initializeGardena();
213         }
214     }
215
216     /**
217      * Schedules a reinitialization, if Gardena smart system account is not reachable.
218      */
219     private void scheduleReinitialize(Duration delay) {
220         ScheduledFuture<?> reInitializationTask = this.reInitializationTask;
221         if (reInitializationTask != null) {
222             reInitializationTask.cancel(false);
223         }
224         this.reInitializationTask = scheduler.schedule(() -> reIninitializeGardena(), delay.getSeconds(),
225                 TimeUnit.SECONDS);
226     }
227
228     @Override
229     public void dispose() {
230         super.dispose();
231         synchronized (reInitializationCodeLock) {
232             ScheduledFuture<?> reInitializeTask = this.reInitializationTask;
233             if (reInitializeTask != null) {
234                 reInitializeTask.cancel(true);
235             }
236             this.reInitializationTask = null;
237         }
238         disposeGardena();
239     }
240
241     /**
242      * Disposes the GardenaSmart account.
243      */
244     private void disposeGardena() {
245         logger.debug("Disposing Gardena account '{}'", getThing().getUID().getId());
246         final GardenaDeviceDiscoveryService discoveryService = this.discoveryService;
247         if (discoveryService != null) {
248             discoveryService.stopScan();
249         }
250         final GardenaSmart gardenaSmart = this.gardenaSmart;
251         if (gardenaSmart != null) {
252             gardenaSmart.dispose();
253         }
254         this.gardenaSmart = null;
255     }
256
257     /**
258      * Returns the Gardena smart system implementation.
259      */
260     public @Nullable GardenaSmart getGardenaSmart() {
261         return gardenaSmart;
262     }
263
264     @Override
265     public Collection<Class<? extends ThingHandlerService>> getServices() {
266         return Collections.singleton(GardenaDeviceDiscoveryService.class);
267     }
268
269     @Override
270     public void handleCommand(ChannelUID channelUID, Command command) {
271         // nothing to do here because the thing has no channels
272     }
273
274     @Override
275     public void onDeviceUpdated(Device device) {
276         for (ThingUID thingUID : UidUtils.getThingUIDs(device, getThing())) {
277             final Thing gardenaThing = getThing().getThing(thingUID);
278             if (gardenaThing == null) {
279                 logger.debug("No thing exists for thingUID:{}", thingUID);
280                 continue;
281             }
282             final ThingHandler thingHandler = gardenaThing.getHandler();
283             if (!(thingHandler instanceof GardenaThingHandler)) {
284                 logger.debug("Handler for thingUID:{} is not a 'GardenaThingHandler' ({})", thingUID, thingHandler);
285                 continue;
286             }
287             final GardenaThingHandler gardenaThingHandler = (GardenaThingHandler) thingHandler;
288             try {
289                 gardenaThingHandler.updateProperties(device);
290                 for (Channel channel : gardenaThing.getChannels()) {
291                     gardenaThingHandler.updateChannel(channel.getUID());
292                 }
293                 gardenaThingHandler.updateStatus(device);
294             } catch (GardenaException ex) {
295                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, ex.getMessage());
296             } catch (AccountHandlerNotAvailableException ignore) {
297             }
298         }
299     }
300
301     @Override
302     public void onNewDevice(Device device) {
303         final GardenaDeviceDiscoveryService discoveryService = this.discoveryService;
304         if (discoveryService != null) {
305             discoveryService.deviceDiscovered(device);
306         }
307         onDeviceUpdated(device);
308     }
309
310     @Override
311     public void onError() {
312         Duration delay = REINITIALIZE_DELAY_SECONDS;
313         synchronized (reInitializationCodeLock) {
314             scheduleReinitialize(delay);
315         }
316         apiCallSuppressionUpdate(delay);
317         updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, getUiText());
318         disposeGardena();
319     }
320 }