]> git.basschouten.com Git - openhab-addons.git/blob
758a13bfbe0e72cbb42940234ce46b5075ae1563
[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.api.Authentication;
27 import org.eclipse.jetty.client.api.Authentication.Result;
28 import org.eclipse.jetty.client.api.AuthenticationStore;
29 import org.eclipse.jetty.client.api.ContentResponse;
30 import org.eclipse.jetty.client.api.Request;
31 import org.eclipse.jetty.client.util.DigestAuthentication;
32 import org.eclipse.jetty.http.HttpMethod;
33 import org.eclipse.jetty.http.HttpStatus;
34 import org.openhab.binding.enphase.internal.EnphaseBindingConstants;
35 import org.openhab.binding.enphase.internal.EnvoyConfiguration;
36 import org.openhab.binding.enphase.internal.EnvoyConnectionException;
37 import org.openhab.binding.enphase.internal.EnvoyNoHostnameException;
38 import org.openhab.binding.enphase.internal.dto.EnvoyEnergyDTO;
39 import org.openhab.binding.enphase.internal.dto.EnvoyErrorDTO;
40 import org.openhab.binding.enphase.internal.dto.InventoryJsonDTO;
41 import org.openhab.binding.enphase.internal.dto.InverterDTO;
42 import org.openhab.binding.enphase.internal.dto.ProductionJsonDTO;
43 import org.slf4j.Logger;
44 import org.slf4j.LoggerFactory;
45
46 import com.google.gson.Gson;
47 import com.google.gson.GsonBuilder;
48 import com.google.gson.JsonSyntaxException;
49
50 /**
51  * Methods to make API calls to the Envoy gateway.
52  *
53  * @author Hilbrand Bouwkamp - Initial contribution
54  */
55 @NonNullByDefault
56 class EnvoyConnector {
57
58     private static final String HTTP = "http://";
59     private static final String PRODUCTION_JSON_URL = "/production.json";
60     private static final String INVENTORY_JSON_URL = "/inventory.json";
61     private static final String PRODUCTION_URL = "/api/v1/production";
62     private static final String CONSUMPTION_URL = "/api/v1/consumption";
63     private static final String INVERTERS_URL = PRODUCTION_URL + "/inverters";
64     private static final long CONNECT_TIMEOUT_SECONDS = 5;
65
66     private final Logger logger = LoggerFactory.getLogger(EnvoyConnector.class);
67     private final Gson gson = new GsonBuilder().create();
68     private final HttpClient httpClient;
69     private String hostname = "";
70     private @Nullable DigestAuthentication envoyAuthn;
71     private @Nullable URI invertersURI;
72
73     public EnvoyConnector(final HttpClient httpClient) {
74         this.httpClient = httpClient;
75     }
76
77     /**
78      * Sets the Envoy connection configuration.
79      *
80      * @param configuration the configuration to set
81      */
82     public void setConfiguration(final EnvoyConfiguration configuration) {
83         hostname = configuration.hostname;
84         if (hostname.isEmpty()) {
85             return;
86         }
87         final String password = configuration.password.isEmpty()
88                 ? EnphaseBindingConstants.defaultPassword(configuration.serialNumber)
89                 : configuration.password;
90         final String username = configuration.username.isEmpty() ? EnvoyConfiguration.DEFAULT_USERNAME
91                 : configuration.username;
92         final AuthenticationStore store = httpClient.getAuthenticationStore();
93
94         if (envoyAuthn != null) {
95             store.removeAuthentication(envoyAuthn);
96         }
97         invertersURI = URI.create(HTTP + hostname + INVERTERS_URL);
98         envoyAuthn = new DigestAuthentication(invertersURI, Authentication.ANY_REALM, username, password);
99         store.addAuthentication(envoyAuthn);
100     }
101
102     /**
103      * @return Returns the production data from the Envoy gateway.
104      */
105     public EnvoyEnergyDTO getProduction() throws EnvoyConnectionException, EnvoyNoHostnameException {
106         return retrieveData(PRODUCTION_URL, this::jsonToEnvoyEnergyDTO);
107     }
108
109     /**
110      * @return Returns the consumption data from the Envoy gateway.
111      */
112     public EnvoyEnergyDTO getConsumption() throws EnvoyConnectionException, EnvoyNoHostnameException {
113         return retrieveData(CONSUMPTION_URL, this::jsonToEnvoyEnergyDTO);
114     }
115
116     private @Nullable EnvoyEnergyDTO jsonToEnvoyEnergyDTO(final String json) {
117         return gson.fromJson(json, EnvoyEnergyDTO.class);
118     }
119
120     /**
121      * @return Returns the production/consumption data from the Envoy gateway.
122      */
123     public ProductionJsonDTO getProductionJson() throws EnvoyConnectionException, EnvoyNoHostnameException {
124         return retrieveData(PRODUCTION_JSON_URL, json -> gson.fromJson(json, ProductionJsonDTO.class));
125     }
126
127     /**
128      * @return Returns the inventory data from the Envoy gateway.
129      */
130     public List<InventoryJsonDTO> getInventoryJson() throws EnvoyConnectionException, EnvoyNoHostnameException {
131         return retrieveData(INVENTORY_JSON_URL, this::jsonToEnvoyInventoryJson);
132     }
133
134     private @Nullable List<InventoryJsonDTO> jsonToEnvoyInventoryJson(final String json) {
135         final InventoryJsonDTO @Nullable [] list = gson.fromJson(json, InventoryJsonDTO[].class);
136
137         return list == null ? null : Arrays.asList(list);
138     }
139
140     /**
141      * @return Returns the production data for the inverters.
142      */
143     public List<InverterDTO> getInverters() throws EnvoyConnectionException, EnvoyNoHostnameException {
144         synchronized (this) {
145             final AuthenticationStore store = httpClient.getAuthenticationStore();
146             final Result invertersResult = store.findAuthenticationResult(invertersURI);
147
148             if (invertersResult != null) {
149                 store.removeAuthenticationResult(invertersResult);
150             }
151         }
152         return retrieveData(INVERTERS_URL, json -> Arrays.asList(gson.fromJson(json, InverterDTO[].class)));
153     }
154
155     private synchronized <T> T retrieveData(final String urlPath, final Function<String, @Nullable T> jsonConverter)
156             throws EnvoyConnectionException, EnvoyNoHostnameException {
157         try {
158             if (hostname.isEmpty()) {
159                 throw new EnvoyNoHostnameException("No host name/ip address known (yet)");
160             }
161             final URI uri = URI.create(HTTP + hostname + urlPath);
162             logger.trace("Retrieving data from '{}'", uri);
163             final Request request = httpClient.newRequest(uri).method(HttpMethod.GET).timeout(CONNECT_TIMEOUT_SECONDS,
164                     TimeUnit.SECONDS);
165             final ContentResponse response = request.send();
166             final String content = response.getContentAsString();
167
168             logger.trace("Envoy returned data for '{}' with status {}: {}", urlPath, response.getStatus(), content);
169             try {
170                 if (response.getStatus() == HttpStatus.OK_200) {
171                     final T result = jsonConverter.apply(content);
172                     if (result == null) {
173                         throw new EnvoyConnectionException("No data received");
174                     }
175                     return result;
176                 } else {
177                     final @Nullable EnvoyErrorDTO error = gson.fromJson(content, EnvoyErrorDTO.class);
178
179                     logger.debug("Envoy returned an error: {}", error);
180                     throw new EnvoyConnectionException(error == null ? response.getReason() : error.info);
181                 }
182             } catch (final JsonSyntaxException e) {
183                 logger.debug("Error parsing json: {}", content, e);
184                 throw new EnvoyConnectionException("Error parsing data: ", e);
185             }
186         } catch (final InterruptedException e) {
187             Thread.currentThread().interrupt();
188             throw new EnvoyConnectionException("Interrupted");
189         } catch (final TimeoutException e) {
190             logger.debug("TimeoutException: {}", e.getMessage());
191             throw new EnvoyConnectionException("Connection timeout: ", e);
192         } catch (final ExecutionException e) {
193             logger.debug("ExecutionException: {}", e.getMessage(), e);
194             throw new EnvoyConnectionException("Could not retrieve data: ", e.getCause());
195         }
196     }
197 }