]> git.basschouten.com Git - openhab-addons.git/blob
03e30718419720e1ffbd7ca9fab127436b683625
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2024 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.Map.Entry;
20 import java.util.concurrent.ConcurrentHashMap;
21 import java.util.concurrent.TimeUnit;
22
23 import org.eclipse.jdt.annotation.NonNull;
24 import org.eclipse.jdt.annotation.NonNullByDefault;
25 import org.eclipse.jdt.annotation.Nullable;
26 import org.eclipse.jetty.client.HttpClient;
27 import org.openhab.binding.miio.internal.MiIoSendCommand;
28 import org.openhab.core.cache.ExpiringCache;
29 import org.openhab.core.io.net.http.HttpClientFactory;
30 import org.openhab.core.io.net.http.HttpUtil;
31 import org.openhab.core.library.types.RawType;
32 import org.osgi.service.component.annotations.Activate;
33 import org.osgi.service.component.annotations.Component;
34 import org.osgi.service.component.annotations.Deactivate;
35 import org.osgi.service.component.annotations.Reference;
36 import org.slf4j.Logger;
37 import org.slf4j.LoggerFactory;
38
39 import com.google.gson.Gson;
40 import com.google.gson.GsonBuilder;
41 import com.google.gson.JsonObject;
42 import com.google.gson.JsonParseException;
43 import com.google.gson.JsonSyntaxException;
44
45 /**
46  * The {@link CloudConnector} is responsible for connecting OH to the Xiaomi cloud communication.
47  *
48  * @author Marcel Verpaalen - Initial contribution
49  */
50 @Component(service = CloudConnector.class)
51 @NonNullByDefault
52 public class CloudConnector {
53
54     private static final long CACHE_EXPIRY = TimeUnit.SECONDS.toMillis(60);
55
56     private enum CloudListState {
57         FAILED,
58         STARTING,
59         REFRESHING,
60         AVAILABLE,
61     }
62
63     private volatile CloudListState deviceListState = CloudListState.STARTING;
64     private volatile CloudListState homeListState = CloudListState.STARTING;
65
66     private String username = "";
67     private String password = "";
68     private String country = "ru,us,tw,sg,cn,de,i2";
69     private List<CloudDeviceDTO> deviceList = new ArrayList<>();
70     private boolean connected;
71     private final HttpClient httpClient;
72     private @Nullable MiCloudConnector cloudConnector;
73     private final Logger logger = LoggerFactory.getLogger(CloudConnector.class);
74
75     private ConcurrentHashMap<@NonNull String, @NonNull HomeListDTO> homeLists = new ConcurrentHashMap<>();
76     private static final Gson GSON = new GsonBuilder().serializeNulls().create();
77
78     private ExpiringCache<Boolean> logonCache = new ExpiringCache<>(CACHE_EXPIRY, () -> {
79         return logon();
80     });
81
82     private ExpiringCache<String> refreshDeviceList = new ExpiringCache<>(CACHE_EXPIRY, () -> {
83         if (deviceListState == CloudListState.FAILED && !isConnected()) {
84             return ("Could not connect to Xiaomi cloud");
85         }
86         final @Nullable MiCloudConnector cl = this.cloudConnector;
87         if (cl == null) {
88             return ("Could not connect to Xiaomi cloud");
89         }
90         deviceListState = CloudListState.REFRESHING;
91         deviceList.clear();
92         for (String server : country.split(",")) {
93             try {
94                 deviceList.addAll(cl.getDevices(server));
95             } catch (JsonParseException e) {
96                 logger.debug("Parsing error getting devices: {}", e.getMessage());
97             }
98         }
99         deviceListState = CloudListState.AVAILABLE;
100         return "done";// deviceList;
101     });
102
103     private ExpiringCache<String> refreshHomeList = new ExpiringCache<>(CACHE_EXPIRY, () -> {
104         if (homeListState == CloudListState.FAILED && !isConnected()) {
105             return ("Could not connect to Xiaomi cloud");
106         }
107         final @Nullable MiCloudConnector cl = this.cloudConnector;
108         if (cl == null) {
109             return ("Could not connect to Xiaomi cloud");
110         }
111         boolean isStarting = homeListState == CloudListState.STARTING;
112         homeListState = CloudListState.REFRESHING;
113         for (String server : country.split(",")) {
114             try {
115                 updateHomeList(server);
116             } catch (JsonParseException e) {
117                 logger.debug("Parsing error getting home details: {}", e.getMessage());
118             }
119         }
120         homeListState = CloudListState.AVAILABLE;
121         if (isStarting) {
122             printHomesandRooms();
123         }
124         return "done";// deviceList;
125     });
126
127     private void printHomesandRooms() {
128         for (Entry<String, HomeListDTO> countryHome : homeLists.entrySet()) {
129             String server = countryHome.getKey();
130             final HomeListDTO homelist = countryHome.getValue();
131             for (HomeDTO home : homelist.getHomelist()) {
132                 logger.debug("Server: {}, Home id: {}, Name {}", server, home.getId(), home.getName());
133                 for (HomeRoomDTO room : home.getRoomlist()) {
134                     logger.debug("Server: {}, Home id: {}, Room id: {}, Name {}", server, home.getId(), room.getId(),
135                             room.getName());
136                 }
137             }
138         }
139     }
140
141     @Activate
142     public CloudConnector(@Reference HttpClientFactory httpClientFactory) {
143         this.httpClient = httpClientFactory.createHttpClient(BINDING_ID);
144     }
145
146     @Deactivate
147     public void dispose() {
148         final MiCloudConnector cl = cloudConnector;
149         if (cl != null) {
150             cl.stopClient();
151         }
152         cloudConnector = null;
153     }
154
155     public boolean isConnected() {
156         return isConnected(false);
157     }
158
159     public boolean isConnected(boolean force) {
160         final MiCloudConnector cl = cloudConnector;
161         if (cl != null && cl.hasLoginToken()) {
162             return true;
163         }
164         if (force) {
165             logonCache.invalidateValue();
166         }
167         final @Nullable Boolean c = logonCache.getValue();
168         if (c != null && c.booleanValue()) {
169             return true;
170         }
171         deviceListState = CloudListState.FAILED;
172         return false;
173     }
174
175     public String sendRPCCommand(String device, String country, MiIoSendCommand command) throws MiCloudException {
176         final @Nullable MiCloudConnector cl = this.cloudConnector;
177         if (cl == null || !isConnected()) {
178             throw new MiCloudException("Cannot execute request. Cloud service not available");
179         }
180         return cl.sendRPCCommand(device, country.trim().toLowerCase(), command.getCommandString());
181     }
182
183     public String sendCloudCommand(String urlPart, String country, String parameters) throws MiCloudException {
184         final @Nullable MiCloudConnector cl = this.cloudConnector;
185         if (cl == null || !isConnected()) {
186             throw new MiCloudException("Cannot execute request. Cloud service not available");
187         }
188         return cl.request(urlPart.startsWith("/") ? urlPart : "/" + urlPart, country, parameters);
189     }
190
191     private void updateHomeList(String country) {
192         final @Nullable MiCloudConnector cl = this.cloudConnector;
193         if (isConnected() && cl != null) {
194             try {
195                 JsonObject homelistInfo = cl.getHomeList(country.trim().toLowerCase());
196                 final HomeListDTO homelist = GSON.fromJson(homelistInfo, HomeListDTO.class);
197                 if (homelist != null && homelist.getHomelist() != null && homelist.getHomelist().size() > 0) {
198                     homeLists.put(country, homelist);
199                 }
200             } catch (JsonSyntaxException e) {
201                 logger.debug("Home List / Room info could not be updated for server '{}': {}", country, e.getMessage());
202             }
203         }
204     }
205
206     public @Nullable RawType getMap(String mapId, String country) throws MiCloudException {
207         logger.debug("Getting vacuum map {} from Xiaomi cloud server: '{}'", mapId, country);
208         String mapCountry;
209         String mapUrl = "";
210         final @Nullable MiCloudConnector cl = this.cloudConnector;
211         if (cl == null || !isConnected()) {
212             throw new MiCloudException("Cannot execute request. Cloud service not available");
213         }
214         if (country.isEmpty()) {
215             logger.debug("Server not defined in thing. Trying servers: {}", this.country);
216             for (String mapCountryServer : this.country.split(",")) {
217                 mapCountry = mapCountryServer.trim().toLowerCase();
218                 mapUrl = cl.getMapUrl(mapId, mapCountry);
219                 logger.debug("Map download from server {} returned {}", mapCountry, mapUrl);
220                 if (!mapUrl.isEmpty()) {
221                     break;
222                 }
223             }
224         } else {
225             mapCountry = country.trim().toLowerCase();
226             mapUrl = cl.getMapUrl(mapId, mapCountry);
227         }
228         if (mapUrl.isBlank()) {
229             logger.debug("Cannot download map data: Returned map URL is empty");
230             return null;
231         }
232         try {
233             RawType mapData = HttpUtil.downloadData(mapUrl, null, false, -1);
234             if (mapData != null) {
235                 return mapData;
236             } else {
237                 logger.debug("Could not download '{}'", mapUrl);
238                 return null;
239             }
240         } catch (IllegalArgumentException e) {
241             logger.debug("Error downloading map: {}", e.getMessage());
242         }
243         return null;
244     }
245
246     public void setCredentials(@Nullable String username, @Nullable String password, @Nullable String country) {
247         if (country != null) {
248             this.country = country;
249         }
250         if (username != null && password != null) {
251             this.username = username;
252             this.password = password;
253         }
254     }
255
256     private boolean logon() {
257         if (username.isEmpty() || password.isEmpty()) {
258             logger.debug("No Xiaomi cloud credentials. Cloud connectivity disabled");
259             logger.debug("Logon details: username: '{}', pass: '{}', country: '{}'", username,
260                     password.replaceAll(".", "*"), country);
261             return connected;
262         }
263         try {
264             final MiCloudConnector cl = new MiCloudConnector(username, password, httpClient);
265             this.cloudConnector = cl;
266             connected = cl.login();
267             if (connected) {
268                 getDevicesList();
269             } else {
270                 deviceListState = CloudListState.FAILED;
271             }
272         } catch (MiCloudException e) {
273             connected = false;
274             deviceListState = CloudListState.FAILED;
275             logger.debug("Xiaomi cloud login failed: {}", e.getMessage());
276         }
277         return connected;
278     }
279
280     public List<CloudDeviceDTO> getDevicesList() {
281         refreshDeviceList.getValue();
282         return deviceList;
283     }
284
285     public @Nullable CloudDeviceDTO getDeviceInfo(String id) {
286         getDevicesList();
287         if (deviceListState != CloudListState.AVAILABLE) {
288             return null;
289         }
290         List<CloudDeviceDTO> devicedata = new ArrayList<>();
291         for (CloudDeviceDTO deviceDetails : deviceList) {
292             if (deviceDetails.getDid().contentEquals(id)) {
293                 devicedata.add(deviceDetails);
294             }
295         }
296         if (devicedata.isEmpty()) {
297             return null;
298         }
299         for (CloudDeviceDTO device : devicedata) {
300             if (device.getIsOnline()) {
301                 return device;
302             }
303         }
304         if (devicedata.size() > 1) {
305             logger.debug("Found multiple servers for device {} {} returning first", devicedata.get(0).getDid(),
306                     devicedata.get(0).getName());
307         }
308         return devicedata.get(0);
309     }
310
311     public HomeListDTO getHomeList(String server) {
312         refreshHomeList.getValue();
313         return homeLists.getOrDefault(server, new HomeListDTO());
314     }
315
316     public ConcurrentHashMap<String, HomeListDTO> getHomeLists() {
317         refreshHomeList.getValue();
318         return homeLists;
319     }
320
321     /**
322      * Get the room from the cloud given the room Id and country server
323      *
324      * @param room id
325      * @param country
326      * @return room
327      */
328
329     public @Nullable HomeRoomDTO getRoom(String id, String country) {
330         @Nullable
331         HomeListDTO homeList = homeLists.getOrDefault(country, new HomeListDTO());
332         if (homeList.getHomelist() != null) {
333             for (HomeDTO home : homeList.getHomelist()) {
334                 for (HomeRoomDTO room : home.getRoomlist()) {
335                     if (room.getId().contentEquals(id)) {
336                         return room;
337                     }
338                 }
339             }
340         }
341         return null;
342     }
343
344     /**
345      * Get the room from the cloud given the room Id
346      *
347      * @param room id
348      * @return room
349      */
350     public @Nullable HomeRoomDTO getRoom(String id) {
351         return getRoom(id, true);
352     }
353
354     private @Nullable HomeRoomDTO getRoom(String id, boolean retry) {
355         for (Entry<String, HomeListDTO> countryHome : homeLists.entrySet()) {
356             HomeRoomDTO room = getRoom(id, countryHome.getKey());
357             if (room != null) {
358                 return room;
359             }
360         }
361         if (retry) {
362             refreshHomeList.getValue();
363             return getRoom(id, false);
364         }
365         return null;
366     }
367 }