2 * Copyright (c) 2010-2021 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
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
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.solaredge.internal.connector;
15 import static org.openhab.binding.solaredge.internal.SolarEdgeBindingConstants.*;
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;
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;
40 * The connector is responsible for communication with the solaredge webportal
42 * @author Alexander Friese - initial contribution
45 public class WebInterface implements AtomicReferenceTrait {
47 private static final int API_KEY_THRESHOLD = 40;
48 private static final int TOKEN_THRESHOLD = 80;
50 private final Logger logger = LoggerFactory.getLogger(WebInterface.class);
55 private SolarEdgeConfiguration config;
58 * handler for updating thing status
60 private final SolarEdgeHandler handler;
63 * holds authentication status
65 private boolean authenticated = false;
68 * HTTP client for asynchronous calls
70 private final HttpClient httpClient;
73 * the scheduler which periodically sends web requests to the solaredge API. Should be initiated with the thing's
74 * existing scheduler instance.
76 private final ScheduledExecutorService scheduler;
81 private final WebRequestExecutor requestExecutor;
84 * periodic request executor job
86 private final AtomicReference<@Nullable Future<?>> requestExecutorJobReference;
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.
92 * @author afriese - initial contribution
94 private class WebRequestExecutor implements Runnable {
97 * queue which holds the commands to execute
99 private final Queue<SolarEdgeCommand> commandQueue;
104 WebRequestExecutor() {
105 this.commandQueue = new BlockingArrayQueue<>(WEB_REQUEST_QUEUE_MAX_SIZE);
109 * puts a command into the queue
113 void enqueue(SolarEdgeCommand command) {
115 commandQueue.add(command);
116 } catch (IllegalStateException ex) {
117 if (commandQueue.size() >= WEB_REQUEST_QUEUE_MAX_SIZE) {
119 "Could not add command to command queue because queue is already full. Maybe SolarEdge is down?");
121 logger.warn("Could not add command to queue - IllegalStateException");
127 * executes the web request
131 if (!isAuthenticated()) {
135 else if (isAuthenticated() && !commandQueue.isEmpty()) {
136 StatusUpdateListener statusUpdater = new StatusUpdateListener() {
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);
146 // no action needed as the thing is already online.
149 handler.setStatusInfo(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
150 status.getMessage());
151 setAuthenticated(false);
157 SolarEdgeCommand command = commandQueue.poll();
158 command.setListener(statusUpdater);
159 command.performAction(httpClient);
165 * Constructor to set up interface
167 * @param config Bridge configuration
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);
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));
186 * queues any command for execution
190 public void enqueueCommand(SolarEdgeCommand command) {
191 requestExecutor.enqueue(command);
195 * authenticates with the Solaredge WEB interface
197 * @throws UnsupportedEncodingException
199 private synchronized void authenticate() {
200 setAuthenticated(false);
203 SolarEdgeCommand tokenCheckCommand;
205 StatusUpdateListener tokenCheckListener = new StatusUpdateListener() {
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";
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";
219 switch (status.getHttpCode()) {
221 handler.setStatusInfo(ThingStatus.ONLINE, ThingStatusDetail.NONE, "logged in");
222 setAuthenticated(true);
225 handler.setStatusInfo(ThingStatus.UNKNOWN, ThingStatusDetail.CONFIGURATION_ERROR,
226 errorMessageCodeFound);
227 setAuthenticated(false);
230 handler.setStatusInfo(ThingStatus.UNKNOWN, ThingStatusDetail.CONFIGURATION_ERROR,
231 errorMessgaeCodeForbidden);
232 setAuthenticated(false);
234 case SERVICE_UNAVAILABLE:
235 handler.setStatusInfo(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE,
236 status.getMessage());
237 setAuthenticated(false);
240 handler.setStatusInfo(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
241 status.getMessage());
242 setAuthenticated(false);
247 if (config.isUsePrivateApi()) {
248 tokenCheckCommand = new PrivateApiTokenCheck(handler, tokenCheckListener);
250 tokenCheckCommand = new PublicApiKeyCheck(handler, tokenCheckListener);
252 tokenCheckCommand.performAction(httpClient);
257 * performs some pre cheks on configuration before attempting to login
259 * @return true on success, false otherwise
261 private boolean preCheck() {
262 String preCheckStatusMessage = "";
263 String localTokenOrApiKey = config.getTokenOrApiKey();
264 String localSolarId = config.getSolarId();
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";
283 this.handler.setStatusInfo(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, preCheckStatusMessage);
288 * calculates requests per day. just an internal helper
292 private long calcRequestsPerDay() {
293 return MINUTES_PER_DAY / this.config.getLiveDataPollingInterval()
294 + 4 * MINUTES_PER_DAY / this.config.getAggregateDataPollingInterval();
298 * will be called by the ThingHandler to abort periodic jobs.
300 public void dispose() {
301 logger.debug("Webinterface disposed.");
302 cancelJobReference(requestExecutorJobReference);
303 setAuthenticated(false);
307 * returns authentication status.
311 private boolean isAuthenticated() {
312 return authenticated;
315 private void setAuthenticated(boolean authenticated) {
316 this.authenticated = authenticated;