2 * Copyright (c) 2010-2023 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
7 * This program and the accompanying materials are made available under the
8 * terms of the Eclipse Public License 2.0 which is available at
9 * http://www.eclipse.org/legal/epl-2.0
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.ecobee.internal.api;
15 import static org.openhab.binding.ecobee.internal.EcobeeBindingConstants.*;
17 import java.util.concurrent.ExecutionException;
18 import java.util.concurrent.TimeUnit;
19 import java.util.concurrent.TimeoutException;
21 import org.eclipse.jdt.annotation.NonNullByDefault;
22 import org.eclipse.jdt.annotation.Nullable;
23 import org.eclipse.jetty.client.HttpClient;
24 import org.eclipse.jetty.client.HttpResponseException;
25 import org.eclipse.jetty.client.api.ContentResponse;
26 import org.eclipse.jetty.client.api.Request;
27 import org.eclipse.jetty.http.HttpStatus;
28 import org.openhab.binding.ecobee.internal.dto.oauth.AuthorizeResponseDTO;
29 import org.openhab.binding.ecobee.internal.dto.oauth.TokenResponseDTO;
30 import org.openhab.binding.ecobee.internal.handler.EcobeeAccountBridgeHandler;
31 import org.openhab.binding.ecobee.internal.util.ExceptionUtils;
32 import org.openhab.core.auth.client.oauth2.AccessTokenResponse;
33 import org.openhab.core.auth.client.oauth2.OAuthClientService;
34 import org.openhab.core.auth.client.oauth2.OAuthException;
35 import org.openhab.core.thing.ThingStatus;
36 import org.openhab.core.thing.ThingStatusDetail;
37 import org.slf4j.Logger;
38 import org.slf4j.LoggerFactory;
40 import com.google.gson.JsonSyntaxException;
43 * The {@link EcobeeAuth} performs the initial OAuth authorization
44 * with the Ecobee authorization servers. Once this process is complete, the
45 * AccessTokenResponse will be imported into the OHC OAuth Client Service. At
46 * that point, the OHC OAuth service will be responsible for refreshing tokens.
48 * @author Mark Hilbush - Initial contribution
51 public class EcobeeAuth {
53 private final Logger logger = LoggerFactory.getLogger(EcobeeAuth.class);
55 private final EcobeeAccountBridgeHandler bridgeHandler;
56 private final String apiKey;
57 private final int apiTimeout;
58 private final OAuthClientService oAuthClientService;
59 private final HttpClient httpClient;
61 private EcobeeAuthState state;
63 private @Nullable AuthorizeResponseDTO authResponse;
65 private long pinExpirationTime;
68 * The authorization code needed to make the first-time request
69 * of the refresh and access tokens. Obtained from the call to {@code authorize()}.
71 private @Nullable String code;
73 public EcobeeAuth(EcobeeAccountBridgeHandler bridgeHandler, String apiKey, int apiTimeout,
74 OAuthClientService oAuthClientService, HttpClient httpClient) {
76 this.apiTimeout = apiTimeout;
77 this.oAuthClientService = oAuthClientService;
78 this.httpClient = httpClient;
79 this.bridgeHandler = bridgeHandler;
80 pinExpirationTime = 0;
81 state = EcobeeAuthState.NEED_PIN;
85 public void setState(EcobeeAuthState newState) {
86 if (newState != state) {
87 logger.debug("EcobeeAuth: Change state from {} to {}", state, newState);
92 public boolean isComplete() {
93 return state == EcobeeAuthState.COMPLETE;
96 public EcobeeAuthState doAuthorization() throws EcobeeAuthException {
105 bridgeHandler.updateBridgeStatus(ThingStatus.ONLINE);
112 * Call the Ecobee authorize endpoint to get the authorization code and PIN
113 * that will be used a) validate the application in the the Ecobee user web portal,
114 * and b) make the first time request for the access and refresh tokens.
115 * Warnings are suppressed to avoid the Gson.fromJson warnings.
117 @SuppressWarnings({ "null", "unused" })
118 private void authorize() throws EcobeeAuthException {
119 logger.debug("EcobeeAuth: State is {}: Executing step: 'authorize'", state);
120 StringBuilder url = new StringBuilder(ECOBEE_AUTHORIZE_URL);
121 url.append("?response_type=ecobeePin");
122 url.append("&client_id=").append(apiKey);
123 url.append("&scope=").append(ECOBEE_SCOPE);
125 logger.trace("EcobeeAuth: Getting authorize URL={}", url);
126 String response = executeUrl("GET", url.toString());
127 logger.trace("EcobeeAuth: Auth response: {}", response);
130 authResponse = EcobeeApi.getGson().fromJson(response, AuthorizeResponseDTO.class);
131 if (authResponse == null) {
132 logger.debug("EcobeeAuth: Got null authorize response from Ecobee API");
133 setState(EcobeeAuthState.NEED_PIN);
135 String error = authResponse.error;
136 if (error != null && !error.isEmpty()) {
137 throw new EcobeeAuthException(error + ": " + authResponse.errorDescription);
139 code = authResponse.code;
140 writeLogMessage(authResponse.pin, authResponse.expiresIn);
141 setPinExpirationTime(authResponse.expiresIn.longValue());
142 updateBridgeStatus();
143 setState(EcobeeAuthState.NEED_TOKEN);
145 } catch (JsonSyntaxException e) {
146 logger.info("EcobeeAuth: Exception while parsing authorize response: {}", e.getMessage());
147 setState(EcobeeAuthState.NEED_PIN);
152 * Call the Ecobee token endpoint to get the access and refresh tokens. Once successfully retrieved,
153 * the access and refresh tokens will be injected into the OHC OAuth service.
154 * Warnings are suppressed to avoid the Gson.fromJson warnings.
156 @SuppressWarnings({ "null", "unused" })
157 private void getTokens() throws EcobeeAuthException {
158 logger.debug("EcobeeAuth: State is {}: Executing step: 'getToken'", state);
159 StringBuilder url = new StringBuilder(ECOBEE_TOKEN_URL);
160 url.append("?grant_type=ecobeePin");
161 url.append("&code=").append(code);
162 url.append("&client_id=").append(apiKey);
164 logger.trace("EcobeeAuth: Posting token URL={}", url);
165 String response = executeUrl("POST", url.toString());
166 logger.trace("EcobeeAuth: Got a valid token response: {}", response);
168 TokenResponseDTO tokenResponse = EcobeeApi.getGson().fromJson(response, TokenResponseDTO.class);
169 if (tokenResponse == null) {
170 logger.debug("EcobeeAuth: Got null token response from Ecobee API");
171 updateBridgeStatus();
172 setState(isPinExpired() ? EcobeeAuthState.NEED_PIN : EcobeeAuthState.NEED_TOKEN);
175 String error = tokenResponse.error;
176 if (error != null && !error.isEmpty()) {
177 throw new EcobeeAuthException(error + ": " + tokenResponse.errorDescription);
179 AccessTokenResponse accessTokenResponse = new AccessTokenResponse();
180 accessTokenResponse.setRefreshToken(tokenResponse.refreshToken);
181 accessTokenResponse.setAccessToken(tokenResponse.accessToken);
182 accessTokenResponse.setScope(tokenResponse.scope);
183 accessTokenResponse.setTokenType(tokenResponse.tokenType);
184 accessTokenResponse.setExpiresIn(tokenResponse.expiresIn);
186 logger.debug("EcobeeAuth: Importing AccessTokenResponse into oAuthClientService!!!");
187 oAuthClientService.importAccessTokenResponse(accessTokenResponse);
188 bridgeHandler.updateBridgeStatus(ThingStatus.ONLINE);
189 setState(EcobeeAuthState.COMPLETE);
191 } catch (OAuthException e) {
192 logger.info("EcobeeAuth: Got OAuthException", e);
193 // No other processing needed here
195 updateBridgeStatus();
196 setState(isPinExpired() ? EcobeeAuthState.NEED_PIN : EcobeeAuthState.NEED_TOKEN);
199 private void writeLogMessage(String pin, Integer expiresIn) {
200 logger.info("#################################################################");
201 logger.info("# Ecobee: U S E R I N T E R A C T I O N R E Q U I R E D !!");
202 logger.info("# Go to the Ecobee web portal, then:");
203 logger.info("# Enter PIN '{}' in My Apps within {} minutes.", pin, expiresIn);
204 logger.info("# NOTE: All API attempts will fail in the meantime.");
205 logger.info("#################################################################");
208 private void updateBridgeStatus() {
209 AuthorizeResponseDTO response = authResponse;
210 if (response != null) {
211 bridgeHandler.updateBridgeStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_PENDING,
212 String.format("Enter PIN '%s' in MyApps. PIN expires in %d minutes", response.pin,
213 getMinutesUntilPinExpiration()));
217 private void setPinExpirationTime(long expiresIn) {
218 pinExpirationTime = expiresIn + TimeUnit.MILLISECONDS.toMinutes(System.currentTimeMillis());
221 private long getMinutesUntilPinExpiration() {
222 return pinExpirationTime - TimeUnit.MILLISECONDS.toMinutes(System.currentTimeMillis());
225 private boolean isPinExpired() {
226 return getMinutesUntilPinExpiration() <= 0;
229 private @Nullable String executeUrl(String method, String url) {
230 Request request = httpClient.newRequest(url);
231 request.timeout(apiTimeout, TimeUnit.MILLISECONDS);
232 request.method(method);
233 EcobeeApi.HTTP_HEADERS.forEach((k, v) -> request.header((String) k, (String) v));
236 ContentResponse contentResponse = request.send();
237 switch (contentResponse.getStatus()) {
238 case HttpStatus.OK_200:
239 return contentResponse.getContentAsString();
240 case HttpStatus.BAD_REQUEST_400:
241 logger.debug("BAD REQUEST(400) response received: {}", contentResponse.getContentAsString());
242 return contentResponse.getContentAsString();
243 case HttpStatus.UNAUTHORIZED_401:
244 logger.debug("UNAUTHORIZED(401) response received: {}", contentResponse.getContentAsString());
245 return contentResponse.getContentAsString();
246 case HttpStatus.NO_CONTENT_204:
247 logger.debug("HTTP response 204: No content. Check configuration");
250 logger.debug("HTTP {} failed: {}, {}", method, contentResponse.getStatus(),
251 contentResponse.getReason());
254 } catch (TimeoutException e) {
255 logger.debug("TimeoutException: Call to Ecobee API timed out");
256 } catch (ExecutionException e) {
257 if (ExceptionUtils.getRootThrowable(e) instanceof HttpResponseException) {
258 logger.info("Awaiting entry of PIN in Ecobee portal. Expires in {} minutes",
259 getMinutesUntilPinExpiration());
261 logger.debug("ExecutionException on call to Ecobee authorization API", e);
263 } catch (InterruptedException e) {
264 logger.debug("InterruptedException on call to Ecobee authorization API: {}", e.getMessage());
265 Thread.currentThread().interrupt();