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.vesync.internal.api;
15 import static org.openhab.binding.vesync.internal.dto.requests.VeSyncProtocolConstants.*;
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;
24 import java.util.concurrent.ExecutionException;
25 import java.util.concurrent.TimeUnit;
26 import java.util.concurrent.TimeoutException;
28 import javax.validation.constraints.NotNull;
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;
54 * @author David Goodyear - Initial contribution
57 public class VeSyncV2ApiHelper {
59 private final Logger logger = LoggerFactory.getLogger(VeSyncV2ApiHelper.class);
61 private @NonNullByDefault({}) HttpClient httpClient;
63 private volatile @Nullable VeSyncUserSession loggedInSession;
65 private Map<String, @NotNull VeSyncManagedDeviceBase> macLookup;
67 public VeSyncV2ApiHelper() {
68 macLookup = new HashMap<>();
71 public Map<String, @NotNull VeSyncManagedDeviceBase> getMacLookupMap() {
76 * Sets the httpClient object to be used for API calls to Vesync.
78 * @param httpClient the client to be used.
80 public void setHttpClient(@Nullable HttpClient httpClient) {
81 this.httpClient = httpClient;
84 public static @NotNull String calculateMd5(final @Nullable String password) {
85 if (password == null) {
89 StringBuilder md5Result = new StringBuilder();
91 md5 = MessageDigest.getInstance("MD5");
92 } catch (NoSuchAlgorithmException e) {
95 byte[] handshakeHash = md5.digest(password.getBytes(StandardCharsets.UTF_8));
96 for (byte handshakeByte : handshakeHash) {
97 md5Result.append(String.format("%02x", handshakeByte));
99 return md5Result.toString();
102 public void discoverDevices() throws AuthenticationException {
104 VeSyncRequestManagedDevicesPage reqDevPage = new VeSyncRequestManagedDevicesPage(loggedInSession);
105 boolean finished = false;
107 HashMap<String, VeSyncManagedDeviceBase> generatedMacLookup = new HashMap<>();
109 reqDevPage.pageNo = String.valueOf(pageNo);
110 reqDevPage.pageSize = String.valueOf(100);
111 final String result = reqV1Authorized(V1_MANAGED_DEVICES_ENDPOINT, reqDevPage);
113 VeSyncManagedDevicesPage resultsPage = VeSyncConstants.GSON.fromJson(result,
114 VeSyncManagedDevicesPage.class);
115 if (resultsPage == null || !resultsPage.outcome.getTotal().equals(resultsPage.outcome.getPageSize())) {
121 if (resultsPage != null) {
122 for (VeSyncManagedDeviceBase device : resultsPage.outcome.list) {
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());
129 // Update the mac address -> device table
130 generatedMacLookup.put(device.getMacId(), device);
134 macLookup = Collections.unmodifiableMap(generatedMacLookup);
135 } catch (final AuthenticationException ae) {
136 logger.warn("Failed background device scan : {}", ae.getMessage());
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");
146 // Apply current session authentication data
147 requestData.applyAuthentication(loggedInSession);
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));
155 ((VeSyncRequestManagedDeviceBypassV2) requestData).cid = deviceData.cid;
156 ((VeSyncRequestManagedDeviceBypassV2) requestData).configModule = deviceData.configModule;
157 ((VeSyncRequestManagedDeviceBypassV2) requestData).deviceRegion = deviceData.deviceRegion;
159 return reqV1Authorized(url, requestData);
162 public String reqV1Authorized(final String url, final VeSyncAuthenticatedRequest requestData)
163 throws AuthenticationException {
165 return directReqV1Authorized(url, requestData);
166 } catch (final AuthenticationException ae) {
171 private String directReqV1Authorized(final String url, final VeSyncAuthenticatedRequest requestData)
172 throws AuthenticationException {
174 Request request = httpClient.POST(url);
176 // No headers for login
177 request.content(new StringContentProvider(VeSyncConstants.GSON.toJson(requestData)));
179 logger.debug("POST @ {} with content\r\n{}", url, VeSyncConstants.GSON.toJson(requestData));
181 request.header(HttpHeader.CONTENT_TYPE, "application/json; utf-8");
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);
188 if (commResponse != null && (commResponse.isMsgSuccess() || commResponse.isMsgDeviceOffline())) {
189 logger.debug("Got OK response {}", response.getContentAsString());
190 return response.getContentAsString();
192 logger.debug("Got FAILED response {}", response.getContentAsString());
193 throw new AuthenticationException("Invalid JSON response from login");
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());
201 } catch (InterruptedException | TimeoutException | ExecutionException e) {
202 throw new AuthenticationException(e);
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;
213 loggedInSession = processLogin(username, password, timezone).getUserSession();
214 } catch (final AuthenticationException ae) {
215 loggedInSession = null;
220 public void updateBridgeData(final VeSyncBridgeHandler bridge) {
221 bridge.handleNewUserSession(loggedInSession);
224 private VeSyncLoginResponse processLogin(String username, String password, String timezone)
225 throws AuthenticationException {
227 Request request = httpClient.POST(V1_LOGIN_ENDPOINT);
229 // No headers for login
230 request.content(new StringContentProvider(
231 VeSyncConstants.GSON.toJson(new VeSyncLoginCredentials(username, password))));
233 request.header(HttpHeader.CONTENT_TYPE, "application/json; utf-8");
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;
243 throw new AuthenticationException("Invalid / unexpected JSON response from login");
246 logger.warn("Login Failed - HTTP Response Code: {} - {}", response.getStatus(), response.getReason());
247 throw new AuthenticationException(
248 "HTTP response " + response.getStatus() + " - " + response.getReason());
250 } catch (InterruptedException | TimeoutException | ExecutionException e) {
251 throw new AuthenticationException(e);