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 statusHandler;
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 statusHandler.updateStatusInfo(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, msg);
139 setAuthenticated(false);
142 String accessToken = Utils.getAsString(jsonObject, JSON_KEY_AUTH_ACCESS_TOKEN);
143 String refreshToken = Utils.getAsString(jsonObject, JSON_KEY_AUTH_REFRESH_TOKEN);
144 int expiresInSeconds = Utils.getAsInt(jsonObject, JSON_KEY_AUTH_EXPIRES_IN);
145 if (accessToken != null && refreshToken != null && expiresInSeconds != 0) {
146 WebInterface.this.accessToken = accessToken;
147 WebInterface.this.refreshToken = refreshToken;
148 tokenRefreshDate = Instant.now();
149 tokenExpiry = tokenRefreshDate.plusSeconds(expiresInSeconds);
151 logger.debug("access token refreshed: {}, expiry: {}", Utils.formatDate(tokenRefreshDate),
152 Utils.formatDate(tokenExpiry));
154 statusHandler.updateStatusInfo(ThingStatus.ONLINE, ThingStatusDetail.NONE,
155 STATUS_TOKEN_VALIDATED);
156 setAuthenticated(true);
157 handler.startDiscovery();
161 statusHandler.updateStatusInfo(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, msg);
162 setAuthenticated(false);
167 * puts a command into the queue
171 void enqueue(EaseeCommand command) {
173 commandQueue.add(command);
174 } catch (IllegalStateException ex) {
175 if (commandQueue.size() >= WEB_REQUEST_QUEUE_MAX_SIZE) {
177 "Could not add command to command queue because queue is already full. Maybe Easee Cloud is down?");
179 logger.warn("Could not add command to queue - IllegalStateException");
185 * executes the web request
189 logger.debug("run queued commands, queue size is {}", commandQueue.size());
190 if (!isAuthenticated()) {
193 refreshAccessToken();
195 if (isAuthenticated() && !commandQueue.isEmpty()) {
198 } catch (Exception ex) {
199 logger.warn("command execution ended with exception:", ex);
206 * authenticates with the Easee Cloud interface.
208 private synchronized void authenticate() {
209 setAuthenticated(false);
210 EaseeCommand loginCommand = new Login(handler);
211 loginCommand.registerResultProcessor(this::processAuthenticationResult);
213 loginCommand.performAction(httpClient, accessToken);
214 } catch (ValidationException e) {
215 // this cannot happen
220 * periodically refreshed the access token.
222 private synchronized void refreshAccessToken() {
223 Instant now = Instant.now();
225 if (now.plus(WEB_REQUEST_TOKEN_EXPIRY_BUFFER_MINUTES, ChronoUnit.MINUTES).isAfter(tokenExpiry)
226 || now.isAfter(tokenRefreshDate.plus(WEB_REQUEST_TOKEN_MAX_AGE_MINUTES, ChronoUnit.MINUTES))) {
227 logger.debug("access token needs to be refreshed, last refresh: {}, expiry: {}",
228 Utils.formatDate(tokenRefreshDate), Utils.formatDate(tokenExpiry));
230 EaseeCommand refreshCommand = new RefreshToken(handler, accessToken, refreshToken);
231 refreshCommand.registerResultProcessor(this::processAuthenticationResult);
233 refreshCommand.performAction(httpClient, accessToken);
234 } catch (ValidationException e) {
235 // this cannot happen
241 * executes the next command in the queue. requires authenticated session.
243 * @throws ValidationException
245 private void executeCommand() throws ValidationException {
246 EaseeCommand command = commandQueue.poll();
247 if (command != null) {
248 command.registerResultProcessor(this::processExecutionResult);
249 command.performAction(httpClient, accessToken);
253 private void processExecutionResult(CommunicationStatus status, JsonObject jsonObject) {
254 String msg = Utils.getAsString(jsonObject, JSON_KEY_ERROR_TITLE);
255 if (msg == null || msg.isBlank()) {
256 msg = status.getMessage();
259 switch (status.getHttpCode()) {
262 // no action needed as the thing is already online.
265 statusHandler.updateStatusInfo(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, msg);
266 setAuthenticated(false);
273 * Constructor to set up interface
275 * @param config Bridge configuration
277 public WebInterface(ScheduledExecutorService scheduler, EaseeBridgeHandler handler, HttpClient httpClient,
278 StatusHandler statusHandler) {
279 this.handler = handler;
280 this.statusHandler = statusHandler;
281 this.scheduler = scheduler;
282 this.httpClient = httpClient;
283 this.tokenExpiry = OUTDATED_DATE;
284 this.tokenRefreshDate = OUTDATED_DATE;
285 this.accessToken = "";
286 this.refreshToken = "";
287 this.requestExecutor = new WebRequestExecutor();
288 this.requestExecutorJobReference = new AtomicReference<>(null);
291 public void start() {
292 setAuthenticated(false);
293 updateJobReference(requestExecutorJobReference, scheduler.scheduleWithFixedDelay(requestExecutor,
294 WEB_REQUEST_INITIAL_DELAY, WEB_REQUEST_INTERVAL, TimeUnit.SECONDS));
298 * queues any command for execution
302 public void enqueueCommand(EaseeCommand command) {
303 requestExecutor.enqueue(command);
307 * will be called by the ThingHandler to abort periodic jobs.
309 public void dispose() {
310 logger.debug("Webinterface disposed.");
311 cancelJobReference(requestExecutorJobReference);
312 setAuthenticated(false);
316 * returns authentication status.
320 private boolean isAuthenticated() {
321 return authenticated;
325 * update the authentication status, also resets token data.
327 * @param authenticated
329 private void setAuthenticated(boolean authenticated) {
330 this.authenticated = authenticated;
331 if (!authenticated) {
332 this.tokenExpiry = OUTDATED_DATE;
333 this.accessToken = "";
334 this.refreshToken = "";
339 * returns the current access token.
343 public String getAccessToken() {