2 * Copyright (c) 2010-2023 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.gardena.internal.handler;
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;
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.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;
59 * The {@link GardenaAccountHandler} is the handler for a Gardena smart system access and connects it to the framework.
61 * @author Gerhard Riegler - Initial contribution
64 public class GardenaAccountHandler extends BaseBridgeHandler implements GardenaSmartEventListener {
65 private final Logger logger = LoggerFactory.getLogger(GardenaAccountHandler.class);
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);
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;
79 // re- initialisation stuff
80 private final Object reInitializationCodeLock = new Object();
81 private @Nullable ScheduledFuture<?> reInitializationTask;
82 private @Nullable Instant apiCallSuppressionUntil;
84 public GardenaAccountHandler(Bridge bridge, HttpClientFactory httpClientFactory, WebSocketFactory webSocketFactory,
85 TimeZoneProvider timeZoneProvider) {
87 this.httpClientFactory = httpClientFactory;
88 this.webSocketFactory = webSocketFactory;
89 this.timeZoneProvider = timeZoneProvider;
93 * Load the api call suppression until property.
95 private void loadApiCallSuppressionUntil() {
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;
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.
109 * @return the duration until the end of the suppression window, or zero.
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;
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.
121 * @param delay the delay until the end of the suppression window.
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;
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());
139 // delay the initialisation
140 scheduleReinitialize(delay);
141 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, getUiText());
145 public void setDiscoveryService(GardenaDeviceDiscoveryService discoveryService) {
146 this.discoveryService = discoveryService;
150 * Format a localized explanatory description regarding active call suppression.
152 * @return the localized description text, or null if call suppression is not active.
154 private @Nullable String getUiText() {
155 Instant until = apiCallSuppressionUntil;
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)) + "\"]";
168 * Initializes the GardenaSmart account.
169 * This method is called on a background thread.
171 private synchronized void initializeGardena() {
173 GardenaConfig gardenaConfig = getThing().getConfiguration().as(GardenaConfig.class);
174 logger.debug("{}", gardenaConfig);
176 gardenaSmart = new GardenaSmartImpl(getThing().getUID(), gardenaConfig, this, scheduler, httpClientFactory,
178 final GardenaDeviceDiscoveryService discoveryService = this.discoveryService;
179 if (discoveryService != null) {
180 discoveryService.startScan(null);
181 discoveryService.waitForScanFinishing();
183 apiCallSuppressionUpdate(Duration.ZERO);
184 updateStatus(ThingStatus.ONLINE);
185 } catch (GardenaException ex) {
186 logger.warn("{}", ex.getMessage());
187 synchronized (reInitializationCodeLock) {
189 int status = ex.getStatus();
191 delay = REINITIALIZE_DELAY_SECONDS;
192 } else if (status == HttpStatus.TOO_MANY_REQUESTS_429) {
193 delay = REINITIALIZE_DELAY_HOURS_LIMIT_EXCEEDED;
195 delay = apiCallSuppressionDelay().plus(REINITIALIZE_DELAY_MINUTES_BACK_OFF);
197 scheduleReinitialize(delay);
198 apiCallSuppressionUpdate(delay);
200 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, getUiText());
206 * Re-initializes the GardenaSmart account.
207 * This method is called on a background thread.
209 private synchronized void reIninitializeGardena() {
210 if (getThing().getStatus() != ThingStatus.UNINITIALIZED) {
216 * Schedules a reinitialization, if Gardena smart system account is not reachable.
218 private void scheduleReinitialize(Duration delay) {
219 ScheduledFuture<?> reInitializationTask = this.reInitializationTask;
220 if (reInitializationTask != null) {
221 reInitializationTask.cancel(false);
223 this.reInitializationTask = scheduler.schedule(() -> reIninitializeGardena(), delay.getSeconds(),
228 public void dispose() {
230 synchronized (reInitializationCodeLock) {
231 ScheduledFuture<?> reInitializeTask = this.reInitializationTask;
232 if (reInitializeTask != null) {
233 reInitializeTask.cancel(true);
235 this.reInitializationTask = null;
241 * Disposes the GardenaSmart account.
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();
249 final GardenaSmart gardenaSmart = this.gardenaSmart;
250 if (gardenaSmart != null) {
251 gardenaSmart.dispose();
253 this.gardenaSmart = null;
257 * Returns the Gardena smart system implementation.
259 public @Nullable GardenaSmart getGardenaSmart() {
264 public Collection<Class<? extends ThingHandlerService>> getServices() {
265 return Set.of(GardenaDeviceDiscoveryService.class);
269 public void handleCommand(ChannelUID channelUID, Command command) {
270 // nothing to do here because the thing has no channels
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);
281 final ThingHandler thingHandler = gardenaThing.getHandler();
282 if (!(thingHandler instanceof GardenaThingHandler gardenaThingHandler)) {
283 logger.debug("Handler for thingUID:{} is not a 'GardenaThingHandler' ({})", thingUID, thingHandler);
287 gardenaThingHandler.updateProperties(device);
288 for (Channel channel : gardenaThing.getChannels()) {
289 gardenaThingHandler.updateChannel(channel.getUID());
291 gardenaThingHandler.updateStatus(device);
292 } catch (GardenaException ex) {
293 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, ex.getMessage());
294 } catch (AccountHandlerNotAvailableException ignore) {
300 public void onNewDevice(Device device) {
301 final GardenaDeviceDiscoveryService discoveryService = this.discoveryService;
302 if (discoveryService != null) {
303 discoveryService.deviceDiscovered(device);
305 onDeviceUpdated(device);
309 public void onError() {
310 Duration delay = REINITIALIZE_DELAY_SECONDS;
311 synchronized (reInitializationCodeLock) {
312 scheduleReinitialize(delay);
314 apiCallSuppressionUpdate(delay);
315 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, getUiText());