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.melcloud.internal.api;
15 import java.io.ByteArrayInputStream;
16 import java.io.IOException;
17 import java.io.InputStream;
18 import java.nio.charset.StandardCharsets;
19 import java.util.ArrayList;
20 import java.util.Arrays;
21 import java.util.List;
22 import java.util.Properties;
24 import org.openhab.binding.melcloud.internal.api.json.Device;
25 import org.openhab.binding.melcloud.internal.api.json.DeviceStatus;
26 import org.openhab.binding.melcloud.internal.api.json.HeatpumpDeviceStatus;
27 import org.openhab.binding.melcloud.internal.api.json.ListDevicesResponse;
28 import org.openhab.binding.melcloud.internal.api.json.LoginClientResponse;
29 import org.openhab.binding.melcloud.internal.exceptions.MelCloudCommException;
30 import org.openhab.binding.melcloud.internal.exceptions.MelCloudLoginException;
31 import org.openhab.core.io.net.http.HttpUtil;
32 import org.slf4j.Logger;
33 import org.slf4j.LoggerFactory;
35 import com.google.gson.FieldNamingPolicy;
36 import com.google.gson.Gson;
37 import com.google.gson.GsonBuilder;
38 import com.google.gson.JsonObject;
39 import com.google.gson.JsonSyntaxException;
42 * The {@link MelCloudConnection} Manage connection to Mitsubishi Cloud (MelCloud).
44 * @author Luca Calcaterra - Initial Contribution
45 * @author Pauli Anttila - Refactoring
46 * @author Wietse van Buitenen - Return all devices, added heatpump device
48 public class MelCloudConnection {
50 private static final String LOGIN_URL = "https://app.melcloud.com/Mitsubishi.Wifi.Client/Login/ClientLogin";
51 private static final String DEVICE_LIST_URL = "https://app.melcloud.com/Mitsubishi.Wifi.Client/User/ListDevices";
52 private static final String DEVICE_URL = "https://app.melcloud.com/Mitsubishi.Wifi.Client/Device";
54 private static final int TIMEOUT_MILLISECONDS = 10000;
56 // Gson objects are safe to share across threads and are somewhat expensive to construct. Use a single instance.
57 private static final Gson gson = new GsonBuilder().excludeFieldsWithoutExposeAnnotation()
58 .setFieldNamingPolicy(FieldNamingPolicy.UPPER_CAMEL_CASE).create();
60 private final Logger logger = LoggerFactory.getLogger(MelCloudConnection.class);
62 private boolean isConnected = false;
63 private String sessionKey;
65 public void login(String username, String password, int languageId)
66 throws MelCloudCommException, MelCloudLoginException {
69 JsonObject jsonReq = new JsonObject();
70 jsonReq.addProperty("Email", username);
71 jsonReq.addProperty("Password", password);
72 jsonReq.addProperty("Language", languageId);
73 jsonReq.addProperty("AppVersion", "1.17.5.0");
74 jsonReq.addProperty("Persist", false);
75 jsonReq.addProperty("CaptchaResponse", (String) null);
76 InputStream data = new ByteArrayInputStream(jsonReq.toString().getBytes(StandardCharsets.UTF_8));
79 String loginResponse = HttpUtil.executeUrl("POST", LOGIN_URL, null, data, "application/json",
80 TIMEOUT_MILLISECONDS);
81 logger.debug("Login response: {}", loginResponse);
82 LoginClientResponse resp = gson.fromJson(loginResponse, LoginClientResponse.class);
83 if (resp.getErrorId() != null) {
84 String errorMsg = String.format("Login failed, error code: %s", resp.getErrorId());
85 if (resp.getErrorMessage() != null) {
86 errorMsg = String.format("%s (%s)", errorMsg, resp.getErrorMessage());
88 throw new MelCloudLoginException(errorMsg);
90 sessionKey = resp.getLoginData().getContextKey();
92 } catch (IOException | JsonSyntaxException e) {
93 throw new MelCloudCommException(String.format("Login error, reason: %s", e.getMessage()), e);
97 public List<Device> fetchDeviceList() throws MelCloudCommException {
100 String response = HttpUtil.executeUrl("GET", DEVICE_LIST_URL, getHeaderProperties(), null, null,
101 TIMEOUT_MILLISECONDS);
102 logger.debug("Device list response: {}", response);
103 List<Device> devices = new ArrayList<>();
104 ListDevicesResponse[] buildings = gson.fromJson(response, ListDevicesResponse[].class);
105 Arrays.asList(buildings).forEach(building -> {
106 if (building.getStructure().getDevices() != null) {
107 devices.addAll(building.getStructure().getDevices());
109 building.getStructure().getAreas().forEach(area -> {
110 if (area.getDevices() != null) {
111 devices.addAll(area.getDevices());
114 building.getStructure().getFloors().forEach(floor -> {
115 if (floor.getDevices() != null) {
116 devices.addAll(floor.getDevices());
118 floor.getAreas().forEach(area -> {
119 if (area.getDevices() != null) {
120 devices.addAll(area.getDevices());
125 logger.debug("Found {} devices", devices.size());
128 } catch (IOException | JsonSyntaxException e) {
130 throw new MelCloudCommException("Error occurred during device list poll", e);
134 public DeviceStatus fetchDeviceStatus(int deviceId, int buildingId) throws MelCloudCommException {
136 String url = DEVICE_URL + String.format("/Get?id=%d&buildingID=%d", deviceId, buildingId);
138 String response = HttpUtil.executeUrl("GET", url, getHeaderProperties(), null, null, TIMEOUT_MILLISECONDS);
139 logger.debug("Device status response: {}", response);
140 return gson.fromJson(response, DeviceStatus.class);
141 } catch (IOException | JsonSyntaxException e) {
143 throw new MelCloudCommException("Error occurred during device status fetch", e);
147 public DeviceStatus sendDeviceStatus(DeviceStatus deviceStatus) throws MelCloudCommException {
149 String content = gson.toJson(deviceStatus, DeviceStatus.class);
150 logger.debug("Sending device status: {}", content);
151 InputStream data = new ByteArrayInputStream(content.getBytes(StandardCharsets.UTF_8));
153 String response = HttpUtil.executeUrl("POST", DEVICE_URL + "/SetAta", getHeaderProperties(), data,
154 "application/json", TIMEOUT_MILLISECONDS);
155 logger.debug("Device status sending response: {}", response);
156 return gson.fromJson(response, DeviceStatus.class);
157 } catch (IOException | JsonSyntaxException e) {
159 throw new MelCloudCommException("Error occurred during device command sending", e);
163 public HeatpumpDeviceStatus fetchHeatpumpDeviceStatus(int deviceId, int buildingId) throws MelCloudCommException {
165 String url = DEVICE_URL + String.format("/Get?id=%d&buildingID=%d", deviceId, buildingId);
167 String response = HttpUtil.executeUrl("GET", url, getHeaderProperties(), null, null, TIMEOUT_MILLISECONDS);
168 logger.debug("Device heatpump status response: {}", response);
169 return gson.fromJson(response, HeatpumpDeviceStatus.class);
170 } catch (IOException | JsonSyntaxException e) {
172 throw new MelCloudCommException("Error occurred during heatpump device status fetch", e);
176 public HeatpumpDeviceStatus sendHeatpumpDeviceStatus(HeatpumpDeviceStatus heatpumpDeviceStatus)
177 throws MelCloudCommException {
179 String content = gson.toJson(heatpumpDeviceStatus, HeatpumpDeviceStatus.class);
180 logger.debug("Sending heatpump device status: {}", content);
181 InputStream data = new ByteArrayInputStream(content.getBytes(StandardCharsets.UTF_8));
183 String response = HttpUtil.executeUrl("POST", DEVICE_URL + "/SetAtw", getHeaderProperties(), data,
184 "application/json", TIMEOUT_MILLISECONDS);
185 logger.debug("Device heatpump status sending response: {}", response);
186 return gson.fromJson(response, HeatpumpDeviceStatus.class);
187 } catch (IOException | JsonSyntaxException e) {
189 throw new MelCloudCommException("Error occurred during heatpump device command sending", e);
193 public synchronized boolean isConnected() {
197 private synchronized void setConnected(boolean state) {
201 private Properties getHeaderProperties() {
202 Properties headers = new Properties();
203 headers.put("X-MitsContextKey", sessionKey);
207 private void assertConnected() throws MelCloudCommException {
209 throw new MelCloudCommException("Not connected to MELCloud");