]> git.basschouten.com Git - openhab-addons.git/blob
6c6d29cf921145bff632d6db7f26611f2326f795
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2023 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
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
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.binding.easee.internal.connector;
14
15 import static org.openhab.binding.easee.internal.EaseeBindingConstants.*;
16
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;
24
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;
41
42 import com.google.gson.JsonObject;
43
44 /**
45  * The connector is responsible for communication with the Easee Cloud API
46  *
47  * @author Alexander Friese - initial contribution
48  */
49 @NonNullByDefault
50 public class WebInterface implements AtomicReferenceTrait {
51
52     private final Logger logger = LoggerFactory.getLogger(WebInterface.class);
53
54     /**
55      * bridge handler
56      */
57     private final EaseeBridgeHandler handler;
58
59     /**
60      * handler for updating bridge status
61      */
62     private final StatusHandler statusHandler;
63
64     /**
65      * holds authentication status
66      */
67     private boolean authenticated = false;
68
69     /**
70      * access token returned by login, needed to authenticate all requests send to API.
71      */
72     private String accessToken;
73
74     /**
75      * refresh token returned by login, needed for refreshing the access token.
76      */
77     private String refreshToken;
78
79     /**
80      * expiry of the access token.
81      */
82     private Instant tokenExpiry;
83
84     /**
85      * last refresh of the access token.
86      */
87     private Instant tokenRefreshDate;
88
89     /**
90      * HTTP client for asynchronous calls
91      */
92     private final HttpClient httpClient;
93
94     /**
95      * the scheduler which periodically sends web requests to the Easee Cloud API. Should be initiated with the thing's
96      * existing scheduler instance.
97      */
98     private final ScheduledExecutorService scheduler;
99
100     /**
101      * request executor
102      */
103     private final WebRequestExecutor requestExecutor;
104
105     /**
106      * periodic request executor job
107      */
108     private final AtomicReference<@Nullable Future<?>> requestExecutorJobReference;
109
110     /**
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.
113      *
114      * @author afriese - initial contribution
115      */
116     private class WebRequestExecutor implements Runnable {
117
118         /**
119          * queue which holds the commands to execute
120          */
121         private final Queue<EaseeCommand> commandQueue;
122
123         /**
124          * constructor
125          */
126         WebRequestExecutor() {
127             this.commandQueue = new BlockingArrayQueue<>(WEB_REQUEST_QUEUE_MAX_SIZE);
128         }
129
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();
134             }
135
136             switch (status.getHttpCode()) {
137                 case BAD_REQUEST:
138                     statusHandler.updateStatusInfo(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, msg);
139                     setAuthenticated(false);
140                     break;
141                 case OK:
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);
150
151                         logger.debug("access token refreshed: {}, expiry: {}", Utils.formatDate(tokenRefreshDate),
152                                 Utils.formatDate(tokenExpiry));
153
154                         statusHandler.updateStatusInfo(ThingStatus.ONLINE, ThingStatusDetail.NONE,
155                                 STATUS_TOKEN_VALIDATED);
156                         setAuthenticated(true);
157                         handler.startDiscovery();
158                         break;
159                     }
160                 default:
161                     statusHandler.updateStatusInfo(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, msg);
162                     setAuthenticated(false);
163             }
164         }
165
166         /**
167          * puts a command into the queue
168          *
169          * @param command
170          */
171         void enqueue(EaseeCommand command) {
172             try {
173                 commandQueue.add(command);
174             } catch (IllegalStateException ex) {
175                 if (commandQueue.size() >= WEB_REQUEST_QUEUE_MAX_SIZE) {
176                     logger.debug(
177                             "Could not add command to command queue because queue is already full. Maybe Easee Cloud is down?");
178                 } else {
179                     logger.warn("Could not add command to queue - IllegalStateException");
180                 }
181             }
182         }
183
184         /**
185          * executes the web request
186          */
187         @Override
188         public void run() {
189             logger.debug("run queued commands, queue size is {}", commandQueue.size());
190             if (!isAuthenticated()) {
191                 authenticate();
192             } else {
193                 refreshAccessToken();
194
195                 if (isAuthenticated() && !commandQueue.isEmpty()) {
196                     try {
197                         executeCommand();
198                     } catch (Exception ex) {
199                         logger.warn("command execution ended with exception:", ex);
200                     }
201                 }
202             }
203         }
204
205         /**
206          * authenticates with the Easee Cloud interface.
207          */
208         private synchronized void authenticate() {
209             setAuthenticated(false);
210             EaseeCommand loginCommand = new Login(handler);
211             loginCommand.registerResultProcessor(this::processAuthenticationResult);
212             try {
213                 loginCommand.performAction(httpClient, accessToken);
214             } catch (ValidationException e) {
215                 // this cannot happen
216             }
217         }
218
219         /**
220          * periodically refreshed the access token.
221          */
222         private synchronized void refreshAccessToken() {
223             Instant now = Instant.now();
224
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));
229
230                 EaseeCommand refreshCommand = new RefreshToken(handler, accessToken, refreshToken);
231                 refreshCommand.registerResultProcessor(this::processAuthenticationResult);
232                 try {
233                     refreshCommand.performAction(httpClient, accessToken);
234                 } catch (ValidationException e) {
235                     // this cannot happen
236                 }
237             }
238         }
239
240         /**
241          * executes the next command in the queue. requires authenticated session.
242          *
243          * @throws ValidationException
244          */
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);
250             }
251         }
252
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();
257             }
258
259             switch (status.getHttpCode()) {
260                 case OK:
261                 case ACCEPTED:
262                     // no action needed as the thing is already online.
263                     break;
264                 default:
265                     statusHandler.updateStatusInfo(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, msg);
266                     setAuthenticated(false);
267
268             }
269         }
270     }
271
272     /**
273      * Constructor to set up interface
274      *
275      * @param config Bridge configuration
276      */
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);
289     }
290
291     public void start() {
292         setAuthenticated(false);
293         updateJobReference(requestExecutorJobReference, scheduler.scheduleWithFixedDelay(requestExecutor,
294                 WEB_REQUEST_INITIAL_DELAY, WEB_REQUEST_INTERVAL, TimeUnit.SECONDS));
295     }
296
297     /**
298      * queues any command for execution
299      *
300      * @param command
301      */
302     public void enqueueCommand(EaseeCommand command) {
303         requestExecutor.enqueue(command);
304     }
305
306     /**
307      * will be called by the ThingHandler to abort periodic jobs.
308      */
309     public void dispose() {
310         logger.debug("Webinterface disposed.");
311         cancelJobReference(requestExecutorJobReference);
312         setAuthenticated(false);
313     }
314
315     /**
316      * returns authentication status.
317      *
318      * @return
319      */
320     private boolean isAuthenticated() {
321         return authenticated;
322     }
323
324     /**
325      * update the authentication status, also resets token data.
326      *
327      * @param authenticated
328      */
329     private void setAuthenticated(boolean authenticated) {
330         this.authenticated = authenticated;
331         if (!authenticated) {
332             this.tokenExpiry = OUTDATED_DATE;
333             this.accessToken = "";
334             this.refreshToken = "";
335         }
336     }
337
338     /**
339      * returns the current access token.
340      *
341      * @return
342      */
343     public String getAccessToken() {
344         return accessToken;
345     }
346 }