]> git.basschouten.com Git - openhab-addons.git/blob
bef2421582d1ee617f52f9a91d37e8e275b2eea3
[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.vesync.internal.api;
14
15 import static org.openhab.binding.vesync.internal.dto.requests.VeSyncProtocolConstants.*;
16
17 import java.net.HttpURLConnection;
18 import java.nio.charset.StandardCharsets;
19 import java.security.MessageDigest;
20 import java.security.NoSuchAlgorithmException;
21 import java.util.Collections;
22 import java.util.HashMap;
23 import java.util.Map;
24 import java.util.concurrent.ExecutionException;
25 import java.util.concurrent.TimeUnit;
26 import java.util.concurrent.TimeoutException;
27
28 import javax.validation.constraints.NotNull;
29
30 import org.eclipse.jdt.annotation.NonNullByDefault;
31 import org.eclipse.jdt.annotation.Nullable;
32 import org.eclipse.jetty.client.HttpClient;
33 import org.eclipse.jetty.client.api.ContentResponse;
34 import org.eclipse.jetty.client.api.Request;
35 import org.eclipse.jetty.client.util.StringContentProvider;
36 import org.eclipse.jetty.http.HttpHeader;
37 import org.openhab.binding.vesync.internal.VeSyncConstants;
38 import org.openhab.binding.vesync.internal.dto.requests.VeSyncAuthenticatedRequest;
39 import org.openhab.binding.vesync.internal.dto.requests.VeSyncLoginCredentials;
40 import org.openhab.binding.vesync.internal.dto.requests.VeSyncRequestManagedDeviceBypassV2;
41 import org.openhab.binding.vesync.internal.dto.requests.VeSyncRequestManagedDevicesPage;
42 import org.openhab.binding.vesync.internal.dto.responses.VeSyncLoginResponse;
43 import org.openhab.binding.vesync.internal.dto.responses.VeSyncManagedDeviceBase;
44 import org.openhab.binding.vesync.internal.dto.responses.VeSyncManagedDevicesPage;
45 import org.openhab.binding.vesync.internal.dto.responses.VeSyncResponse;
46 import org.openhab.binding.vesync.internal.dto.responses.VeSyncUserSession;
47 import org.openhab.binding.vesync.internal.exceptions.AuthenticationException;
48 import org.openhab.binding.vesync.internal.exceptions.DeviceUnknownException;
49 import org.openhab.binding.vesync.internal.handlers.VeSyncBridgeHandler;
50 import org.slf4j.Logger;
51 import org.slf4j.LoggerFactory;
52
53 /**
54  * @author David Goodyear - Initial contribution
55  */
56 @NonNullByDefault
57 public class VeSyncV2ApiHelper {
58
59     private final Logger logger = LoggerFactory.getLogger(VeSyncV2ApiHelper.class);
60
61     private @NonNullByDefault({}) HttpClient httpClient;
62
63     private volatile @Nullable VeSyncUserSession loggedInSession;
64
65     private Map<String, @NotNull VeSyncManagedDeviceBase> macLookup;
66
67     public VeSyncV2ApiHelper() {
68         macLookup = new HashMap<>();
69     }
70
71     public Map<String, @NotNull VeSyncManagedDeviceBase> getMacLookupMap() {
72         return macLookup;
73     }
74
75     /**
76      * Sets the httpClient object to be used for API calls to Vesync.
77      *
78      * @param httpClient the client to be used.
79      */
80     public void setHttpClient(@Nullable HttpClient httpClient) {
81         this.httpClient = httpClient;
82     }
83
84     public static @NotNull String calculateMd5(final @Nullable String password) {
85         if (password == null) {
86             return "";
87         }
88         MessageDigest md5;
89         StringBuilder md5Result = new StringBuilder();
90         try {
91             md5 = MessageDigest.getInstance("MD5");
92         } catch (NoSuchAlgorithmException e) {
93             return "";
94         }
95         byte[] handshakeHash = md5.digest(password.getBytes(StandardCharsets.UTF_8));
96         for (byte handshakeByte : handshakeHash) {
97             md5Result.append(String.format("%02x", handshakeByte));
98         }
99         return md5Result.toString();
100     }
101
102     public void discoverDevices() throws AuthenticationException {
103         try {
104             VeSyncRequestManagedDevicesPage reqDevPage = new VeSyncRequestManagedDevicesPage(loggedInSession);
105             boolean finished = false;
106             int pageNo = 1;
107             HashMap<String, VeSyncManagedDeviceBase> generatedMacLookup = new HashMap<>();
108             while (!finished) {
109                 reqDevPage.pageNo = String.valueOf(pageNo);
110                 reqDevPage.pageSize = String.valueOf(100);
111                 final String result = reqV1Authorized(V1_MANAGED_DEVICES_ENDPOINT, reqDevPage);
112
113                 VeSyncManagedDevicesPage resultsPage = VeSyncConstants.GSON.fromJson(result,
114                         VeSyncManagedDevicesPage.class);
115                 if (resultsPage == null || !resultsPage.outcome.getTotal().equals(resultsPage.outcome.getPageSize())) {
116                     finished = true;
117                 } else {
118                     ++pageNo;
119                 }
120
121                 if (resultsPage != null) {
122                     for (VeSyncManagedDeviceBase device : resultsPage.outcome.list) {
123                         logger.debug(
124                                 "Found device : {}, type: {}, deviceType: {}, connectionState: {}, deviceStatus: {}, deviceRegion: {}, cid: {}, configModule: {}, macID: {}, uuid: {}",
125                                 device.getDeviceName(), device.getType(), device.getDeviceType(),
126                                 device.getConnectionStatus(), device.getDeviceStatus(), device.getDeviceRegion(),
127                                 device.getCid(), device.getConfigModule(), device.getMacId(), device.getUuid());
128
129                         // Update the mac address -> device table
130                         generatedMacLookup.put(device.getMacId(), device);
131                     }
132                 }
133             }
134             macLookup = Collections.unmodifiableMap(generatedMacLookup);
135         } catch (final AuthenticationException ae) {
136             logger.warn("Failed background device scan : {}", ae.getMessage());
137             throw ae;
138         }
139     }
140
141     public String reqV2Authorized(final String url, final String macId, final VeSyncAuthenticatedRequest requestData)
142             throws AuthenticationException, DeviceUnknownException {
143         if (loggedInSession == null) {
144             throw new AuthenticationException("User is not logged in");
145         }
146         // Apply current session authentication data
147         requestData.applyAuthentication(loggedInSession);
148
149         // Apply specific addressing parameters
150         if (requestData instanceof VeSyncRequestManagedDeviceBypassV2) {
151             final VeSyncManagedDeviceBase deviceData = macLookup.get(macId);
152             if (deviceData == null) {
153                 throw new DeviceUnknownException(String.format("Device not discovered with mac id: %s", macId));
154             }
155             ((VeSyncRequestManagedDeviceBypassV2) requestData).cid = deviceData.cid;
156             ((VeSyncRequestManagedDeviceBypassV2) requestData).configModule = deviceData.configModule;
157             ((VeSyncRequestManagedDeviceBypassV2) requestData).deviceRegion = deviceData.deviceRegion;
158         }
159         return reqV1Authorized(url, requestData);
160     }
161
162     public String reqV1Authorized(final String url, final VeSyncAuthenticatedRequest requestData)
163             throws AuthenticationException {
164         try {
165             return directReqV1Authorized(url, requestData);
166         } catch (final AuthenticationException ae) {
167             throw ae;
168         }
169     }
170
171     private String directReqV1Authorized(final String url, final VeSyncAuthenticatedRequest requestData)
172             throws AuthenticationException {
173         try {
174             Request request = httpClient.POST(url);
175
176             // No headers for login
177             request.content(new StringContentProvider(VeSyncConstants.GSON.toJson(requestData)));
178
179             logger.debug("POST @ {} with content\r\n{}", url, VeSyncConstants.GSON.toJson(requestData));
180
181             request.header(HttpHeader.CONTENT_TYPE, "application/json; utf-8");
182
183             ContentResponse response = request.timeout(5, TimeUnit.SECONDS).send();
184             if (response.getStatus() == HttpURLConnection.HTTP_OK) {
185                 VeSyncResponse commResponse = VeSyncConstants.GSON.fromJson(response.getContentAsString(),
186                         VeSyncResponse.class);
187
188                 if (commResponse != null && (commResponse.isMsgSuccess() || commResponse.isMsgDeviceOffline())) {
189                     logger.debug("Got OK response {}", response.getContentAsString());
190                     return response.getContentAsString();
191                 } else {
192                     logger.debug("Got FAILED response {}", response.getContentAsString());
193                     throw new AuthenticationException("Invalid JSON response from login");
194                 }
195             } else {
196                 logger.debug("HTTP Response Code: {}", response.getStatus());
197                 logger.debug("HTTP Response Msg: {}", response.getReason());
198                 throw new AuthenticationException(
199                         "HTTP response " + response.getStatus() + " - " + response.getReason());
200             }
201         } catch (InterruptedException | TimeoutException | ExecutionException e) {
202             throw new AuthenticationException(e);
203         }
204     }
205
206     public synchronized void login(final @Nullable String username, final @Nullable String password,
207             final @Nullable String timezone) throws AuthenticationException {
208         if (username == null || password == null || timezone == null) {
209             loggedInSession = null;
210             return;
211         }
212         try {
213             loggedInSession = processLogin(username, password, timezone).getUserSession();
214         } catch (final AuthenticationException ae) {
215             loggedInSession = null;
216             throw ae;
217         }
218     }
219
220     public void updateBridgeData(final VeSyncBridgeHandler bridge) {
221         bridge.handleNewUserSession(loggedInSession);
222     }
223
224     private VeSyncLoginResponse processLogin(String username, String password, String timezone)
225             throws AuthenticationException {
226         try {
227             Request request = httpClient.POST(V1_LOGIN_ENDPOINT);
228
229             // No headers for login
230             request.content(new StringContentProvider(
231                     VeSyncConstants.GSON.toJson(new VeSyncLoginCredentials(username, password))));
232
233             request.header(HttpHeader.CONTENT_TYPE, "application/json; utf-8");
234
235             ContentResponse response = request.timeout(5, TimeUnit.SECONDS).send();
236             if (response.getStatus() == HttpURLConnection.HTTP_OK) {
237                 VeSyncLoginResponse loginResponse = VeSyncConstants.GSON.fromJson(response.getContentAsString(),
238                         VeSyncLoginResponse.class);
239                 if (loginResponse != null && loginResponse.isMsgSuccess()) {
240                     logger.debug("Login successful");
241                     return loginResponse;
242                 } else {
243                     throw new AuthenticationException("Invalid / unexpected JSON response from login");
244                 }
245             } else {
246                 logger.warn("Login Failed - HTTP Response Code: {} - {}", response.getStatus(), response.getReason());
247                 throw new AuthenticationException(
248                         "HTTP response " + response.getStatus() + " - " + response.getReason());
249             }
250         } catch (InterruptedException | TimeoutException | ExecutionException e) {
251             throw new AuthenticationException(e);
252         }
253     }
254 }