]> git.basschouten.com Git - openhab-addons.git/commitdiff
[gardena] Improve API rate limit handling (#13016)
authorAndrew Fiddian-Green <software@whitebear.ch>
Thu, 4 Aug 2022 18:20:40 +0000 (19:20 +0100)
committerGitHub <noreply@github.com>
Thu, 4 Aug 2022 18:20:40 +0000 (20:20 +0200)
* [gardena] eliminate dangling references on dispose
* [gardena] add fixes for 429 errors
* [gardena] apply rate limiting to binding restarts
* [gardena] eliminate NPE if startup fails with exception

Signed-off-by: Andrew Fiddian-Green <software@whitebear.ch>
bundles/org.openhab.binding.gardena/README.md
bundles/org.openhab.binding.gardena/src/main/java/org/openhab/binding/gardena/internal/GardenaBindingConstants.java
bundles/org.openhab.binding.gardena/src/main/java/org/openhab/binding/gardena/internal/GardenaSmartImpl.java
bundles/org.openhab.binding.gardena/src/main/java/org/openhab/binding/gardena/internal/handler/GardenaAccountHandler.java
bundles/org.openhab.binding.gardena/src/main/java/org/openhab/binding/gardena/internal/handler/GardenaHandlerFactory.java
bundles/org.openhab.binding.gardena/src/main/java/org/openhab/binding/gardena/internal/model/dto/Device.java
bundles/org.openhab.binding.gardena/src/main/java/org/openhab/binding/gardena/internal/model/dto/api/CreateWebSocketRequest.java
bundles/org.openhab.binding.gardena/src/main/resources/OH-INF/i18n/gardena.properties

index f5933d05b3d08665eab8dd5c618f4b4b7db3f5f0..e0fdf7eaa159a77f9fbe254b393526b6b6b9524e 100644 (file)
@@ -115,6 +115,20 @@ DateTime LastUpdate "LastUpdate [%1$td.%1$tm.%1$tY %1$tH:%1$tM]" { channel="gard
 openhab:send LastUpdate REFRESH
 ```
 
+### Server Call Rate Limitation
+
+The Gardena server imposes call rate limits to prevent malicious use of its API.
+The limits are:
+
+- On average not more than one call every 15 minutes.
+- 3000 calls per month.
+
+Normally the binding does not exceed these limits.
+But from time to time the server may nevertheless consider the limits to have been exceeded, in which case it reports an HTTP 429 Error (Limit Exceeded).
+If such an error occurs you will be locked out of your Gardena account for 24 hours.
+In this case the binding will wait in an offline state for the respective 24 hours, after which it will automatically try to reconnect again.
+Attempting to force reconnect within the 24 hours causes the call rate to be exceeded further, and therefore just exacerbates the problem.
+
 ### Debugging and Tracing
 
 If you want to see what's going on in the binding, switch the loglevel to TRACE in the Karaf console
index 93ab150b2e22f8a19a2de3597ff7b1d4532714b5..0b5f3b762cb46ba068bd9e6ad8269aa088990eae 100644 (file)
@@ -37,4 +37,6 @@ public class GardenaBindingConstants {
     public static final String DEVICE_TYPE_WATER_CONTROL = "water_control";
     public static final String DEVICE_TYPE_SENSOR = "sensor";
     public static final String DEVICE_TYPE_POWER = "power";
+
+    public static final String API_CALL_SUPPRESSION_UNTIL = "apiCallSuppressionUntil";
 }
index 2425df59508b29a2d8b54848da9ed87b1b2aa80e..d0340f5e06e8d2fb90a81d7e5dc86562ebc92705 100644 (file)
  */
 package org.openhab.binding.gardena.internal;
 
+import java.util.ArrayList;
 import java.util.Collection;
 import java.util.HashMap;
 import java.util.Iterator;
+import java.util.List;
 import java.util.Map;
 import java.util.Set;
 import java.util.concurrent.ConcurrentHashMap;
@@ -79,23 +81,25 @@ public class GardenaSmartImpl implements GardenaSmart, GardenaSmartWebSocketList
     private static final String URL_API_LOCATIONS = URL_API_GARDENA + "/locations";
     private static final String URL_API_COMMAND = URL_API_GARDENA + "/command";
 
-    private String id;
-    private GardenaConfig config;
-    private ScheduledExecutorService scheduler;
+    private final String id;
+    private final GardenaConfig config;
+    private final ScheduledExecutorService scheduler;
 
-    private Map<String, Device> allDevicesById = new HashMap<>();
-    private LocationsResponse locationsResponse;
-    private GardenaSmartEventListener eventListener;
+    private final Map<String, Device> allDevicesById = new HashMap<>();
+    private @Nullable LocationsResponse locationsResponse = null;
+    private final GardenaSmartEventListener eventListener;
 
-    private HttpClient httpClient;
-    private Map<String, GardenaSmartWebSocket> webSockets = new HashMap<>();
+    private final HttpClient httpClient;
+    private final Map<String, GardenaSmartWebSocket> webSockets = new HashMap<>();
     private @Nullable PostOAuth2Response token;
     private boolean initialized = false;
-    private WebSocketClient webSocketClient;
+    private final WebSocketClient webSocketClient;
 
-    private Set<Device> devicesToNotify = ConcurrentHashMap.newKeySet();
-    private @Nullable ScheduledFuture<?> deviceToNotifyFuture;
-    private @Nullable ScheduledFuture<?> newDeviceFuture;
+    private final Set<Device> devicesToNotify = ConcurrentHashMap.newKeySet();
+    private final Object deviceUpdateTaskLock = new Object();
+    private @Nullable ScheduledFuture<?> deviceUpdateTask;
+    private final Object newDeviceTasksLock = new Object();
+    private final List<ScheduledFuture<?>> newDeviceTasks = new ArrayList<>();
 
     public GardenaSmartImpl(String id, GardenaConfig config, GardenaSmartEventListener eventListener,
             ScheduledExecutorService scheduler, HttpClientFactory httpClientFactory, WebSocketFactory webSocketFactory)
@@ -121,14 +125,17 @@ public class GardenaSmartImpl implements GardenaSmart, GardenaSmartWebSocketList
 
             // initially load access token
             verifyToken();
-            locationsResponse = loadLocations();
+            LocationsResponse locationsResponse = loadLocations();
+            this.locationsResponse = locationsResponse;
 
             // assemble devices
-            for (LocationDataItem location : locationsResponse.data) {
-                LocationResponse locationResponse = loadLocation(location.id);
-                if (locationResponse.included != null) {
-                    for (DataItem<?> dataItem : locationResponse.included) {
-                        handleDataItem(dataItem);
+            if (locationsResponse.data != null) {
+                for (LocationDataItem location : locationsResponse.data) {
+                    LocationResponse locationResponse = loadLocation(location.id);
+                    if (locationResponse.included != null) {
+                        for (DataItem<?> dataItem : locationResponse.included) {
+                            handleDataItem(dataItem);
+                        }
                     }
                 }
             }
@@ -153,16 +160,19 @@ public class GardenaSmartImpl implements GardenaSmart, GardenaSmartWebSocketList
      * Starts the websockets for each location.
      */
     private void startWebsockets() throws Exception {
-        for (LocationDataItem location : locationsResponse.data) {
-            WebSocketCreatedResponse webSocketCreatedResponse = getWebsocketInfo(location.id);
-            Location locationAttributes = location.attributes;
-            WebSocket webSocketAttributes = webSocketCreatedResponse.data.attributes;
-            if (locationAttributes == null || webSocketAttributes == null) {
-                continue;
+        LocationsResponse locationsResponse = this.locationsResponse;
+        if (locationsResponse != null) {
+            for (LocationDataItem location : locationsResponse.data) {
+                WebSocketCreatedResponse webSocketCreatedResponse = getWebsocketInfo(location.id);
+                Location locationAttributes = location.attributes;
+                WebSocket webSocketAttributes = webSocketCreatedResponse.data.attributes;
+                if (locationAttributes == null || webSocketAttributes == null) {
+                    continue;
+                }
+                String socketId = id + "-" + locationAttributes.name;
+                webSockets.put(location.id, new GardenaSmartWebSocket(this, webSocketClient, scheduler,
+                        webSocketAttributes.url, token, socketId, location.id));
             }
-            String socketId = id + "-" + locationAttributes.name;
-            webSockets.put(location.id, new GardenaSmartWebSocket(this, webSocketClient, scheduler,
-                    webSocketAttributes.url, token, socketId, location.id));
         }
     }
 
@@ -299,15 +309,22 @@ public class GardenaSmartImpl implements GardenaSmart, GardenaSmartWebSocketList
     @Override
     public void dispose() {
         logger.debug("Disposing GardenaSmart");
-
-        final ScheduledFuture<?> newDeviceFuture = this.newDeviceFuture;
-        if (newDeviceFuture != null) {
-            newDeviceFuture.cancel(true);
+        initialized = false;
+        synchronized (newDeviceTasksLock) {
+            for (ScheduledFuture<?> task : newDeviceTasks) {
+                if (!task.isDone()) {
+                    task.cancel(true);
+                }
+            }
+            newDeviceTasks.clear();
         }
-
-        final ScheduledFuture<?> deviceToNotifyFuture = this.deviceToNotifyFuture;
-        if (deviceToNotifyFuture != null) {
-            deviceToNotifyFuture.cancel(true);
+        synchronized (deviceUpdateTaskLock) {
+            devicesToNotify.clear();
+            ScheduledFuture<?> task = deviceUpdateTask;
+            if (task != null) {
+                task.cancel(true);
+            }
+            deviceUpdateTask = null;
         }
         stopWebsockets();
         try {
@@ -318,9 +335,8 @@ public class GardenaSmartImpl implements GardenaSmart, GardenaSmartWebSocketList
         }
         httpClient.destroy();
         webSocketClient.destroy();
-        locationsResponse = new LocationsResponse();
         allDevicesById.clear();
-        initialized = false;
+        locationsResponse = null;
     }
 
     /**
@@ -353,16 +369,21 @@ public class GardenaSmartImpl implements GardenaSmart, GardenaSmartWebSocketList
             device = new Device(deviceId);
             allDevicesById.put(device.id, device);
 
-            if (initialized) {
-                newDeviceFuture = scheduler.schedule(() -> {
-                    Device newDevice = allDevicesById.get(deviceId);
-                    if (newDevice != null) {
-                        newDevice.evaluateDeviceType();
-                        if (newDevice.deviceType != null) {
-                            eventListener.onNewDevice(newDevice);
+            synchronized (newDeviceTasksLock) {
+                // remove prior completed tasks from the list
+                newDeviceTasks.removeIf(task -> task.isDone());
+                // add a new scheduled task to the list
+                newDeviceTasks.add(scheduler.schedule(() -> {
+                    if (initialized) {
+                        Device newDevice = allDevicesById.get(deviceId);
+                        if (newDevice != null) {
+                            newDevice.evaluateDeviceType();
+                            if (newDevice.deviceType != null) {
+                                eventListener.onNewDevice(newDevice);
+                            }
                         }
                     }
-                }, 3, TimeUnit.SECONDS);
+                }, 3, TimeUnit.SECONDS));
             }
         }
 
@@ -417,18 +438,14 @@ public class GardenaSmartImpl implements GardenaSmart, GardenaSmartWebSocketList
                 handleDataItem(dataItem);
                 Device device = allDevicesById.get(dataItem.getDeviceId());
                 if (device != null && device.active) {
-                    devicesToNotify.add(device);
-
-                    // delay the deviceUpdated event to filter multiple events for the same device dataItem property
-                    if (deviceToNotifyFuture == null) {
-                        deviceToNotifyFuture = scheduler.schedule(() -> {
-                            deviceToNotifyFuture = null;
-                            Iterator<Device> notifyIterator = devicesToNotify.iterator();
-                            while (notifyIterator.hasNext()) {
-                                eventListener.onDeviceUpdated(notifyIterator.next());
-                                notifyIterator.remove();
-                            }
-                        }, 1, TimeUnit.SECONDS);
+                    synchronized (deviceUpdateTaskLock) {
+                        devicesToNotify.add(device);
+
+                        // delay the deviceUpdated event to filter multiple events for the same device dataItem property
+                        ScheduledFuture<?> task = this.deviceUpdateTask;
+                        if (task == null || task.isDone()) {
+                            deviceUpdateTask = scheduler.schedule(() -> notifyDevicesUpdated(), 1, TimeUnit.SECONDS);
+                        }
                     }
                 }
             }
@@ -437,6 +454,21 @@ public class GardenaSmartImpl implements GardenaSmart, GardenaSmartWebSocketList
         }
     }
 
+    /**
+     * Helper scheduler task to update devices
+     */
+    private void notifyDevicesUpdated() {
+        synchronized (deviceUpdateTaskLock) {
+            if (initialized) {
+                Iterator<Device> notifyIterator = devicesToNotify.iterator();
+                while (notifyIterator.hasNext()) {
+                    eventListener.onDeviceUpdated(notifyIterator.next());
+                    notifyIterator.remove();
+                }
+            }
+        }
+    }
+
     @Override
     public Device getDevice(String deviceId) throws GardenaDeviceNotFoundException {
         Device device = allDevicesById.get(deviceId);
index 3a35ef9532232879320f12162e3fd06154c3cc8b..61e9ae429f65a4a955f251892f74c2f4c09e8f33 100644 (file)
  */
 package org.openhab.binding.gardena.internal.handler;
 
+import java.time.Duration;
+import java.time.Instant;
+import java.time.LocalDate;
+import java.time.ZoneId;
+import java.time.ZonedDateTime;
+import java.time.format.DateTimeFormatter;
+import java.time.format.DateTimeParseException;
+import java.time.format.FormatStyle;
 import java.util.Collection;
 import java.util.Collections;
+import java.util.Map;
+import java.util.concurrent.ScheduledFuture;
 import java.util.concurrent.TimeUnit;
 
 import org.eclipse.jdt.annotation.NonNullByDefault;
 import org.eclipse.jdt.annotation.Nullable;
+import org.eclipse.jetty.http.HttpStatus;
+import org.openhab.binding.gardena.internal.GardenaBindingConstants;
 import org.openhab.binding.gardena.internal.GardenaSmart;
 import org.openhab.binding.gardena.internal.GardenaSmartEventListener;
 import org.openhab.binding.gardena.internal.GardenaSmartImpl;
@@ -26,6 +38,7 @@ import org.openhab.binding.gardena.internal.discovery.GardenaDeviceDiscoveryServ
 import org.openhab.binding.gardena.internal.exception.GardenaException;
 import org.openhab.binding.gardena.internal.model.dto.Device;
 import org.openhab.binding.gardena.internal.util.UidUtils;
+import org.openhab.core.i18n.TimeZoneProvider;
 import org.openhab.core.io.net.http.HttpClientFactory;
 import org.openhab.core.io.net.http.WebSocketFactory;
 import org.openhab.core.thing.Bridge;
@@ -39,7 +52,6 @@ import org.openhab.core.thing.binding.BaseBridgeHandler;
 import org.openhab.core.thing.binding.ThingHandler;
 import org.openhab.core.thing.binding.ThingHandlerService;
 import org.openhab.core.types.Command;
-import org.openhab.core.types.RefreshType;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -51,80 +63,178 @@ import org.slf4j.LoggerFactory;
 @NonNullByDefault
 public class GardenaAccountHandler extends BaseBridgeHandler implements GardenaSmartEventListener {
     private final Logger logger = LoggerFactory.getLogger(GardenaAccountHandler.class);
-    private static final long REINITIALIZE_DELAY_SECONDS = 120;
-    private static final long REINITIALIZE_DELAY_HOURS_LIMIT_EXCEEDED = 24;
 
-    private @Nullable GardenaDeviceDiscoveryService discoveryService;
+    // timing constants
+    private static final Duration REINITIALIZE_DELAY_SECONDS = Duration.ofSeconds(120);
+    private static final Duration REINITIALIZE_DELAY_MINUTES_BACK_OFF = Duration.ofMinutes(15).plusSeconds(10);
+    private static final Duration REINITIALIZE_DELAY_HOURS_LIMIT_EXCEEDED = Duration.ofHours(24).plusSeconds(10);
 
+    // assets
+    private @Nullable GardenaDeviceDiscoveryService discoveryService;
     private @Nullable GardenaSmart gardenaSmart;
-    private HttpClientFactory httpClientFactory;
-    private WebSocketFactory webSocketFactory;
+    private final HttpClientFactory httpClientFactory;
+    private final WebSocketFactory webSocketFactory;
+    private final TimeZoneProvider timeZoneProvider;
+
+    // re- initialisation stuff
+    private final Object reInitializationCodeLock = new Object();
+    private @Nullable ScheduledFuture<?> reInitializationTask;
+    private @Nullable Instant apiCallSuppressionUntil;
 
-    public GardenaAccountHandler(Bridge bridge, HttpClientFactory httpClientFactory,
-            WebSocketFactory webSocketFactory) {
+    public GardenaAccountHandler(Bridge bridge, HttpClientFactory httpClientFactory, WebSocketFactory webSocketFactory,
+            TimeZoneProvider timeZoneProvider) {
         super(bridge);
         this.httpClientFactory = httpClientFactory;
         this.webSocketFactory = webSocketFactory;
+        this.timeZoneProvider = timeZoneProvider;
+    }
+
+    /**
+     * Load the api call suppression until property.
+     */
+    private void loadApiCallSuppressionUntil() {
+        try {
+            Map<String, String> properties = getThing().getProperties();
+            apiCallSuppressionUntil = Instant
+                    .parse(properties.getOrDefault(GardenaBindingConstants.API_CALL_SUPPRESSION_UNTIL, ""));
+        } catch (DateTimeParseException e) {
+            apiCallSuppressionUntil = null;
+        }
+    }
+
+    /**
+     * Get the duration remaining until the end of the api call suppression window, or Duration.ZERO if we are outside
+     * the call suppression window.
+     *
+     * @return the duration until the end of the suppression window, or zero.
+     */
+    private Duration apiCallSuppressionDelay() {
+        Instant now = Instant.now();
+        Instant until = apiCallSuppressionUntil;
+        return (until != null) && now.isBefore(until) ? Duration.between(now, until) : Duration.ZERO;
+    }
+
+    /**
+     * Updates the time when api call suppression ends to now() plus the given delay. If delay is zero or negative, the
+     * suppression time is nulled. Saves the value as a property to ensure consistent behaviour across restarts.
+     *
+     * @param delay the delay until the end of the suppression window.
+     */
+    private void apiCallSuppressionUpdate(Duration delay) {
+        Instant until = (delay.isZero() || delay.isNegative()) ? null : Instant.now().plus(delay);
+        getThing().setProperty(GardenaBindingConstants.API_CALL_SUPPRESSION_UNTIL,
+                until == null ? null : until.toString());
+        apiCallSuppressionUntil = until;
     }
 
     @Override
     public void initialize() {
         logger.debug("Initializing Gardena account '{}'", getThing().getUID().getId());
-        initializeGardena();
+        loadApiCallSuppressionUntil();
+        Duration delay = apiCallSuppressionDelay();
+        if (delay.isZero()) {
+            // do immediate initialisation
+            scheduler.submit(() -> initializeGardena());
+        } else {
+            // delay the initialisation
+            scheduleReinitialize(delay);
+            updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, getUiText());
+        }
     }
 
     public void setDiscoveryService(GardenaDeviceDiscoveryService discoveryService) {
         this.discoveryService = discoveryService;
     }
 
+    /**
+     * Format a localized explanatory description regarding active call suppression.
+     *
+     * @return the localized description text, or null if call suppression is not active.
+     */
+    private @Nullable String getUiText() {
+        Instant until = apiCallSuppressionUntil;
+        if (until != null) {
+            ZoneId zone = timeZoneProvider.getTimeZone();
+            boolean isToday = LocalDate.now(zone).equals(LocalDate.ofInstant(until, zone));
+            DateTimeFormatter formatter = isToday ? DateTimeFormatter.ofLocalizedTime(FormatStyle.MEDIUM)
+                    : DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM);
+            return "@text/accounthandler.waiting-until-to-reconnect [\""
+                    + formatter.format(ZonedDateTime.ofInstant(until, zone)) + "\"]";
+        }
+        return null;
+    }
+
     /**
      * Initializes the GardenaSmart account.
+     * This method is called on a background thread.
      */
-    private void initializeGardena() {
-        final GardenaAccountHandler instance = this;
-        scheduler.execute(() -> {
-            try {
-                GardenaConfig gardenaConfig = getThing().getConfiguration().as(GardenaConfig.class);
-                logger.debug("{}", gardenaConfig);
-
-                String id = getThing().getUID().getId();
-                gardenaSmart = new GardenaSmartImpl(id, gardenaConfig, instance, scheduler, httpClientFactory,
-                        webSocketFactory);
-                final GardenaDeviceDiscoveryService discoveryService = this.discoveryService;
-                if (discoveryService != null) {
-                    discoveryService.startScan(null);
-                    discoveryService.waitForScanFinishing();
-                }
-                updateStatus(ThingStatus.ONLINE);
-            } catch (GardenaException ex) {
-                updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, ex.getMessage());
-                disposeGardena();
-                if (ex.getStatus() == 429) {
-                    // if there was an error 429 (Too Many Requests), wait for 24 hours before trying again
-                    scheduleReinitialize(REINITIALIZE_DELAY_HOURS_LIMIT_EXCEEDED, TimeUnit.HOURS);
+    private synchronized void initializeGardena() {
+        try {
+            GardenaConfig gardenaConfig = getThing().getConfiguration().as(GardenaConfig.class);
+            logger.debug("{}", gardenaConfig);
+
+            String id = getThing().getUID().getId();
+            gardenaSmart = new GardenaSmartImpl(id, gardenaConfig, this, scheduler, httpClientFactory,
+                    webSocketFactory);
+            final GardenaDeviceDiscoveryService discoveryService = this.discoveryService;
+            if (discoveryService != null) {
+                discoveryService.startScan(null);
+                discoveryService.waitForScanFinishing();
+            }
+            apiCallSuppressionUpdate(Duration.ZERO);
+            updateStatus(ThingStatus.ONLINE);
+        } catch (GardenaException ex) {
+            logger.warn("{}", ex.getMessage());
+            synchronized (reInitializationCodeLock) {
+                Duration delay;
+                int status = ex.getStatus();
+                if (status <= 0) {
+                    delay = REINITIALIZE_DELAY_SECONDS;
+                } else if (status == HttpStatus.TOO_MANY_REQUESTS_429) {
+                    delay = REINITIALIZE_DELAY_HOURS_LIMIT_EXCEEDED;
                 } else {
-                    // otherwise reinitialize after 120 seconds
-                    scheduleReinitialize(REINITIALIZE_DELAY_SECONDS, TimeUnit.SECONDS);
+                    delay = apiCallSuppressionDelay().plus(REINITIALIZE_DELAY_MINUTES_BACK_OFF);
                 }
-                logger.warn("{}", ex.getMessage());
+                scheduleReinitialize(delay);
+                apiCallSuppressionUpdate(delay);
             }
-        });
+            updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, getUiText());
+            disposeGardena();
+        }
+    }
+
+    /**
+     * Re-initializes the GardenaSmart account.
+     * This method is called on a background thread.
+     */
+    private synchronized void reIninitializeGardena() {
+        if (getThing().getStatus() != ThingStatus.UNINITIALIZED) {
+            initializeGardena();
+        }
     }
 
     /**
      * Schedules a reinitialization, if Gardena smart system account is not reachable.
      */
-    private void scheduleReinitialize(long delay, TimeUnit unit) {
-        scheduler.schedule(() -> {
-            if (getThing().getStatus() != ThingStatus.UNINITIALIZED) {
-                initializeGardena();
-            }
-        }, delay, unit);
+    private void scheduleReinitialize(Duration delay) {
+        ScheduledFuture<?> reInitializationTask = this.reInitializationTask;
+        if (reInitializationTask != null) {
+            reInitializationTask.cancel(false);
+        }
+        this.reInitializationTask = scheduler.schedule(() -> reIninitializeGardena(), delay.getSeconds(),
+                TimeUnit.SECONDS);
     }
 
     @Override
     public void dispose() {
         super.dispose();
+        synchronized (reInitializationCodeLock) {
+            ScheduledFuture<?> reInitializeTask = this.reInitializationTask;
+            if (reInitializeTask != null) {
+                reInitializeTask.cancel(true);
+            }
+            this.reInitializationTask = null;
+        }
         disposeGardena();
     }
 
@@ -141,6 +251,7 @@ public class GardenaAccountHandler extends BaseBridgeHandler implements GardenaS
         if (gardenaSmart != null) {
             gardenaSmart.dispose();
         }
+        this.gardenaSmart = null;
     }
 
     /**
@@ -157,11 +268,7 @@ public class GardenaAccountHandler extends BaseBridgeHandler implements GardenaS
 
     @Override
     public void handleCommand(ChannelUID channelUID, Command command) {
-        if (RefreshType.REFRESH == command) {
-            logger.debug("Refreshing Gardena account '{}'", getThing().getUID().getId());
-            disposeGardena();
-            initializeGardena();
-        }
+        // nothing to do here because the thing has no channels
     }
 
     @Override
@@ -202,8 +309,12 @@ public class GardenaAccountHandler extends BaseBridgeHandler implements GardenaS
 
     @Override
     public void onError() {
-        updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Connection lost");
+        Duration delay = REINITIALIZE_DELAY_SECONDS;
+        synchronized (reInitializationCodeLock) {
+            scheduleReinitialize(delay);
+        }
+        apiCallSuppressionUpdate(delay);
+        updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, getUiText());
         disposeGardena();
-        scheduleReinitialize(REINITIALIZE_DELAY_SECONDS, TimeUnit.SECONDS);
     }
 }
index b758b4692c34e72fb8ae6dc24fbfc9e01df3742c..25e4f54773d36b2c2d159945214ded53f4ed471d 100644 (file)
@@ -12,8 +12,7 @@
  */
 package org.openhab.binding.gardena.internal.handler;
 
-import static org.openhab.binding.gardena.internal.GardenaBindingConstants.BINDING_ID;
-import static org.openhab.binding.gardena.internal.GardenaBindingConstants.THING_TYPE_ACCOUNT;
+import static org.openhab.binding.gardena.internal.GardenaBindingConstants.*;
 
 import org.eclipse.jdt.annotation.NonNullByDefault;
 import org.eclipse.jdt.annotation.Nullable;
@@ -58,7 +57,7 @@ public class GardenaHandlerFactory extends BaseThingHandlerFactory {
     @Override
     protected @Nullable ThingHandler createHandler(Thing thing) {
         if (THING_TYPE_ACCOUNT.equals(thing.getThingTypeUID())) {
-            return new GardenaAccountHandler((Bridge) thing, httpClientFactory, webSocketFactory);
+            return new GardenaAccountHandler((Bridge) thing, httpClientFactory, webSocketFactory, timeZoneProvider);
         } else {
             return new GardenaThingHandler(thing, timeZoneProvider);
         }
index b9801ed6dafb38e658f29813727762cae9587b9a..381ba73d543a2a350676ca303b7d2ad4f3281666 100644 (file)
@@ -152,8 +152,12 @@ public class Device {
             throw new GardenaException("Unknown dataItem with id: " + dataItem.id);
         }
 
-        if (common != null && common.attributes != null) {
-            common.attributes.lastUpdate.timestamp = new Date();
+        if (common != null) {
+            CommonService attributes = common.attributes;
+            if (attributes != null) {
+                attributes.lastUpdate.timestamp = new Date();
+            }
+            common.attributes = attributes;
         }
     }
 
index 5c0e796252a32637349192da077a7040ea13e6de..ed68304714a78b87792696d5d4640ff425b31616 100644 (file)
@@ -28,7 +28,8 @@ public class CreateWebSocketRequest {
         data = new CreateWebSocketDataItem();
         data.id = "wsreq-" + locationId;
         data.type = "WEBSOCKET";
-        data.attributes = new CreateWebSocket();
-        data.attributes.locationId = locationId;
+        CreateWebSocket attributes = new CreateWebSocket();
+        attributes.locationId = locationId;
+        data.attributes = attributes;
     }
 }
index 46b3b3b838b1eee4e557e932d6f0273d61882b77..2d950508618da21d15366ca8480324d576071a1e 100644 (file)
@@ -260,3 +260,7 @@ channel-type.gardena.timestampRefresh.label = Timestamp
 channel-type.gardena.timestampRefresh.description = Timestamp
 channel-type.gardena.valveCommandDuration.label = Command Duration
 channel-type.gardena.valveCommandDuration.description = A duration in minutes for a command
+
+# other messages
+
+accounthandler.waiting-until-to-reconnect = Waiting until {0} to make automatic reconnection attempt