]> git.basschouten.com Git - openhab-addons.git/commitdiff
[boschindego] Refactor OAuth2 implementation (#14950)
authorJacob Laursen <jacob-github@vindvejr.dk>
Thu, 11 May 2023 08:51:16 +0000 (10:51 +0200)
committerGitHub <noreply@github.com>
Thu, 11 May 2023 08:51:16 +0000 (10:51 +0200)
* Delete OAuth2 token when thing is removed
* Fix reinitialization
* Introduce abstraction for OAuthClientService
* Improve thing status synchronization

---------

Signed-off-by: Jacob Laursen <jacob-github@vindvejr.dk>
bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/AuthorizationController.java [new file with mode: 0644]
bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/AuthorizationListener.java [new file with mode: 0644]
bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/AuthorizationProvider.java [new file with mode: 0644]
bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/IndegoController.java
bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/IndegoDeviceController.java
bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/handler/BoschAccountHandler.java
bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/handler/BoschIndegoHandler.java
bundles/org.openhab.binding.boschindego/src/main/resources/OH-INF/i18n/boschindego.properties

diff --git a/bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/AuthorizationController.java b/bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/AuthorizationController.java
new file mode 100644 (file)
index 0000000..0482e3e
--- /dev/null
@@ -0,0 +1,103 @@
+/**
+ * Copyright (c) 2010-2023 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.boschindego.internal;
+
+import static org.openhab.binding.boschindego.internal.BoschIndegoBindingConstants.*;
+
+import java.io.IOException;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.boschindego.internal.exceptions.IndegoAuthenticationException;
+import org.openhab.core.auth.client.oauth2.AccessTokenResponse;
+import org.openhab.core.auth.client.oauth2.OAuthClientService;
+import org.openhab.core.auth.client.oauth2.OAuthException;
+import org.openhab.core.auth.client.oauth2.OAuthResponseException;
+
+/**
+ * The {@link AuthorizationController} acts as a bridge between
+ * {@link OAuthClientService} and {@link IndegoController}.
+ * 
+ * @author Jacob Laursen - Initial contribution
+ */
+@NonNullByDefault
+public class AuthorizationController implements AuthorizationProvider {
+
+    private static final String BEARER = "Bearer ";
+
+    private final AuthorizationListener listener;
+
+    private OAuthClientService oAuthClientService;
+
+    public AuthorizationController(OAuthClientService oAuthClientService, AuthorizationListener listener) {
+        this.oAuthClientService = oAuthClientService;
+        this.listener = listener;
+    }
+
+    public void setOAuthClientService(OAuthClientService oAuthClientService) {
+        this.oAuthClientService = oAuthClientService;
+    }
+
+    public String getAuthorizationHeader() throws IndegoAuthenticationException {
+        final AccessTokenResponse accessTokenResponse;
+        try {
+            accessTokenResponse = getAccessToken();
+        } catch (OAuthException | OAuthResponseException e) {
+            var throwable = new IndegoAuthenticationException(
+                    "Error fetching access token. Invalid authcode? Please generate a new one -> "
+                            + getAuthorizationUrl(),
+                    e);
+            listener.onFailedAuthorization(throwable);
+            throw throwable;
+        } catch (IOException e) {
+            var throwable = new IndegoAuthenticationException("An unexpected IOException occurred: " + e.getMessage(),
+                    e);
+            listener.onFailedAuthorization(throwable);
+            throw throwable;
+        }
+
+        String accessToken = accessTokenResponse.getAccessToken();
+        if (accessToken == null || accessToken.isEmpty()) {
+            var throwable = new IndegoAuthenticationException(
+                    "No access token. Is this thing authorized? -> " + getAuthorizationUrl());
+            listener.onFailedAuthorization(throwable);
+            throw throwable;
+        }
+        if (accessTokenResponse.getRefreshToken() == null || accessTokenResponse.getRefreshToken().isEmpty()) {
+            var throwable = new IndegoAuthenticationException(
+                    "No refresh token. Please reauthorize -> " + getAuthorizationUrl());
+            listener.onFailedAuthorization(throwable);
+            throw throwable;
+        }
+
+        listener.onSuccessfulAuthorization();
+
+        return BEARER + accessToken;
+    }
+
+    public AccessTokenResponse getAccessToken() throws OAuthException, OAuthResponseException, IOException {
+        AccessTokenResponse accessTokenResponse = oAuthClientService.getAccessTokenResponse();
+        if (accessTokenResponse == null) {
+            throw new OAuthException("No access token response");
+        }
+
+        return accessTokenResponse;
+    }
+
+    private String getAuthorizationUrl() {
+        try {
+            return oAuthClientService.getAuthorizationUrl(BSK_REDIRECT_URI, BSK_SCOPE, null);
+        } catch (OAuthException e) {
+            return "";
+        }
+    }
+}
diff --git a/bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/AuthorizationListener.java b/bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/AuthorizationListener.java
new file mode 100644 (file)
index 0000000..a57c1f4
--- /dev/null
@@ -0,0 +1,40 @@
+/**
+ * Copyright (c) 2010-2023 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.boschindego.internal;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * {@link} AuthorizationListener} is used for notifying {@link BoschAccountHandler}
+ * when authorization state has changed and for notifying {@link BoschIndegoHandler}
+ * when authorization flow is completed.
+ *
+ * @author Jacob Laursen - Initial contribution
+ */
+@NonNullByDefault
+public interface AuthorizationListener {
+    /**
+     * Called upon successful OAuth authorization.
+     */
+    void onSuccessfulAuthorization();
+
+    /**
+     * Called upon failed OAuth authorization.
+     */
+    void onFailedAuthorization(Throwable throwable);
+
+    /**
+     * Called upon successful completion of OAuth authorization flow.
+     */
+    void onAuthorizationFlowCompleted();
+}
diff --git a/bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/AuthorizationProvider.java b/bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/AuthorizationProvider.java
new file mode 100644 (file)
index 0000000..e016b2a
--- /dev/null
@@ -0,0 +1,34 @@
+/**
+ * Copyright (c) 2010-2023 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.boschindego.internal;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.boschindego.internal.exceptions.IndegoException;
+
+/**
+ * The {@link AuthorizationProvider} is responsible for providing
+ * authorization headers needed for communicating with the Bosch Indego
+ * cloud services.
+ * 
+ * @author Jacob Laursen - Initial contribution
+ */
+@NonNullByDefault
+public interface AuthorizationProvider {
+    /**
+     * Get HTTP authorization header for authenticating with Bosch Indego services.
+     *
+     * @return the header contents
+     * @throws IndegoException if not authorized
+     */
+    String getAuthorizationHeader() throws IndegoException;
+}
index 5cd191307ba7bba2e95f9967b02efe3048b10cbf..22c734d7427294630723dbd58bffbeb2e2b7130e 100644 (file)
@@ -12,9 +12,6 @@
  */
 package org.openhab.binding.boschindego.internal;
 
-import static org.openhab.binding.boschindego.internal.BoschIndegoBindingConstants.*;
-
-import java.io.IOException;
 import java.time.Instant;
 import java.util.Arrays;
 import java.util.Collection;
@@ -41,10 +38,6 @@ import org.openhab.binding.boschindego.internal.exceptions.IndegoException;
 import org.openhab.binding.boschindego.internal.exceptions.IndegoInvalidCommandException;
 import org.openhab.binding.boschindego.internal.exceptions.IndegoInvalidResponseException;
 import org.openhab.binding.boschindego.internal.exceptions.IndegoTimeoutException;
-import org.openhab.core.auth.client.oauth2.AccessTokenResponse;
-import org.openhab.core.auth.client.oauth2.OAuthClientService;
-import org.openhab.core.auth.client.oauth2.OAuthException;
-import org.openhab.core.auth.client.oauth2.OAuthResponseException;
 import org.openhab.core.library.types.RawType;
 import org.osgi.framework.FrameworkUtil;
 import org.slf4j.Logger;
@@ -66,23 +59,22 @@ public class IndegoController {
 
     private static final String BASE_URL = "https://api.indego-cloud.iot.bosch-si.com/api/v1/";
     private static final String CONTENT_TYPE_HEADER = "application/json";
-    private static final String BEARER = "Bearer ";
 
     private final Logger logger = LoggerFactory.getLogger(IndegoController.class);
     private final Gson gson = new GsonBuilder().registerTypeAdapter(Instant.class, new InstantDeserializer()).create();
     private final HttpClient httpClient;
-    private final OAuthClientService oAuthClientService;
+    private final AuthorizationProvider authorizationProvider;
     private final String userAgent;
 
     /**
      * Initialize the controller instance.
      * 
      * @param httpClient the HttpClient for communicating with the service
-     * @param oAuthClientService the OAuthClientService for authorization
+     * @param authorizationProvider the AuthorizationProvider for authenticating with the service
      */
-    public IndegoController(HttpClient httpClient, OAuthClientService oAuthClientService) {
+    public IndegoController(HttpClient httpClient, AuthorizationProvider authorizationProvider) {
         this.httpClient = httpClient;
-        this.oAuthClientService = oAuthClientService;
+        this.authorizationProvider = authorizationProvider;
         userAgent = "openHAB/" + FrameworkUtil.getBundle(this.getClass()).getVersion().toString();
     }
 
@@ -112,39 +104,6 @@ public class IndegoController {
         return getRequest(SERIAL_NUMBER_SUBPATH + serialNumber + "/", DevicePropertiesResponse.class);
     }
 
-    private String getAuthorizationUrl() {
-        try {
-            return oAuthClientService.getAuthorizationUrl(BSK_REDIRECT_URI, BSK_SCOPE, null);
-        } catch (OAuthException e) {
-            return "";
-        }
-    }
-
-    private String getAuthorizationHeader() throws IndegoException {
-        final AccessTokenResponse accessTokenResponse;
-        try {
-            accessTokenResponse = oAuthClientService.getAccessTokenResponse();
-        } catch (OAuthException | OAuthResponseException e) {
-            logger.debug("Error fetching access token: {}", e.getMessage(), e);
-            throw new IndegoAuthenticationException(
-                    "Error fetching access token. Invalid authcode? Please generate a new one -> "
-                            + getAuthorizationUrl(),
-                    e);
-        } catch (IOException e) {
-            throw new IndegoException("An unexpected IOException occurred: " + e.getMessage(), e);
-        }
-        if (accessTokenResponse == null || accessTokenResponse.getAccessToken() == null
-                || accessTokenResponse.getAccessToken().isEmpty()) {
-            throw new IndegoAuthenticationException(
-                    "No access token. Is this thing authorized? -> " + getAuthorizationUrl());
-        }
-        if (accessTokenResponse.getRefreshToken() == null || accessTokenResponse.getRefreshToken().isEmpty()) {
-            throw new IndegoAuthenticationException("No refresh token. Please reauthorize -> " + getAuthorizationUrl());
-        }
-
-        return BEARER + accessTokenResponse.getAccessToken();
-    }
-
     /**
      * Sends a GET request to the server and returns the deserialized JSON response.
      * 
@@ -160,7 +119,7 @@ public class IndegoController {
         int status = 0;
         try {
             Request request = httpClient.newRequest(BASE_URL + path).method(HttpMethod.GET)
-                    .header(HttpHeader.AUTHORIZATION, getAuthorizationHeader()).agent(userAgent);
+                    .header(HttpHeader.AUTHORIZATION, authorizationProvider.getAuthorizationHeader()).agent(userAgent);
             if (logger.isTraceEnabled()) {
                 logger.trace("GET request for {}", BASE_URL + path);
             }
@@ -226,7 +185,7 @@ public class IndegoController {
         int status = 0;
         try {
             Request request = httpClient.newRequest(BASE_URL + path).method(HttpMethod.GET)
-                    .header(HttpHeader.AUTHORIZATION, getAuthorizationHeader()).agent(userAgent);
+                    .header(HttpHeader.AUTHORIZATION, authorizationProvider.getAuthorizationHeader()).agent(userAgent);
             if (logger.isTraceEnabled()) {
                 logger.trace("GET request for {}", BASE_URL + path);
             }
@@ -312,7 +271,7 @@ public class IndegoController {
             throws IndegoAuthenticationException, IndegoException {
         try {
             Request request = httpClient.newRequest(BASE_URL + path).method(method)
-                    .header(HttpHeader.AUTHORIZATION, getAuthorizationHeader())
+                    .header(HttpHeader.AUTHORIZATION, authorizationProvider.getAuthorizationHeader())
                     .header(HttpHeader.CONTENT_TYPE, CONTENT_TYPE_HEADER).agent(userAgent);
             if (requestDto != null) {
                 String payload = gson.toJson(requestDto);
index eb506b771667b5a47f7a838125a047d0572a56b4..72bff9f463cb73ff745297f53aa51a18ed92e5bc 100644 (file)
@@ -35,7 +35,6 @@ import org.openhab.binding.boschindego.internal.exceptions.IndegoException;
 import org.openhab.binding.boschindego.internal.exceptions.IndegoInvalidCommandException;
 import org.openhab.binding.boschindego.internal.exceptions.IndegoInvalidResponseException;
 import org.openhab.binding.boschindego.internal.exceptions.IndegoTimeoutException;
-import org.openhab.core.auth.client.oauth2.OAuthClientService;
 import org.openhab.core.library.types.RawType;
 
 /**
@@ -61,11 +60,12 @@ public class IndegoDeviceController extends IndegoController {
      * Initialize the controller instance.
      * 
      * @param httpClient the HttpClient for communicating with the service
-     * @param oAuthClientService the OAuthClientService for authorization
+     * @param authorizationProvider the AuthorizationProvider for authenticating with the service
      * @param serialNumber the serial number of the device instance
      */
-    public IndegoDeviceController(HttpClient httpClient, OAuthClientService oAuthClientService, String serialNumber) {
-        super(httpClient, oAuthClientService);
+    public IndegoDeviceController(HttpClient httpClient, AuthorizationProvider authorizationProvider,
+            String serialNumber) {
+        super(httpClient, authorizationProvider);
         if (serialNumber.isBlank()) {
             throw new IllegalArgumentException("Serial number must be provided");
         }
index 69ffed8fd54218a0b59c07de6d56c1aeb7ae48f1..cb8624915a491d7fa3095a75fec6d12417265bcb 100644 (file)
@@ -18,15 +18,19 @@ import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.List;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
 
 import org.eclipse.jdt.annotation.NonNullByDefault;
 import org.eclipse.jetty.client.HttpClient;
+import org.openhab.binding.boschindego.internal.AuthorizationController;
+import org.openhab.binding.boschindego.internal.AuthorizationListener;
+import org.openhab.binding.boschindego.internal.AuthorizationProvider;
 import org.openhab.binding.boschindego.internal.IndegoController;
 import org.openhab.binding.boschindego.internal.discovery.IndegoDiscoveryService;
 import org.openhab.binding.boschindego.internal.dto.response.DevicePropertiesResponse;
 import org.openhab.binding.boschindego.internal.exceptions.IndegoAuthenticationException;
 import org.openhab.binding.boschindego.internal.exceptions.IndegoException;
-import org.openhab.core.auth.client.oauth2.AccessTokenResponse;
 import org.openhab.core.auth.client.oauth2.OAuthClientService;
 import org.openhab.core.auth.client.oauth2.OAuthException;
 import org.openhab.core.auth.client.oauth2.OAuthFactory;
@@ -48,12 +52,14 @@ import org.slf4j.LoggerFactory;
  * @author Jacob Laursen - Initial contribution
  */
 @NonNullByDefault
-public class BoschAccountHandler extends BaseBridgeHandler {
+public class BoschAccountHandler extends BaseBridgeHandler implements AuthorizationListener {
 
     private final Logger logger = LoggerFactory.getLogger(BoschAccountHandler.class);
     private final OAuthFactory oAuthFactory;
+    private final Set<AuthorizationListener> authorizationListeners = ConcurrentHashMap.newKeySet();
 
     private OAuthClientService oAuthClientService;
+    private AuthorizationController authorizationController;
     private IndegoController controller;
 
     public BoschAccountHandler(Bridge bridge, HttpClient httpClient, OAuthFactory oAuthFactory) {
@@ -61,24 +67,27 @@ public class BoschAccountHandler extends BaseBridgeHandler {
 
         this.oAuthFactory = oAuthFactory;
 
-        oAuthClientService = oAuthFactory.createOAuthClientService(getThing().getUID().getAsString(), BSK_TOKEN_URI,
+        oAuthClientService = oAuthFactory.createOAuthClientService(thing.getUID().getAsString(), BSK_TOKEN_URI,
                 BSK_AUTH_URI, BSK_CLIENT_ID, null, BSK_SCOPE, false);
-        controller = new IndegoController(httpClient, oAuthClientService);
+        authorizationController = new AuthorizationController(oAuthClientService, this);
+        controller = new IndegoController(httpClient, authorizationController);
     }
 
     @Override
     public void initialize() {
+        OAuthClientService oAuthClientService = oAuthFactory.getOAuthClientService(thing.getUID().getAsString());
+        if (oAuthClientService == null) {
+            throw new IllegalStateException("OAuth handle doesn't exist");
+        }
+        authorizationController.setOAuthClientService(oAuthClientService);
+        this.oAuthClientService = oAuthClientService;
+
         updateStatus(ThingStatus.UNKNOWN);
 
         scheduler.execute(() -> {
             try {
-                AccessTokenResponse accessTokenResponse = this.oAuthClientService.getAccessTokenResponse();
-                if (accessTokenResponse == null) {
-                    updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR,
-                            "@text/offline.conf-error.oauth2-unauthorized");
-                } else {
-                    updateStatus(ThingStatus.ONLINE);
-                }
+                authorizationController.getAccessToken();
+                updateStatus(ThingStatus.ONLINE);
             } catch (OAuthException | OAuthResponseException e) {
                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR,
                         "@text/offline.conf-error.oauth2-unauthorized");
@@ -91,13 +100,54 @@ public class BoschAccountHandler extends BaseBridgeHandler {
 
     @Override
     public void dispose() {
-        oAuthFactory.ungetOAuthService(this.getThing().getUID().getAsString());
+        oAuthFactory.ungetOAuthService(thing.getUID().getAsString());
+        authorizationListeners.clear();
     }
 
     @Override
     public void handleCommand(ChannelUID channelUID, Command command) {
     }
 
+    @Override
+    public void handleRemoval() {
+        oAuthFactory.deleteServiceAndAccessToken(thing.getUID().getAsString());
+        super.handleRemoval();
+    }
+
+    public AuthorizationProvider getAuthorizationProvider() {
+        return authorizationController;
+    }
+
+    public void registerAuthorizationListener(AuthorizationListener listener) {
+        if (!authorizationListeners.add(listener)) {
+            throw new IllegalStateException("Attempt to register already registered authorization listener");
+        }
+    }
+
+    public void unregisterAuthorizationListener(AuthorizationListener listener) {
+        if (!authorizationListeners.remove(listener)) {
+            throw new IllegalStateException("Attempt to unregister authorization listener which is not registered");
+        }
+    }
+
+    public void onSuccessfulAuthorization() {
+        updateStatus(ThingStatus.ONLINE);
+    }
+
+    public void onFailedAuthorization(Throwable throwable) {
+        logger.debug("Authorization failure", throwable);
+        if (throwable instanceof IndegoAuthenticationException) {
+            updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
+                    "@text/offline.comm-error.authentication-failure");
+        } else {
+            updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, throwable.getMessage());
+        }
+    }
+
+    public void onAuthorizationFlowCompleted() {
+        // Ignore
+    }
+
     @Override
     public Collection<Class<? extends ThingHandlerService>> getServices() {
         return List.of(IndegoDiscoveryService.class);
@@ -114,11 +164,9 @@ public class BoschAccountHandler extends BaseBridgeHandler {
 
         logger.info("Authorization completed successfully");
 
-        updateStatus(ThingStatus.ONLINE);
-    }
+        updateStatus(ThingStatus.ONLINE, ThingStatusDetail.NONE, "@text/online.authorization-completed");
 
-    public OAuthClientService getOAuthClientService() {
-        return oAuthClientService;
+        authorizationListeners.forEach(l -> l.onAuthorizationFlowCompleted());
     }
 
     public Collection<DevicePropertiesResponse> getDevices() throws IndegoException {
index 672a9756cd75745c6fb0a659a2bedd3d0c6cab82..9cf3fdd9e509b5dacee911ebeefc5076dc187332 100644 (file)
@@ -28,6 +28,8 @@ import java.util.concurrent.TimeUnit;
 import org.eclipse.jdt.annotation.NonNullByDefault;
 import org.eclipse.jdt.annotation.Nullable;
 import org.eclipse.jetty.client.HttpClient;
+import org.openhab.binding.boschindego.internal.AuthorizationListener;
+import org.openhab.binding.boschindego.internal.AuthorizationProvider;
 import org.openhab.binding.boschindego.internal.BoschIndegoTranslationProvider;
 import org.openhab.binding.boschindego.internal.DeviceStatus;
 import org.openhab.binding.boschindego.internal.IndegoDeviceController;
@@ -41,7 +43,6 @@ import org.openhab.binding.boschindego.internal.exceptions.IndegoAuthenticationE
 import org.openhab.binding.boschindego.internal.exceptions.IndegoException;
 import org.openhab.binding.boschindego.internal.exceptions.IndegoInvalidCommandException;
 import org.openhab.binding.boschindego.internal.exceptions.IndegoTimeoutException;
-import org.openhab.core.auth.client.oauth2.OAuthClientService;
 import org.openhab.core.i18n.TimeZoneProvider;
 import org.openhab.core.library.types.DateTimeType;
 import org.openhab.core.library.types.DecimalType;
@@ -59,7 +60,6 @@ import org.openhab.core.thing.ThingStatus;
 import org.openhab.core.thing.ThingStatusDetail;
 import org.openhab.core.thing.ThingStatusInfo;
 import org.openhab.core.thing.binding.BaseThingHandler;
-import org.openhab.core.thing.binding.ThingHandler;
 import org.openhab.core.types.Command;
 import org.openhab.core.types.RefreshType;
 import org.openhab.core.types.UnDefType;
@@ -74,7 +74,7 @@ import org.slf4j.LoggerFactory;
  * @author Jacob Laursen - Refactoring, bugfixing and removal of dependency towards abandoned library
  */
 @NonNullByDefault
-public class BoschIndegoHandler extends BaseThingHandler {
+public class BoschIndegoHandler extends BaseThingHandler implements AuthorizationListener {
 
     private static final String MAP_POSITION_STROKE_COLOR = "#8c8b6d";
     private static final String MAP_POSITION_FILL_COLOR = "#fff701";
@@ -94,7 +94,7 @@ public class BoschIndegoHandler extends BaseThingHandler {
     private final TimeZoneProvider timeZoneProvider;
     private Instant devicePropertiesUpdated = Instant.MIN;
 
-    private @NonNullByDefault({}) OAuthClientService oAuthClientService;
+    private @NonNullByDefault({}) AuthorizationProvider authorizationProvider;
     private @NonNullByDefault({}) IndegoDeviceController controller;
     private @Nullable ScheduledFuture<?> statePollFuture;
     private @Nullable ScheduledFuture<?> cuttingTimePollFuture;
@@ -130,9 +130,9 @@ public class BoschIndegoHandler extends BaseThingHandler {
             return;
         }
 
-        ThingHandler handler = bridge.getHandler();
-        if (handler instanceof BoschAccountHandler accountHandler) {
-            this.oAuthClientService = accountHandler.getOAuthClientService();
+        if (bridge.getHandler() instanceof BoschAccountHandler accountHandler) {
+            authorizationProvider = accountHandler.getAuthorizationProvider();
+            accountHandler.registerAuthorizationListener(this);
         } else {
             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR,
                     "@text/offline.conf-error.missing-bridge");
@@ -142,7 +142,7 @@ public class BoschIndegoHandler extends BaseThingHandler {
         devicePropertiesUpdated = Instant.MIN;
         updateProperty(Thing.PROPERTY_SERIAL_NUMBER, config.serialNumber);
 
-        controller = new IndegoDeviceController(httpClient, oAuthClientService, config.serialNumber);
+        controller = new IndegoDeviceController(httpClient, authorizationProvider, config.serialNumber);
 
         updateStatus(ThingStatus.UNKNOWN);
         previousStateCode = Optional.empty();
@@ -155,13 +155,25 @@ public class BoschIndegoHandler extends BaseThingHandler {
     public void bridgeStatusChanged(ThingStatusInfo bridgeStatusInfo) {
         if (bridgeStatusInfo.getStatus() == ThingStatus.ONLINE
                 && getThing().getStatusInfo().getStatus() == ThingStatus.OFFLINE) {
-            // Trigger immediate state refresh upon authorization success.
-            rescheduleStatePoll(0, stateInactiveRefreshIntervalSeconds, true);
+            updateStatus(ThingStatus.UNKNOWN);
         } else if (bridgeStatusInfo.getStatus() == ThingStatus.OFFLINE) {
             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
         }
     }
 
+    public void onSuccessfulAuthorization() {
+        // Ignore
+    }
+
+    public void onFailedAuthorization(Throwable throwable) {
+        // Ignore
+    }
+
+    public void onAuthorizationFlowCompleted() {
+        // Trigger immediate state refresh upon authorization success.
+        rescheduleStatePoll(0, stateInactiveRefreshIntervalSeconds, true);
+    }
+
     private boolean rescheduleStatePoll(int delaySeconds, int refreshIntervalSeconds, boolean force) {
         ScheduledFuture<?> statePollFuture = this.statePollFuture;
         if (statePollFuture != null) {
@@ -182,6 +194,13 @@ public class BoschIndegoHandler extends BaseThingHandler {
 
     @Override
     public void dispose() {
+        Bridge bridge = getBridge();
+        if (bridge != null) {
+            if (bridge.getHandler() instanceof BoschAccountHandler accountHandler) {
+                accountHandler.unregisterAuthorizationListener(this);
+            }
+        }
+
         ScheduledFuture<?> pollFuture = this.statePollFuture;
         if (pollFuture != null) {
             pollFuture.cancel(true);
@@ -211,8 +230,7 @@ public class BoschIndegoHandler extends BaseThingHandler {
                 sendCommand(((DecimalType) command).intValue());
             }
         } catch (IndegoAuthenticationException e) {
-            updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
-                    "@text/offline.comm-error.authentication-failure");
+            // Ignore, will be handled by bridge
         } catch (IndegoTimeoutException e) {
             updateStatus(lastOperatingDataStatus = ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
                     "@text/offline.comm-error.unreachable");
@@ -297,9 +315,7 @@ public class BoschIndegoHandler extends BaseThingHandler {
         try {
             refreshState();
         } catch (IndegoAuthenticationException e) {
-            logger.warn("Failed to authenticate: {}", e.getMessage());
-            updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
-                    "@text/offline.comm-error.authentication-failure");
+            // Ignore, will be handled by bridge
         } catch (IndegoTimeoutException e) {
             updateStatus(lastOperatingDataStatus = ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
                     "@text/offline.comm-error.unreachable");
@@ -420,8 +436,7 @@ public class BoschIndegoHandler extends BaseThingHandler {
             refreshLastCuttingTime();
             refreshNextCuttingTime();
         } catch (IndegoAuthenticationException e) {
-            updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
-                    "@text/offline.comm-error.authentication-failure");
+            // Ignore, will be handled by bridge
         } catch (IndegoException e) {
             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
         }
@@ -443,8 +458,7 @@ public class BoschIndegoHandler extends BaseThingHandler {
         try {
             refreshNextCuttingTime();
         } catch (IndegoAuthenticationException e) {
-            updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
-                    "@text/offline.comm-error.authentication-failure");
+            // Ignore, will be handled by bridge
         } catch (IndegoException e) {
             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
         }
index 8d7cccf4ef32128728f61857966bcf01f385139c..079403e7a9bc37be1cd196504e0e5e0439475b73 100644 (file)
@@ -58,6 +58,7 @@ offline.conf-error.oauth2-unauthorized = Unauthorized
 offline.comm-error.oauth2-authorization-failed = Failed to authorize
 offline.comm-error.authentication-failure = Failed to authenticate with Bosch SingleKey ID
 offline.comm-error.unreachable = Device is unreachable
+online.authorization-completed = Authorization completed
 
 # indego states