]> git.basschouten.com Git - openhab-addons.git/blob
e3660dada0e203b8640c6b279da3b8f57dff550a
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2020 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.core.cache.ExpiringCache;
25 import org.openhab.core.io.net.http.HttpClientFactory;
26 import org.openhab.core.io.net.http.HttpUtil;
27 import org.openhab.core.library.types.RawType;
28 import org.osgi.service.component.annotations.Activate;
29 import org.osgi.service.component.annotations.Component;
30 import org.osgi.service.component.annotations.Deactivate;
31 import org.osgi.service.component.annotations.Reference;
32 import org.slf4j.Logger;
33 import org.slf4j.LoggerFactory;
34
35 import com.google.gson.JsonParseException;
36
37 /**
38  * The {@link CloudConnector} is responsible for connecting OH to the Xiaomi cloud communication.
39  *
40  * @author Marcel Verpaalen - Initial contribution
41  */
42 @Component(service = CloudConnector.class)
43 @NonNullByDefault
44 public class CloudConnector {
45
46     private static final long CACHE_EXPIRY = TimeUnit.SECONDS.toMillis(60);
47
48     private static enum DeviceListState {
49         FAILED,
50         STARTING,
51         REFRESHING,
52         AVAILABLE,
53     }
54
55     private volatile DeviceListState deviceListState = DeviceListState.STARTING;
56
57     private String username = "";
58     private String password = "";
59     private String country = "ru,us,tw,sg,cn,de,i2";
60     private List<CloudDeviceDTO> deviceList = new ArrayList<>();
61     private boolean connected;
62     private final HttpClient httpClient;
63     private @Nullable MiCloudConnector cloudConnector;
64     private final Logger logger = LoggerFactory.getLogger(CloudConnector.class);
65
66     private ExpiringCache<Boolean> logonCache = new ExpiringCache<Boolean>(CACHE_EXPIRY, () -> {
67         return logon();
68     });
69
70     private ExpiringCache<String> refreshDeviceList = new ExpiringCache<String>(CACHE_EXPIRY, () -> {
71         if (deviceListState == DeviceListState.FAILED && !isConnected()) {
72             return ("Could not connect to Xiaomi cloud");
73         }
74         final @Nullable MiCloudConnector cl = this.cloudConnector;
75         if (cl == null) {
76             return ("Could not connect to Xiaomi cloud");
77         }
78         deviceListState = DeviceListState.REFRESHING;
79         deviceList.clear();
80         for (String server : country.split(",")) {
81             try {
82                 deviceList.addAll(cl.getDevices(server));
83             } catch (JsonParseException e) {
84                 logger.debug("Parsing error getting devices: {}", e.getMessage());
85             }
86         }
87         deviceListState = DeviceListState.AVAILABLE;
88         return "done";// deviceList;
89     });
90
91     @Activate
92     public CloudConnector(@Reference HttpClientFactory httpClientFactory) {
93         this.httpClient = httpClientFactory.createHttpClient(BINDING_ID);
94     }
95
96     @Deactivate
97     public void dispose() {
98         final MiCloudConnector cl = cloudConnector;
99         if (cl != null) {
100             cl.stopClient();
101         }
102         cloudConnector = null;
103     }
104
105     public boolean isConnected() {
106         final MiCloudConnector cl = cloudConnector;
107         if (cl != null && cl.hasLoginToken()) {
108             return true;
109         }
110         final @Nullable Boolean c = logonCache.getValue();
111         if (c != null && c.booleanValue()) {
112             return true;
113         }
114         deviceListState = DeviceListState.FAILED;
115         return false;
116     }
117
118     public @Nullable RawType getMap(String mapId, String country) throws MiCloudException {
119         logger.debug("Getting vacuum map {} from Xiaomi cloud server: '{}'", mapId, country);
120         String mapCountry;
121         String mapUrl = "";
122         final @Nullable MiCloudConnector cl = this.cloudConnector;
123         if (cl == null || !isConnected()) {
124             throw new MiCloudException("Cannot execute request. Cloudservice not available");
125         }
126         if (country.isEmpty()) {
127             logger.debug("Server not defined in thing. Trying servers: {}", this.country);
128             for (String mapCountryServer : this.country.split(",")) {
129                 mapCountry = mapCountryServer.trim().toLowerCase();
130                 mapUrl = cl.getMapUrl(mapId, mapCountry);
131                 logger.debug("Map download from server {} returned {}", mapCountry, mapUrl);
132                 if (!mapUrl.isEmpty()) {
133                     break;
134                 }
135             }
136         } else {
137             mapCountry = country.trim().toLowerCase();
138             mapUrl = cl.getMapUrl(mapId, mapCountry);
139         }
140         if (mapUrl.isBlank()) {
141             logger.debug("Cannot download map data: Returned map URL is empty");
142             return null;
143         }
144         try {
145             RawType mapData = HttpUtil.downloadData(mapUrl, null, false, -1);
146             if (mapData != null) {
147                 return mapData;
148             } else {
149                 logger.debug("Could not download '{}'", mapUrl);
150                 return null;
151             }
152         } catch (IllegalArgumentException e) {
153             logger.debug("Error downloading map: {}", e.getMessage());
154         }
155         return null;
156     }
157
158     public void setCredentials(@Nullable String username, @Nullable String password, @Nullable String country) {
159         if (country != null) {
160             this.country = country;
161         }
162         if (username != null && password != null) {
163             this.username = username;
164             this.password = password;
165         }
166     }
167
168     private boolean logon() {
169         if (username.isEmpty() || password.isEmpty()) {
170             logger.debug("No Xiaomi cloud credentials. Cloud connectivity disabled");
171             logger.debug("Logon details: username: '{}', pass: '{}', country: '{}'", username,
172                     password.replaceAll(".", "*"), country);
173             return connected;
174         }
175         try {
176             final MiCloudConnector cl = new MiCloudConnector(username, password, httpClient);
177             this.cloudConnector = cl;
178             connected = cl.login();
179             if (connected) {
180                 getDevicesList();
181             } else {
182                 deviceListState = DeviceListState.FAILED;
183             }
184         } catch (MiCloudException e) {
185             connected = false;
186             deviceListState = DeviceListState.FAILED;
187             logger.debug("Xiaomi cloud login failed: {}", e.getMessage());
188         }
189         return connected;
190     }
191
192     public List<CloudDeviceDTO> getDevicesList() {
193         refreshDeviceList.getValue();
194         return deviceList;
195     }
196
197     public @Nullable CloudDeviceDTO getDeviceInfo(String id) {
198         getDevicesList();
199         if (deviceListState != DeviceListState.AVAILABLE) {
200             return null;
201         }
202         String did = Long.toString(Long.parseUnsignedLong(id, 16));
203         List<CloudDeviceDTO> devicedata = new ArrayList<>();
204         for (CloudDeviceDTO deviceDetails : deviceList) {
205             if (deviceDetails.getDid().contentEquals(did)) {
206                 devicedata.add(deviceDetails);
207             }
208         }
209         if (devicedata.isEmpty()) {
210             return null;
211         }
212         for (CloudDeviceDTO device : devicedata) {
213             if (device.getIsOnline()) {
214                 return device;
215             }
216         }
217         if (devicedata.size() > 1) {
218             logger.debug("Found multiple servers for device {} {} returning first", devicedata.get(0).getDid(),
219                     devicedata.get(0).getName());
220         }
221         return devicedata.get(0);
222     }
223 }