]> git.basschouten.com Git - openhab-addons.git/blob
c6d8758402177e842ec9efef7765230553d7efb6
[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.miio.internal.cloud;
14
15 import static org.openhab.binding.miio.internal.MiIoBindingConstants.BINDING_ID;
16
17 import java.util.ArrayList;
18 import java.util.List;
19 import java.util.concurrent.TimeUnit;
20
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;
35
36 import com.google.gson.JsonParseException;
37
38 /**
39  * The {@link CloudConnector} is responsible for connecting OH to the Xiaomi cloud communication.
40  *
41  * @author Marcel Verpaalen - Initial contribution
42  */
43 @Component(service = CloudConnector.class)
44 @NonNullByDefault
45 public class CloudConnector {
46
47     private static final long CACHE_EXPIRY = TimeUnit.SECONDS.toMillis(60);
48
49     private enum DeviceListState {
50         FAILED,
51         STARTING,
52         REFRESHING,
53         AVAILABLE,
54     }
55
56     private volatile DeviceListState deviceListState = DeviceListState.STARTING;
57
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);
66
67     private ExpiringCache<Boolean> logonCache = new ExpiringCache<Boolean>(CACHE_EXPIRY, () -> {
68         return logon();
69     });
70
71     private ExpiringCache<String> refreshDeviceList = new ExpiringCache<String>(CACHE_EXPIRY, () -> {
72         if (deviceListState == DeviceListState.FAILED && !isConnected()) {
73             return ("Could not connect to Xiaomi cloud");
74         }
75         final @Nullable MiCloudConnector cl = this.cloudConnector;
76         if (cl == null) {
77             return ("Could not connect to Xiaomi cloud");
78         }
79         deviceListState = DeviceListState.REFRESHING;
80         deviceList.clear();
81         for (String server : country.split(",")) {
82             try {
83                 deviceList.addAll(cl.getDevices(server));
84             } catch (JsonParseException e) {
85                 logger.debug("Parsing error getting devices: {}", e.getMessage());
86             }
87         }
88         deviceListState = DeviceListState.AVAILABLE;
89         return "done";// deviceList;
90     });
91
92     @Activate
93     public CloudConnector(@Reference HttpClientFactory httpClientFactory) {
94         this.httpClient = httpClientFactory.createHttpClient(BINDING_ID);
95     }
96
97     @Deactivate
98     public void dispose() {
99         final MiCloudConnector cl = cloudConnector;
100         if (cl != null) {
101             cl.stopClient();
102         }
103         cloudConnector = null;
104     }
105
106     public boolean isConnected() {
107         return isConnected(false);
108     }
109
110     public boolean isConnected(boolean force) {
111         final MiCloudConnector cl = cloudConnector;
112         if (cl != null && cl.hasLoginToken()) {
113             return true;
114         }
115         if (force) {
116             logonCache.invalidateValue();
117         }
118         final @Nullable Boolean c = logonCache.getValue();
119         if (c != null && c.booleanValue()) {
120             return true;
121         }
122         deviceListState = DeviceListState.FAILED;
123         return false;
124     }
125
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");
130         }
131         return cl.sendRPCCommand(device, country.trim().toLowerCase(), command.getCommandString());
132     }
133
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");
138         }
139         return cl.request(urlPart.startsWith("/") ? urlPart : "/" + urlPart, country, parameters);
140     }
141
142     public @Nullable RawType getMap(String mapId, String country) throws MiCloudException {
143         logger.debug("Getting vacuum map {} from Xiaomi cloud server: '{}'", mapId, country);
144         String mapCountry;
145         String mapUrl = "";
146         final @Nullable MiCloudConnector cl = this.cloudConnector;
147         if (cl == null || !isConnected()) {
148             throw new MiCloudException("Cannot execute request. Cloud service not available");
149         }
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()) {
157                     break;
158                 }
159             }
160         } else {
161             mapCountry = country.trim().toLowerCase();
162             mapUrl = cl.getMapUrl(mapId, mapCountry);
163         }
164         if (mapUrl.isBlank()) {
165             logger.debug("Cannot download map data: Returned map URL is empty");
166             return null;
167         }
168         try {
169             RawType mapData = HttpUtil.downloadData(mapUrl, null, false, -1);
170             if (mapData != null) {
171                 return mapData;
172             } else {
173                 logger.debug("Could not download '{}'", mapUrl);
174                 return null;
175             }
176         } catch (IllegalArgumentException e) {
177             logger.debug("Error downloading map: {}", e.getMessage());
178         }
179         return null;
180     }
181
182     public void setCredentials(@Nullable String username, @Nullable String password, @Nullable String country) {
183         if (country != null) {
184             this.country = country;
185         }
186         if (username != null && password != null) {
187             this.username = username;
188             this.password = password;
189         }
190     }
191
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);
197             return connected;
198         }
199         try {
200             final MiCloudConnector cl = new MiCloudConnector(username, password, httpClient);
201             this.cloudConnector = cl;
202             connected = cl.login();
203             if (connected) {
204                 getDevicesList();
205             } else {
206                 deviceListState = DeviceListState.FAILED;
207             }
208         } catch (MiCloudException e) {
209             connected = false;
210             deviceListState = DeviceListState.FAILED;
211             logger.debug("Xiaomi cloud login failed: {}", e.getMessage());
212         }
213         return connected;
214     }
215
216     public List<CloudDeviceDTO> getDevicesList() {
217         refreshDeviceList.getValue();
218         return deviceList;
219     }
220
221     public @Nullable CloudDeviceDTO getDeviceInfo(String id) {
222         getDevicesList();
223         if (deviceListState != DeviceListState.AVAILABLE) {
224             return null;
225         }
226         List<CloudDeviceDTO> devicedata = new ArrayList<>();
227         for (CloudDeviceDTO deviceDetails : deviceList) {
228             if (deviceDetails.getDid().contentEquals(id)) {
229                 devicedata.add(deviceDetails);
230             }
231         }
232         if (devicedata.isEmpty()) {
233             return null;
234         }
235         for (CloudDeviceDTO device : devicedata) {
236             if (device.getIsOnline()) {
237                 return device;
238             }
239         }
240         if (devicedata.size() > 1) {
241             logger.debug("Found multiple servers for device {} {} returning first", devicedata.get(0).getDid(),
242                     devicedata.get(0).getName());
243         }
244         return devicedata.get(0);
245     }
246 }