homebridge-midea-platform 1.2.0-beta.2 → 1.2.0-beta.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.husky/pre-commit +0 -0
- package/AC_ori.lua +5140 -0
- package/config.schema.json +14 -2
- package/dist/accessory/AccessoryFactory.d.ts +13 -12
- package/dist/accessory/AccessoryFactory.d.ts.map +1 -1
- package/dist/accessory/AccessoryFactory.js +37 -31
- package/dist/accessory/AccessoryFactory.js.map +1 -1
- package/dist/accessory/AirConditionerAccessory.d.ts +98 -92
- package/dist/accessory/AirConditionerAccessory.d.ts.map +1 -1
- package/dist/accessory/AirConditionerAccessory.js +662 -607
- package/dist/accessory/AirConditionerAccessory.js.map +1 -1
- package/dist/accessory/BaseAccessory.d.ts +11 -11
- package/dist/accessory/BaseAccessory.js +21 -21
- package/dist/accessory/DehumidifierAccessory.d.ts +45 -45
- package/dist/accessory/DehumidifierAccessory.js +344 -344
- package/dist/accessory/DishwasherAccessory.d.ts +30 -30
- package/dist/accessory/DishwasherAccessory.js +63 -63
- package/dist/accessory/ElectricWaterHeaterAccessory.d.ts +44 -44
- package/dist/accessory/ElectricWaterHeaterAccessory.js +176 -176
- package/dist/accessory/FanAccessory.d.ts +39 -39
- package/dist/accessory/FanAccessory.js +123 -123
- package/dist/accessory/FrontLoadWasherAccessory.d.ts +30 -30
- package/dist/accessory/FrontLoadWasherAccessory.js +66 -66
- package/dist/accessory/GasWaterHeaterAccessory.d.ts +51 -51
- package/dist/accessory/GasWaterHeaterAccessory.js +216 -216
- package/dist/core/MideaCloud.d.ts +35 -35
- package/dist/core/MideaCloud.js +350 -350
- package/dist/core/MideaConstants.d.ts +50 -50
- package/dist/core/MideaConstants.js +58 -58
- package/dist/core/MideaDevice.d.ts +76 -76
- package/dist/core/MideaDevice.js +409 -409
- package/dist/core/MideaDiscover.d.ts +35 -35
- package/dist/core/MideaDiscover.js +212 -212
- package/dist/core/MideaMessage.d.ts +75 -75
- package/dist/core/MideaMessage.js +184 -184
- package/dist/core/MideaPacketBuilder.d.ts +10 -10
- package/dist/core/MideaPacketBuilder.js +60 -60
- package/dist/core/MideaSecurity.d.ts +63 -63
- package/dist/core/MideaSecurity.js +251 -251
- package/dist/core/MideaUtils.d.ts +32 -32
- package/dist/core/MideaUtils.js +181 -181
- package/dist/devices/DeviceFactory.d.ts +13 -13
- package/dist/devices/DeviceFactory.d.ts.map +1 -1
- package/dist/devices/DeviceFactory.js +37 -36
- package/dist/devices/DeviceFactory.js.map +1 -1
- package/dist/devices/a1/MideaA1Device.d.ts +76 -76
- package/dist/devices/a1/MideaA1Device.js +145 -145
- package/dist/devices/a1/MideaA1Message.d.ts +40 -40
- package/dist/devices/a1/MideaA1Message.js +198 -198
- package/dist/devices/ac/MideaACDevice.d.ts +106 -104
- package/dist/devices/ac/MideaACDevice.d.ts.map +1 -1
- package/dist/devices/ac/MideaACDevice.js +400 -384
- package/dist/devices/ac/MideaACDevice.js.map +1 -1
- package/dist/devices/ac/MideaACMessage.d.ts +95 -94
- package/dist/devices/ac/MideaACMessage.d.ts.map +1 -1
- package/dist/devices/ac/MideaACMessage.js +619 -611
- package/dist/devices/ac/MideaACMessage.js.map +1 -1
- package/dist/devices/db/MideaDBDevice.d.ts +29 -29
- package/dist/devices/db/MideaDBDevice.js +100 -100
- package/dist/devices/db/MideaDBMessage.d.ts +32 -32
- package/dist/devices/db/MideaDBMessage.js +101 -101
- package/dist/devices/e1/MideaE1Device.d.ts +56 -56
- package/dist/devices/e1/MideaE1Device.js +128 -128
- package/dist/devices/e1/MideaE1Message.d.ts +28 -28
- package/dist/devices/e1/MideaE1Message.js +107 -107
- package/dist/devices/e2/MideaE2Device.d.ts +44 -44
- package/dist/devices/e2/MideaE2Device.js +129 -129
- package/dist/devices/e2/MideaE2Message.d.ts +33 -33
- package/dist/devices/e2/MideaE2Message.js +132 -132
- package/dist/devices/e3/MideaE3Device.d.ts +43 -43
- package/dist/devices/e3/MideaE3Device.js +137 -137
- package/dist/devices/e3/MideaE3Message.d.ts +51 -51
- package/dist/devices/e3/MideaE3Message.js +136 -136
- package/dist/devices/fa/MideaFADevice.d.ts +36 -36
- package/dist/devices/fa/MideaFADevice.js +106 -106
- package/dist/devices/fa/MideaFAMessage.d.ts +38 -38
- package/dist/devices/fa/MideaFAMessage.js +98 -98
- package/dist/index.d.ts +6 -6
- package/dist/index.js +6 -6
- package/dist/platform.d.ts +60 -60
- package/dist/platform.js +212 -212
- package/dist/platformUtils.d.ts +108 -106
- package/dist/platformUtils.d.ts.map +1 -1
- package/dist/platformUtils.js +103 -101
- package/dist/platformUtils.js.map +1 -1
- package/dist/settings.d.ts +8 -8
- package/dist/settings.js +11 -11
- package/docs/ac.md +6 -0
- package/package.json +1 -1
package/dist/core/MideaCloud.js
CHANGED
|
@@ -1,351 +1,351 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
-
};
|
|
5
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
/***********************************************************************
|
|
7
|
-
* Midea Cloud access functions
|
|
8
|
-
*
|
|
9
|
-
* Copyright (c) 2023 Kovalovszky Patrik, https://github.com/kovapatrik
|
|
10
|
-
* Portions Copyright (c) 2023 David Kerr, https://github.com/dkerr64
|
|
11
|
-
*
|
|
12
|
-
* With thanks to https://github.com/georgezhao2010/midea_ac_lan and
|
|
13
|
-
* https://github.com/mill1000/midea-msmart
|
|
14
|
-
*
|
|
15
|
-
*/
|
|
16
|
-
const axios_1 = __importDefault(require("axios"));
|
|
17
|
-
const crypto_1 = require("crypto");
|
|
18
|
-
const luxon_1 = require("luxon");
|
|
19
|
-
const semaphore_promise_1 = require("semaphore-promise");
|
|
20
|
-
const MideaSecurity_1 = require("./MideaSecurity");
|
|
21
|
-
const MideaUtils_1 = require("./MideaUtils");
|
|
22
|
-
class CloudBase {
|
|
23
|
-
constructor(account, password, security) {
|
|
24
|
-
this.account = account;
|
|
25
|
-
this.password = password;
|
|
26
|
-
this.security = security;
|
|
27
|
-
this.CLIENT_TYPE = 1;
|
|
28
|
-
this.FORMAT = 2;
|
|
29
|
-
this.LANGUAGE = 'en_US';
|
|
30
|
-
this.DEVICE_ID = (0, crypto_1.randomBytes)(8).toString('hex');
|
|
31
|
-
this.loggedIn = false;
|
|
32
|
-
// Required to serialize access to some cloud functions.
|
|
33
|
-
this.semaphore = new semaphore_promise_1.Semaphore();
|
|
34
|
-
}
|
|
35
|
-
timestamp() {
|
|
36
|
-
return luxon_1.DateTime.now().toFormat('yyyyMMddHHmmss');
|
|
37
|
-
}
|
|
38
|
-
async getLoginId() {
|
|
39
|
-
try {
|
|
40
|
-
const response = await this.apiRequest('/v1/user/login/id/get', {
|
|
41
|
-
...this.buildRequestData(),
|
|
42
|
-
loginAccount: this.account,
|
|
43
|
-
});
|
|
44
|
-
if (response) {
|
|
45
|
-
return response['loginId'];
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
catch (e) {
|
|
49
|
-
const msg = e instanceof Error ? e.stack : e;
|
|
50
|
-
throw new Error(`Failed to get login ID:\n${msg}`);
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
async getTokenKey(device_id, endianess) {
|
|
54
|
-
const udpid = MideaSecurity_1.CloudSecurity.getUDPID((0, MideaUtils_1.numberToUint8Array)(device_id, 6, endianess));
|
|
55
|
-
const response = await this.apiRequest('/v1/iot/secure/getToken', {
|
|
56
|
-
...this.buildRequestData(),
|
|
57
|
-
udpid: udpid,
|
|
58
|
-
});
|
|
59
|
-
if (response) {
|
|
60
|
-
for (const token of response['tokenlist']) {
|
|
61
|
-
if (token['udpId'] === udpid) {
|
|
62
|
-
return [Buffer.from(token['token'], 'hex'), Buffer.from(token['key'], 'hex')];
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
else {
|
|
67
|
-
throw new Error('Failed to get token.');
|
|
68
|
-
}
|
|
69
|
-
throw new Error(`No token/key found for udpid ${udpid}.`);
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
class ProxiedCloudBase extends CloudBase {
|
|
73
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
74
|
-
async apiRequest(endpoint, data) {
|
|
75
|
-
const url = `${this.API_URL}${endpoint}`;
|
|
76
|
-
const random = (0, crypto_1.randomBytes)(16).toString('hex');
|
|
77
|
-
const sign = this.security.sign(JSON.stringify(data), random);
|
|
78
|
-
const headers = {
|
|
79
|
-
'Content-Type': 'application/json',
|
|
80
|
-
secretVersion: '1',
|
|
81
|
-
sign: sign,
|
|
82
|
-
random: random,
|
|
83
|
-
};
|
|
84
|
-
if (this.uid) {
|
|
85
|
-
headers['uid'] = this.uid;
|
|
86
|
-
}
|
|
87
|
-
if (this.access_token) {
|
|
88
|
-
headers['accessToken'] = this.access_token;
|
|
89
|
-
}
|
|
90
|
-
for (let i = 0; i < 3; i++) {
|
|
91
|
-
try {
|
|
92
|
-
const response = await axios_1.default.post(url, data, { headers: headers, timeout: 10000 });
|
|
93
|
-
if (response.data['code'] !== undefined) {
|
|
94
|
-
if (Number.parseInt(response.data['code']) === 0) {
|
|
95
|
-
return response.data['data'];
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
throw new Error(`Error response from API: ${JSON.stringify(response.data)}`);
|
|
99
|
-
}
|
|
100
|
-
catch (error) {
|
|
101
|
-
throw new Error(`Error while sending request to ${url}: ${error}`);
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
throw new Error(`Failed to send request to ${url}.`);
|
|
105
|
-
}
|
|
106
|
-
buildRequestData() {
|
|
107
|
-
return {
|
|
108
|
-
appId: this.APP_ID,
|
|
109
|
-
format: this.FORMAT,
|
|
110
|
-
clientType: this.CLIENT_TYPE,
|
|
111
|
-
language: this.LANGUAGE,
|
|
112
|
-
src: this.APP_ID,
|
|
113
|
-
stamp: this.timestamp(),
|
|
114
|
-
deviceId: this.DEVICE_ID,
|
|
115
|
-
reqId: (0, crypto_1.randomBytes)(16).toString('hex'),
|
|
116
|
-
};
|
|
117
|
-
}
|
|
118
|
-
async login() {
|
|
119
|
-
const releaseSemaphore = await this.semaphore.acquire('Obtain login semaphore');
|
|
120
|
-
try {
|
|
121
|
-
if (this.loggedIn) {
|
|
122
|
-
return;
|
|
123
|
-
}
|
|
124
|
-
// Not logged in so proceed...
|
|
125
|
-
const login_id = await this.getLoginId();
|
|
126
|
-
const iotData = this.buildRequestData();
|
|
127
|
-
delete iotData['uid'];
|
|
128
|
-
const response = await this.apiRequest('/mj/user/login', {
|
|
129
|
-
data: {
|
|
130
|
-
platform: this.FORMAT,
|
|
131
|
-
deviceId: this.DEVICE_ID,
|
|
132
|
-
},
|
|
133
|
-
iotData: {
|
|
134
|
-
appId: this.APP_ID,
|
|
135
|
-
clientType: this.CLIENT_TYPE,
|
|
136
|
-
iampwd: this.security.encrpytIAMPassword(login_id, this.password),
|
|
137
|
-
loginAccount: this.account,
|
|
138
|
-
password: this.security.encrpytPassword(login_id, this.password),
|
|
139
|
-
pushToken: (0, crypto_1.randomBytes)(16).toString('base64url'),
|
|
140
|
-
reqId: (0, crypto_1.randomBytes)(16).toString('hex'),
|
|
141
|
-
src: this.APP_ID,
|
|
142
|
-
stamp: this.timestamp(),
|
|
143
|
-
},
|
|
144
|
-
});
|
|
145
|
-
if (response) {
|
|
146
|
-
this.access_token = response['mdata']['accessToken'];
|
|
147
|
-
if (response['key'] !== undefined) {
|
|
148
|
-
this.key = response['key'];
|
|
149
|
-
}
|
|
150
|
-
this.loggedIn = true;
|
|
151
|
-
}
|
|
152
|
-
else {
|
|
153
|
-
this.loggedIn = false;
|
|
154
|
-
throw new Error('Failed to login.');
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
catch (e) {
|
|
158
|
-
const msg = e instanceof Error ? e.stack : e;
|
|
159
|
-
throw new Error(`Error in Adding new accessory:\n${msg}`);
|
|
160
|
-
}
|
|
161
|
-
finally {
|
|
162
|
-
releaseSemaphore();
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
async getProtocolLua(deviceType, serialNumber) {
|
|
166
|
-
const response = await this.apiRequest('/v2/luaEncryption/luaGet', {
|
|
167
|
-
...this.buildRequestData(),
|
|
168
|
-
applianceMFCode: '0000',
|
|
169
|
-
applianceSn: this.security.encryptAESAppKey(Buffer.from(serialNumber, 'utf8')).toString('hex'),
|
|
170
|
-
applianceType: `0x${deviceType.toString(16).padStart(2, '0')}`,
|
|
171
|
-
encryptedType: 2,
|
|
172
|
-
version: '0',
|
|
173
|
-
});
|
|
174
|
-
if (response && response['url']) {
|
|
175
|
-
const lua = await axios_1.default.get(response['url']);
|
|
176
|
-
const encrypted_data = Buffer.from(lua.data, 'hex');
|
|
177
|
-
const file_data = this.security.decryptAESAppKey(encrypted_data).toString('utf8');
|
|
178
|
-
if (file_data) {
|
|
179
|
-
return file_data;
|
|
180
|
-
}
|
|
181
|
-
else {
|
|
182
|
-
throw new Error('Failed to decrypt plugin.');
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
else {
|
|
186
|
-
throw new Error('Failed to get protocol.');
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
async getPlugin(deviceType, serialNumber) {
|
|
190
|
-
var _a;
|
|
191
|
-
const response = await this.apiRequest('/v1/plugin/update/overseas/get', {
|
|
192
|
-
...this.buildRequestData(),
|
|
193
|
-
clientVersion: '0',
|
|
194
|
-
uid: (_a = this.uid) !== null && _a !== void 0 ? _a : (0, crypto_1.randomBytes)(16).toString('hex'),
|
|
195
|
-
applianceList: [
|
|
196
|
-
{
|
|
197
|
-
appModel: serialNumber.substring(9, 17),
|
|
198
|
-
appType: `0x${deviceType.toString(16).padStart(2, '0')}`,
|
|
199
|
-
modelNumber: '0',
|
|
200
|
-
},
|
|
201
|
-
],
|
|
202
|
-
});
|
|
203
|
-
if (response) {
|
|
204
|
-
return response;
|
|
205
|
-
}
|
|
206
|
-
else {
|
|
207
|
-
throw new Error('Failed to get plugin.');
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
class MSmartHomeCloud extends ProxiedCloudBase {
|
|
212
|
-
constructor(account, password) {
|
|
213
|
-
super(account, password, new MideaSecurity_1.MSmartHomeCloudSecurity());
|
|
214
|
-
this.APP_ID = '1010';
|
|
215
|
-
this.API_URL = 'https://mp-prod.appsmb.com/mas/v5/app/proxy?alias=';
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
class MeijuCloud extends ProxiedCloudBase {
|
|
219
|
-
constructor(account, password) {
|
|
220
|
-
super(account, password, new MideaSecurity_1.MeijuCloudSecurity());
|
|
221
|
-
this.APP_ID = '1010';
|
|
222
|
-
this.API_URL = 'https://mp-prod.smartmidea.net/mas/v5/app/proxy?alias=';
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
class SimpleCloud extends CloudBase {
|
|
226
|
-
constructor(account, password, security) {
|
|
227
|
-
super(account, password, security);
|
|
228
|
-
}
|
|
229
|
-
buildRequestData() {
|
|
230
|
-
const data = {
|
|
231
|
-
appId: this.APP_ID,
|
|
232
|
-
format: 2,
|
|
233
|
-
clientType: 1,
|
|
234
|
-
language: this.LANGUAGE,
|
|
235
|
-
src: this.APP_ID,
|
|
236
|
-
stamp: this.timestamp(),
|
|
237
|
-
deviceId: this.DEVICE_ID,
|
|
238
|
-
};
|
|
239
|
-
if (this.sessionId) {
|
|
240
|
-
data['sessionId'] = this.sessionId;
|
|
241
|
-
}
|
|
242
|
-
return data;
|
|
243
|
-
}
|
|
244
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
245
|
-
async apiRequest(endpoint, data, header) {
|
|
246
|
-
const headers = {
|
|
247
|
-
...header,
|
|
248
|
-
};
|
|
249
|
-
if (data['stamp'] === undefined) {
|
|
250
|
-
data['stamp'] = this.timestamp();
|
|
251
|
-
}
|
|
252
|
-
const url = `${this.API_URL}${endpoint}`;
|
|
253
|
-
const queryParams = new URLSearchParams(data);
|
|
254
|
-
queryParams.sort();
|
|
255
|
-
data['sign'] = this.security.sign(url, queryParams.toString());
|
|
256
|
-
if (this.uid) {
|
|
257
|
-
headers['uid'] = this.uid;
|
|
258
|
-
}
|
|
259
|
-
if (this.access_token) {
|
|
260
|
-
headers['accessToken'] = this.access_token;
|
|
261
|
-
}
|
|
262
|
-
const payload = new URLSearchParams(data);
|
|
263
|
-
for (let i = 0; i < 3; i++) {
|
|
264
|
-
try {
|
|
265
|
-
const response = await axios_1.default.post(url, payload.toString(), { headers: headers });
|
|
266
|
-
if (response.data['errorCode'] !== undefined &&
|
|
267
|
-
Number.parseInt(response.data['errorCode']) === 0 &&
|
|
268
|
-
response.data['result'] !== undefined) {
|
|
269
|
-
return response.data['result'];
|
|
270
|
-
}
|
|
271
|
-
else {
|
|
272
|
-
throw new Error(`Error response from API: ${JSON.stringify(response.data)}`);
|
|
273
|
-
}
|
|
274
|
-
}
|
|
275
|
-
catch (error) {
|
|
276
|
-
throw new Error(`Error while sending request to ${url}: ${error}`);
|
|
277
|
-
}
|
|
278
|
-
}
|
|
279
|
-
throw new Error(`Failed to send request to ${url}.`);
|
|
280
|
-
}
|
|
281
|
-
async login() {
|
|
282
|
-
// We need to protect against multiple attempts to login, so we only login if not already
|
|
283
|
-
// logged in. Protect this block with a semaphone.
|
|
284
|
-
const releaseSemaphore = await this.semaphore.acquire('Obtain login semaphore');
|
|
285
|
-
try {
|
|
286
|
-
if (this.loggedIn) {
|
|
287
|
-
return;
|
|
288
|
-
}
|
|
289
|
-
// Not logged in so proceed...
|
|
290
|
-
const login_id = await this.getLoginId();
|
|
291
|
-
const data = {
|
|
292
|
-
...this.buildRequestData(),
|
|
293
|
-
loginAccount: this.account,
|
|
294
|
-
password: this.security.encrpytPassword(login_id, this.password),
|
|
295
|
-
};
|
|
296
|
-
if (this.sessionId) {
|
|
297
|
-
data['sessionId'] = this.sessionId;
|
|
298
|
-
}
|
|
299
|
-
const response = await this.apiRequest('/v1/user/login', data);
|
|
300
|
-
if (response) {
|
|
301
|
-
this.access_token = response['accessToken'];
|
|
302
|
-
this.sessionId = response['sessionId'];
|
|
303
|
-
this.uid = response['userId'];
|
|
304
|
-
this.loggedIn = true;
|
|
305
|
-
}
|
|
306
|
-
else {
|
|
307
|
-
this.loggedIn = false;
|
|
308
|
-
throw new Error('Failed to login.');
|
|
309
|
-
}
|
|
310
|
-
}
|
|
311
|
-
catch (e) {
|
|
312
|
-
const msg = e instanceof Error ? e.stack : e;
|
|
313
|
-
throw new Error(`Error in Adding new accessory:\n${msg}`);
|
|
314
|
-
}
|
|
315
|
-
finally {
|
|
316
|
-
releaseSemaphore();
|
|
317
|
-
}
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
class NetHomePlusCloud extends SimpleCloud {
|
|
321
|
-
constructor(account, password) {
|
|
322
|
-
super(account, password, new MideaSecurity_1.NetHomePlusSecurity());
|
|
323
|
-
this.APP_ID = '1017';
|
|
324
|
-
this.API_URL = 'https://mapp.appsmb.com';
|
|
325
|
-
}
|
|
326
|
-
}
|
|
327
|
-
class AristonClimaCloud extends SimpleCloud {
|
|
328
|
-
constructor(account, password) {
|
|
329
|
-
super(account, password, new MideaSecurity_1.ArtisonClimaSecurity());
|
|
330
|
-
this.APP_ID = '1005';
|
|
331
|
-
this.API_URL = 'https://mapp.appsmb.com';
|
|
332
|
-
}
|
|
333
|
-
}
|
|
334
|
-
class CloudFactory {
|
|
335
|
-
static createCloud(account, password, cloud) {
|
|
336
|
-
switch (cloud) {
|
|
337
|
-
case 'Midea SmartHome (MSmartHome)':
|
|
338
|
-
return new MSmartHomeCloud(account, password);
|
|
339
|
-
case 'Meiju':
|
|
340
|
-
return new MeijuCloud(account, password);
|
|
341
|
-
case 'NetHome Plus':
|
|
342
|
-
return new NetHomePlusCloud(account, password);
|
|
343
|
-
case 'Ariston Clima':
|
|
344
|
-
return new AristonClimaCloud(account, password);
|
|
345
|
-
default:
|
|
346
|
-
throw new Error(`Cloud ${cloud} is not supported.`);
|
|
347
|
-
}
|
|
348
|
-
}
|
|
349
|
-
}
|
|
350
|
-
exports.default = CloudFactory;
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
/***********************************************************************
|
|
7
|
+
* Midea Cloud access functions
|
|
8
|
+
*
|
|
9
|
+
* Copyright (c) 2023 Kovalovszky Patrik, https://github.com/kovapatrik
|
|
10
|
+
* Portions Copyright (c) 2023 David Kerr, https://github.com/dkerr64
|
|
11
|
+
*
|
|
12
|
+
* With thanks to https://github.com/georgezhao2010/midea_ac_lan and
|
|
13
|
+
* https://github.com/mill1000/midea-msmart
|
|
14
|
+
*
|
|
15
|
+
*/
|
|
16
|
+
const axios_1 = __importDefault(require("axios"));
|
|
17
|
+
const crypto_1 = require("crypto");
|
|
18
|
+
const luxon_1 = require("luxon");
|
|
19
|
+
const semaphore_promise_1 = require("semaphore-promise");
|
|
20
|
+
const MideaSecurity_1 = require("./MideaSecurity");
|
|
21
|
+
const MideaUtils_1 = require("./MideaUtils");
|
|
22
|
+
class CloudBase {
|
|
23
|
+
constructor(account, password, security) {
|
|
24
|
+
this.account = account;
|
|
25
|
+
this.password = password;
|
|
26
|
+
this.security = security;
|
|
27
|
+
this.CLIENT_TYPE = 1;
|
|
28
|
+
this.FORMAT = 2;
|
|
29
|
+
this.LANGUAGE = 'en_US';
|
|
30
|
+
this.DEVICE_ID = (0, crypto_1.randomBytes)(8).toString('hex');
|
|
31
|
+
this.loggedIn = false;
|
|
32
|
+
// Required to serialize access to some cloud functions.
|
|
33
|
+
this.semaphore = new semaphore_promise_1.Semaphore();
|
|
34
|
+
}
|
|
35
|
+
timestamp() {
|
|
36
|
+
return luxon_1.DateTime.now().toFormat('yyyyMMddHHmmss');
|
|
37
|
+
}
|
|
38
|
+
async getLoginId() {
|
|
39
|
+
try {
|
|
40
|
+
const response = await this.apiRequest('/v1/user/login/id/get', {
|
|
41
|
+
...this.buildRequestData(),
|
|
42
|
+
loginAccount: this.account,
|
|
43
|
+
});
|
|
44
|
+
if (response) {
|
|
45
|
+
return response['loginId'];
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
catch (e) {
|
|
49
|
+
const msg = e instanceof Error ? e.stack : e;
|
|
50
|
+
throw new Error(`Failed to get login ID:\n${msg}`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
async getTokenKey(device_id, endianess) {
|
|
54
|
+
const udpid = MideaSecurity_1.CloudSecurity.getUDPID((0, MideaUtils_1.numberToUint8Array)(device_id, 6, endianess));
|
|
55
|
+
const response = await this.apiRequest('/v1/iot/secure/getToken', {
|
|
56
|
+
...this.buildRequestData(),
|
|
57
|
+
udpid: udpid,
|
|
58
|
+
});
|
|
59
|
+
if (response) {
|
|
60
|
+
for (const token of response['tokenlist']) {
|
|
61
|
+
if (token['udpId'] === udpid) {
|
|
62
|
+
return [Buffer.from(token['token'], 'hex'), Buffer.from(token['key'], 'hex')];
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
throw new Error('Failed to get token.');
|
|
68
|
+
}
|
|
69
|
+
throw new Error(`No token/key found for udpid ${udpid}.`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
class ProxiedCloudBase extends CloudBase {
|
|
73
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
74
|
+
async apiRequest(endpoint, data) {
|
|
75
|
+
const url = `${this.API_URL}${endpoint}`;
|
|
76
|
+
const random = (0, crypto_1.randomBytes)(16).toString('hex');
|
|
77
|
+
const sign = this.security.sign(JSON.stringify(data), random);
|
|
78
|
+
const headers = {
|
|
79
|
+
'Content-Type': 'application/json',
|
|
80
|
+
secretVersion: '1',
|
|
81
|
+
sign: sign,
|
|
82
|
+
random: random,
|
|
83
|
+
};
|
|
84
|
+
if (this.uid) {
|
|
85
|
+
headers['uid'] = this.uid;
|
|
86
|
+
}
|
|
87
|
+
if (this.access_token) {
|
|
88
|
+
headers['accessToken'] = this.access_token;
|
|
89
|
+
}
|
|
90
|
+
for (let i = 0; i < 3; i++) {
|
|
91
|
+
try {
|
|
92
|
+
const response = await axios_1.default.post(url, data, { headers: headers, timeout: 10000 });
|
|
93
|
+
if (response.data['code'] !== undefined) {
|
|
94
|
+
if (Number.parseInt(response.data['code']) === 0) {
|
|
95
|
+
return response.data['data'];
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
throw new Error(`Error response from API: ${JSON.stringify(response.data)}`);
|
|
99
|
+
}
|
|
100
|
+
catch (error) {
|
|
101
|
+
throw new Error(`Error while sending request to ${url}: ${error}`);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
throw new Error(`Failed to send request to ${url}.`);
|
|
105
|
+
}
|
|
106
|
+
buildRequestData() {
|
|
107
|
+
return {
|
|
108
|
+
appId: this.APP_ID,
|
|
109
|
+
format: this.FORMAT,
|
|
110
|
+
clientType: this.CLIENT_TYPE,
|
|
111
|
+
language: this.LANGUAGE,
|
|
112
|
+
src: this.APP_ID,
|
|
113
|
+
stamp: this.timestamp(),
|
|
114
|
+
deviceId: this.DEVICE_ID,
|
|
115
|
+
reqId: (0, crypto_1.randomBytes)(16).toString('hex'),
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
async login() {
|
|
119
|
+
const releaseSemaphore = await this.semaphore.acquire('Obtain login semaphore');
|
|
120
|
+
try {
|
|
121
|
+
if (this.loggedIn) {
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
// Not logged in so proceed...
|
|
125
|
+
const login_id = await this.getLoginId();
|
|
126
|
+
const iotData = this.buildRequestData();
|
|
127
|
+
delete iotData['uid'];
|
|
128
|
+
const response = await this.apiRequest('/mj/user/login', {
|
|
129
|
+
data: {
|
|
130
|
+
platform: this.FORMAT,
|
|
131
|
+
deviceId: this.DEVICE_ID,
|
|
132
|
+
},
|
|
133
|
+
iotData: {
|
|
134
|
+
appId: this.APP_ID,
|
|
135
|
+
clientType: this.CLIENT_TYPE,
|
|
136
|
+
iampwd: this.security.encrpytIAMPassword(login_id, this.password),
|
|
137
|
+
loginAccount: this.account,
|
|
138
|
+
password: this.security.encrpytPassword(login_id, this.password),
|
|
139
|
+
pushToken: (0, crypto_1.randomBytes)(16).toString('base64url'),
|
|
140
|
+
reqId: (0, crypto_1.randomBytes)(16).toString('hex'),
|
|
141
|
+
src: this.APP_ID,
|
|
142
|
+
stamp: this.timestamp(),
|
|
143
|
+
},
|
|
144
|
+
});
|
|
145
|
+
if (response) {
|
|
146
|
+
this.access_token = response['mdata']['accessToken'];
|
|
147
|
+
if (response['key'] !== undefined) {
|
|
148
|
+
this.key = response['key'];
|
|
149
|
+
}
|
|
150
|
+
this.loggedIn = true;
|
|
151
|
+
}
|
|
152
|
+
else {
|
|
153
|
+
this.loggedIn = false;
|
|
154
|
+
throw new Error('Failed to login.');
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
catch (e) {
|
|
158
|
+
const msg = e instanceof Error ? e.stack : e;
|
|
159
|
+
throw new Error(`Error in Adding new accessory:\n${msg}`);
|
|
160
|
+
}
|
|
161
|
+
finally {
|
|
162
|
+
releaseSemaphore();
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
async getProtocolLua(deviceType, serialNumber) {
|
|
166
|
+
const response = await this.apiRequest('/v2/luaEncryption/luaGet', {
|
|
167
|
+
...this.buildRequestData(),
|
|
168
|
+
applianceMFCode: '0000',
|
|
169
|
+
applianceSn: this.security.encryptAESAppKey(Buffer.from(serialNumber, 'utf8')).toString('hex'),
|
|
170
|
+
applianceType: `0x${deviceType.toString(16).padStart(2, '0')}`,
|
|
171
|
+
encryptedType: 2,
|
|
172
|
+
version: '0',
|
|
173
|
+
});
|
|
174
|
+
if (response && response['url']) {
|
|
175
|
+
const lua = await axios_1.default.get(response['url']);
|
|
176
|
+
const encrypted_data = Buffer.from(lua.data, 'hex');
|
|
177
|
+
const file_data = this.security.decryptAESAppKey(encrypted_data).toString('utf8');
|
|
178
|
+
if (file_data) {
|
|
179
|
+
return file_data;
|
|
180
|
+
}
|
|
181
|
+
else {
|
|
182
|
+
throw new Error('Failed to decrypt plugin.');
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
else {
|
|
186
|
+
throw new Error('Failed to get protocol.');
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
async getPlugin(deviceType, serialNumber) {
|
|
190
|
+
var _a;
|
|
191
|
+
const response = await this.apiRequest('/v1/plugin/update/overseas/get', {
|
|
192
|
+
...this.buildRequestData(),
|
|
193
|
+
clientVersion: '0',
|
|
194
|
+
uid: (_a = this.uid) !== null && _a !== void 0 ? _a : (0, crypto_1.randomBytes)(16).toString('hex'),
|
|
195
|
+
applianceList: [
|
|
196
|
+
{
|
|
197
|
+
appModel: serialNumber.substring(9, 17),
|
|
198
|
+
appType: `0x${deviceType.toString(16).padStart(2, '0')}`,
|
|
199
|
+
modelNumber: '0',
|
|
200
|
+
},
|
|
201
|
+
],
|
|
202
|
+
});
|
|
203
|
+
if (response) {
|
|
204
|
+
return response;
|
|
205
|
+
}
|
|
206
|
+
else {
|
|
207
|
+
throw new Error('Failed to get plugin.');
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
class MSmartHomeCloud extends ProxiedCloudBase {
|
|
212
|
+
constructor(account, password) {
|
|
213
|
+
super(account, password, new MideaSecurity_1.MSmartHomeCloudSecurity());
|
|
214
|
+
this.APP_ID = '1010';
|
|
215
|
+
this.API_URL = 'https://mp-prod.appsmb.com/mas/v5/app/proxy?alias=';
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
class MeijuCloud extends ProxiedCloudBase {
|
|
219
|
+
constructor(account, password) {
|
|
220
|
+
super(account, password, new MideaSecurity_1.MeijuCloudSecurity());
|
|
221
|
+
this.APP_ID = '1010';
|
|
222
|
+
this.API_URL = 'https://mp-prod.smartmidea.net/mas/v5/app/proxy?alias=';
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
class SimpleCloud extends CloudBase {
|
|
226
|
+
constructor(account, password, security) {
|
|
227
|
+
super(account, password, security);
|
|
228
|
+
}
|
|
229
|
+
buildRequestData() {
|
|
230
|
+
const data = {
|
|
231
|
+
appId: this.APP_ID,
|
|
232
|
+
format: 2,
|
|
233
|
+
clientType: 1,
|
|
234
|
+
language: this.LANGUAGE,
|
|
235
|
+
src: this.APP_ID,
|
|
236
|
+
stamp: this.timestamp(),
|
|
237
|
+
deviceId: this.DEVICE_ID,
|
|
238
|
+
};
|
|
239
|
+
if (this.sessionId) {
|
|
240
|
+
data['sessionId'] = this.sessionId;
|
|
241
|
+
}
|
|
242
|
+
return data;
|
|
243
|
+
}
|
|
244
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
245
|
+
async apiRequest(endpoint, data, header) {
|
|
246
|
+
const headers = {
|
|
247
|
+
...header,
|
|
248
|
+
};
|
|
249
|
+
if (data['stamp'] === undefined) {
|
|
250
|
+
data['stamp'] = this.timestamp();
|
|
251
|
+
}
|
|
252
|
+
const url = `${this.API_URL}${endpoint}`;
|
|
253
|
+
const queryParams = new URLSearchParams(data);
|
|
254
|
+
queryParams.sort();
|
|
255
|
+
data['sign'] = this.security.sign(url, queryParams.toString());
|
|
256
|
+
if (this.uid) {
|
|
257
|
+
headers['uid'] = this.uid;
|
|
258
|
+
}
|
|
259
|
+
if (this.access_token) {
|
|
260
|
+
headers['accessToken'] = this.access_token;
|
|
261
|
+
}
|
|
262
|
+
const payload = new URLSearchParams(data);
|
|
263
|
+
for (let i = 0; i < 3; i++) {
|
|
264
|
+
try {
|
|
265
|
+
const response = await axios_1.default.post(url, payload.toString(), { headers: headers });
|
|
266
|
+
if (response.data['errorCode'] !== undefined &&
|
|
267
|
+
Number.parseInt(response.data['errorCode']) === 0 &&
|
|
268
|
+
response.data['result'] !== undefined) {
|
|
269
|
+
return response.data['result'];
|
|
270
|
+
}
|
|
271
|
+
else {
|
|
272
|
+
throw new Error(`Error response from API: ${JSON.stringify(response.data)}`);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
catch (error) {
|
|
276
|
+
throw new Error(`Error while sending request to ${url}: ${error}`);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
throw new Error(`Failed to send request to ${url}.`);
|
|
280
|
+
}
|
|
281
|
+
async login() {
|
|
282
|
+
// We need to protect against multiple attempts to login, so we only login if not already
|
|
283
|
+
// logged in. Protect this block with a semaphone.
|
|
284
|
+
const releaseSemaphore = await this.semaphore.acquire('Obtain login semaphore');
|
|
285
|
+
try {
|
|
286
|
+
if (this.loggedIn) {
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
// Not logged in so proceed...
|
|
290
|
+
const login_id = await this.getLoginId();
|
|
291
|
+
const data = {
|
|
292
|
+
...this.buildRequestData(),
|
|
293
|
+
loginAccount: this.account,
|
|
294
|
+
password: this.security.encrpytPassword(login_id, this.password),
|
|
295
|
+
};
|
|
296
|
+
if (this.sessionId) {
|
|
297
|
+
data['sessionId'] = this.sessionId;
|
|
298
|
+
}
|
|
299
|
+
const response = await this.apiRequest('/v1/user/login', data);
|
|
300
|
+
if (response) {
|
|
301
|
+
this.access_token = response['accessToken'];
|
|
302
|
+
this.sessionId = response['sessionId'];
|
|
303
|
+
this.uid = response['userId'];
|
|
304
|
+
this.loggedIn = true;
|
|
305
|
+
}
|
|
306
|
+
else {
|
|
307
|
+
this.loggedIn = false;
|
|
308
|
+
throw new Error('Failed to login.');
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
catch (e) {
|
|
312
|
+
const msg = e instanceof Error ? e.stack : e;
|
|
313
|
+
throw new Error(`Error in Adding new accessory:\n${msg}`);
|
|
314
|
+
}
|
|
315
|
+
finally {
|
|
316
|
+
releaseSemaphore();
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
class NetHomePlusCloud extends SimpleCloud {
|
|
321
|
+
constructor(account, password) {
|
|
322
|
+
super(account, password, new MideaSecurity_1.NetHomePlusSecurity());
|
|
323
|
+
this.APP_ID = '1017';
|
|
324
|
+
this.API_URL = 'https://mapp.appsmb.com';
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
class AristonClimaCloud extends SimpleCloud {
|
|
328
|
+
constructor(account, password) {
|
|
329
|
+
super(account, password, new MideaSecurity_1.ArtisonClimaSecurity());
|
|
330
|
+
this.APP_ID = '1005';
|
|
331
|
+
this.API_URL = 'https://mapp.appsmb.com';
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
class CloudFactory {
|
|
335
|
+
static createCloud(account, password, cloud) {
|
|
336
|
+
switch (cloud) {
|
|
337
|
+
case 'Midea SmartHome (MSmartHome)':
|
|
338
|
+
return new MSmartHomeCloud(account, password);
|
|
339
|
+
case 'Meiju':
|
|
340
|
+
return new MeijuCloud(account, password);
|
|
341
|
+
case 'NetHome Plus':
|
|
342
|
+
return new NetHomePlusCloud(account, password);
|
|
343
|
+
case 'Ariston Clima':
|
|
344
|
+
return new AristonClimaCloud(account, password);
|
|
345
|
+
default:
|
|
346
|
+
throw new Error(`Cloud ${cloud} is not supported.`);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
exports.default = CloudFactory;
|
|
351
351
|
//# sourceMappingURL=MideaCloud.js.map
|