diff --git a/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/NetatmoBindingConstants.java b/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/NetatmoBindingConstants.java index f7d9a2972b54b..bf034a98e0e72 100644 --- a/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/NetatmoBindingConstants.java +++ b/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/NetatmoBindingConstants.java @@ -25,7 +25,6 @@ */ @NonNullByDefault public class NetatmoBindingConstants { - public static final String BINDING_ID = "netatmo"; public static final String VENDOR = "Netatmo"; diff --git a/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/config/ApiHandlerConfiguration.java b/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/config/ApiHandlerConfiguration.java index 754b4aa0cc024..6e5b3be1a6946 100644 --- a/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/config/ApiHandlerConfiguration.java +++ b/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/config/ApiHandlerConfiguration.java @@ -29,4 +29,13 @@ public class ApiHandlerConfiguration { public String webHookUrl = ""; public String webHookPostfix = ""; 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; + } } diff --git a/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/config/ConfigurationLevel.java b/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/config/ConfigurationLevel.java index 4eaa0ca19ec90..aeaf18e56a554 100644 --- a/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/config/ConfigurationLevel.java +++ b/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/config/ConfigurationLevel.java @@ -23,7 +23,6 @@ public enum ConfigurationLevel { EMPTY_CLIENT_ID("@text/conf-error-no-client-id"), EMPTY_CLIENT_SECRET("@text/conf-error-no-client-secret"), - REFRESH_TOKEN_NEEDED("@text/conf-error-grant-needed [ \"%s\" ]"), COMPLETED(""); public String message; diff --git a/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/handler/ApiBridgeHandler.java b/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/handler/ApiBridgeHandler.java index 5e9f8f1a830f0..c53844ae5400f 100644 --- a/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/handler/ApiBridgeHandler.java +++ b/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/handler/ApiBridgeHandler.java @@ -18,7 +18,6 @@ import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; -import java.lang.reflect.Constructor; import java.net.URI; import java.nio.charset.StandardCharsets; import java.time.Instant; @@ -103,6 +102,7 @@ @NonNullByDefault public class ApiBridgeHandler extends BaseBridgeHandler { 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 AuthenticationApi connectApi = new AuthenticationApi(this); @@ -128,7 +128,6 @@ public ApiBridgeHandler(Bridge bridge, HttpClient httpClient, NADeserializer des this.deserializer = deserializer; this.httpService = httpService; this.oAuthFactory = oAuthFactory; - requestCountChannelUID = new ChannelUID(thing.getUID(), GROUP_MONITORING, CHANNEL_REQUEST_COUNT); } @@ -138,15 +137,9 @@ public void initialize() { ApiHandlerConfiguration configuration = getConfiguration(); - if (configuration.clientId.isBlank()) { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, - ConfigurationLevel.EMPTY_CLIENT_ID.message); - return; - } - - if (configuration.clientSecret.isBlank()) { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, - ConfigurationLevel.EMPTY_CLIENT_SECRET.message); + ConfigurationLevel confLevel = configuration.check(); + if (!ConfigurationLevel.COMPLETED.equals(confLevel)) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, confLevel.message); return; } @@ -170,15 +163,13 @@ public void openConnection(@Nullable String code, @Nullable String redirectUri) logger.debug("Connected to Netatmo API."); ApiHandlerConfiguration configuration = getConfiguration(); - if (!configuration.webHookUrl.isBlank()) { - SecurityApi securityApi = getRestManager(SecurityApi.class); - if (securityApi != null) { - webHookServlet.ifPresent(servlet -> servlet.dispose()); - WebhookServlet servlet = new WebhookServlet(this, httpService, deserializer, securityApi, - configuration.webHookUrl, configuration.webHookPostfix); - servlet.startListening(); - this.webHookServlet = Optional.of(servlet); - } + if (!configuration.webHookUrl.isBlank() + && getRestManager(SecurityApi.class) instanceof SecurityApi securityApi) { + webHookServlet.ifPresent(servlet -> servlet.dispose()); + WebhookServlet servlet = new WebhookServlet(this, httpService, deserializer, securityApi, + configuration.webHookUrl, configuration.webHookPostfix); + servlet.startListening(); + this.webHookServlet = Optional.of(servlet); } updateStatus(ThingStatus.ONLINE); @@ -197,8 +188,7 @@ private boolean authenticate(@Nullable String code, @Nullable String redirectUri accessTokenResponse = oAuthClientService.getAccessTokenResponseByAuthorizationCode(code, redirectUri); // Dispose grant servlet upon completion of authorization flow. - grantServlet.ifPresent(servlet -> servlet.dispose()); - grantServlet = Optional.empty(); + freeGrantServlet(); } else { accessTokenResponse = oAuthClientService.getAccessTokenResponse(); } @@ -207,8 +197,7 @@ private boolean authenticate(@Nullable String code, @Nullable String redirectUri startAuthorizationFlow(); return false; } catch (IOException e) { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage()); - prepareReconnection(code, redirectUri); + prepareReconnection(getConfiguration().reconnectInterval, e.getMessage(), code, redirectUri); return false; } @@ -227,7 +216,7 @@ private void startAuthorizationFlow() { servlet.startListening(); grantServlet = Optional.of(servlet); 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(); } @@ -235,11 +224,15 @@ public ApiHandlerConfiguration getConfiguration() { 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(); freeConnectJob(); - connectJob = Optional.of(scheduler.schedule(() -> openConnection(code, redirectUri), - getConfiguration().reconnectInterval, TimeUnit.SECONDS)); + connectJob = Optional.of(scheduler.schedule(() -> openConnection(code, redirectUri), delay, TimeUnit.SECONDS)); + logger.debug("Reconnection scheduled in {} seconds", delay); } private void freeConnectJob() { @@ -247,6 +240,11 @@ private void freeConnectJob() { connectJob = Optional.empty(); } + private void freeGrantServlet() { + grantServlet.ifPresent(servlet -> servlet.dispose()); + grantServlet = Optional.empty(); + } + @Override public void dispose() { logger.debug("Shutting down Netatmo API bridge handler."); @@ -254,8 +252,7 @@ public void dispose() { webHookServlet.ifPresent(servlet -> servlet.dispose()); webHookServlet = Optional.empty(); - grantServlet.ifPresent(servlet -> servlet.dispose()); - grantServlet = Optional.empty(); + freeGrantServlet(); connectApi.dispose(); freeConnectJob(); @@ -280,13 +277,12 @@ public void handleCommand(ChannelUID channelUID, Command command) { public @Nullable T getRestManager(Class clazz) { if (!managers.containsKey(clazz)) { try { - Constructor constructor = clazz.getConstructor(getClass()); - T instance = constructor.newInstance(this); + T instance = clazz.getConstructor(getClass()).newInstance(this); Set expected = instance.getRequiredScopes(); if (connectApi.matchesScopes(expected)) { managers.put(clazz, instance); } 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) { logger.warn("Error invoking RestManager constructor for class {}: {}", clazz, e.getMessage()); @@ -303,7 +299,7 @@ public synchronized T executeUri(URI uri, HttpMethod method, Class clazz, Request request = httpClient.newRequest(uri).method(method).timeout(TIMEOUT_S, TimeUnit.SECONDS); if (!authenticate(null, null)) { - prepareReconnection(null, null); + prepareReconnection(getConfiguration().reconnectInterval, "@text/status-bridge-offline", null, null); throw new NetatmoException("Not authenticated"); } connectApi.getAuthorization().ifPresent(auth -> request.header(HttpHeader.AUTHORIZATION, auth)); @@ -345,29 +341,30 @@ public synchronized T executeUri(URI uri, HttpMethod method, Class clazz, try { exception = new NetatmoException(deserializer.deserialize(ApiError.class, responseBody)); } 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())); + } } - 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(); + 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 e; + throw exception; } catch (InterruptedException e) { Thread.currentThread().interrupt(); updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage()); - throw new NetatmoException("Request interrupted"); + throw new NetatmoException(e, "Request interrupted"); } catch (TimeoutException | ExecutionException e) { 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); } - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "@text/request-time-out"); - prepareReconnection(null, null); - throw new NetatmoException(String.format("%s: \"%s\"", e.getClass().getName(), e.getMessage())); + prepareReconnection(getConfiguration().reconnectInterval, "@text/request-time-out", null, e.getMessage()); + throw new NetatmoException("%s: \"%s\"".formatted(e.getClass().getName(), e.getMessage())); } } diff --git a/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/handler/capability/AlarmEventCapability.java b/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/handler/capability/AlarmEventCapability.java index 44a27ccb1b375..239f4f27c30b9 100644 --- a/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/handler/capability/AlarmEventCapability.java +++ b/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/handler/capability/AlarmEventCapability.java @@ -49,6 +49,7 @@ protected void updateWebhookEvent(WebhookEvent event) { handler.updateState(GROUP_LAST_EVENT, CHANNEL_EVENT_TIME, toDateTimeType(event.getTime())); handler.updateState(GROUP_LAST_EVENT, CHANNEL_EVENT_SUBTYPE, Objects.requireNonNull( event.getSubTypeDescription().map(ChannelTypeUtils::toStringType).orElse(UnDefType.NULL))); + final String message = event.getName(); handler.updateState(GROUP_LAST_EVENT, CHANNEL_EVENT_MESSAGE, message == null || message.isBlank() ? UnDefType.NULL : toStringType(message)); diff --git a/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/handler/capability/CameraCapability.java b/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/handler/capability/CameraCapability.java index 8095dad4b7459..d6c07f50e89d2 100644 --- a/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/handler/capability/CameraCapability.java +++ b/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/handler/capability/CameraCapability.java @@ -73,7 +73,7 @@ public CameraCapability(CommonInterface handler, NetatmoDescriptionProvider desc List channelHelpers) { super(handler, descriptionProvider, channelHelpers); 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( "CameraCapability must find a CameraChannelHelper, please file a bug report.")); } diff --git a/bundles/org.openhab.binding.netatmo/src/main/resources/OH-INF/i18n/netatmo.properties b/bundles/org.openhab.binding.netatmo/src/main/resources/OH-INF/i18n/netatmo.properties index 32c9a5185bbe0..f88ca9f90d299 100644 --- a/bundles/org.openhab.binding.netatmo/src/main/resources/OH-INF/i18n/netatmo.properties +++ b/bundles/org.openhab.binding.netatmo/src/main/resources/OH-INF/i18n/netatmo.properties @@ -465,7 +465,7 @@ device-not-connected = Thing is not reachable data-over-limit = Data seems quite old request-time-out = Request timed out - will attempt reconnection later 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-internal-error = Internal error