[boschindego] Refactor OAuth2 implementation (#14950)

* 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>
This commit is contained in:
Jacob Laursen 2023-05-11 10:51:16 +02:00 committed by GitHub
parent 6a6fe00b7b
commit 1fafec5d11
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 286 additions and 87 deletions

View File

@ -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 "";
}
}
}

View File

@ -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();
}

View File

@ -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;
}

View File

@ -12,9 +12,6 @@
*/ */
package org.openhab.binding.boschindego.internal; package org.openhab.binding.boschindego.internal;
import static org.openhab.binding.boschindego.internal.BoschIndegoBindingConstants.*;
import java.io.IOException;
import java.time.Instant; import java.time.Instant;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collection; 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.IndegoInvalidCommandException;
import org.openhab.binding.boschindego.internal.exceptions.IndegoInvalidResponseException; import org.openhab.binding.boschindego.internal.exceptions.IndegoInvalidResponseException;
import org.openhab.binding.boschindego.internal.exceptions.IndegoTimeoutException; 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.openhab.core.library.types.RawType;
import org.osgi.framework.FrameworkUtil; import org.osgi.framework.FrameworkUtil;
import org.slf4j.Logger; 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 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 CONTENT_TYPE_HEADER = "application/json";
private static final String BEARER = "Bearer ";
private final Logger logger = LoggerFactory.getLogger(IndegoController.class); private final Logger logger = LoggerFactory.getLogger(IndegoController.class);
private final Gson gson = new GsonBuilder().registerTypeAdapter(Instant.class, new InstantDeserializer()).create(); private final Gson gson = new GsonBuilder().registerTypeAdapter(Instant.class, new InstantDeserializer()).create();
private final HttpClient httpClient; private final HttpClient httpClient;
private final OAuthClientService oAuthClientService; private final AuthorizationProvider authorizationProvider;
private final String userAgent; private final String userAgent;
/** /**
* Initialize the controller instance. * Initialize the controller instance.
* *
* @param httpClient the HttpClient for communicating with the service * @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.httpClient = httpClient;
this.oAuthClientService = oAuthClientService; this.authorizationProvider = authorizationProvider;
userAgent = "openHAB/" + FrameworkUtil.getBundle(this.getClass()).getVersion().toString(); userAgent = "openHAB/" + FrameworkUtil.getBundle(this.getClass()).getVersion().toString();
} }
@ -112,39 +104,6 @@ public class IndegoController {
return getRequest(SERIAL_NUMBER_SUBPATH + serialNumber + "/", DevicePropertiesResponse.class); 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. * Sends a GET request to the server and returns the deserialized JSON response.
* *
@ -160,7 +119,7 @@ public class IndegoController {
int status = 0; int status = 0;
try { try {
Request request = httpClient.newRequest(BASE_URL + path).method(HttpMethod.GET) 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()) { if (logger.isTraceEnabled()) {
logger.trace("GET request for {}", BASE_URL + path); logger.trace("GET request for {}", BASE_URL + path);
} }
@ -226,7 +185,7 @@ public class IndegoController {
int status = 0; int status = 0;
try { try {
Request request = httpClient.newRequest(BASE_URL + path).method(HttpMethod.GET) 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()) { if (logger.isTraceEnabled()) {
logger.trace("GET request for {}", BASE_URL + path); logger.trace("GET request for {}", BASE_URL + path);
} }
@ -312,7 +271,7 @@ public class IndegoController {
throws IndegoAuthenticationException, IndegoException { throws IndegoAuthenticationException, IndegoException {
try { try {
Request request = httpClient.newRequest(BASE_URL + path).method(method) 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); .header(HttpHeader.CONTENT_TYPE, CONTENT_TYPE_HEADER).agent(userAgent);
if (requestDto != null) { if (requestDto != null) {
String payload = gson.toJson(requestDto); String payload = gson.toJson(requestDto);

View 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.IndegoInvalidCommandException;
import org.openhab.binding.boschindego.internal.exceptions.IndegoInvalidResponseException; import org.openhab.binding.boschindego.internal.exceptions.IndegoInvalidResponseException;
import org.openhab.binding.boschindego.internal.exceptions.IndegoTimeoutException; import org.openhab.binding.boschindego.internal.exceptions.IndegoTimeoutException;
import org.openhab.core.auth.client.oauth2.OAuthClientService;
import org.openhab.core.library.types.RawType; import org.openhab.core.library.types.RawType;
/** /**
@ -61,11 +60,12 @@ public class IndegoDeviceController extends IndegoController {
* Initialize the controller instance. * Initialize the controller instance.
* *
* @param httpClient the HttpClient for communicating with the service * @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 * @param serialNumber the serial number of the device instance
*/ */
public IndegoDeviceController(HttpClient httpClient, OAuthClientService oAuthClientService, String serialNumber) { public IndegoDeviceController(HttpClient httpClient, AuthorizationProvider authorizationProvider,
super(httpClient, oAuthClientService); String serialNumber) {
super(httpClient, authorizationProvider);
if (serialNumber.isBlank()) { if (serialNumber.isBlank()) {
throw new IllegalArgumentException("Serial number must be provided"); throw new IllegalArgumentException("Serial number must be provided");
} }

View File

@ -18,15 +18,19 @@ import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.List; import java.util.List;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jetty.client.HttpClient; 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.IndegoController;
import org.openhab.binding.boschindego.internal.discovery.IndegoDiscoveryService; import org.openhab.binding.boschindego.internal.discovery.IndegoDiscoveryService;
import org.openhab.binding.boschindego.internal.dto.response.DevicePropertiesResponse; import org.openhab.binding.boschindego.internal.dto.response.DevicePropertiesResponse;
import org.openhab.binding.boschindego.internal.exceptions.IndegoAuthenticationException; import org.openhab.binding.boschindego.internal.exceptions.IndegoAuthenticationException;
import org.openhab.binding.boschindego.internal.exceptions.IndegoException; 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.OAuthClientService;
import org.openhab.core.auth.client.oauth2.OAuthException; import org.openhab.core.auth.client.oauth2.OAuthException;
import org.openhab.core.auth.client.oauth2.OAuthFactory; import org.openhab.core.auth.client.oauth2.OAuthFactory;
@ -48,12 +52,14 @@ import org.slf4j.LoggerFactory;
* @author Jacob Laursen - Initial contribution * @author Jacob Laursen - Initial contribution
*/ */
@NonNullByDefault @NonNullByDefault
public class BoschAccountHandler extends BaseBridgeHandler { public class BoschAccountHandler extends BaseBridgeHandler implements AuthorizationListener {
private final Logger logger = LoggerFactory.getLogger(BoschAccountHandler.class); private final Logger logger = LoggerFactory.getLogger(BoschAccountHandler.class);
private final OAuthFactory oAuthFactory; private final OAuthFactory oAuthFactory;
private final Set<AuthorizationListener> authorizationListeners = ConcurrentHashMap.newKeySet();
private OAuthClientService oAuthClientService; private OAuthClientService oAuthClientService;
private AuthorizationController authorizationController;
private IndegoController controller; private IndegoController controller;
public BoschAccountHandler(Bridge bridge, HttpClient httpClient, OAuthFactory oAuthFactory) { public BoschAccountHandler(Bridge bridge, HttpClient httpClient, OAuthFactory oAuthFactory) {
@ -61,24 +67,27 @@ public class BoschAccountHandler extends BaseBridgeHandler {
this.oAuthFactory = oAuthFactory; 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); 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 @Override
public void initialize() { 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); updateStatus(ThingStatus.UNKNOWN);
scheduler.execute(() -> { scheduler.execute(() -> {
try { try {
AccessTokenResponse accessTokenResponse = this.oAuthClientService.getAccessTokenResponse(); authorizationController.getAccessToken();
if (accessTokenResponse == null) { updateStatus(ThingStatus.ONLINE);
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR,
"@text/offline.conf-error.oauth2-unauthorized");
} else {
updateStatus(ThingStatus.ONLINE);
}
} catch (OAuthException | OAuthResponseException e) { } catch (OAuthException | OAuthResponseException e) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR,
"@text/offline.conf-error.oauth2-unauthorized"); "@text/offline.conf-error.oauth2-unauthorized");
@ -91,13 +100,54 @@ public class BoschAccountHandler extends BaseBridgeHandler {
@Override @Override
public void dispose() { public void dispose() {
oAuthFactory.ungetOAuthService(this.getThing().getUID().getAsString()); oAuthFactory.ungetOAuthService(thing.getUID().getAsString());
authorizationListeners.clear();
} }
@Override @Override
public void handleCommand(ChannelUID channelUID, Command command) { 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 @Override
public Collection<Class<? extends ThingHandlerService>> getServices() { public Collection<Class<? extends ThingHandlerService>> getServices() {
return List.of(IndegoDiscoveryService.class); return List.of(IndegoDiscoveryService.class);
@ -114,11 +164,9 @@ public class BoschAccountHandler extends BaseBridgeHandler {
logger.info("Authorization completed successfully"); logger.info("Authorization completed successfully");
updateStatus(ThingStatus.ONLINE); updateStatus(ThingStatus.ONLINE, ThingStatusDetail.NONE, "@text/online.authorization-completed");
}
public OAuthClientService getOAuthClientService() { authorizationListeners.forEach(l -> l.onAuthorizationFlowCompleted());
return oAuthClientService;
} }
public Collection<DevicePropertiesResponse> getDevices() throws IndegoException { public Collection<DevicePropertiesResponse> getDevices() throws IndegoException {

View File

@ -28,6 +28,8 @@ import java.util.concurrent.TimeUnit;
import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable; import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.client.HttpClient; 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.BoschIndegoTranslationProvider;
import org.openhab.binding.boschindego.internal.DeviceStatus; import org.openhab.binding.boschindego.internal.DeviceStatus;
import org.openhab.binding.boschindego.internal.IndegoDeviceController; 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.IndegoException;
import org.openhab.binding.boschindego.internal.exceptions.IndegoInvalidCommandException; import org.openhab.binding.boschindego.internal.exceptions.IndegoInvalidCommandException;
import org.openhab.binding.boschindego.internal.exceptions.IndegoTimeoutException; 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.i18n.TimeZoneProvider;
import org.openhab.core.library.types.DateTimeType; import org.openhab.core.library.types.DateTimeType;
import org.openhab.core.library.types.DecimalType; 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.ThingStatusDetail;
import org.openhab.core.thing.ThingStatusInfo; import org.openhab.core.thing.ThingStatusInfo;
import org.openhab.core.thing.binding.BaseThingHandler; 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.Command;
import org.openhab.core.types.RefreshType; import org.openhab.core.types.RefreshType;
import org.openhab.core.types.UnDefType; 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 * @author Jacob Laursen - Refactoring, bugfixing and removal of dependency towards abandoned library
*/ */
@NonNullByDefault @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_STROKE_COLOR = "#8c8b6d";
private static final String MAP_POSITION_FILL_COLOR = "#fff701"; private static final String MAP_POSITION_FILL_COLOR = "#fff701";
@ -94,7 +94,7 @@ public class BoschIndegoHandler extends BaseThingHandler {
private final TimeZoneProvider timeZoneProvider; private final TimeZoneProvider timeZoneProvider;
private Instant devicePropertiesUpdated = Instant.MIN; private Instant devicePropertiesUpdated = Instant.MIN;
private @NonNullByDefault({}) OAuthClientService oAuthClientService; private @NonNullByDefault({}) AuthorizationProvider authorizationProvider;
private @NonNullByDefault({}) IndegoDeviceController controller; private @NonNullByDefault({}) IndegoDeviceController controller;
private @Nullable ScheduledFuture<?> statePollFuture; private @Nullable ScheduledFuture<?> statePollFuture;
private @Nullable ScheduledFuture<?> cuttingTimePollFuture; private @Nullable ScheduledFuture<?> cuttingTimePollFuture;
@ -130,9 +130,9 @@ public class BoschIndegoHandler extends BaseThingHandler {
return; return;
} }
ThingHandler handler = bridge.getHandler(); if (bridge.getHandler() instanceof BoschAccountHandler accountHandler) {
if (handler instanceof BoschAccountHandler accountHandler) { authorizationProvider = accountHandler.getAuthorizationProvider();
this.oAuthClientService = accountHandler.getOAuthClientService(); accountHandler.registerAuthorizationListener(this);
} else { } else {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR,
"@text/offline.conf-error.missing-bridge"); "@text/offline.conf-error.missing-bridge");
@ -142,7 +142,7 @@ public class BoschIndegoHandler extends BaseThingHandler {
devicePropertiesUpdated = Instant.MIN; devicePropertiesUpdated = Instant.MIN;
updateProperty(Thing.PROPERTY_SERIAL_NUMBER, config.serialNumber); updateProperty(Thing.PROPERTY_SERIAL_NUMBER, config.serialNumber);
controller = new IndegoDeviceController(httpClient, oAuthClientService, config.serialNumber); controller = new IndegoDeviceController(httpClient, authorizationProvider, config.serialNumber);
updateStatus(ThingStatus.UNKNOWN); updateStatus(ThingStatus.UNKNOWN);
previousStateCode = Optional.empty(); previousStateCode = Optional.empty();
@ -155,13 +155,25 @@ public class BoschIndegoHandler extends BaseThingHandler {
public void bridgeStatusChanged(ThingStatusInfo bridgeStatusInfo) { public void bridgeStatusChanged(ThingStatusInfo bridgeStatusInfo) {
if (bridgeStatusInfo.getStatus() == ThingStatus.ONLINE if (bridgeStatusInfo.getStatus() == ThingStatus.ONLINE
&& getThing().getStatusInfo().getStatus() == ThingStatus.OFFLINE) { && getThing().getStatusInfo().getStatus() == ThingStatus.OFFLINE) {
// Trigger immediate state refresh upon authorization success. updateStatus(ThingStatus.UNKNOWN);
rescheduleStatePoll(0, stateInactiveRefreshIntervalSeconds, true);
} else if (bridgeStatusInfo.getStatus() == ThingStatus.OFFLINE) { } else if (bridgeStatusInfo.getStatus() == ThingStatus.OFFLINE) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_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) { private boolean rescheduleStatePoll(int delaySeconds, int refreshIntervalSeconds, boolean force) {
ScheduledFuture<?> statePollFuture = this.statePollFuture; ScheduledFuture<?> statePollFuture = this.statePollFuture;
if (statePollFuture != null) { if (statePollFuture != null) {
@ -182,6 +194,13 @@ public class BoschIndegoHandler extends BaseThingHandler {
@Override @Override
public void dispose() { public void dispose() {
Bridge bridge = getBridge();
if (bridge != null) {
if (bridge.getHandler() instanceof BoschAccountHandler accountHandler) {
accountHandler.unregisterAuthorizationListener(this);
}
}
ScheduledFuture<?> pollFuture = this.statePollFuture; ScheduledFuture<?> pollFuture = this.statePollFuture;
if (pollFuture != null) { if (pollFuture != null) {
pollFuture.cancel(true); pollFuture.cancel(true);
@ -211,8 +230,7 @@ public class BoschIndegoHandler extends BaseThingHandler {
sendCommand(((DecimalType) command).intValue()); sendCommand(((DecimalType) command).intValue());
} }
} catch (IndegoAuthenticationException e) { } catch (IndegoAuthenticationException e) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, // Ignore, will be handled by bridge
"@text/offline.comm-error.authentication-failure");
} catch (IndegoTimeoutException e) { } catch (IndegoTimeoutException e) {
updateStatus(lastOperatingDataStatus = ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, updateStatus(lastOperatingDataStatus = ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
"@text/offline.comm-error.unreachable"); "@text/offline.comm-error.unreachable");
@ -297,9 +315,7 @@ public class BoschIndegoHandler extends BaseThingHandler {
try { try {
refreshState(); refreshState();
} catch (IndegoAuthenticationException e) { } catch (IndegoAuthenticationException e) {
logger.warn("Failed to authenticate: {}", e.getMessage()); // Ignore, will be handled by bridge
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
"@text/offline.comm-error.authentication-failure");
} catch (IndegoTimeoutException e) { } catch (IndegoTimeoutException e) {
updateStatus(lastOperatingDataStatus = ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, updateStatus(lastOperatingDataStatus = ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
"@text/offline.comm-error.unreachable"); "@text/offline.comm-error.unreachable");
@ -420,8 +436,7 @@ public class BoschIndegoHandler extends BaseThingHandler {
refreshLastCuttingTime(); refreshLastCuttingTime();
refreshNextCuttingTime(); refreshNextCuttingTime();
} catch (IndegoAuthenticationException e) { } catch (IndegoAuthenticationException e) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, // Ignore, will be handled by bridge
"@text/offline.comm-error.authentication-failure");
} catch (IndegoException e) { } catch (IndegoException e) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage()); updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
} }
@ -443,8 +458,7 @@ public class BoschIndegoHandler extends BaseThingHandler {
try { try {
refreshNextCuttingTime(); refreshNextCuttingTime();
} catch (IndegoAuthenticationException e) { } catch (IndegoAuthenticationException e) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, // Ignore, will be handled by bridge
"@text/offline.comm-error.authentication-failure");
} catch (IndegoException e) { } catch (IndegoException e) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage()); updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
} }

View File

@ -58,6 +58,7 @@ offline.conf-error.oauth2-unauthorized = Unauthorized
offline.comm-error.oauth2-authorization-failed = Failed to authorize offline.comm-error.oauth2-authorization-failed = Failed to authorize
offline.comm-error.authentication-failure = Failed to authenticate with Bosch SingleKey ID offline.comm-error.authentication-failure = Failed to authenticate with Bosch SingleKey ID
offline.comm-error.unreachable = Device is unreachable offline.comm-error.unreachable = Device is unreachable
online.authorization-completed = Authorization completed
# indego states # indego states