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
|
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
|
||||||
|
@ -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";
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
|
@ -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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
Loading…
Reference in New Issue
Block a user