[netatmo] Consolidate OAuth2 by using core implementation and storage (#14780)

* Consolidate OAuth2 by using core implementation and storage

Fixes #14755

---------

Signed-off-by: Jacob Laursen <jacob-github@vindvejr.dk>
This commit is contained in:
Jacob Laursen 2023-04-21 20:52:51 +02:00 committed by GitHub
parent 3ae20b776c
commit cf3c3f1025
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 236 additions and 165 deletions

View File

@ -65,7 +65,7 @@ The Account bridge has the following configuration elements:
1. The bridge thing will go _OFFLINE_ / _CONFIGURATION_ERROR_ - this is fine. You have to authorize this bridge with Netatmo Connect.
1. Go to the authorization page of your server. `http://<your openHAB address>:8080/netatmo/connect/<_CLIENT_ID_>`. Your newly added bridge should be listed there (no need for you to expose your openHAB server outside your local network for this).
1. Press the _"Authorize Thing"_ button. This will take you either to the login page of Netatmo Connect or directly to the authorization screen. Login and/or authorize the application. You will be returned and the entry should go green.
1. The bridge configuration will be updated with a refresh token and go _ONLINE_. The refresh token is used to re-authorize the bridge with Netatmo Connect Web API whenever required. So you can consult this token by opening the Thing page in MainUI, this is the value of the advanced parameter named “Refresh Token”.
1. The bridge will go _ONLINE_.
Now that you have got your bridge _ONLINE_ you can now start a scan with the binding to auto discover your things.

View File

@ -42,6 +42,7 @@ import org.openhab.binding.netatmo.internal.handler.capability.RoomCapability;
import org.openhab.binding.netatmo.internal.handler.capability.WeatherCapability;
import org.openhab.binding.netatmo.internal.handler.channelhelper.ChannelHelper;
import org.openhab.binding.netatmo.internal.providers.NetatmoDescriptionProvider;
import org.openhab.core.auth.client.oauth2.OAuthFactory;
import org.openhab.core.config.core.ConfigParser;
import org.openhab.core.io.net.http.HttpClientFactory;
import org.openhab.core.thing.Bridge;
@ -75,15 +76,18 @@ public class NetatmoHandlerFactory extends BaseThingHandlerFactory {
private final NADeserializer deserializer;
private final HttpClient httpClient;
private final HttpService httpService;
private final OAuthFactory oAuthFactory;
@Activate
public NetatmoHandlerFactory(@Reference NetatmoDescriptionProvider stateDescriptionProvider,
@Reference HttpClientFactory factory, @Reference NADeserializer deserializer,
@Reference HttpService httpService, Map<String, @Nullable Object> config) {
public NetatmoHandlerFactory(final @Reference NetatmoDescriptionProvider stateDescriptionProvider,
final @Reference HttpClientFactory factory, final @Reference NADeserializer deserializer,
final @Reference HttpService httpService, final @Reference OAuthFactory oAuthFactory,
Map<String, @Nullable Object> config) {
this.stateDescriptionProvider = stateDescriptionProvider;
this.httpClient = factory.getCommonHttpClient();
this.deserializer = deserializer;
this.httpService = httpService;
this.oAuthFactory = oAuthFactory;
configChanged(config);
}
@ -109,7 +113,8 @@ public class NetatmoHandlerFactory extends BaseThingHandlerFactory {
private BaseThingHandler buildHandler(Thing thing, ModuleType moduleType) {
if (ModuleType.ACCOUNT.equals(moduleType)) {
return new ApiBridgeHandler((Bridge) thing, httpClient, deserializer, configuration, httpService);
return new ApiBridgeHandler((Bridge) thing, httpClient, deserializer, configuration, httpService,
oAuthFactory);
}
CommonInterface handler = moduleType.isABridge() ? new DeviceHandler((Bridge) thing) : new ModuleHandler(thing);

View File

@ -16,14 +16,10 @@ import static org.openhab.binding.netatmo.internal.api.data.NetatmoConstants.*;
import static org.openhab.core.auth.oauth2client.internal.Keyword.*;
import java.net.URI;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.stream.Stream;
import javax.ws.rs.core.UriBuilder;
@ -31,83 +27,41 @@ import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.netatmo.internal.api.data.NetatmoConstants.FeatureArea;
import org.openhab.binding.netatmo.internal.api.data.NetatmoConstants.Scope;
import org.openhab.binding.netatmo.internal.api.dto.AccessTokenResponse;
import org.openhab.binding.netatmo.internal.config.ApiHandlerConfiguration;
import org.openhab.binding.netatmo.internal.handler.ApiBridgeHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link AuthenticationApi} handles oAuth2 authentication and token refreshing
*
* @author Gaël L'hopital - Initial contribution
* @author Jacob Laursen - Refactored to use standard OAuth2 implementation
*/
@NonNullByDefault
public class AuthenticationApi extends RestManager {
private static final URI TOKEN_URI = getApiBaseBuilder(PATH_OAUTH, SUB_PATH_TOKEN).build();
public static final URI TOKEN_URI = getApiBaseBuilder(PATH_OAUTH, SUB_PATH_TOKEN).build();
public static final URI AUTH_URI = getApiBaseBuilder(PATH_OAUTH, SUB_PATH_AUTHORIZE).build();
private final Logger logger = LoggerFactory.getLogger(AuthenticationApi.class);
private final ScheduledExecutorService scheduler;
private Optional<ScheduledFuture<?>> refreshTokenJob = Optional.empty();
private List<Scope> grantedScope = List.of();
private @Nullable String authorization;
public AuthenticationApi(ApiBridgeHandler bridge, ScheduledExecutorService scheduler) {
public AuthenticationApi(ApiBridgeHandler bridge) {
super(bridge, FeatureArea.NONE);
this.scheduler = scheduler;
}
public void authorize(ApiHandlerConfiguration credentials, String refreshToken, @Nullable String code,
@Nullable String redirectUri) throws NetatmoException {
if (!(credentials.clientId.isBlank() || credentials.clientSecret.isBlank())) {
Map<String, String> params = new HashMap<>(Map.of(SCOPE, FeatureArea.ALL_SCOPES));
if (!refreshToken.isBlank()) {
params.put(REFRESH_TOKEN, refreshToken);
} else if (code != null && redirectUri != null) {
params.putAll(Map.of(REDIRECT_URI, redirectUri, CODE, code));
}
if (params.size() > 1) {
requestToken(credentials.clientId, credentials.clientSecret, params);
return;
}
public void setAccessToken(@Nullable String accessToken) {
if (accessToken != null) {
authorization = "Bearer " + accessToken;
} else {
authorization = null;
}
throw new IllegalArgumentException("Inconsistent configuration state, please file a bug report.");
}
private void requestToken(String clientId, String secret, Map<String, String> entries) throws NetatmoException {
disconnect();
Map<String, String> payload = new HashMap<>(entries);
payload.putAll(Map.of(GRANT_TYPE, payload.keySet().contains(CODE) ? AUTHORIZATION_CODE : REFRESH_TOKEN,
CLIENT_ID, clientId, CLIENT_SECRET, secret));
AccessTokenResponse response = post(TOKEN_URI, AccessTokenResponse.class, payload);
refreshTokenJob = Optional.of(scheduler.schedule(() -> {
try {
requestToken(clientId, secret, Map.of(REFRESH_TOKEN, response.getRefreshToken()));
} catch (NetatmoException e) {
logger.warn("Unable to refresh access token : {}", e.getMessage());
}
}, Math.round(response.getExpiresIn() * 0.9), TimeUnit.SECONDS));
grantedScope = response.getScope();
authorization = "Bearer %s".formatted(response.getAccessToken());
apiBridge.storeRefreshToken(response.getRefreshToken());
}
public void disconnect() {
authorization = null;
grantedScope = List.of();
public void setScope(String scope) {
grantedScope = Stream.of(scope.split(" ")).map(s -> Scope.valueOf(s.toUpperCase())).toList();
}
public void dispose() {
disconnect();
refreshTokenJob.ifPresent(job -> job.cancel(true));
refreshTokenJob = Optional.empty();
authorization = null;
grantedScope = List.of();
}
public Optional<String> getAuthorization() {

View File

@ -13,28 +13,38 @@
package org.openhab.binding.netatmo.internal.api.dto;
import java.util.List;
import java.util.StringJoiner;
import org.openhab.binding.netatmo.internal.api.data.NetatmoConstants.Scope;
import org.openhab.core.auth.client.oauth2.AccessTokenResponse;
import com.google.gson.annotations.SerializedName;
/**
* This is the Access Token Response, a simple value-object holding the result of an Access Token Request, as
* provided by Netatmo API.
*
* This is different from {@link AccessTokenResponse} because it violates RFC 6749 by having {@link #scope}
* defined as an array of strings.
*
* @author Gaël L'hopital - Initial contribution
*/
public final class AccessTokenResponse {
public final class NetatmoAccessTokenResponse {
/**
* The access token issued by the authorization server. It is used
* by the client to gain access to a resource.
*
*/
@SerializedName("access_token")
private String accessToken;
@SerializedName("token_type")
private String tokenType;
/**
* Number of seconds that this OAuthToken is valid for since the time it was created.
*
*/
@SerializedName("expires_in")
private long expiresIn;
/**
@ -44,10 +54,22 @@ public final class AccessTokenResponse {
* to resource servers.
*
*/
@SerializedName("refresh_token")
private String refreshToken;
/**
* A list of scopes. This is not compliant with RFC 6749 which defines scope
* as a list of space-delimited case-sensitive strings.
*
* @see <a href="https://tools.ietf.org/html/rfc6749#section-3.3">rfc6749 section-3.3</a>
*/
private List<Scope> scope;
/**
* State from prior access token request (if present).
*/
private String state;
public String getAccessToken() {
return accessToken;
}
@ -66,7 +88,28 @@ public final class AccessTokenResponse {
@Override
public String toString() {
return "AccessTokenResponse [accessToken=" + accessToken + ", expiresIn=" + expiresIn + ", refreshToken="
+ refreshToken + ", scope=" + scope + "]";
return "AccessTokenResponse [accessToken=" + accessToken + ", tokenType=" + tokenType + ", expiresIn="
+ expiresIn + ", refreshToken=" + refreshToken + ", scope=" + scope + ", state=" + state + "]";
}
/**
* Convert Netatmo-specific DTO to standard DTO in core resembling RFC 6749.
*
* @return response converted into {@link AccessTokenResponse}
*/
public AccessTokenResponse toStandard() {
var standardResponse = new AccessTokenResponse();
standardResponse.setAccessToken(accessToken);
standardResponse.setTokenType(tokenType);
standardResponse.setExpiresIn(expiresIn);
standardResponse.setRefreshToken(refreshToken);
StringJoiner stringJoiner = new StringJoiner(" ");
scope.forEach(s -> stringJoiner.add(s.name().toLowerCase()));
standardResponse.setScope(stringJoiner.toString());
standardResponse.setState(state);
return standardResponse;
}
}

View File

@ -29,15 +29,4 @@ public class ApiHandlerConfiguration {
public String webHookUrl = "";
public String webHookPostfix = "";
public int reconnectInterval = 300;
public ConfigurationLevel check(String refreshToken) {
if (clientId.isBlank()) {
return ConfigurationLevel.EMPTY_CLIENT_ID;
} else if (clientSecret.isBlank()) {
return ConfigurationLevel.EMPTY_CLIENT_SECRET;
} else if (refreshToken.isBlank()) {
return ConfigurationLevel.REFRESH_TOKEN_NEEDED;
}
return ConfigurationLevel.COMPLETED;
}
}

View File

@ -0,0 +1,48 @@
/**
* 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.netatmo.internal.deserialization;
import java.lang.reflect.Type;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.netatmo.internal.api.dto.NetatmoAccessTokenResponse;
import org.openhab.core.auth.client.oauth2.AccessTokenResponse;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonDeserializationContext;
import com.google.gson.JsonDeserializer;
import com.google.gson.JsonElement;
import com.google.gson.JsonParseException;
/**
* Specialized deserializer for {@link NetatmoAccessTokenResponse}
*
* @author Jacob Laursen - Initial contribution
*/
@NonNullByDefault
public class AccessTokenResponseDeserializer implements JsonDeserializer<AccessTokenResponse> {
private final Gson gson = new GsonBuilder().create();
@Override
public @Nullable AccessTokenResponse deserialize(JsonElement element, Type arg1, JsonDeserializationContext arg2)
throws JsonParseException {
NetatmoAccessTokenResponse response = gson.fromJson(element, NetatmoAccessTokenResponse.class);
if (response == null) {
return null;
}
return response.toStandard();
}
}

View File

@ -21,9 +21,6 @@ import java.io.InputStream;
import java.lang.reflect.Constructor;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.LocalDateTime;
import java.util.ArrayDeque;
import java.util.Collection;
@ -70,11 +67,16 @@ import org.openhab.binding.netatmo.internal.api.dto.NAModule;
import org.openhab.binding.netatmo.internal.config.ApiHandlerConfiguration;
import org.openhab.binding.netatmo.internal.config.BindingConfiguration;
import org.openhab.binding.netatmo.internal.config.ConfigurationLevel;
import org.openhab.binding.netatmo.internal.deserialization.AccessTokenResponseDeserializer;
import org.openhab.binding.netatmo.internal.deserialization.NADeserializer;
import org.openhab.binding.netatmo.internal.discovery.NetatmoDiscoveryService;
import org.openhab.binding.netatmo.internal.servlet.GrantServlet;
import org.openhab.binding.netatmo.internal.servlet.WebhookServlet;
import org.openhab.core.OpenHAB;
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;
import org.openhab.core.auth.client.oauth2.OAuthResponseException;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.ChannelUID;
@ -89,130 +91,147 @@ import org.osgi.service.http.HttpService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gson.GsonBuilder;
/**
* {@link ApiBridgeHandler} is the handler for a Netatmo API and connects it to the framework.
*
* @author Gaël L'hopital - Initial contribution
*
* @author Jacob Laursen - Refactored to use standard OAuth2 implementation
*/
@NonNullByDefault
public class ApiBridgeHandler extends BaseBridgeHandler {
private static final int TIMEOUT_S = 20;
private static final String REFRESH_TOKEN = "refreshToken";
private final Logger logger = LoggerFactory.getLogger(ApiBridgeHandler.class);
private final AuthenticationApi connectApi = new AuthenticationApi(this, scheduler);
private final AuthenticationApi connectApi = new AuthenticationApi(this);
private final Map<Class<? extends RestManager>, RestManager> managers = new HashMap<>();
private final Deque<LocalDateTime> requestsTimestamps = new ArrayDeque<>(200);
private final BindingConfiguration bindingConf;
private final HttpClient httpClient;
private final OAuthFactory oAuthFactory;
private final NADeserializer deserializer;
private final HttpService httpService;
private final ChannelUID requestCountChannelUID;
private final Path tokenFile;
private @Nullable OAuthClientService oAuthClientService;
private Optional<ScheduledFuture<?>> connectJob = Optional.empty();
private Optional<WebhookServlet> webHookServlet = Optional.empty();
private Optional<GrantServlet> grantServlet = Optional.empty();
public ApiBridgeHandler(Bridge bridge, HttpClient httpClient, NADeserializer deserializer,
BindingConfiguration configuration, HttpService httpService) {
BindingConfiguration configuration, HttpService httpService, OAuthFactory oAuthFactory) {
super(bridge);
this.bindingConf = configuration;
this.httpClient = httpClient;
this.deserializer = deserializer;
this.httpService = httpService;
this.requestCountChannelUID = new ChannelUID(thing.getUID(), GROUP_MONITORING, CHANNEL_REQUEST_COUNT);
this.oAuthFactory = oAuthFactory;
Path homeFolder = Paths.get(OpenHAB.getUserDataFolder(), BINDING_ID);
if (Files.notExists(homeFolder)) {
try {
Files.createDirectory(homeFolder);
} catch (IOException e) {
logger.warn("Unable to create {} folder : {}", homeFolder.toString(), e.getMessage());
}
}
tokenFile = homeFolder.resolve(REFRESH_TOKEN + "_" + thing.getUID().toString().replace(":", "_"));
requestCountChannelUID = new ChannelUID(thing.getUID(), GROUP_MONITORING, CHANNEL_REQUEST_COUNT);
}
@Override
public void initialize() {
logger.debug("Initializing Netatmo API bridge handler.");
ApiHandlerConfiguration configuration = getConfiguration();
if (configuration.clientId.isBlank()) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
ConfigurationLevel.EMPTY_CLIENT_ID.message);
return;
}
if (configuration.clientSecret.isBlank()) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
ConfigurationLevel.EMPTY_CLIENT_SECRET.message);
return;
}
oAuthClientService = oAuthFactory
.createOAuthClientService(this.getThing().getUID().getAsString(),
AuthenticationApi.TOKEN_URI.toString(), AuthenticationApi.AUTH_URI.toString(),
configuration.clientId, configuration.clientSecret, FeatureArea.ALL_SCOPES, false)
.withGsonBuilder(new GsonBuilder().registerTypeAdapter(AccessTokenResponse.class,
new AccessTokenResponseDeserializer()));
updateStatus(ThingStatus.UNKNOWN);
scheduler.execute(() -> openConnection(null, null));
}
public void openConnection(@Nullable String code, @Nullable String redirectUri) {
if (!authenticate(code, redirectUri)) {
return;
}
logger.debug("Connecting to Netatmo API.");
ApiHandlerConfiguration configuration = getConfiguration();
String refreshToken = readRefreshToken();
ConfigurationLevel level = configuration.check(refreshToken);
switch (level) {
case EMPTY_CLIENT_ID:
case EMPTY_CLIENT_SECRET:
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, level.message);
break;
case REFRESH_TOKEN_NEEDED:
if (code == null || redirectUri == null) {
GrantServlet servlet = new GrantServlet(this, httpService);
servlet.startListening();
grantServlet = Optional.of(servlet);
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, level.message);
break;
} // else we can proceed to get the token refresh
case COMPLETED:
try {
logger.debug("Connecting to Netatmo API.");
connectApi.authorize(configuration, refreshToken, code, redirectUri);
if (!configuration.webHookUrl.isBlank()) {
SecurityApi securityApi = getRestManager(SecurityApi.class);
if (securityApi != null) {
WebhookServlet servlet = new WebhookServlet(this, httpService, deserializer, securityApi,
configuration.webHookUrl, configuration.webHookPostfix);
servlet.startListening();
this.webHookServlet = Optional.of(servlet);
}
}
updateStatus(ThingStatus.ONLINE);
getThing().getThings().stream().filter(Thing::isEnabled).map(Thing::getHandler)
.filter(Objects::nonNull).map(CommonInterface.class::cast)
.forEach(CommonInterface::expireData);
} catch (NetatmoException e) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
prepareReconnection(code, redirectUri);
}
break;
}
}
private String readRefreshToken() {
if (Files.exists(tokenFile)) {
try {
return Files.readString(tokenFile);
} catch (IOException e) {
logger.warn("Unable to read token file {} : {}", tokenFile.toString(), e.getMessage());
if (!configuration.webHookUrl.isBlank()) {
SecurityApi securityApi = getRestManager(SecurityApi.class);
if (securityApi != null) {
webHookServlet.ifPresent(servlet -> servlet.dispose());
WebhookServlet servlet = new WebhookServlet(this, httpService, deserializer, securityApi,
configuration.webHookUrl, configuration.webHookPostfix);
servlet.startListening();
this.webHookServlet = Optional.of(servlet);
}
}
return "";
updateStatus(ThingStatus.ONLINE);
getThing().getThings().stream().filter(Thing::isEnabled).map(Thing::getHandler).filter(Objects::nonNull)
.map(CommonInterface.class::cast).forEach(CommonInterface::expireData);
}
public void storeRefreshToken(String refreshToken) {
if (refreshToken.isBlank()) {
logger.trace("Blank refresh token received - ignored");
} else {
logger.trace("Updating refresh token in {} : {}", tokenFile.toString(), refreshToken);
try {
Files.write(tokenFile, refreshToken.getBytes());
} catch (IOException e) {
logger.warn("Error saving refresh token to {} : {}", tokenFile.toString(), e.getMessage());
}
private boolean authenticate(@Nullable String code, @Nullable String redirectUri) {
OAuthClientService oAuthClientService = this.oAuthClientService;
if (oAuthClientService == null) {
logger.debug("ApiBridgeHandler is not ready, OAuthClientService not initialized");
return false;
}
AccessTokenResponse accessTokenResponse;
try {
if (code != null) {
accessTokenResponse = oAuthClientService.getAccessTokenResponseByAuthorizationCode(code, redirectUri);
// Dispose grant servlet upon completion of authorization flow.
grantServlet.ifPresent(servlet -> servlet.dispose());
grantServlet = Optional.empty();
} else {
accessTokenResponse = oAuthClientService.getAccessTokenResponse();
}
} catch (OAuthException | OAuthResponseException e) {
logger.debug("Failed to load access token: {}", e.getMessage());
startAuthorizationFlow();
return false;
} catch (IOException e) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
prepareReconnection(code, redirectUri);
return false;
}
if (accessTokenResponse == null) {
logger.debug("Authorization failed, restarting authorization flow");
startAuthorizationFlow();
return false;
}
connectApi.setAccessToken(accessTokenResponse.getAccessToken());
connectApi.setScope(accessTokenResponse.getScope());
return true;
}
private void startAuthorizationFlow() {
GrantServlet servlet = new GrantServlet(this, httpService);
servlet.startListening();
grantServlet = Optional.of(servlet);
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
ConfigurationLevel.REFRESH_TOKEN_NEEDED.message);
}
public ApiHandlerConfiguration getConfiguration() {
@ -220,7 +239,7 @@ public class ApiBridgeHandler extends BaseBridgeHandler {
}
private void prepareReconnection(@Nullable String code, @Nullable String redirectUri) {
connectApi.disconnect();
connectApi.dispose();
freeConnectJob();
connectJob = Optional.of(scheduler.schedule(() -> openConnection(code, redirectUri),
getConfiguration().reconnectInterval, TimeUnit.SECONDS));
@ -243,9 +262,18 @@ public class ApiBridgeHandler extends BaseBridgeHandler {
connectApi.dispose();
freeConnectJob();
oAuthFactory.ungetOAuthService(this.getThing().getUID().getAsString());
super.dispose();
}
@Override
public void handleRemoval() {
oAuthFactory.deleteServiceAndAccessToken(this.getThing().getUID().getAsString());
super.handleRemoval();
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
logger.debug("Netatmo Bridge is read-only and does not handle commands");
@ -277,6 +305,10 @@ public class ApiBridgeHandler extends BaseBridgeHandler {
Request request = httpClient.newRequest(uri).method(method).timeout(TIMEOUT_S, TimeUnit.SECONDS);
if (!authenticate(null, null)) {
prepareReconnection(null, null);
throw new NetatmoException("Not authenticated");
}
connectApi.getAuthorization().ifPresent(auth -> request.header(HttpHeader.AUTHORIZATION, auth));
if (payload != null && contentType != null