mirror of
https://github.com/openhab/openhab-addons.git
synced 2025-01-10 15:11:59 +01:00
[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:
parent
9f3a23c55f
commit
ac12e5bfed
@ -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
|
||||
|
@ -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";
|
||||
}
|
||||
|
@ -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<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);
|
||||
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<Device> 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<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);
|
||||
|
@ -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<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) {
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user