From af874dec34928221058267c1a5e5d57144d7b64d Mon Sep 17 00:00:00 2001 From: Jacob Laursen Date: Sat, 6 May 2023 23:18:37 +0200 Subject: [PATCH 1/4] Delete OAuth2 token when thing is removed Signed-off-by: Jacob Laursen --- .../internal/handler/BoschAccountHandler.java | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/handler/BoschAccountHandler.java b/bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/handler/BoschAccountHandler.java index 69ffed8fd5421..0799d089ccbf9 100644 --- a/bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/handler/BoschAccountHandler.java +++ b/bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/handler/BoschAccountHandler.java @@ -61,7 +61,7 @@ public BoschAccountHandler(Bridge bridge, HttpClient httpClient, OAuthFactory oA 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); } @@ -72,7 +72,7 @@ public void initialize() { scheduler.execute(() -> { try { - AccessTokenResponse accessTokenResponse = this.oAuthClientService.getAccessTokenResponse(); + AccessTokenResponse accessTokenResponse = oAuthClientService.getAccessTokenResponse(); if (accessTokenResponse == null) { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, "@text/offline.conf-error.oauth2-unauthorized"); @@ -91,13 +91,19 @@ public void initialize() { @Override public void dispose() { - oAuthFactory.ungetOAuthService(this.getThing().getUID().getAsString()); + oAuthFactory.ungetOAuthService(thing.getUID().getAsString()); } @Override public void handleCommand(ChannelUID channelUID, Command command) { } + @Override + public void handleRemoval() { + oAuthFactory.deleteServiceAndAccessToken(thing.getUID().getAsString()); + super.handleRemoval(); + } + @Override public Collection> getServices() { return List.of(IndegoDiscoveryService.class); From 29dd6d8a4eb8caf5bd5f95a662f4bb412c2c9eb5 Mon Sep 17 00:00:00 2001 From: Jacob Laursen Date: Sun, 7 May 2023 09:08:39 +0200 Subject: [PATCH 2/4] Fix reinitialization Signed-off-by: Jacob Laursen --- .../boschindego/internal/handler/BoschAccountHandler.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/handler/BoschAccountHandler.java b/bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/handler/BoschAccountHandler.java index 0799d089ccbf9..cf4744fb29156 100644 --- a/bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/handler/BoschAccountHandler.java +++ b/bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/handler/BoschAccountHandler.java @@ -68,6 +68,12 @@ public BoschAccountHandler(Bridge bridge, HttpClient httpClient, OAuthFactory oA @Override public void initialize() { + OAuthClientService oAuthClientService = oAuthFactory.getOAuthClientService(thing.getUID().getAsString()); + if (oAuthClientService == null) { + throw new IllegalStateException("OAuth handle doesn't exist"); + } + this.oAuthClientService = oAuthClientService; + updateStatus(ThingStatus.UNKNOWN); scheduler.execute(() -> { From 0981df51ea5b8590120b33a0cff8d622cc19b21d Mon Sep 17 00:00:00 2001 From: Jacob Laursen Date: Sun, 7 May 2023 13:29:56 +0200 Subject: [PATCH 3/4] Introduce abstraction for OAuthClientService Signed-off-by: Jacob Laursen --- .../internal/AuthorizationController.java | 92 +++++++++++++++++++ .../internal/AuthorizationProvider.java | 34 +++++++ .../internal/IndegoController.java | 55 ++--------- .../internal/IndegoDeviceController.java | 8 +- .../internal/handler/BoschAccountHandler.java | 25 +++-- .../internal/handler/BoschIndegoHandler.java | 8 +- 6 files changed, 153 insertions(+), 69 deletions(-) create mode 100644 bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/AuthorizationController.java create mode 100644 bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/AuthorizationProvider.java 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 index 0000000000000..3feedb12343be --- /dev/null +++ b/bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/AuthorizationController.java @@ -0,0 +1,92 @@ +/** + * 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.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.OAuthResponseException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * 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 Logger logger = LoggerFactory.getLogger(AuthorizationController.class); + + private OAuthClientService oAuthClientService; + + public AuthorizationController(OAuthClientService oAuthClientService) { + this.oAuthClientService = oAuthClientService; + } + + public void setOAuthClientService(OAuthClientService oAuthClientService) { + this.oAuthClientService = oAuthClientService; + } + + public String getAuthorizationHeader() throws IndegoException { + final AccessTokenResponse accessTokenResponse; + try { + accessTokenResponse = getAccessToken(); + } 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.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(); + } + + 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/AuthorizationProvider.java b/bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/AuthorizationProvider.java new file mode 100644 index 0000000000000..e016b2a3577fc --- /dev/null +++ b/bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/AuthorizationProvider.java @@ -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; +} diff --git a/bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/IndegoController.java b/bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/IndegoController.java index 5cd191307ba7b..22c734d742729 100644 --- a/bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/IndegoController.java +++ b/bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/IndegoController.java @@ -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.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 DevicePropertiesResponse getDeviceProperties(String serialNumber) 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 @@ protected T getRequest(String path, Class dtoClass) 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 @@ protected RawType getRawRequest(String path) throws IndegoAuthenticationExceptio 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 @@ protected void putPostRequest(HttpMethod method, String path, @Nullable Object r 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); diff --git a/bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/IndegoDeviceController.java b/bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/IndegoDeviceController.java index eb506b771667b..72bff9f463cb7 100644 --- a/bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/IndegoDeviceController.java +++ b/bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/IndegoDeviceController.java @@ -35,7 +35,6 @@ 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"); } diff --git a/bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/handler/BoschAccountHandler.java b/bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/handler/BoschAccountHandler.java index cf4744fb29156..b67d5c5f625c1 100644 --- a/bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/handler/BoschAccountHandler.java +++ b/bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/handler/BoschAccountHandler.java @@ -21,12 +21,13 @@ 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.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; @@ -54,6 +55,7 @@ public class BoschAccountHandler extends BaseBridgeHandler { private final OAuthFactory oAuthFactory; private OAuthClientService oAuthClientService; + private AuthorizationController authorizationController; private IndegoController controller; public BoschAccountHandler(Bridge bridge, HttpClient httpClient, OAuthFactory oAuthFactory) { @@ -63,7 +65,8 @@ public BoschAccountHandler(Bridge bridge, HttpClient httpClient, OAuthFactory oA 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); + controller = new IndegoController(httpClient, authorizationController); } @Override @@ -72,19 +75,15 @@ public void initialize() { 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 = 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"); @@ -110,6 +109,10 @@ public void handleRemoval() { super.handleRemoval(); } + public AuthorizationProvider getAuthorizationProvider() { + return authorizationController; + } + @Override public Collection> getServices() { return List.of(IndegoDiscoveryService.class); @@ -129,10 +132,6 @@ public void authorize(String authCode) throws IndegoAuthenticationException { updateStatus(ThingStatus.ONLINE); } - public OAuthClientService getOAuthClientService() { - return oAuthClientService; - } - public Collection getDevices() throws IndegoException { Collection serialNumbers = controller.getSerialNumbers(); List devices = new ArrayList(serialNumbers.size()); diff --git a/bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/handler/BoschIndegoHandler.java b/bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/handler/BoschIndegoHandler.java index 672a9756cd757..9a2b3256c2cad 100644 --- a/bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/handler/BoschIndegoHandler.java +++ b/bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/handler/BoschIndegoHandler.java @@ -28,6 +28,7 @@ import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.eclipse.jetty.client.HttpClient; +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 +42,6 @@ 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; @@ -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; @@ -132,7 +132,7 @@ public void initialize() { ThingHandler handler = bridge.getHandler(); if (handler instanceof BoschAccountHandler accountHandler) { - this.oAuthClientService = accountHandler.getOAuthClientService(); + this.authorizationProvider = accountHandler.getAuthorizationProvider(); } else { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, "@text/offline.conf-error.missing-bridge"); @@ -142,7 +142,7 @@ public void initialize() { 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(); From f8c55e07194f4d271deeca7f44b903dfcb01ab99 Mon Sep 17 00:00:00 2001 From: Jacob Laursen Date: Sun, 7 May 2023 23:04:55 +0200 Subject: [PATCH 4/4] Improve thing status synchronization Signed-off-by: Jacob Laursen --- .../internal/AuthorizationController.java | 37 +++++++++------ .../internal/AuthorizationListener.java | 40 ++++++++++++++++ .../internal/handler/BoschAccountHandler.java | 43 +++++++++++++++-- .../internal/handler/BoschIndegoHandler.java | 46 ++++++++++++------- .../OH-INF/i18n/boschindego.properties | 1 + 5 files changed, 135 insertions(+), 32 deletions(-) create mode 100644 bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/AuthorizationListener.java 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 index 3feedb12343be..0482e3e1ee609 100644 --- 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 @@ -18,13 +18,10 @@ import org.eclipse.jdt.annotation.NonNullByDefault; 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.OAuthResponseException; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; /** * The {@link AuthorizationController} acts as a bridge between @@ -37,40 +34,54 @@ public class AuthorizationController implements AuthorizationProvider { private static final String BEARER = "Bearer "; - private final Logger logger = LoggerFactory.getLogger(AuthorizationController.class); + private final AuthorizationListener listener; private OAuthClientService oAuthClientService; - public AuthorizationController(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 IndegoException { + public String getAuthorizationHeader() throws IndegoAuthenticationException { final AccessTokenResponse accessTokenResponse; try { accessTokenResponse = getAccessToken(); } catch (OAuthException | OAuthResponseException e) { - logger.debug("Error fetching access token: {}", e.getMessage(), e); - throw new IndegoAuthenticationException( + 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) { - throw new IndegoException("An unexpected IOException occurred: " + e.getMessage(), e); + var throwable = new IndegoAuthenticationException("An unexpected IOException occurred: " + e.getMessage(), + e); + listener.onFailedAuthorization(throwable); + throw throwable; } - if (accessTokenResponse.getAccessToken() == null || accessTokenResponse.getAccessToken().isEmpty()) { - throw new IndegoAuthenticationException( + + 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()) { - throw new IndegoAuthenticationException("No refresh token. Please reauthorize -> " + getAuthorizationUrl()); + var throwable = new IndegoAuthenticationException( + "No refresh token. Please reauthorize -> " + getAuthorizationUrl()); + listener.onFailedAuthorization(throwable); + throw throwable; } - return BEARER + accessTokenResponse.getAccessToken(); + listener.onSuccessfulAuthorization(); + + return BEARER + accessToken; } public AccessTokenResponse getAccessToken() throws OAuthException, OAuthResponseException, IOException { 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 index 0000000000000..a57c1f4c439d6 --- /dev/null +++ b/bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/AuthorizationListener.java @@ -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/handler/BoschAccountHandler.java b/bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/handler/BoschAccountHandler.java index b67d5c5f625c1..cb8624915a491 100644 --- a/bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/handler/BoschAccountHandler.java +++ b/bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/handler/BoschAccountHandler.java @@ -18,10 +18,13 @@ 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; @@ -49,10 +52,11 @@ * @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 authorizationListeners = ConcurrentHashMap.newKeySet(); private OAuthClientService oAuthClientService; private AuthorizationController authorizationController; @@ -65,7 +69,7 @@ public BoschAccountHandler(Bridge bridge, HttpClient httpClient, OAuthFactory oA oAuthClientService = oAuthFactory.createOAuthClientService(thing.getUID().getAsString(), BSK_TOKEN_URI, BSK_AUTH_URI, BSK_CLIENT_ID, null, BSK_SCOPE, false); - authorizationController = new AuthorizationController(oAuthClientService); + authorizationController = new AuthorizationController(oAuthClientService, this); controller = new IndegoController(httpClient, authorizationController); } @@ -97,6 +101,7 @@ public void initialize() { @Override public void dispose() { oAuthFactory.ungetOAuthService(thing.getUID().getAsString()); + authorizationListeners.clear(); } @Override @@ -113,6 +118,36 @@ 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> getServices() { return List.of(IndegoDiscoveryService.class); @@ -129,7 +164,9 @@ public void authorize(String authCode) throws IndegoAuthenticationException { logger.info("Authorization completed successfully"); - updateStatus(ThingStatus.ONLINE); + updateStatus(ThingStatus.ONLINE, ThingStatusDetail.NONE, "@text/online.authorization-completed"); + + authorizationListeners.forEach(l -> l.onAuthorizationFlowCompleted()); } public Collection getDevices() throws IndegoException { diff --git a/bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/handler/BoschIndegoHandler.java b/bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/handler/BoschIndegoHandler.java index 9a2b3256c2cad..9cf3fdd9e509b 100644 --- a/bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/handler/BoschIndegoHandler.java +++ b/bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/handler/BoschIndegoHandler.java @@ -28,6 +28,7 @@ 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; @@ -59,7 +60,6 @@ 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 @@ * @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"; @@ -130,9 +130,9 @@ public void initialize() { return; } - ThingHandler handler = bridge.getHandler(); - if (handler instanceof BoschAccountHandler accountHandler) { - this.authorizationProvider = accountHandler.getAuthorizationProvider(); + 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"); @@ -155,13 +155,25 @@ public void initialize() { 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 @@ private boolean rescheduleStatePoll(int delaySeconds, int refreshIntervalSeconds @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 void handleCommand(ChannelUID channelUID, Command command) { 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 @@ private void refreshStateWithExceptionHandling() { 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 @@ private void refreshCuttingTimesWithExceptionHandling() { 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 @@ private void refreshNextCuttingTimeWithExceptionHandling() { 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()); } diff --git a/bundles/org.openhab.binding.boschindego/src/main/resources/OH-INF/i18n/boschindego.properties b/bundles/org.openhab.binding.boschindego/src/main/resources/OH-INF/i18n/boschindego.properties index 8d7cccf4ef321..079403e7a9bc3 100644 --- a/bundles/org.openhab.binding.boschindego/src/main/resources/OH-INF/i18n/boschindego.properties +++ b/bundles/org.openhab.binding.boschindego/src/main/resources/OH-INF/i18n/boschindego.properties @@ -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