]> git.basschouten.com Git - openhab-addons.git/blob
b9fff4b80bc3a4d9d47f27568559a232fe0499e9
[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.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         private void processAuthenticationResult(CommunicationStatus status) {
108             String errorMessageCodeFound;
109             String errorMessgaeCodeForbidden = STATUS_INVALID_SOLAR_ID;
110             if (config.isUsePrivateApi()) {
111                 errorMessageCodeFound = STATUS_INVALID_TOKEN;
112             } else {
113                 errorMessageCodeFound = STATUS_UNKNOWN_ERROR;
114             }
115
116             switch (status.getHttpCode()) {
117                 case OK:
118                     handler.setStatusInfo(ThingStatus.ONLINE, ThingStatusDetail.NONE, null);
119                     setAuthenticated(true);
120                     break;
121                 case FOUND:
122                     handler.setStatusInfo(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
123                             errorMessageCodeFound);
124                     setAuthenticated(false);
125                     break;
126                 case FORBIDDEN:
127                     handler.setStatusInfo(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
128                             errorMessgaeCodeForbidden);
129                     setAuthenticated(false);
130                     break;
131                 case SERVICE_UNAVAILABLE:
132                     handler.setStatusInfo(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE, status.getMessage());
133                     setAuthenticated(false);
134                     break;
135                 default:
136                     handler.setStatusInfo(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
137                             status.getMessage());
138                     setAuthenticated(false);
139             }
140         }
141
142         /**
143          * authenticates with the Solaredge WEB interface
144          */
145         private synchronized void authenticate() {
146             setAuthenticated(false);
147
148             if (preCheck()) {
149                 SolarEdgeCommand tokenCheckCommand;
150
151                 if (config.isUsePrivateApi()) {
152                     tokenCheckCommand = new PrivateApiTokenCheck(handler, this::processAuthenticationResult);
153                 } else {
154                     tokenCheckCommand = new PublicApiKeyCheck(handler, this::processAuthenticationResult);
155                 }
156                 tokenCheckCommand.performAction(httpClient);
157             }
158         }
159
160         /**
161          * performs some pre cheks on configuration before attempting to login
162          *
163          * @return true on success, false otherwise
164          */
165         private boolean preCheck() {
166             String preCheckStatusMessage = "";
167             String localTokenOrApiKey = config.getTokenOrApiKey();
168
169             if (config.isUsePrivateApi() && localTokenOrApiKey.length() < TOKEN_THRESHOLD) {
170                 preCheckStatusMessage = STATUS_INVALID_TOKEN_LENGTH;
171             } else if (!config.isUsePrivateApi() && localTokenOrApiKey.length() > API_KEY_THRESHOLD) {
172                 preCheckStatusMessage = STATUS_INVALID_API_KEY_LENGTH;
173             } else if (!config.isUsePrivateApi() && calcRequestsPerDay() > WEB_REQUEST_PUBLIC_API_DAY_LIMIT) {
174                 preCheckStatusMessage = STATUS_REQUEST_LIMIT_EXCEEDED;
175             } else if (config.isUsePrivateApi() && !config.isMeterInstalled()) {
176                 preCheckStatusMessage = STATUS_NO_METER_CONFIGURED;
177             } else {
178                 return true;
179             }
180
181             handler.setStatusInfo(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, preCheckStatusMessage);
182             return false;
183         }
184
185         /**
186          * calculates requests per day. just an internal helper
187          *
188          * @return
189          */
190         private long calcRequestsPerDay() {
191             return MINUTES_PER_DAY / config.getLiveDataPollingInterval()
192                     + 4 * MINUTES_PER_DAY / config.getAggregateDataPollingInterval();
193         }
194
195         /**
196          * puts a command into the queue
197          *
198          * @param command
199          */
200         void enqueue(SolarEdgeCommand command) {
201             try {
202                 commandQueue.add(command);
203             } catch (IllegalStateException ex) {
204                 if (commandQueue.size() >= WEB_REQUEST_QUEUE_MAX_SIZE) {
205                     logger.debug(
206                             "Could not add command to command queue because queue is already full. Maybe SolarEdge is down?");
207                 } else {
208                     logger.warn("Could not add command to queue - IllegalStateException");
209                 }
210             }
211         }
212
213         /**
214          * executes the web request
215          */
216         @Override
217         public void run() {
218             if (!isAuthenticated()) {
219                 authenticate();
220             }
221
222             if (isAuthenticated() && !commandQueue.isEmpty()) {
223                 try {
224                     executeCommand();
225                 } catch (Exception ex) {
226                     logger.warn("command execution ended with exception:", ex);
227                 }
228             }
229         }
230
231         /**
232          * executes the next command in the queue. requires authenticated session.
233          *
234          * @throws ValidationException
235          */
236         private void executeCommand() {
237             SolarEdgeCommand command = commandQueue.poll();
238             if (command != null) {
239                 command.performAction(httpClient);
240             }
241         }
242     }
243
244     /**
245      * Constructor to set up interface
246      *
247      * @param config Bridge configuration
248      */
249     public WebInterface(ScheduledExecutorService scheduler, SolarEdgeHandler handler, HttpClient httpClient) {
250         this.config = handler.getConfiguration();
251         this.handler = handler;
252         this.scheduler = scheduler;
253         this.httpClient = httpClient;
254         this.requestExecutor = new WebRequestExecutor();
255         this.requestExecutorJobReference = new AtomicReference<>(null);
256     }
257
258     public void start() {
259         this.config = handler.getConfiguration();
260         setAuthenticated(false);
261         updateJobReference(requestExecutorJobReference, scheduler.scheduleWithFixedDelay(requestExecutor,
262                 WEB_REQUEST_INITIAL_DELAY, WEB_REQUEST_INTERVAL, TimeUnit.MILLISECONDS));
263     }
264
265     /**
266      * queues any command for execution
267      *
268      * @param command
269      */
270     public void enqueueCommand(SolarEdgeCommand command) {
271         requestExecutor.enqueue(command);
272     }
273
274     /**
275      * will be called by the ThingHandler to abort periodic jobs.
276      */
277     public void dispose() {
278         logger.debug("Webinterface disposed.");
279         cancelJobReference(requestExecutorJobReference);
280         setAuthenticated(false);
281     }
282
283     /**
284      * returns authentication status.
285      *
286      * @return
287      */
288     private boolean isAuthenticated() {
289         return authenticated;
290     }
291
292     private void setAuthenticated(boolean authenticated) {
293         this.authenticated = authenticated;
294     }
295 }