[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:
parent
6a6fe00b7b
commit
1fafec5d11
|
@ -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 "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
|
@ -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);
|
||||||
|
|
|
@ -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");
|
||||||
}
|
}
|
||||||
|
|
|
@ -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.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR,
|
|
||||||
"@text/offline.conf-error.oauth2-unauthorized");
|
|
||||||
} else {
|
|
||||||
updateStatus(ThingStatus.ONLINE);
|
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 {
|
||||||
|
|
|
@ -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());
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue