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.easee.internal.connector;
15 import static org.openhab.binding.easee.internal.EaseeBindingConstants.*;
17 import java.time.Instant;
18 import java.time.temporal.ChronoUnit;
19 import java.util.Queue;
20 import java.util.concurrent.Future;
21 import java.util.concurrent.ScheduledExecutorService;
22 import java.util.concurrent.TimeUnit;
23 import java.util.concurrent.atomic.AtomicReference;
25 import org.eclipse.jdt.annotation.NonNullByDefault;
26 import org.eclipse.jdt.annotation.Nullable;
27 import org.eclipse.jetty.client.HttpClient;
28 import org.eclipse.jetty.util.BlockingArrayQueue;
29 import org.openhab.binding.easee.internal.AtomicReferenceTrait;
30 import org.openhab.binding.easee.internal.Utils;
31 import org.openhab.binding.easee.internal.command.EaseeCommand;
32 import org.openhab.binding.easee.internal.command.account.Login;
33 import org.openhab.binding.easee.internal.command.account.RefreshToken;
34 import org.openhab.binding.easee.internal.handler.EaseeBridgeHandler;
35 import org.openhab.binding.easee.internal.handler.StatusHandler;
36 import org.openhab.binding.easee.internal.model.ValidationException;
37 import org.openhab.core.thing.ThingStatus;
38 import org.openhab.core.thing.ThingStatusDetail;
39 import org.slf4j.Logger;
40 import org.slf4j.LoggerFactory;
42 import com.google.gson.JsonObject;
45 * The connector is responsible for communication with the Easee Cloud API
47 * @author Alexander Friese - initial contribution
50 public class WebInterface implements AtomicReferenceTrait {
52 private final Logger logger = LoggerFactory.getLogger(WebInterface.class);
57 private final EaseeBridgeHandler handler;
60 * handler for updating bridge status
62 private final StatusHandler bridgeStatusHandler;
65 * holds authentication status
67 private boolean authenticated = false;
70 * access token returned by login, needed to authenticate all requests send to API.
72 private String accessToken;
75 * refresh token returned by login, needed for refreshing the access token.
77 private String refreshToken;
80 * expiry of the access token.
82 private Instant tokenExpiry;
85 * last refresh of the access token.
87 private Instant tokenRefreshDate;
90 * HTTP client for asynchronous calls
92 private final HttpClient httpClient;
95 * the scheduler which periodically sends web requests to the Easee Cloud API. Should be initiated with the thing's
96 * existing scheduler instance.
98 private final ScheduledExecutorService scheduler;
103 private final WebRequestExecutor requestExecutor;
106 * periodic request executor job
108 private final AtomicReference<@Nullable Future<?>> requestExecutorJobReference;
111 * this class is responsible for executing periodic web requests. This ensures that only one request is executed
112 * at the same time and there will be a guaranteed minimum delay between subsequent requests.
114 * @author afriese - initial contribution
116 private class WebRequestExecutor implements Runnable {
119 * queue which holds the commands to execute
121 private final Queue<EaseeCommand> commandQueue;
126 WebRequestExecutor() {
127 this.commandQueue = new BlockingArrayQueue<>(WEB_REQUEST_QUEUE_MAX_SIZE);
130 private void processAuthenticationResult(CommunicationStatus status, JsonObject jsonObject) {
131 String msg = Utils.getAsString(jsonObject, JSON_KEY_ERROR_TITLE);
132 if (msg == null || msg.isBlank()) {
133 msg = status.getMessage();
136 switch (status.getHttpCode()) {
138 bridgeStatusHandler.updateStatusInfo(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
140 setAuthenticated(false);
143 String accessToken = Utils.getAsString(jsonObject, JSON_KEY_AUTH_ACCESS_TOKEN);
144 String refreshToken = Utils.getAsString(jsonObject, JSON_KEY_AUTH_REFRESH_TOKEN);
145 int expiresInSeconds = Utils.getAsInt(jsonObject, JSON_KEY_AUTH_EXPIRES_IN);
146 if (accessToken != null && refreshToken != null && expiresInSeconds != 0) {
147 WebInterface.this.accessToken = accessToken;
148 WebInterface.this.refreshToken = refreshToken;
149 tokenRefreshDate = Instant.now();
150 tokenExpiry = tokenRefreshDate.plusSeconds(expiresInSeconds);
152 logger.debug("access token refreshed: {}, expiry: {}", Utils.formatDate(tokenRefreshDate),
153 Utils.formatDate(tokenExpiry));
155 bridgeStatusHandler.updateStatusInfo(ThingStatus.ONLINE, ThingStatusDetail.NONE,
156 STATUS_TOKEN_VALIDATED);
157 setAuthenticated(true);
158 handler.startDiscovery();
162 bridgeStatusHandler.updateStatusInfo(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
164 setAuthenticated(false);
169 * puts a command into the queue
173 void enqueue(EaseeCommand command) {
175 commandQueue.add(command);
176 } catch (IllegalStateException ex) {
177 if (commandQueue.size() >= WEB_REQUEST_QUEUE_MAX_SIZE) {
179 "Could not add command to command queue because queue is already full. Maybe Easee Cloud is down?");
181 logger.warn("Could not add command to queue - IllegalStateException");
187 * executes the web request
191 logger.debug("run queued commands, queue size is {}", commandQueue.size());
192 if (!isAuthenticated()) {
195 refreshAccessToken();
197 if (isAuthenticated() && !commandQueue.isEmpty()) {
200 } catch (Exception ex) {
201 logger.warn("command execution ended with exception:", ex);
208 * authenticates with the Easee Cloud interface.
210 private synchronized void authenticate() {
211 setAuthenticated(false);
212 EaseeCommand loginCommand = new Login(handler, this::processAuthenticationResult);
214 loginCommand.performAction(httpClient, accessToken);
215 } catch (ValidationException e) {
216 // this cannot happen
221 * periodically refreshed the access token.
223 private synchronized void refreshAccessToken() {
224 Instant now = Instant.now();
226 if (now.plus(WEB_REQUEST_TOKEN_EXPIRY_BUFFER_MINUTES, ChronoUnit.MINUTES).isAfter(tokenExpiry)
227 || now.isAfter(tokenRefreshDate.plus(WEB_REQUEST_TOKEN_MAX_AGE_MINUTES, ChronoUnit.MINUTES))) {
228 logger.debug("access token needs to be refreshed, last refresh: {}, expiry: {}",
229 Utils.formatDate(tokenRefreshDate), Utils.formatDate(tokenExpiry));
231 EaseeCommand refreshCommand = new RefreshToken(handler, accessToken, refreshToken,
232 this::processAuthenticationResult);
234 refreshCommand.performAction(httpClient, accessToken);
235 } catch (ValidationException e) {
236 // this cannot happen
242 * executes the next command in the queue. requires authenticated session.
244 * @throws ValidationException
246 private void executeCommand() throws ValidationException {
247 EaseeCommand command = commandQueue.poll();
248 if (command != null) {
249 command.performAction(httpClient, accessToken);
255 * Constructor to set up interface
257 * @param config Bridge configuration
259 public WebInterface(ScheduledExecutorService scheduler, EaseeBridgeHandler handler, HttpClient httpClient,
260 StatusHandler bridgeStatusHandler) {
261 this.handler = handler;
262 this.bridgeStatusHandler = bridgeStatusHandler;
263 this.scheduler = scheduler;
264 this.httpClient = httpClient;
265 this.tokenExpiry = OUTDATED_DATE;
266 this.tokenRefreshDate = OUTDATED_DATE;
267 this.accessToken = "";
268 this.refreshToken = "";
269 this.requestExecutor = new WebRequestExecutor();
270 this.requestExecutorJobReference = new AtomicReference<>(null);
273 public void start() {
274 setAuthenticated(false);
275 updateJobReference(requestExecutorJobReference, scheduler.scheduleWithFixedDelay(requestExecutor,
276 WEB_REQUEST_INITIAL_DELAY, WEB_REQUEST_INTERVAL, TimeUnit.SECONDS));
280 * queues any command for execution
284 public void enqueueCommand(EaseeCommand command) {
285 requestExecutor.enqueue(command);
289 * will be called by the ThingHandler to abort periodic jobs.
291 public void dispose() {
292 logger.debug("Webinterface disposed.");
293 cancelJobReference(requestExecutorJobReference);
294 setAuthenticated(false);
298 * returns authentication status.
302 private boolean isAuthenticated() {
303 return authenticated;
307 * update the authentication status, also resets token data.
309 * @param authenticated
311 private void setAuthenticated(boolean authenticated) {
312 this.authenticated = authenticated;
313 if (!authenticated) {
314 this.tokenExpiry = OUTDATED_DATE;
315 this.accessToken = "";
316 this.refreshToken = "";
321 * returns the current access token.
325 public String getAccessToken() {