]> git.basschouten.com Git - openhab-addons.git/blob
0a677f36dd73a3a2720bbc1bb5c0f647de02c887
[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 bridgeStatusHandler;
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                     bridgeStatusHandler.updateStatusInfo(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
139                             msg);
140                     setAuthenticated(false);
141                     break;
142                 case OK:
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);
151
152                         logger.debug("access token refreshed: {}, expiry: {}", tokenRefreshDate.toString(),
153                                 tokenExpiry.toString());
154
155                         bridgeStatusHandler.updateStatusInfo(ThingStatus.ONLINE, ThingStatusDetail.NONE,
156                                 STATUS_TOKEN_VALIDATED);
157                         setAuthenticated(true);
158                         handler.startDiscovery();
159                         break;
160                     }
161                 default:
162                     bridgeStatusHandler.updateStatusInfo(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
163                             msg);
164                     setAuthenticated(false);
165             }
166         }
167
168         /**
169          * puts a command into the queue
170          *
171          * @param command
172          */
173         void enqueue(EaseeCommand command) {
174             try {
175                 commandQueue.add(command);
176             } catch (IllegalStateException ex) {
177                 if (commandQueue.size() >= WEB_REQUEST_QUEUE_MAX_SIZE) {
178                     logger.debug(
179                             "Could not add command to command queue because queue is already full. Maybe Easee Cloud is down?");
180                 } else {
181                     logger.warn("Could not add command to queue - IllegalStateException");
182                 }
183             }
184         }
185
186         /**
187          * executes the web request
188          */
189         @Override
190         public void run() {
191             logger.debug("run queued commands, queue size is {}", commandQueue.size());
192             if (!isAuthenticated()) {
193                 authenticate();
194             } else {
195                 refreshAccessToken();
196
197                 if (isAuthenticated() && !commandQueue.isEmpty()) {
198                     try {
199                         executeCommand();
200                     } catch (Exception ex) {
201                         logger.warn("command execution ended with exception:", ex);
202                     }
203                 }
204             }
205         }
206
207         /**
208          * authenticates with the Easee Cloud interface.
209          */
210         private synchronized void authenticate() {
211             setAuthenticated(false);
212             EaseeCommand loginCommand = new Login(handler, this::processAuthenticationResult);
213             try {
214                 loginCommand.performAction(httpClient, accessToken);
215             } catch (ValidationException e) {
216                 // this cannot happen
217             }
218         }
219
220         /**
221          * periodically refreshed the access token.
222          */
223         private synchronized void refreshAccessToken() {
224             Instant now = Instant.now();
225
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                         tokenRefreshDate.toString(), tokenExpiry.toString());
230
231                 EaseeCommand refreshCommand = new RefreshToken(handler, accessToken, refreshToken,
232                         this::processAuthenticationResult);
233                 try {
234                     refreshCommand.performAction(httpClient, accessToken);
235                 } catch (ValidationException e) {
236                     // this cannot happen
237                 }
238             }
239         }
240
241         /**
242          * executes the next command in the queue. requires authenticated session.
243          *
244          * @throws ValidationException
245          */
246         private void executeCommand() throws ValidationException {
247             EaseeCommand command = commandQueue.poll();
248             if (command != null) {
249                 command.performAction(httpClient, accessToken);
250             }
251         }
252     }
253
254     /**
255      * Constructor to set up interface
256      */
257     public WebInterface(ScheduledExecutorService scheduler, EaseeBridgeHandler handler, HttpClient httpClient,
258             StatusHandler bridgeStatusHandler) {
259         this.handler = handler;
260         this.bridgeStatusHandler = bridgeStatusHandler;
261         this.scheduler = scheduler;
262         this.httpClient = httpClient;
263         this.tokenExpiry = OUTDATED_DATE;
264         this.tokenRefreshDate = OUTDATED_DATE;
265         this.accessToken = "";
266         this.refreshToken = "";
267         this.requestExecutor = new WebRequestExecutor();
268         this.requestExecutorJobReference = new AtomicReference<>(null);
269     }
270
271     public void start() {
272         setAuthenticated(false);
273         updateJobReference(requestExecutorJobReference, scheduler.scheduleWithFixedDelay(requestExecutor,
274                 WEB_REQUEST_INITIAL_DELAY, WEB_REQUEST_INTERVAL, TimeUnit.SECONDS));
275     }
276
277     /**
278      * queues any command for execution
279      *
280      * @param command
281      */
282     public void enqueueCommand(EaseeCommand command) {
283         requestExecutor.enqueue(command);
284     }
285
286     /**
287      * will be called by the ThingHandler to abort periodic jobs.
288      */
289     public void dispose() {
290         logger.debug("Webinterface disposed.");
291         cancelJobReference(requestExecutorJobReference);
292         setAuthenticated(false);
293     }
294
295     /**
296      * returns authentication status.
297      *
298      * @return
299      */
300     private boolean isAuthenticated() {
301         return authenticated;
302     }
303
304     /**
305      * update the authentication status, also resets token data.
306      *
307      * @param authenticated
308      */
309     private void setAuthenticated(boolean authenticated) {
310         this.authenticated = authenticated;
311         if (!authenticated) {
312             this.tokenExpiry = OUTDATED_DATE;
313             this.accessToken = "";
314             this.refreshToken = "";
315         }
316     }
317
318     /**
319      * returns the current access token.
320      *
321      * @return
322      */
323     public String getAccessToken() {
324         return accessToken;
325     }
326 }