2 * Copyright (c) 2010-2024 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.Map.Entry;
20 import java.util.concurrent.ConcurrentHashMap;
21 import java.util.concurrent.TimeUnit;
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;
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;
46 * The {@link CloudConnector} is responsible for connecting OH to the Xiaomi cloud communication.
48 * @author Marcel Verpaalen - Initial contribution
50 @Component(service = CloudConnector.class)
52 public class CloudConnector {
54 private static final long CACHE_EXPIRY = TimeUnit.SECONDS.toMillis(60);
56 private enum CloudListState {
63 private volatile CloudListState deviceListState = CloudListState.STARTING;
64 private volatile CloudListState homeListState = CloudListState.STARTING;
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);
75 private ConcurrentHashMap<@NonNull String, @NonNull HomeListDTO> homeLists = new ConcurrentHashMap<>();
76 private static final Gson GSON = new GsonBuilder().serializeNulls().create();
78 private ExpiringCache<Boolean> logonCache = new ExpiringCache<>(CACHE_EXPIRY, () -> {
82 private ExpiringCache<String> refreshDeviceList = new ExpiringCache<>(CACHE_EXPIRY, () -> {
83 if (deviceListState == CloudListState.FAILED && !isConnected()) {
84 return ("Could not connect to Xiaomi cloud");
86 final @Nullable MiCloudConnector cl = this.cloudConnector;
88 return ("Could not connect to Xiaomi cloud");
90 deviceListState = CloudListState.REFRESHING;
92 for (String server : country.split(",")) {
94 deviceList.addAll(cl.getDevices(server));
95 } catch (JsonParseException e) {
96 logger.debug("Parsing error getting devices: {}", e.getMessage());
99 deviceListState = CloudListState.AVAILABLE;
100 return "done";// deviceList;
103 private ExpiringCache<String> refreshHomeList = new ExpiringCache<>(CACHE_EXPIRY, () -> {
104 if (homeListState == CloudListState.FAILED && !isConnected()) {
105 return ("Could not connect to Xiaomi cloud");
107 final @Nullable MiCloudConnector cl = this.cloudConnector;
109 return ("Could not connect to Xiaomi cloud");
111 boolean isStarting = homeListState == CloudListState.STARTING;
112 homeListState = CloudListState.REFRESHING;
113 for (String server : country.split(",")) {
115 updateHomeList(server);
116 } catch (JsonParseException e) {
117 logger.debug("Parsing error getting home details: {}", e.getMessage());
120 homeListState = CloudListState.AVAILABLE;
122 printHomesandRooms();
124 return "done";// deviceList;
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(),
142 public CloudConnector(@Reference HttpClientFactory httpClientFactory) {
143 this.httpClient = httpClientFactory.createHttpClient(BINDING_ID);
147 public void dispose() {
148 final MiCloudConnector cl = cloudConnector;
152 cloudConnector = null;
155 public boolean isConnected() {
156 return isConnected(false);
159 public boolean isConnected(boolean force) {
160 final MiCloudConnector cl = cloudConnector;
161 if (cl != null && cl.hasLoginToken()) {
165 logonCache.invalidateValue();
167 final @Nullable Boolean c = logonCache.getValue();
168 if (c != null && c.booleanValue()) {
171 deviceListState = CloudListState.FAILED;
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");
180 return cl.sendRPCCommand(device, country.trim().toLowerCase(), command.getCommandString());
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");
188 return cl.request(urlPart.startsWith("/") ? urlPart : "/" + urlPart, country, parameters);
191 private void updateHomeList(String country) {
192 final @Nullable MiCloudConnector cl = this.cloudConnector;
193 if (isConnected() && cl != null) {
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);
200 } catch (JsonSyntaxException e) {
201 logger.debug("Home List / Room info could not be updated for server '{}': {}", country, e.getMessage());
206 public @Nullable RawType getMap(String mapId, String country) throws MiCloudException {
207 logger.debug("Getting vacuum map {} from Xiaomi cloud server: '{}'", mapId, country);
210 final @Nullable MiCloudConnector cl = this.cloudConnector;
211 if (cl == null || !isConnected()) {
212 throw new MiCloudException("Cannot execute request. Cloud service not available");
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()) {
225 mapCountry = country.trim().toLowerCase();
226 mapUrl = cl.getMapUrl(mapId, mapCountry);
228 if (mapUrl.isBlank()) {
229 logger.debug("Cannot download map data: Returned map URL is empty");
233 RawType mapData = HttpUtil.downloadData(mapUrl, null, false, -1);
234 if (mapData != null) {
237 logger.debug("Could not download '{}'", mapUrl);
240 } catch (IllegalArgumentException e) {
241 logger.debug("Error downloading map: {}", e.getMessage());
246 public void setCredentials(@Nullable String username, @Nullable String password, @Nullable String country) {
247 if (country != null) {
248 this.country = country;
250 if (username != null && password != null) {
251 this.username = username;
252 this.password = password;
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);
264 final MiCloudConnector cl = new MiCloudConnector(username, password, httpClient);
265 this.cloudConnector = cl;
266 connected = cl.login();
270 deviceListState = CloudListState.FAILED;
272 } catch (MiCloudException e) {
274 deviceListState = CloudListState.FAILED;
275 logger.debug("Xiaomi cloud login failed: {}", e.getMessage());
280 public List<CloudDeviceDTO> getDevicesList() {
281 refreshDeviceList.getValue();
285 public @Nullable CloudDeviceDTO getDeviceInfo(String id) {
287 if (deviceListState != CloudListState.AVAILABLE) {
290 List<CloudDeviceDTO> devicedata = new ArrayList<>();
291 for (CloudDeviceDTO deviceDetails : deviceList) {
292 if (deviceDetails.getDid().contentEquals(id)) {
293 devicedata.add(deviceDetails);
296 if (devicedata.isEmpty()) {
299 for (CloudDeviceDTO device : devicedata) {
300 if (device.getIsOnline()) {
304 if (devicedata.size() > 1) {
305 logger.debug("Found multiple servers for device {} {} returning first", devicedata.get(0).getDid(),
306 devicedata.get(0).getName());
308 return devicedata.get(0);
311 public HomeListDTO getHomeList(String server) {
312 refreshHomeList.getValue();
313 return homeLists.getOrDefault(server, new HomeListDTO());
316 public ConcurrentHashMap<String, HomeListDTO> getHomeLists() {
317 refreshHomeList.getValue();
322 * Get the room from the cloud given the room Id and country server
329 public @Nullable HomeRoomDTO getRoom(String id, String country) {
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)) {
345 * Get the room from the cloud given the room Id
350 public @Nullable HomeRoomDTO getRoom(String id) {
351 return getRoom(id, true);
354 private @Nullable HomeRoomDTO getRoom(String id, boolean retry) {
355 for (Entry<String, HomeListDTO> countryHome : homeLists.entrySet()) {
356 HomeRoomDTO room = getRoom(id, countryHome.getKey());
362 refreshHomeList.getValue();
363 return getRoom(id, false);