[gardena] Improve API rate limit handling (#13016)

* [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>
This commit is contained in:
Andrew Fiddian-Green 2022-08-04 19:20:40 +01:00 committed by GitHub
parent 9f3a23c55f
commit ac12e5bfed
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 280 additions and 113 deletions

View File

@ -115,6 +115,20 @@ DateTime LastUpdate "LastUpdate [%1$td.%1$tm.%1$tY %1$tH:%1$tM]" { channel="gard
openhab:send LastUpdate REFRESH 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 ### Debugging and Tracing
If you want to see what's going on in the binding, switch the loglevel to TRACE in the Karaf console If you want to see what's going on in the binding, switch the loglevel to TRACE in the Karaf console

View File

@ -37,4 +37,6 @@ public class GardenaBindingConstants {
public static final String DEVICE_TYPE_WATER_CONTROL = "water_control"; public static final String DEVICE_TYPE_WATER_CONTROL = "water_control";
public static final String DEVICE_TYPE_SENSOR = "sensor"; public static final String DEVICE_TYPE_SENSOR = "sensor";
public static final String DEVICE_TYPE_POWER = "power"; public static final String DEVICE_TYPE_POWER = "power";
public static final String API_CALL_SUPPRESSION_UNTIL = "apiCallSuppressionUntil";
} }

View File

@ -12,9 +12,11 @@
*/ */
package org.openhab.binding.gardena.internal; package org.openhab.binding.gardena.internal;
import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.HashMap; import java.util.HashMap;
import java.util.Iterator; import java.util.Iterator;
import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.concurrent.ConcurrentHashMap; 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_LOCATIONS = URL_API_GARDENA + "/locations";
private static final String URL_API_COMMAND = URL_API_GARDENA + "/command"; private static final String URL_API_COMMAND = URL_API_GARDENA + "/command";
private String id; private final String id;
private GardenaConfig config; private final GardenaConfig config;
private ScheduledExecutorService scheduler; private final ScheduledExecutorService scheduler;
private Map<String, Device> allDevicesById = new HashMap<>(); private final Map<String, Device> allDevicesById = new HashMap<>();
private LocationsResponse locationsResponse; private @Nullable LocationsResponse locationsResponse = null;
private GardenaSmartEventListener eventListener; private final GardenaSmartEventListener eventListener;
private HttpClient httpClient; private final HttpClient httpClient;
private Map<String, GardenaSmartWebSocket> webSockets = new HashMap<>(); private final Map<String, GardenaSmartWebSocket> webSockets = new HashMap<>();
private @Nullable PostOAuth2Response token; private @Nullable PostOAuth2Response token;
private boolean initialized = false; private boolean initialized = false;
private WebSocketClient webSocketClient; private final WebSocketClient webSocketClient;
private Set<Device> devicesToNotify = ConcurrentHashMap.newKeySet(); private final Set<Device> devicesToNotify = ConcurrentHashMap.newKeySet();
private @Nullable ScheduledFuture<?> deviceToNotifyFuture; private final Object deviceUpdateTaskLock = new Object();
private @Nullable ScheduledFuture<?> newDeviceFuture; 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, public GardenaSmartImpl(String id, GardenaConfig config, GardenaSmartEventListener eventListener,
ScheduledExecutorService scheduler, HttpClientFactory httpClientFactory, WebSocketFactory webSocketFactory) ScheduledExecutorService scheduler, HttpClientFactory httpClientFactory, WebSocketFactory webSocketFactory)
@ -121,14 +125,17 @@ public class GardenaSmartImpl implements GardenaSmart, GardenaSmartWebSocketList
// initially load access token // initially load access token
verifyToken(); verifyToken();
locationsResponse = loadLocations(); LocationsResponse locationsResponse = loadLocations();
this.locationsResponse = locationsResponse;
// assemble devices // assemble devices
for (LocationDataItem location : locationsResponse.data) { if (locationsResponse.data != null) {
LocationResponse locationResponse = loadLocation(location.id); for (LocationDataItem location : locationsResponse.data) {
if (locationResponse.included != null) { LocationResponse locationResponse = loadLocation(location.id);
for (DataItem<?> dataItem : locationResponse.included) { if (locationResponse.included != null) {
handleDataItem(dataItem); for (DataItem<?> dataItem : locationResponse.included) {
handleDataItem(dataItem);
}
} }
} }
} }
@ -153,16 +160,19 @@ public class GardenaSmartImpl implements GardenaSmart, GardenaSmartWebSocketList
* Starts the websockets for each location. * Starts the websockets for each location.
*/ */
private void startWebsockets() throws Exception { private void startWebsockets() throws Exception {
for (LocationDataItem location : locationsResponse.data) { LocationsResponse locationsResponse = this.locationsResponse;
WebSocketCreatedResponse webSocketCreatedResponse = getWebsocketInfo(location.id); if (locationsResponse != null) {
Location locationAttributes = location.attributes; for (LocationDataItem location : locationsResponse.data) {
WebSocket webSocketAttributes = webSocketCreatedResponse.data.attributes; WebSocketCreatedResponse webSocketCreatedResponse = getWebsocketInfo(location.id);
if (locationAttributes == null || webSocketAttributes == null) { Location locationAttributes = location.attributes;
continue; 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 @Override
public void dispose() { public void dispose() {
logger.debug("Disposing GardenaSmart"); logger.debug("Disposing GardenaSmart");
initialized = false;
final ScheduledFuture<?> newDeviceFuture = this.newDeviceFuture; synchronized (newDeviceTasksLock) {
if (newDeviceFuture != null) { for (ScheduledFuture<?> task : newDeviceTasks) {
newDeviceFuture.cancel(true); if (!task.isDone()) {
task.cancel(true);
}
}
newDeviceTasks.clear();
} }
synchronized (deviceUpdateTaskLock) {
final ScheduledFuture<?> deviceToNotifyFuture = this.deviceToNotifyFuture; devicesToNotify.clear();
if (deviceToNotifyFuture != null) { ScheduledFuture<?> task = deviceUpdateTask;
deviceToNotifyFuture.cancel(true); if (task != null) {
task.cancel(true);
}
deviceUpdateTask = null;
} }
stopWebsockets(); stopWebsockets();
try { try {
@ -318,9 +335,8 @@ public class GardenaSmartImpl implements GardenaSmart, GardenaSmartWebSocketList
} }
httpClient.destroy(); httpClient.destroy();
webSocketClient.destroy(); webSocketClient.destroy();
locationsResponse = new LocationsResponse();
allDevicesById.clear(); allDevicesById.clear();
initialized = false; locationsResponse = null;
} }
/** /**
@ -353,16 +369,21 @@ public class GardenaSmartImpl implements GardenaSmart, GardenaSmartWebSocketList
device = new Device(deviceId); device = new Device(deviceId);
allDevicesById.put(device.id, device); allDevicesById.put(device.id, device);
if (initialized) { synchronized (newDeviceTasksLock) {
newDeviceFuture = scheduler.schedule(() -> { // remove prior completed tasks from the list
Device newDevice = allDevicesById.get(deviceId); newDeviceTasks.removeIf(task -> task.isDone());
if (newDevice != null) { // add a new scheduled task to the list
newDevice.evaluateDeviceType(); newDeviceTasks.add(scheduler.schedule(() -> {
if (newDevice.deviceType != null) { if (initialized) {
eventListener.onNewDevice(newDevice); 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); handleDataItem(dataItem);
Device device = allDevicesById.get(dataItem.getDeviceId()); Device device = allDevicesById.get(dataItem.getDeviceId());
if (device != null && device.active) { 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 // delay the deviceUpdated event to filter multiple events for the same device dataItem property
if (deviceToNotifyFuture == null) { ScheduledFuture<?> task = this.deviceUpdateTask;
deviceToNotifyFuture = scheduler.schedule(() -> { if (task == null || task.isDone()) {
deviceToNotifyFuture = null; deviceUpdateTask = scheduler.schedule(() -> notifyDevicesUpdated(), 1, TimeUnit.SECONDS);
Iterator<Device> notifyIterator = devicesToNotify.iterator(); }
while (notifyIterator.hasNext()) {
eventListener.onDeviceUpdated(notifyIterator.next());
notifyIterator.remove();
}
}, 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 @Override
public Device getDevice(String deviceId) throws GardenaDeviceNotFoundException { public Device getDevice(String deviceId) throws GardenaDeviceNotFoundException {
Device device = allDevicesById.get(deviceId); Device device = allDevicesById.get(deviceId);

View File

@ -12,12 +12,24 @@
*/ */
package org.openhab.binding.gardena.internal.handler; 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.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.Map;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable; 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.GardenaSmart;
import org.openhab.binding.gardena.internal.GardenaSmartEventListener; import org.openhab.binding.gardena.internal.GardenaSmartEventListener;
import org.openhab.binding.gardena.internal.GardenaSmartImpl; 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.exception.GardenaException;
import org.openhab.binding.gardena.internal.model.dto.Device; import org.openhab.binding.gardena.internal.model.dto.Device;
import org.openhab.binding.gardena.internal.util.UidUtils; 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.HttpClientFactory;
import org.openhab.core.io.net.http.WebSocketFactory; import org.openhab.core.io.net.http.WebSocketFactory;
import org.openhab.core.thing.Bridge; 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.ThingHandler;
import org.openhab.core.thing.binding.ThingHandlerService; import org.openhab.core.thing.binding.ThingHandlerService;
import org.openhab.core.types.Command; import org.openhab.core.types.Command;
import org.openhab.core.types.RefreshType;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@ -51,26 +63,83 @@ import org.slf4j.LoggerFactory;
@NonNullByDefault @NonNullByDefault
public class GardenaAccountHandler extends BaseBridgeHandler implements GardenaSmartEventListener { public class GardenaAccountHandler extends BaseBridgeHandler implements GardenaSmartEventListener {
private final Logger logger = LoggerFactory.getLogger(GardenaAccountHandler.class); 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 GardenaDeviceDiscoveryService discoveryService;
private @Nullable GardenaSmart gardenaSmart; private @Nullable GardenaSmart gardenaSmart;
private HttpClientFactory httpClientFactory; private final HttpClientFactory httpClientFactory;
private WebSocketFactory webSocketFactory; private final WebSocketFactory webSocketFactory;
private final TimeZoneProvider timeZoneProvider;
public GardenaAccountHandler(Bridge bridge, HttpClientFactory httpClientFactory, // re- initialisation stuff
WebSocketFactory webSocketFactory) { 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); super(bridge);
this.httpClientFactory = httpClientFactory; this.httpClientFactory = httpClientFactory;
this.webSocketFactory = webSocketFactory; 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 @Override
public void initialize() { public void initialize() {
logger.debug("Initializing Gardena account '{}'", getThing().getUID().getId()); 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) { 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() { private @Nullable String getUiText() {
final GardenaAccountHandler instance = this; Instant until = apiCallSuppressionUntil;
scheduler.execute(() -> { if (until != null) {
try { ZoneId zone = timeZoneProvider.getTimeZone();
GardenaConfig gardenaConfig = getThing().getConfiguration().as(GardenaConfig.class); boolean isToday = LocalDate.now(zone).equals(LocalDate.ofInstant(until, zone));
logger.debug("{}", gardenaConfig); 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, * Initializes the GardenaSmart account.
webSocketFactory); * This method is called on a background thread.
final GardenaDeviceDiscoveryService discoveryService = this.discoveryService; */
if (discoveryService != null) { private synchronized void initializeGardena() {
discoveryService.startScan(null); try {
discoveryService.waitForScanFinishing(); GardenaConfig gardenaConfig = getThing().getConfiguration().as(GardenaConfig.class);
} logger.debug("{}", gardenaConfig);
updateStatus(ThingStatus.ONLINE);
} catch (GardenaException ex) { String id = getThing().getUID().getId();
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, ex.getMessage()); gardenaSmart = new GardenaSmartImpl(id, gardenaConfig, this, scheduler, httpClientFactory,
disposeGardena(); webSocketFactory);
if (ex.getStatus() == 429) { final GardenaDeviceDiscoveryService discoveryService = this.discoveryService;
// if there was an error 429 (Too Many Requests), wait for 24 hours before trying again if (discoveryService != null) {
scheduleReinitialize(REINITIALIZE_DELAY_HOURS_LIMIT_EXCEEDED, TimeUnit.HOURS); discoveryService.startScan(null);
} else { discoveryService.waitForScanFinishing();
// otherwise reinitialize after 120 seconds
scheduleReinitialize(REINITIALIZE_DELAY_SECONDS, TimeUnit.SECONDS);
}
logger.warn("{}", ex.getMessage());
} }
}); 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. * Schedules a reinitialization, if Gardena smart system account is not reachable.
*/ */
private void scheduleReinitialize(long delay, TimeUnit unit) { private void scheduleReinitialize(Duration delay) {
scheduler.schedule(() -> { ScheduledFuture<?> reInitializationTask = this.reInitializationTask;
if (getThing().getStatus() != ThingStatus.UNINITIALIZED) { if (reInitializationTask != null) {
initializeGardena(); reInitializationTask.cancel(false);
} }
}, delay, unit); this.reInitializationTask = scheduler.schedule(() -> reIninitializeGardena(), delay.getSeconds(),
TimeUnit.SECONDS);
} }
@Override @Override
public void dispose() { public void dispose() {
super.dispose(); super.dispose();
synchronized (reInitializationCodeLock) {
ScheduledFuture<?> reInitializeTask = this.reInitializationTask;
if (reInitializeTask != null) {
reInitializeTask.cancel(true);
}
this.reInitializationTask = null;
}
disposeGardena(); disposeGardena();
} }
@ -141,6 +251,7 @@ public class GardenaAccountHandler extends BaseBridgeHandler implements GardenaS
if (gardenaSmart != null) { if (gardenaSmart != null) {
gardenaSmart.dispose(); gardenaSmart.dispose();
} }
this.gardenaSmart = null;
} }
/** /**
@ -157,11 +268,7 @@ public class GardenaAccountHandler extends BaseBridgeHandler implements GardenaS
@Override @Override
public void handleCommand(ChannelUID channelUID, Command command) { public void handleCommand(ChannelUID channelUID, Command command) {
if (RefreshType.REFRESH == command) { // nothing to do here because the thing has no channels
logger.debug("Refreshing Gardena account '{}'", getThing().getUID().getId());
disposeGardena();
initializeGardena();
}
} }
@Override @Override
@ -202,8 +309,12 @@ public class GardenaAccountHandler extends BaseBridgeHandler implements GardenaS
@Override @Override
public void onError() { 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(); disposeGardena();
scheduleReinitialize(REINITIALIZE_DELAY_SECONDS, TimeUnit.SECONDS);
} }
} }

View File

@ -12,8 +12,7 @@
*/ */
package org.openhab.binding.gardena.internal.handler; 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.*;
import static org.openhab.binding.gardena.internal.GardenaBindingConstants.THING_TYPE_ACCOUNT;
import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable; import org.eclipse.jdt.annotation.Nullable;
@ -58,7 +57,7 @@ public class GardenaHandlerFactory extends BaseThingHandlerFactory {
@Override @Override
protected @Nullable ThingHandler createHandler(Thing thing) { protected @Nullable ThingHandler createHandler(Thing thing) {
if (THING_TYPE_ACCOUNT.equals(thing.getThingTypeUID())) { if (THING_TYPE_ACCOUNT.equals(thing.getThingTypeUID())) {
return new GardenaAccountHandler((Bridge) thing, httpClientFactory, webSocketFactory); return new GardenaAccountHandler((Bridge) thing, httpClientFactory, webSocketFactory, timeZoneProvider);
} else { } else {
return new GardenaThingHandler(thing, timeZoneProvider); return new GardenaThingHandler(thing, timeZoneProvider);
} }

View File

@ -152,8 +152,12 @@ public class Device {
throw new GardenaException("Unknown dataItem with id: " + dataItem.id); throw new GardenaException("Unknown dataItem with id: " + dataItem.id);
} }
if (common != null && common.attributes != null) { if (common != null) {
common.attributes.lastUpdate.timestamp = new Date(); CommonService attributes = common.attributes;
if (attributes != null) {
attributes.lastUpdate.timestamp = new Date();
}
common.attributes = attributes;
} }
} }

View File

@ -28,7 +28,8 @@ public class CreateWebSocketRequest {
data = new CreateWebSocketDataItem(); data = new CreateWebSocketDataItem();
data.id = "wsreq-" + locationId; data.id = "wsreq-" + locationId;
data.type = "WEBSOCKET"; data.type = "WEBSOCKET";
data.attributes = new CreateWebSocket(); CreateWebSocket attributes = new CreateWebSocket();
data.attributes.locationId = locationId; attributes.locationId = locationId;
data.attributes = attributes;
} }
} }

View File

@ -260,3 +260,7 @@ channel-type.gardena.timestampRefresh.label = Timestamp
channel-type.gardena.timestampRefresh.description = Timestamp channel-type.gardena.timestampRefresh.description = Timestamp
channel-type.gardena.valveCommandDuration.label = Command Duration channel-type.gardena.valveCommandDuration.label = Command Duration
channel-type.gardena.valveCommandDuration.description = A duration in minutes for a command 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