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.miio.internal.cloud;
15 import static org.openhab.binding.miio.internal.MiIoBindingConstants.BINDING_ID;
17 import java.util.ArrayList;
18 import java.util.List;
19 import java.util.concurrent.TimeUnit;
21 import org.eclipse.jdt.annotation.NonNullByDefault;
22 import org.eclipse.jdt.annotation.Nullable;
23 import org.eclipse.jetty.client.HttpClient;
24 import org.openhab.binding.miio.internal.MiIoSendCommand;
25 import org.openhab.core.cache.ExpiringCache;
26 import org.openhab.core.io.net.http.HttpClientFactory;
27 import org.openhab.core.io.net.http.HttpUtil;
28 import org.openhab.core.library.types.RawType;
29 import org.osgi.service.component.annotations.Activate;
30 import org.osgi.service.component.annotations.Component;
31 import org.osgi.service.component.annotations.Deactivate;
32 import org.osgi.service.component.annotations.Reference;
33 import org.slf4j.Logger;
34 import org.slf4j.LoggerFactory;
36 import com.google.gson.JsonParseException;
39 * The {@link CloudConnector} is responsible for connecting OH to the Xiaomi cloud communication.
41 * @author Marcel Verpaalen - Initial contribution
43 @Component(service = CloudConnector.class)
45 public class CloudConnector {
47 private static final long CACHE_EXPIRY = TimeUnit.SECONDS.toMillis(60);
49 private static enum DeviceListState {
56 private volatile DeviceListState deviceListState = DeviceListState.STARTING;
58 private String username = "";
59 private String password = "";
60 private String country = "ru,us,tw,sg,cn,de,i2";
61 private List<CloudDeviceDTO> deviceList = new ArrayList<>();
62 private boolean connected;
63 private final HttpClient httpClient;
64 private @Nullable MiCloudConnector cloudConnector;
65 private final Logger logger = LoggerFactory.getLogger(CloudConnector.class);
67 private ExpiringCache<Boolean> logonCache = new ExpiringCache<Boolean>(CACHE_EXPIRY, () -> {
71 private ExpiringCache<String> refreshDeviceList = new ExpiringCache<String>(CACHE_EXPIRY, () -> {
72 if (deviceListState == DeviceListState.FAILED && !isConnected()) {
73 return ("Could not connect to Xiaomi cloud");
75 final @Nullable MiCloudConnector cl = this.cloudConnector;
77 return ("Could not connect to Xiaomi cloud");
79 deviceListState = DeviceListState.REFRESHING;
81 for (String server : country.split(",")) {
83 deviceList.addAll(cl.getDevices(server));
84 } catch (JsonParseException e) {
85 logger.debug("Parsing error getting devices: {}", e.getMessage());
88 deviceListState = DeviceListState.AVAILABLE;
89 return "done";// deviceList;
93 public CloudConnector(@Reference HttpClientFactory httpClientFactory) {
94 this.httpClient = httpClientFactory.createHttpClient(BINDING_ID);
98 public void dispose() {
99 final MiCloudConnector cl = cloudConnector;
103 cloudConnector = null;
106 public boolean isConnected() {
107 return isConnected(false);
110 public boolean isConnected(boolean force) {
111 final MiCloudConnector cl = cloudConnector;
112 if (cl != null && cl.hasLoginToken()) {
116 logonCache.invalidateValue();
118 final @Nullable Boolean c = logonCache.getValue();
119 if (c != null && c.booleanValue()) {
122 deviceListState = DeviceListState.FAILED;
126 public String sendRPCCommand(String device, String country, MiIoSendCommand command) throws MiCloudException {
127 final @Nullable MiCloudConnector cl = this.cloudConnector;
128 if (cl == null || !isConnected()) {
129 throw new MiCloudException("Cannot execute request. Cloud service not available");
131 return cl.sendRPCCommand(device, country.trim().toLowerCase(), command.getCommandString());
134 public String sendCloudCommand(String urlPart, String country, String parameters) throws MiCloudException {
135 final @Nullable MiCloudConnector cl = this.cloudConnector;
136 if (cl == null || !isConnected()) {
137 throw new MiCloudException("Cannot execute request. Cloud service not available");
139 return cl.request(urlPart.startsWith("/") ? urlPart : "/" + urlPart, country, parameters);
142 public @Nullable RawType getMap(String mapId, String country) throws MiCloudException {
143 logger.debug("Getting vacuum map {} from Xiaomi cloud server: '{}'", mapId, country);
146 final @Nullable MiCloudConnector cl = this.cloudConnector;
147 if (cl == null || !isConnected()) {
148 throw new MiCloudException("Cannot execute request. Cloud service not available");
150 if (country.isEmpty()) {
151 logger.debug("Server not defined in thing. Trying servers: {}", this.country);
152 for (String mapCountryServer : this.country.split(",")) {
153 mapCountry = mapCountryServer.trim().toLowerCase();
154 mapUrl = cl.getMapUrl(mapId, mapCountry);
155 logger.debug("Map download from server {} returned {}", mapCountry, mapUrl);
156 if (!mapUrl.isEmpty()) {
161 mapCountry = country.trim().toLowerCase();
162 mapUrl = cl.getMapUrl(mapId, mapCountry);
164 if (mapUrl.isBlank()) {
165 logger.debug("Cannot download map data: Returned map URL is empty");
169 RawType mapData = HttpUtil.downloadData(mapUrl, null, false, -1);
170 if (mapData != null) {
173 logger.debug("Could not download '{}'", mapUrl);
176 } catch (IllegalArgumentException e) {
177 logger.debug("Error downloading map: {}", e.getMessage());
182 public void setCredentials(@Nullable String username, @Nullable String password, @Nullable String country) {
183 if (country != null) {
184 this.country = country;
186 if (username != null && password != null) {
187 this.username = username;
188 this.password = password;
192 private boolean logon() {
193 if (username.isEmpty() || password.isEmpty()) {
194 logger.debug("No Xiaomi cloud credentials. Cloud connectivity disabled");
195 logger.debug("Logon details: username: '{}', pass: '{}', country: '{}'", username,
196 password.replaceAll(".", "*"), country);
200 final MiCloudConnector cl = new MiCloudConnector(username, password, httpClient);
201 this.cloudConnector = cl;
202 connected = cl.login();
206 deviceListState = DeviceListState.FAILED;
208 } catch (MiCloudException e) {
210 deviceListState = DeviceListState.FAILED;
211 logger.debug("Xiaomi cloud login failed: {}", e.getMessage());
216 public List<CloudDeviceDTO> getDevicesList() {
217 refreshDeviceList.getValue();
221 public @Nullable CloudDeviceDTO getDeviceInfo(String id) {
223 if (deviceListState != DeviceListState.AVAILABLE) {
226 List<CloudDeviceDTO> devicedata = new ArrayList<>();
227 for (CloudDeviceDTO deviceDetails : deviceList) {
228 if (deviceDetails.getDid().contentEquals(id)) {
229 devicedata.add(deviceDetails);
232 if (devicedata.isEmpty()) {
235 for (CloudDeviceDTO device : devicedata) {
236 if (device.getIsOnline()) {
240 if (devicedata.size() > 1) {
241 logger.debug("Found multiple servers for device {} {} returning first", devicedata.get(0).getDid(),
242 devicedata.get(0).getName());
244 return devicedata.get(0);