]> git.basschouten.com Git - openhab-addons.git/blob
c17565cb5263c9b5876caaa4c62c9f16e6089e6c
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2021 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.solaredge.internal.connector;
14
15 import static org.openhab.binding.solaredge.internal.SolarEdgeBindingConstants.*;
16
17 import java.io.UnsupportedEncodingException;
18 import java.util.Queue;
19 import java.util.concurrent.Future;
20 import java.util.concurrent.ScheduledExecutorService;
21 import java.util.concurrent.TimeUnit;
22 import java.util.concurrent.atomic.AtomicReference;
23
24 import org.eclipse.jdt.annotation.NonNullByDefault;
25 import org.eclipse.jdt.annotation.Nullable;
26 import org.eclipse.jetty.client.HttpClient;
27 import org.eclipse.jetty.util.BlockingArrayQueue;
28 import org.openhab.binding.solaredge.internal.AtomicReferenceTrait;
29 import org.openhab.binding.solaredge.internal.command.PrivateApiTokenCheck;
30 import org.openhab.binding.solaredge.internal.command.PublicApiKeyCheck;
31 import org.openhab.binding.solaredge.internal.command.SolarEdgeCommand;
32 import org.openhab.binding.solaredge.internal.config.SolarEdgeConfiguration;
33 import org.openhab.binding.solaredge.internal.handler.SolarEdgeHandler;
34 import org.openhab.core.thing.ThingStatus;
35 import org.openhab.core.thing.ThingStatusDetail;
36 import org.slf4j.Logger;
37 import org.slf4j.LoggerFactory;
38
39 /**
40  * The connector is responsible for communication with the solaredge webportal
41  *
42  * @author Alexander Friese - initial contribution
43  */
44 @NonNullByDefault
45 public class WebInterface implements AtomicReferenceTrait {
46
47     private static final int API_KEY_THRESHOLD = 40;
48     private static final int TOKEN_THRESHOLD = 80;
49
50     private final Logger logger = LoggerFactory.getLogger(WebInterface.class);
51
52     /**
53      * Configuration
54      */
55     private SolarEdgeConfiguration config;
56
57     /**
58      * handler for updating thing status
59      */
60     private final SolarEdgeHandler handler;
61
62     /**
63      * holds authentication status
64      */
65     private boolean authenticated = false;
66
67     /**
68      * HTTP client for asynchronous calls
69      */
70     private final HttpClient httpClient;
71
72     /**
73      * the scheduler which periodically sends web requests to the solaredge API. Should be initiated with the thing's
74      * existing scheduler instance.
75      */
76     private final ScheduledExecutorService scheduler;
77
78     /**
79      * request executor
80      */
81     private final WebRequestExecutor requestExecutor;
82
83     /**
84      * periodic request executor job
85      */
86     private final AtomicReference<@Nullable Future<?>> requestExecutorJobReference;
87
88     /**
89      * this class is responsible for executing periodic web requests. This ensures that only one request is executed at
90      * the same time and there will be a guaranteed minimum delay between subsequent requests.
91      *
92      * @author afriese - initial contribution
93      */
94     private class WebRequestExecutor implements Runnable {
95
96         /**
97          * queue which holds the commands to execute
98          */
99         private final Queue<SolarEdgeCommand> commandQueue;
100
101         /**
102          * constructor
103          */
104         WebRequestExecutor() {
105             this.commandQueue = new BlockingArrayQueue<>(WEB_REQUEST_QUEUE_MAX_SIZE);
106         }
107
108         /**
109          * puts a command into the queue
110          *
111          * @param command
112          */
113         void enqueue(SolarEdgeCommand command) {
114             try {
115                 commandQueue.add(command);
116             } catch (IllegalStateException ex) {
117                 if (commandQueue.size() >= WEB_REQUEST_QUEUE_MAX_SIZE) {
118                     logger.debug(
119                             "Could not add command to command queue because queue is already full. Maybe SolarEdge is down?");
120                 } else {
121                     logger.warn("Could not add command to queue - IllegalStateException");
122                 }
123             }
124         }
125
126         /**
127          * executes the web request
128          */
129         @Override
130         public void run() {
131             if (!isAuthenticated()) {
132                 authenticate();
133             }
134
135             else if (isAuthenticated() && !commandQueue.isEmpty()) {
136                 StatusUpdateListener statusUpdater = new StatusUpdateListener() {
137                     @Override
138                     public void update(CommunicationStatus status) {
139                         switch (status.getHttpCode()) {
140                             case SERVICE_UNAVAILABLE:
141                                 handler.setStatusInfo(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE,
142                                         status.getMessage());
143                                 setAuthenticated(false);
144                                 break;
145                             case OK:
146                                 // no action needed as the thing is already online.
147                                 break;
148                             default:
149                                 handler.setStatusInfo(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
150                                         status.getMessage());
151                                 setAuthenticated(false);
152
153                         }
154                     }
155                 };
156
157                 SolarEdgeCommand command = commandQueue.poll();
158                 command.setListener(statusUpdater);
159                 command.performAction(httpClient);
160             }
161         }
162     }
163
164     /**
165      * Constructor to set up interface
166      *
167      * @param config Bridge configuration
168      */
169     public WebInterface(ScheduledExecutorService scheduler, SolarEdgeHandler handler, HttpClient httpClient) {
170         this.config = handler.getConfiguration();
171         this.handler = handler;
172         this.scheduler = scheduler;
173         this.httpClient = httpClient;
174         this.requestExecutor = new WebRequestExecutor();
175         this.requestExecutorJobReference = new AtomicReference<>(null);
176     }
177
178     public void start() {
179         this.config = handler.getConfiguration();
180         setAuthenticated(false);
181         updateJobReference(requestExecutorJobReference, scheduler.scheduleWithFixedDelay(requestExecutor,
182                 WEB_REQUEST_INITIAL_DELAY, WEB_REQUEST_INTERVAL, TimeUnit.MILLISECONDS));
183     }
184
185     /**
186      * queues any command for execution
187      *
188      * @param command
189      */
190     public void enqueueCommand(SolarEdgeCommand command) {
191         requestExecutor.enqueue(command);
192     }
193
194     /**
195      * authenticates with the Solaredge WEB interface
196      *
197      * @throws UnsupportedEncodingException
198      */
199     private synchronized void authenticate() {
200         setAuthenticated(false);
201
202         if (preCheck()) {
203             SolarEdgeCommand tokenCheckCommand;
204
205             StatusUpdateListener tokenCheckListener = new StatusUpdateListener() {
206
207                 @Override
208                 public void update(CommunicationStatus status) {
209                     String errorMessageCodeFound;
210                     String errorMessgaeCodeForbidden;
211                     if (config.isUsePrivateApi()) {
212                         errorMessageCodeFound = "login error with private API: invalid token";
213                         errorMessgaeCodeForbidden = "login error with private API: invalid solarId";
214                     } else {
215                         errorMessageCodeFound = "login error with public API: unknown error";
216                         errorMessgaeCodeForbidden = "login error with public API: invalid api key or solarId is not valid for this api key";
217                     }
218
219                     switch (status.getHttpCode()) {
220                         case OK:
221                             handler.setStatusInfo(ThingStatus.ONLINE, ThingStatusDetail.NONE, "logged in");
222                             setAuthenticated(true);
223                             break;
224                         case FOUND:
225                             handler.setStatusInfo(ThingStatus.UNKNOWN, ThingStatusDetail.CONFIGURATION_ERROR,
226                                     errorMessageCodeFound);
227                             setAuthenticated(false);
228                             break;
229                         case FORBIDDEN:
230                             handler.setStatusInfo(ThingStatus.UNKNOWN, ThingStatusDetail.CONFIGURATION_ERROR,
231                                     errorMessgaeCodeForbidden);
232                             setAuthenticated(false);
233                             break;
234                         case SERVICE_UNAVAILABLE:
235                             handler.setStatusInfo(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE,
236                                     status.getMessage());
237                             setAuthenticated(false);
238                             break;
239                         default:
240                             handler.setStatusInfo(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
241                                     status.getMessage());
242                             setAuthenticated(false);
243                     }
244                 }
245             };
246
247             if (config.isUsePrivateApi()) {
248                 tokenCheckCommand = new PrivateApiTokenCheck(handler, tokenCheckListener);
249             } else {
250                 tokenCheckCommand = new PublicApiKeyCheck(handler, tokenCheckListener);
251             }
252             tokenCheckCommand.performAction(httpClient);
253         }
254     }
255
256     /**
257      * performs some pre cheks on configuration before attempting to login
258      *
259      * @return true on success, false otherwise
260      */
261     private boolean preCheck() {
262         String preCheckStatusMessage = "";
263         String localTokenOrApiKey = config.getTokenOrApiKey();
264         String localSolarId = config.getSolarId();
265
266         if (localTokenOrApiKey == null || localTokenOrApiKey.isEmpty()) {
267             preCheckStatusMessage = "please configure token/api_key first";
268         } else if (localSolarId == null || localSolarId.isEmpty()) {
269             preCheckStatusMessage = "please configure solarId first";
270         } else if (config.isUsePrivateApi() && localTokenOrApiKey.length() < TOKEN_THRESHOLD) {
271             preCheckStatusMessage = "you will have to use a 'token' and not an 'api key' when using private API";
272         } else if (!config.isUsePrivateApi() && localTokenOrApiKey.length() > API_KEY_THRESHOLD) {
273             preCheckStatusMessage = "you will have to use an 'api key' and not a 'token' when using public API";
274         } else if (!config.isUsePrivateApi() && calcRequestsPerDay() > WEB_REQUEST_PUBLIC_API_DAY_LIMIT) {
275             preCheckStatusMessage = "daily request limit (" + WEB_REQUEST_PUBLIC_API_DAY_LIMIT + ") exceeded: "
276                     + calcRequestsPerDay();
277         } else if (config.isUsePrivateApi() && !config.isMeterInstalled()) {
278             preCheckStatusMessage = "a meter must be present in order to use the private API";
279         } else {
280             return true;
281         }
282
283         this.handler.setStatusInfo(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, preCheckStatusMessage);
284         return false;
285     }
286
287     /**
288      * calculates requests per day. just an internal helper
289      *
290      * @return
291      */
292     private long calcRequestsPerDay() {
293         return MINUTES_PER_DAY / this.config.getLiveDataPollingInterval()
294                 + 4 * MINUTES_PER_DAY / this.config.getAggregateDataPollingInterval();
295     }
296
297     /**
298      * will be called by the ThingHandler to abort periodic jobs.
299      */
300     public void dispose() {
301         logger.debug("Webinterface disposed.");
302         cancelJobReference(requestExecutorJobReference);
303         setAuthenticated(false);
304     }
305
306     /**
307      * returns authentication status.
308      *
309      * @return
310      */
311     private boolean isAuthenticated() {
312         return authenticated;
313     }
314
315     private void setAuthenticated(boolean authenticated) {
316         this.authenticated = authenticated;
317     }
318 }