mirror of
https://github.com/openhab/openhab-addons.git
synced 2025-01-10 07:02:02 +01:00
[netatmo] API limit reached handling (#16489)
Signed-off-by: Gaël L'hopital <gael@lhopital.org>
This commit is contained in:
parent
7b986e03d0
commit
df1ee5fda3
@ -25,7 +25,6 @@ import org.eclipse.jdt.annotation.NonNullByDefault;
|
|||||||
*/
|
*/
|
||||||
@NonNullByDefault
|
@NonNullByDefault
|
||||||
public class NetatmoBindingConstants {
|
public class NetatmoBindingConstants {
|
||||||
|
|
||||||
public static final String BINDING_ID = "netatmo";
|
public static final String BINDING_ID = "netatmo";
|
||||||
public static final String VENDOR = "Netatmo";
|
public static final String VENDOR = "Netatmo";
|
||||||
|
|
||||||
|
@ -29,4 +29,13 @@ public class ApiHandlerConfiguration {
|
|||||||
public String webHookUrl = "";
|
public String webHookUrl = "";
|
||||||
public String webHookPostfix = "";
|
public String webHookPostfix = "";
|
||||||
public int reconnectInterval = 300;
|
public int reconnectInterval = 300;
|
||||||
|
|
||||||
|
public ConfigurationLevel check() {
|
||||||
|
if (clientId.isBlank()) {
|
||||||
|
return ConfigurationLevel.EMPTY_CLIENT_ID;
|
||||||
|
} else if (clientSecret.isBlank()) {
|
||||||
|
return ConfigurationLevel.EMPTY_CLIENT_SECRET;
|
||||||
|
}
|
||||||
|
return ConfigurationLevel.COMPLETED;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -23,7 +23,6 @@ import org.eclipse.jdt.annotation.NonNullByDefault;
|
|||||||
public enum ConfigurationLevel {
|
public enum ConfigurationLevel {
|
||||||
EMPTY_CLIENT_ID("@text/conf-error-no-client-id"),
|
EMPTY_CLIENT_ID("@text/conf-error-no-client-id"),
|
||||||
EMPTY_CLIENT_SECRET("@text/conf-error-no-client-secret"),
|
EMPTY_CLIENT_SECRET("@text/conf-error-no-client-secret"),
|
||||||
REFRESH_TOKEN_NEEDED("@text/conf-error-grant-needed [ \"%s\" ]"),
|
|
||||||
COMPLETED("");
|
COMPLETED("");
|
||||||
|
|
||||||
public String message;
|
public String message;
|
||||||
|
@ -18,7 +18,6 @@ import static org.openhab.binding.netatmo.internal.NetatmoBindingConstants.*;
|
|||||||
import java.io.ByteArrayInputStream;
|
import java.io.ByteArrayInputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.lang.reflect.Constructor;
|
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
@ -103,6 +102,7 @@ import com.google.gson.GsonBuilder;
|
|||||||
@NonNullByDefault
|
@NonNullByDefault
|
||||||
public class ApiBridgeHandler extends BaseBridgeHandler {
|
public class ApiBridgeHandler extends BaseBridgeHandler {
|
||||||
private static final int TIMEOUT_S = 20;
|
private static final int TIMEOUT_S = 20;
|
||||||
|
private static final int API_LIMIT_INTERVAL_S = 3600;
|
||||||
|
|
||||||
private final Logger logger = LoggerFactory.getLogger(ApiBridgeHandler.class);
|
private final Logger logger = LoggerFactory.getLogger(ApiBridgeHandler.class);
|
||||||
private final AuthenticationApi connectApi = new AuthenticationApi(this);
|
private final AuthenticationApi connectApi = new AuthenticationApi(this);
|
||||||
@ -128,7 +128,6 @@ public class ApiBridgeHandler extends BaseBridgeHandler {
|
|||||||
this.deserializer = deserializer;
|
this.deserializer = deserializer;
|
||||||
this.httpService = httpService;
|
this.httpService = httpService;
|
||||||
this.oAuthFactory = oAuthFactory;
|
this.oAuthFactory = oAuthFactory;
|
||||||
|
|
||||||
requestCountChannelUID = new ChannelUID(thing.getUID(), GROUP_MONITORING, CHANNEL_REQUEST_COUNT);
|
requestCountChannelUID = new ChannelUID(thing.getUID(), GROUP_MONITORING, CHANNEL_REQUEST_COUNT);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -138,15 +137,9 @@ public class ApiBridgeHandler extends BaseBridgeHandler {
|
|||||||
|
|
||||||
ApiHandlerConfiguration configuration = getConfiguration();
|
ApiHandlerConfiguration configuration = getConfiguration();
|
||||||
|
|
||||||
if (configuration.clientId.isBlank()) {
|
ConfigurationLevel confLevel = configuration.check();
|
||||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
|
if (!ConfigurationLevel.COMPLETED.equals(confLevel)) {
|
||||||
ConfigurationLevel.EMPTY_CLIENT_ID.message);
|
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, confLevel.message);
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (configuration.clientSecret.isBlank()) {
|
|
||||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
|
|
||||||
ConfigurationLevel.EMPTY_CLIENT_SECRET.message);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -170,15 +163,13 @@ public class ApiBridgeHandler extends BaseBridgeHandler {
|
|||||||
logger.debug("Connected to Netatmo API.");
|
logger.debug("Connected to Netatmo API.");
|
||||||
|
|
||||||
ApiHandlerConfiguration configuration = getConfiguration();
|
ApiHandlerConfiguration configuration = getConfiguration();
|
||||||
if (!configuration.webHookUrl.isBlank()) {
|
if (!configuration.webHookUrl.isBlank()
|
||||||
SecurityApi securityApi = getRestManager(SecurityApi.class);
|
&& getRestManager(SecurityApi.class) instanceof SecurityApi securityApi) {
|
||||||
if (securityApi != null) {
|
webHookServlet.ifPresent(servlet -> servlet.dispose());
|
||||||
webHookServlet.ifPresent(servlet -> servlet.dispose());
|
WebhookServlet servlet = new WebhookServlet(this, httpService, deserializer, securityApi,
|
||||||
WebhookServlet servlet = new WebhookServlet(this, httpService, deserializer, securityApi,
|
configuration.webHookUrl, configuration.webHookPostfix);
|
||||||
configuration.webHookUrl, configuration.webHookPostfix);
|
servlet.startListening();
|
||||||
servlet.startListening();
|
this.webHookServlet = Optional.of(servlet);
|
||||||
this.webHookServlet = Optional.of(servlet);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
updateStatus(ThingStatus.ONLINE);
|
updateStatus(ThingStatus.ONLINE);
|
||||||
@ -197,8 +188,7 @@ public class ApiBridgeHandler extends BaseBridgeHandler {
|
|||||||
accessTokenResponse = oAuthClientService.getAccessTokenResponseByAuthorizationCode(code, redirectUri);
|
accessTokenResponse = oAuthClientService.getAccessTokenResponseByAuthorizationCode(code, redirectUri);
|
||||||
|
|
||||||
// Dispose grant servlet upon completion of authorization flow.
|
// Dispose grant servlet upon completion of authorization flow.
|
||||||
grantServlet.ifPresent(servlet -> servlet.dispose());
|
freeGrantServlet();
|
||||||
grantServlet = Optional.empty();
|
|
||||||
} else {
|
} else {
|
||||||
accessTokenResponse = oAuthClientService.getAccessTokenResponse();
|
accessTokenResponse = oAuthClientService.getAccessTokenResponse();
|
||||||
}
|
}
|
||||||
@ -207,8 +197,7 @@ public class ApiBridgeHandler extends BaseBridgeHandler {
|
|||||||
startAuthorizationFlow();
|
startAuthorizationFlow();
|
||||||
return false;
|
return false;
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
|
prepareReconnection(getConfiguration().reconnectInterval, e.getMessage(), code, redirectUri);
|
||||||
prepareReconnection(code, redirectUri);
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -227,7 +216,7 @@ public class ApiBridgeHandler extends BaseBridgeHandler {
|
|||||||
servlet.startListening();
|
servlet.startListening();
|
||||||
grantServlet = Optional.of(servlet);
|
grantServlet = Optional.of(servlet);
|
||||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
|
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
|
||||||
ConfigurationLevel.REFRESH_TOKEN_NEEDED.message.formatted(servlet.getPath()));
|
"@text/conf-error-grant-needed [ \"%s\" ]".formatted(servlet.getPath()));
|
||||||
connectApi.dispose();
|
connectApi.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -235,11 +224,15 @@ public class ApiBridgeHandler extends BaseBridgeHandler {
|
|||||||
return getConfigAs(ApiHandlerConfiguration.class);
|
return getConfigAs(ApiHandlerConfiguration.class);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void prepareReconnection(@Nullable String code, @Nullable String redirectUri) {
|
private void prepareReconnection(int delay, @Nullable String message, @Nullable String code,
|
||||||
|
@Nullable String redirectUri) {
|
||||||
|
if (!ThingStatus.OFFLINE.equals(thing.getStatus())) {
|
||||||
|
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, message);
|
||||||
|
}
|
||||||
connectApi.dispose();
|
connectApi.dispose();
|
||||||
freeConnectJob();
|
freeConnectJob();
|
||||||
connectJob = Optional.of(scheduler.schedule(() -> openConnection(code, redirectUri),
|
connectJob = Optional.of(scheduler.schedule(() -> openConnection(code, redirectUri), delay, TimeUnit.SECONDS));
|
||||||
getConfiguration().reconnectInterval, TimeUnit.SECONDS));
|
logger.debug("Reconnection scheduled in {} seconds", delay);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void freeConnectJob() {
|
private void freeConnectJob() {
|
||||||
@ -247,6 +240,11 @@ public class ApiBridgeHandler extends BaseBridgeHandler {
|
|||||||
connectJob = Optional.empty();
|
connectJob = Optional.empty();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void freeGrantServlet() {
|
||||||
|
grantServlet.ifPresent(servlet -> servlet.dispose());
|
||||||
|
grantServlet = Optional.empty();
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void dispose() {
|
public void dispose() {
|
||||||
logger.debug("Shutting down Netatmo API bridge handler.");
|
logger.debug("Shutting down Netatmo API bridge handler.");
|
||||||
@ -254,8 +252,7 @@ public class ApiBridgeHandler extends BaseBridgeHandler {
|
|||||||
webHookServlet.ifPresent(servlet -> servlet.dispose());
|
webHookServlet.ifPresent(servlet -> servlet.dispose());
|
||||||
webHookServlet = Optional.empty();
|
webHookServlet = Optional.empty();
|
||||||
|
|
||||||
grantServlet.ifPresent(servlet -> servlet.dispose());
|
freeGrantServlet();
|
||||||
grantServlet = Optional.empty();
|
|
||||||
|
|
||||||
connectApi.dispose();
|
connectApi.dispose();
|
||||||
freeConnectJob();
|
freeConnectJob();
|
||||||
@ -280,13 +277,12 @@ public class ApiBridgeHandler extends BaseBridgeHandler {
|
|||||||
public <T extends RestManager> @Nullable T getRestManager(Class<T> clazz) {
|
public <T extends RestManager> @Nullable T getRestManager(Class<T> clazz) {
|
||||||
if (!managers.containsKey(clazz)) {
|
if (!managers.containsKey(clazz)) {
|
||||||
try {
|
try {
|
||||||
Constructor<T> constructor = clazz.getConstructor(getClass());
|
T instance = clazz.getConstructor(getClass()).newInstance(this);
|
||||||
T instance = constructor.newInstance(this);
|
|
||||||
Set<Scope> expected = instance.getRequiredScopes();
|
Set<Scope> expected = instance.getRequiredScopes();
|
||||||
if (connectApi.matchesScopes(expected)) {
|
if (connectApi.matchesScopes(expected)) {
|
||||||
managers.put(clazz, instance);
|
managers.put(clazz, instance);
|
||||||
} else {
|
} else {
|
||||||
logger.info("Unable to instantiate {}, expected scope {} is not active", clazz, expected);
|
logger.warn("Unable to instantiate {}, expected scope {} is not active", clazz, expected);
|
||||||
}
|
}
|
||||||
} catch (SecurityException | ReflectiveOperationException e) {
|
} catch (SecurityException | ReflectiveOperationException e) {
|
||||||
logger.warn("Error invoking RestManager constructor for class {}: {}", clazz, e.getMessage());
|
logger.warn("Error invoking RestManager constructor for class {}: {}", clazz, e.getMessage());
|
||||||
@ -303,7 +299,7 @@ public class ApiBridgeHandler extends BaseBridgeHandler {
|
|||||||
Request request = httpClient.newRequest(uri).method(method).timeout(TIMEOUT_S, TimeUnit.SECONDS);
|
Request request = httpClient.newRequest(uri).method(method).timeout(TIMEOUT_S, TimeUnit.SECONDS);
|
||||||
|
|
||||||
if (!authenticate(null, null)) {
|
if (!authenticate(null, null)) {
|
||||||
prepareReconnection(null, null);
|
prepareReconnection(getConfiguration().reconnectInterval, "@text/status-bridge-offline", null, null);
|
||||||
throw new NetatmoException("Not authenticated");
|
throw new NetatmoException("Not authenticated");
|
||||||
}
|
}
|
||||||
connectApi.getAuthorization().ifPresent(auth -> request.header(HttpHeader.AUTHORIZATION, auth));
|
connectApi.getAuthorization().ifPresent(auth -> request.header(HttpHeader.AUTHORIZATION, auth));
|
||||||
@ -345,29 +341,30 @@ public class ApiBridgeHandler extends BaseBridgeHandler {
|
|||||||
try {
|
try {
|
||||||
exception = new NetatmoException(deserializer.deserialize(ApiError.class, responseBody));
|
exception = new NetatmoException(deserializer.deserialize(ApiError.class, responseBody));
|
||||||
} catch (NetatmoException e) {
|
} catch (NetatmoException e) {
|
||||||
exception = new NetatmoException("Error deserializing error: %s".formatted(statusCode.getMessage()));
|
if (statusCode == Code.TOO_MANY_REQUESTS) {
|
||||||
|
exception = new NetatmoException(statusCode.getMessage());
|
||||||
|
} else {
|
||||||
|
exception = new NetatmoException(
|
||||||
|
"Error deserializing error: %s".formatted(statusCode.getMessage()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (statusCode == Code.TOO_MANY_REQUESTS
|
||||||
|
|| exception.getStatusCode() == ServiceError.MAXIMUM_USAGE_REACHED) {
|
||||||
|
prepareReconnection(API_LIMIT_INTERVAL_S,
|
||||||
|
"@text/maximum-usage-reached [ \"%d\" ]".formatted(API_LIMIT_INTERVAL_S), null, null);
|
||||||
}
|
}
|
||||||
throw exception;
|
throw exception;
|
||||||
} catch (NetatmoException e) {
|
|
||||||
if (e.getStatusCode() == ServiceError.MAXIMUM_USAGE_REACHED) {
|
|
||||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "@text/maximum-usage-reached");
|
|
||||||
prepareReconnection(null, null);
|
|
||||||
} else if (e.getStatusCode() == ServiceError.INVALID_TOKEN_MISSING) {
|
|
||||||
startAuthorizationFlow();
|
|
||||||
}
|
|
||||||
throw e;
|
|
||||||
} catch (InterruptedException e) {
|
} catch (InterruptedException e) {
|
||||||
Thread.currentThread().interrupt();
|
Thread.currentThread().interrupt();
|
||||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
|
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
|
||||||
throw new NetatmoException("Request interrupted");
|
throw new NetatmoException(e, "Request interrupted");
|
||||||
} catch (TimeoutException | ExecutionException e) {
|
} catch (TimeoutException | ExecutionException e) {
|
||||||
if (retryCount > 0) {
|
if (retryCount > 0) {
|
||||||
logger.debug("Request timedout, retry counter: {}", retryCount);
|
logger.debug("Request error, retry counter: {}", retryCount);
|
||||||
return executeUri(uri, method, clazz, payload, contentType, retryCount - 1);
|
return executeUri(uri, method, clazz, payload, contentType, retryCount - 1);
|
||||||
}
|
}
|
||||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "@text/request-time-out");
|
prepareReconnection(getConfiguration().reconnectInterval, "@text/request-time-out", null, e.getMessage());
|
||||||
prepareReconnection(null, null);
|
throw new NetatmoException("%s: \"%s\"".formatted(e.getClass().getName(), e.getMessage()));
|
||||||
throw new NetatmoException(String.format("%s: \"%s\"", e.getClass().getName(), e.getMessage()));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -49,6 +49,7 @@ public class AlarmEventCapability extends HomeSecurityThingCapability {
|
|||||||
handler.updateState(GROUP_LAST_EVENT, CHANNEL_EVENT_TIME, toDateTimeType(event.getTime()));
|
handler.updateState(GROUP_LAST_EVENT, CHANNEL_EVENT_TIME, toDateTimeType(event.getTime()));
|
||||||
handler.updateState(GROUP_LAST_EVENT, CHANNEL_EVENT_SUBTYPE, Objects.requireNonNull(
|
handler.updateState(GROUP_LAST_EVENT, CHANNEL_EVENT_SUBTYPE, Objects.requireNonNull(
|
||||||
event.getSubTypeDescription().map(ChannelTypeUtils::toStringType).orElse(UnDefType.NULL)));
|
event.getSubTypeDescription().map(ChannelTypeUtils::toStringType).orElse(UnDefType.NULL)));
|
||||||
|
|
||||||
final String message = event.getName();
|
final String message = event.getName();
|
||||||
handler.updateState(GROUP_LAST_EVENT, CHANNEL_EVENT_MESSAGE,
|
handler.updateState(GROUP_LAST_EVENT, CHANNEL_EVENT_MESSAGE,
|
||||||
message == null || message.isBlank() ? UnDefType.NULL : toStringType(message));
|
message == null || message.isBlank() ? UnDefType.NULL : toStringType(message));
|
||||||
|
@ -73,7 +73,7 @@ public class CameraCapability extends HomeSecurityThingCapability {
|
|||||||
List<ChannelHelper> channelHelpers) {
|
List<ChannelHelper> channelHelpers) {
|
||||||
super(handler, descriptionProvider, channelHelpers);
|
super(handler, descriptionProvider, channelHelpers);
|
||||||
this.personChannelUID = new ChannelUID(thingUID, GROUP_LAST_EVENT, CHANNEL_EVENT_PERSON_ID);
|
this.personChannelUID = new ChannelUID(thingUID, GROUP_LAST_EVENT, CHANNEL_EVENT_PERSON_ID);
|
||||||
this.cameraHelper = (CameraChannelHelper) channelHelpers.stream().filter(c -> c instanceof CameraChannelHelper)
|
this.cameraHelper = (CameraChannelHelper) channelHelpers.stream().filter(CameraChannelHelper.class::isInstance)
|
||||||
.findFirst().orElseThrow(() -> new IllegalArgumentException(
|
.findFirst().orElseThrow(() -> new IllegalArgumentException(
|
||||||
"CameraCapability must find a CameraChannelHelper, please file a bug report."));
|
"CameraCapability must find a CameraChannelHelper, please file a bug report."));
|
||||||
}
|
}
|
||||||
|
@ -465,7 +465,7 @@ device-not-connected = Thing is not reachable
|
|||||||
data-over-limit = Data seems quite old
|
data-over-limit = Data seems quite old
|
||||||
request-time-out = Request timed out - will attempt reconnection later
|
request-time-out = Request timed out - will attempt reconnection later
|
||||||
deserialization-unknown = Deserialization lead to an unknown code
|
deserialization-unknown = Deserialization lead to an unknown code
|
||||||
maximum-usage-reached = Maximum usage reached. Will try reconnection after `reconnectInterval` seconds.
|
maximum-usage-reached = Maximum usage reached, will reconnect in {0} seconds.
|
||||||
|
|
||||||
homestatus-unknown-error = Unknown error
|
homestatus-unknown-error = Unknown error
|
||||||
homestatus-internal-error = Internal error
|
homestatus-internal-error = Internal error
|
||||||
|
Loading…
Reference in New Issue
Block a user