2 * Copyright (c) 2010-2023 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.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;
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;
39 * The connector is responsible for communication with the solaredge webportal
41 * @author Alexander Friese - initial contribution
44 public class WebInterface implements AtomicReferenceTrait {
46 private static final int API_KEY_THRESHOLD = 40;
47 private static final int TOKEN_THRESHOLD = 80;
49 private final Logger logger = LoggerFactory.getLogger(WebInterface.class);
54 private SolarEdgeConfiguration config;
57 * handler for updating thing status
59 private final SolarEdgeHandler handler;
62 * holds authentication status
64 private boolean authenticated = false;
67 * HTTP client for asynchronous calls
69 private final HttpClient httpClient;
72 * the scheduler which periodically sends web requests to the solaredge API. Should be initiated with the thing's
73 * existing scheduler instance.
75 private final ScheduledExecutorService scheduler;
80 private final WebRequestExecutor requestExecutor;
83 * periodic request executor job
85 private final AtomicReference<@Nullable Future<?>> requestExecutorJobReference;
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.
91 * @author afriese - initial contribution
93 private class WebRequestExecutor implements Runnable {
96 * queue which holds the commands to execute
98 private final Queue<SolarEdgeCommand> commandQueue;
103 WebRequestExecutor() {
104 this.commandQueue = new BlockingArrayQueue<>(WEB_REQUEST_QUEUE_MAX_SIZE);
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;
113 errorMessageCodeFound = STATUS_UNKNOWN_ERROR;
116 switch (status.getHttpCode()) {
118 handler.setStatusInfo(ThingStatus.ONLINE, ThingStatusDetail.NONE, null);
119 setAuthenticated(true);
122 handler.setStatusInfo(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
123 errorMessageCodeFound);
124 setAuthenticated(false);
127 handler.setStatusInfo(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
128 errorMessgaeCodeForbidden);
129 setAuthenticated(false);
131 case SERVICE_UNAVAILABLE:
132 handler.setStatusInfo(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE, status.getMessage());
133 setAuthenticated(false);
136 handler.setStatusInfo(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
137 status.getMessage());
138 setAuthenticated(false);
143 * authenticates with the Solaredge WEB interface
145 private synchronized void authenticate() {
146 setAuthenticated(false);
149 SolarEdgeCommand tokenCheckCommand;
151 if (config.isUsePrivateApi()) {
152 tokenCheckCommand = new PrivateApiTokenCheck(handler, this::processAuthenticationResult);
154 tokenCheckCommand = new PublicApiKeyCheck(handler, this::processAuthenticationResult);
156 tokenCheckCommand.performAction(httpClient);
161 * performs some pre cheks on configuration before attempting to login
163 * @return true on success, false otherwise
165 private boolean preCheck() {
166 String preCheckStatusMessage = "";
167 String localTokenOrApiKey = config.getTokenOrApiKey();
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;
181 handler.setStatusInfo(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, preCheckStatusMessage);
186 * calculates requests per day. just an internal helper
190 private long calcRequestsPerDay() {
191 return MINUTES_PER_DAY / config.getLiveDataPollingInterval()
192 + 4 * MINUTES_PER_DAY / config.getAggregateDataPollingInterval();
196 * puts a command into the queue
200 void enqueue(SolarEdgeCommand command) {
202 commandQueue.add(command);
203 } catch (IllegalStateException ex) {
204 if (commandQueue.size() >= WEB_REQUEST_QUEUE_MAX_SIZE) {
206 "Could not add command to command queue because queue is already full. Maybe SolarEdge is down?");
208 logger.warn("Could not add command to queue - IllegalStateException");
214 * executes the web request
218 if (!isAuthenticated()) {
222 if (isAuthenticated() && !commandQueue.isEmpty()) {
225 } catch (Exception ex) {
226 logger.warn("command execution ended with exception:", ex);
232 * executes the next command in the queue. requires authenticated session.
234 * @throws ValidationException
236 private void executeCommand() {
237 SolarEdgeCommand command = commandQueue.poll();
238 if (command != null) {
239 command.performAction(httpClient);
245 * Constructor to set up interface
247 * @param config Bridge configuration
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);
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));
266 * queues any command for execution
270 public void enqueueCommand(SolarEdgeCommand command) {
271 requestExecutor.enqueue(command);
275 * will be called by the ThingHandler to abort periodic jobs.
277 public void dispose() {
278 logger.debug("Webinterface disposed.");
279 cancelJobReference(requestExecutorJobReference);
280 setAuthenticated(false);
284 * returns authentication status.
288 private boolean isAuthenticated() {
289 return authenticated;
292 private void setAuthenticated(boolean authenticated) {
293 this.authenticated = authenticated;