diff --git a/bundles/org.openhab.binding.gardena/README.md b/bundles/org.openhab.binding.gardena/README.md index f5933d05b3d..e0fdf7eaa15 100644 --- a/bundles/org.openhab.binding.gardena/README.md +++ b/bundles/org.openhab.binding.gardena/README.md @@ -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 diff --git a/bundles/org.openhab.binding.gardena/src/main/java/org/openhab/binding/gardena/internal/GardenaBindingConstants.java b/bundles/org.openhab.binding.gardena/src/main/java/org/openhab/binding/gardena/internal/GardenaBindingConstants.java index 93ab150b2e2..0b5f3b762cb 100644 --- a/bundles/org.openhab.binding.gardena/src/main/java/org/openhab/binding/gardena/internal/GardenaBindingConstants.java +++ b/bundles/org.openhab.binding.gardena/src/main/java/org/openhab/binding/gardena/internal/GardenaBindingConstants.java @@ -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"; } diff --git a/bundles/org.openhab.binding.gardena/src/main/java/org/openhab/binding/gardena/internal/GardenaSmartImpl.java b/bundles/org.openhab.binding.gardena/src/main/java/org/openhab/binding/gardena/internal/GardenaSmartImpl.java index 2425df59508..d0340f5e06e 100644 --- a/bundles/org.openhab.binding.gardena/src/main/java/org/openhab/binding/gardena/internal/GardenaSmartImpl.java +++ b/bundles/org.openhab.binding.gardena/src/main/java/org/openhab/binding/gardena/internal/GardenaSmartImpl.java @@ -12,9 +12,11 @@ */ 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 allDevicesById = new HashMap<>(); - private LocationsResponse locationsResponse; - private GardenaSmartEventListener eventListener; + private final Map allDevicesById = new HashMap<>(); + private @Nullable LocationsResponse locationsResponse = null; + private final GardenaSmartEventListener eventListener; - private HttpClient httpClient; - private Map webSockets = new HashMap<>(); + private final HttpClient httpClient; + private final Map webSockets = new HashMap<>(); private @Nullable PostOAuth2Response token; private boolean initialized = false; - private WebSocketClient webSocketClient; + private final WebSocketClient webSocketClient; - private Set devicesToNotify = ConcurrentHashMap.newKeySet(); - private @Nullable ScheduledFuture deviceToNotifyFuture; - private @Nullable ScheduledFuture newDeviceFuture; + private final Set devicesToNotify = ConcurrentHashMap.newKeySet(); + private final Object deviceUpdateTaskLock = new Object(); + private @Nullable ScheduledFuture deviceUpdateTask; + private final Object newDeviceTasksLock = new Object(); + private final List> 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); + synchronized (deviceUpdateTaskLock) { + 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 notifyIterator = devicesToNotify.iterator(); - while (notifyIterator.hasNext()) { - eventListener.onDeviceUpdated(notifyIterator.next()); - notifyIterator.remove(); - } - }, 1, TimeUnit.SECONDS); + // 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 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); diff --git a/bundles/org.openhab.binding.gardena/src/main/java/org/openhab/binding/gardena/internal/handler/GardenaAccountHandler.java b/bundles/org.openhab.binding.gardena/src/main/java/org/openhab/binding/gardena/internal/handler/GardenaAccountHandler.java index 3a35ef95322..61e9ae429f6 100644 --- a/bundles/org.openhab.binding.gardena/src/main/java/org/openhab/binding/gardena/internal/handler/GardenaAccountHandler.java +++ b/bundles/org.openhab.binding.gardena/src/main/java/org/openhab/binding/gardena/internal/handler/GardenaAccountHandler.java @@ -12,12 +12,24 @@ */ 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,26 +63,83 @@ 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; + // 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; - public GardenaAccountHandler(Bridge bridge, HttpClientFactory httpClientFactory, - WebSocketFactory webSocketFactory) { + // 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, + 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 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) { @@ -78,53 +147,94 @@ public class GardenaAccountHandler extends BaseBridgeHandler implements GardenaS } /** - * Initializes the GardenaSmart account. + * Format a localized explanatory description regarding active call suppression. + * + * @return the localized description text, or null if call suppression is not active. */ - private void initializeGardena() { - final GardenaAccountHandler instance = this; - scheduler.execute(() -> { - try { - GardenaConfig gardenaConfig = getThing().getConfiguration().as(GardenaConfig.class); - logger.debug("{}", gardenaConfig); + 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; + } - 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); - } else { - // otherwise reinitialize after 120 seconds - scheduleReinitialize(REINITIALIZE_DELAY_SECONDS, TimeUnit.SECONDS); - } - logger.warn("{}", ex.getMessage()); + /** + * Initializes the GardenaSmart account. + * This method is called on a background thread. + */ + 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 { + delay = apiCallSuppressionDelay().plus(REINITIALIZE_DELAY_MINUTES_BACK_OFF); + } + 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); } } diff --git a/bundles/org.openhab.binding.gardena/src/main/java/org/openhab/binding/gardena/internal/handler/GardenaHandlerFactory.java b/bundles/org.openhab.binding.gardena/src/main/java/org/openhab/binding/gardena/internal/handler/GardenaHandlerFactory.java index b758b4692c3..25e4f54773d 100644 --- a/bundles/org.openhab.binding.gardena/src/main/java/org/openhab/binding/gardena/internal/handler/GardenaHandlerFactory.java +++ b/bundles/org.openhab.binding.gardena/src/main/java/org/openhab/binding/gardena/internal/handler/GardenaHandlerFactory.java @@ -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); } diff --git a/bundles/org.openhab.binding.gardena/src/main/java/org/openhab/binding/gardena/internal/model/dto/Device.java b/bundles/org.openhab.binding.gardena/src/main/java/org/openhab/binding/gardena/internal/model/dto/Device.java index b9801ed6daf..381ba73d543 100644 --- a/bundles/org.openhab.binding.gardena/src/main/java/org/openhab/binding/gardena/internal/model/dto/Device.java +++ b/bundles/org.openhab.binding.gardena/src/main/java/org/openhab/binding/gardena/internal/model/dto/Device.java @@ -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; } } diff --git a/bundles/org.openhab.binding.gardena/src/main/java/org/openhab/binding/gardena/internal/model/dto/api/CreateWebSocketRequest.java b/bundles/org.openhab.binding.gardena/src/main/java/org/openhab/binding/gardena/internal/model/dto/api/CreateWebSocketRequest.java index 5c0e796252a..ed68304714a 100644 --- a/bundles/org.openhab.binding.gardena/src/main/java/org/openhab/binding/gardena/internal/model/dto/api/CreateWebSocketRequest.java +++ b/bundles/org.openhab.binding.gardena/src/main/java/org/openhab/binding/gardena/internal/model/dto/api/CreateWebSocketRequest.java @@ -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; } } diff --git a/bundles/org.openhab.binding.gardena/src/main/resources/OH-INF/i18n/gardena.properties b/bundles/org.openhab.binding.gardena/src/main/resources/OH-INF/i18n/gardena.properties index 46b3b3b838b..2d950508618 100644 --- a/bundles/org.openhab.binding.gardena/src/main/resources/OH-INF/i18n/gardena.properties +++ b/bundles/org.openhab.binding.gardena/src/main/resources/OH-INF/i18n/gardena.properties @@ -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