]> git.basschouten.com Git - openhab-addons.git/blob
d824b10e932e9597d4dc586f6632bd0a40900255
[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.enphase.internal.handler;
14
15 import java.net.URI;
16 import java.util.Arrays;
17 import java.util.List;
18 import java.util.concurrent.ExecutionException;
19 import java.util.concurrent.TimeUnit;
20 import java.util.concurrent.TimeoutException;
21 import java.util.function.Function;
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.client.HttpResponseException;
27 import org.eclipse.jetty.client.api.Authentication;
28 import org.eclipse.jetty.client.api.Authentication.Result;
29 import org.eclipse.jetty.client.api.AuthenticationStore;
30 import org.eclipse.jetty.client.api.ContentResponse;
31 import org.eclipse.jetty.client.api.Request;
32 import org.eclipse.jetty.client.util.DigestAuthentication;
33 import org.eclipse.jetty.http.HttpMethod;
34 import org.eclipse.jetty.http.HttpStatus;
35 import org.openhab.binding.enphase.internal.EnphaseBindingConstants;
36 import org.openhab.binding.enphase.internal.EnvoyConfiguration;
37 import org.openhab.binding.enphase.internal.dto.EnvoyEnergyDTO;
38 import org.openhab.binding.enphase.internal.dto.EnvoyErrorDTO;
39 import org.openhab.binding.enphase.internal.dto.InventoryJsonDTO;
40 import org.openhab.binding.enphase.internal.dto.InverterDTO;
41 import org.openhab.binding.enphase.internal.dto.ProductionJsonDTO;
42 import org.openhab.binding.enphase.internal.exception.EnphaseException;
43 import org.openhab.binding.enphase.internal.exception.EnvoyConnectionException;
44 import org.openhab.binding.enphase.internal.exception.EnvoyNoHostnameException;
45 import org.slf4j.Logger;
46 import org.slf4j.LoggerFactory;
47
48 import com.google.gson.Gson;
49 import com.google.gson.GsonBuilder;
50 import com.google.gson.JsonSyntaxException;
51
52 /**
53  * Methods to make API calls to the Envoy gateway.
54  *
55  * @author Hilbrand Bouwkamp - Initial contribution
56  */
57 @NonNullByDefault
58 public class EnvoyConnector {
59
60     protected static final long CONNECT_TIMEOUT_SECONDS = 10;
61
62     private static final String HTTP = "http://";
63     private static final String PRODUCTION_JSON_URL = "/production.json";
64     private static final String INVENTORY_JSON_URL = "/inventory.json";
65     private static final String PRODUCTION_URL = "/api/v1/production";
66     private static final String CONSUMPTION_URL = "/api/v1/consumption";
67     private static final String INVERTERS_URL = PRODUCTION_URL + "/inverters";
68     private static final String INFO_XML = "/info.xml";
69
70     private static final String INFO_SOFTWARE_BEGIN = "<software>";
71     private static final String INFO_SOFTWARE_END = "</software>";
72
73     protected final HttpClient httpClient;
74
75     private final Logger logger = LoggerFactory.getLogger(EnvoyConnector.class);
76     private final Gson gson = new GsonBuilder().create();
77     private final String schema;
78
79     private @Nullable DigestAuthentication envoyAuthn;
80     private @Nullable URI invertersURI;
81
82     protected @NonNullByDefault({}) EnvoyConfiguration configuration;
83
84     public EnvoyConnector(final HttpClient httpClient) {
85         this(httpClient, HTTP);
86     }
87
88     protected EnvoyConnector(final HttpClient httpClient, final String schema) {
89         this.httpClient = httpClient;
90         this.schema = schema;
91     }
92
93     /**
94      * Sets the Envoy connection configuration.
95      *
96      * @param configuration the configuration to set
97      * @return configuration error message or empty string if no configuration errors present
98      */
99     public String setConfiguration(final EnvoyConfiguration configuration) {
100         this.configuration = configuration;
101
102         if (configuration.hostname.isEmpty()) {
103             return "";
104         }
105         final String password = configuration.password.isEmpty()
106                 ? EnphaseBindingConstants.defaultPassword(configuration.serialNumber)
107                 : configuration.password;
108         final String username = configuration.username.isEmpty() ? EnvoyConfiguration.DEFAULT_USERNAME
109                 : configuration.username;
110
111         final AuthenticationStore store = httpClient.getAuthenticationStore();
112
113         if (envoyAuthn != null) {
114             store.removeAuthentication(envoyAuthn);
115         }
116         invertersURI = URI.create(schema + configuration.hostname + INVERTERS_URL);
117         envoyAuthn = new DigestAuthentication(invertersURI, Authentication.ANY_REALM, username, password);
118         store.addAuthentication(envoyAuthn);
119         return "";
120     }
121
122     /**
123      * Checks if data can be read from the Envoy, and to determine the software version returned by the Envoy.
124      *
125      * @param hostname hostname of the Envoy.
126      * @return software version number as reported by the the Envoy or null if connection could be made or software
127      *         version not detected.
128      */
129     protected @Nullable String checkConnection(final String hostname) {
130         try {
131             final String url = hostname + INFO_XML;
132             logger.debug("Check connection to '{}'", url);
133             final Request createRequest = createRequest(url);
134             final ContentResponse response = send(createRequest);
135
136             logger.debug("Checkconnection status from request is: {}", response.getStatus());
137             if (response.getStatus() == HttpStatus.OK_200) {
138                 final String content = response.getContentAsString();
139                 final int begin = content.indexOf(INFO_SOFTWARE_BEGIN);
140                 final int end = content.lastIndexOf(INFO_SOFTWARE_END);
141
142                 if (begin > 0 && end > 0) {
143                     final String version = content.substring(begin + INFO_SOFTWARE_BEGIN.length(), end);
144
145                     logger.debug("Found Envoy version number '{}' in info.xml", version);
146                     return Character.isDigit(version.charAt(0)) ? version : version.substring(1);
147                 }
148             }
149         } catch (EnphaseException | HttpResponseException e) {
150             logger.debug("Exception trying to check the connection.", e);
151         }
152         return null;
153     }
154
155     /**
156      * @return Returns the production data from the Envoy gateway.
157      */
158     public EnvoyEnergyDTO getProduction() throws EnphaseException {
159         return retrieveData(PRODUCTION_URL, this::jsonToEnvoyEnergyDTO);
160     }
161
162     /**
163      * @return Returns the consumption data from the Envoy gateway.
164      */
165     public EnvoyEnergyDTO getConsumption() throws EnphaseException {
166         return retrieveData(CONSUMPTION_URL, this::jsonToEnvoyEnergyDTO);
167     }
168
169     private @Nullable EnvoyEnergyDTO jsonToEnvoyEnergyDTO(final String json) {
170         return gson.fromJson(json, EnvoyEnergyDTO.class);
171     }
172
173     /**
174      * @return Returns the production/consumption data from the Envoy gateway.
175      */
176     public ProductionJsonDTO getProductionJson() throws EnphaseException {
177         return retrieveData(PRODUCTION_JSON_URL, json -> gson.fromJson(json, ProductionJsonDTO.class));
178     }
179
180     /**
181      * @return Returns the inventory data from the Envoy gateway.
182      */
183     public List<InventoryJsonDTO> getInventoryJson() throws EnphaseException {
184         return retrieveData(INVENTORY_JSON_URL, this::jsonToEnvoyInventoryJson);
185     }
186
187     private @Nullable List<InventoryJsonDTO> jsonToEnvoyInventoryJson(final String json) {
188         final InventoryJsonDTO @Nullable [] list = gson.fromJson(json, InventoryJsonDTO[].class);
189
190         return list == null ? null : Arrays.asList(list);
191     }
192
193     /**
194      * @return Returns the production data for the inverters.
195      */
196     public List<InverterDTO> getInverters() throws EnphaseException {
197         synchronized (this) {
198             final AuthenticationStore store = httpClient.getAuthenticationStore();
199             final Result invertersResult = store.findAuthenticationResult(invertersURI);
200
201             if (invertersResult != null) {
202                 store.removeAuthenticationResult(invertersResult);
203             }
204         }
205         return retrieveData(INVERTERS_URL, json -> Arrays.asList(gson.fromJson(json, InverterDTO[].class)));
206     }
207
208     private synchronized <T> T retrieveData(final String urlPath, final Function<String, @Nullable T> jsonConverter)
209             throws EnphaseException {
210         final Request request = createRequest(configuration.hostname + urlPath);
211
212         constructRequest(request);
213         final ContentResponse response = send(request);
214         final String content = response.getContentAsString();
215
216         logger.trace("Envoy returned data for '{}' with status {}: {}", urlPath, response.getStatus(), content);
217         try {
218             if (response.getStatus() == HttpStatus.OK_200) {
219                 final T result = jsonConverter.apply(content);
220                 if (result == null) {
221                     throw new EnvoyConnectionException("No data received");
222                 }
223                 return result;
224             } else {
225                 final @Nullable EnvoyErrorDTO error = gson.fromJson(content, EnvoyErrorDTO.class);
226
227                 logger.debug("Envoy returned an error: {}", error);
228                 throw new EnvoyConnectionException(error == null ? response.getReason() : error.info);
229             }
230         } catch (final JsonSyntaxException e) {
231             logger.debug("Error parsing json: {}", content, e);
232             throw new EnvoyConnectionException("Error parsing data: ", e);
233         }
234     }
235
236     private Request createRequest(final String urlPath) throws EnvoyNoHostnameException {
237         return httpClient.newRequest(URI.create(schema + urlPath)).method(HttpMethod.GET)
238                 .timeout(CONNECT_TIMEOUT_SECONDS, TimeUnit.SECONDS);
239     }
240
241     protected void constructRequest(final Request request) throws EnphaseException {
242         logger.trace("Retrieving data from '{}' ", request.getURI());
243     }
244
245     protected ContentResponse send(final Request request) throws EnvoyConnectionException {
246         try {
247             return request.send();
248         } catch (final InterruptedException e) {
249             Thread.currentThread().interrupt();
250             throw new EnvoyConnectionException("Interrupted");
251         } catch (final TimeoutException e) {
252             logger.debug("TimeoutException: {}", e.getMessage());
253             throw new EnvoyConnectionException("Connection timeout: ", e);
254         } catch (final ExecutionException e) {
255             logger.debug("ExecutionException: {}", e.getMessage(), e);
256             throw new EnvoyConnectionException("Could not retrieve data: ", e.getCause());
257         }
258     }
259 }