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