homebridge-melcloud-control 4.2.3-beta.6 → 4.2.3-beta.61
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/config.schema.json +433 -25
- package/homebridge-ui/public/index.html +85 -15
- package/homebridge-ui/server.js +2 -1
- package/index.js +6 -5
- package/package.json +2 -2
- package/src/constants.js +48 -5
- package/src/deviceata.js +273 -155
- package/src/deviceatw.js +125 -119
- package/src/deviceerv.js +41 -42
- package/src/melcloud.js +27 -362
- package/src/melcloudata.js +35 -21
- package/src/melcloudatw.js +38 -29
- package/src/melclouderv.js +29 -22
- package/src/melcloudhome.js +403 -0
package/src/melcloud.js
CHANGED
|
@@ -1,13 +1,8 @@
|
|
|
1
|
-
import fs from 'fs';
|
|
2
1
|
import axios from 'axios';
|
|
3
|
-
import { exec } from 'child_process';
|
|
4
|
-
import { promisify } from 'util';
|
|
5
2
|
import EventEmitter from 'events';
|
|
6
|
-
import puppeteer from 'puppeteer';
|
|
7
3
|
import ImpulseGenerator from './impulsegenerator.js';
|
|
8
4
|
import Functions from './functions.js';
|
|
9
|
-
import { ApiUrls
|
|
10
|
-
const execPromise = promisify(exec);
|
|
5
|
+
import { ApiUrls } from './constants.js';
|
|
11
6
|
|
|
12
7
|
class MelCloud extends EventEmitter {
|
|
13
8
|
constructor(account, accountFile, buildingsFile, devicesFile, pluginStart = false) {
|
|
@@ -22,7 +17,8 @@ class MelCloud extends EventEmitter {
|
|
|
22
17
|
this.accountFile = accountFile;
|
|
23
18
|
this.buildingsFile = buildingsFile;
|
|
24
19
|
this.devicesFile = devicesFile;
|
|
25
|
-
this.
|
|
20
|
+
this.headers = {};
|
|
21
|
+
|
|
26
22
|
this.functions = new Functions(this.logWarn, this.logError, this.logDebug)
|
|
27
23
|
.on('warn', warn => this.emit('warn', warn))
|
|
28
24
|
.on('error', error => this.emit('error', error))
|
|
@@ -31,13 +27,9 @@ class MelCloud extends EventEmitter {
|
|
|
31
27
|
if (pluginStart) {
|
|
32
28
|
//lock flags
|
|
33
29
|
this.locks = {
|
|
34
|
-
connect: false,
|
|
35
30
|
checkDevicesList: false
|
|
36
31
|
};
|
|
37
32
|
this.impulseGenerator = new ImpulseGenerator()
|
|
38
|
-
.on('connect', () => this.handleWithLock('connect', async () => {
|
|
39
|
-
await this.connect(true);
|
|
40
|
-
}))
|
|
41
33
|
.on('checkDevicesList', () => this.handleWithLock('checkDevicesList', async () => {
|
|
42
34
|
await this.checkDevicesList();
|
|
43
35
|
}))
|
|
@@ -61,20 +53,15 @@ class MelCloud extends EventEmitter {
|
|
|
61
53
|
}
|
|
62
54
|
|
|
63
55
|
// MELCloud
|
|
64
|
-
async
|
|
56
|
+
async checkDevicesList() {
|
|
65
57
|
try {
|
|
66
|
-
const devicesList = { State: false, Info: null, Devices: [] }
|
|
67
|
-
const headers = {
|
|
68
|
-
'X-MitsContextKey': this.contextKey,
|
|
69
|
-
'Content-Type': 'application/json'
|
|
70
|
-
}
|
|
71
|
-
|
|
58
|
+
const devicesList = { State: false, Info: null, Devices: [], Scenes: [] }
|
|
72
59
|
if (this.logDebug) this.emit('debug', `Scanning for devices...`);
|
|
73
60
|
const listDevicesData = await axios(ApiUrls.ListDevices, {
|
|
74
61
|
method: 'GET',
|
|
75
62
|
baseURL: ApiUrls.BaseURL,
|
|
76
63
|
timeout: 15000,
|
|
77
|
-
headers: headers
|
|
64
|
+
headers: this.headers
|
|
78
65
|
});
|
|
79
66
|
|
|
80
67
|
if (!listDevicesData || !listDevicesData.data) {
|
|
@@ -93,6 +80,7 @@ class MelCloud extends EventEmitter {
|
|
|
93
80
|
await this.functions.saveData(this.buildingsFile, buildingsList);
|
|
94
81
|
if (this.logDebug) this.emit('debug', `Buildings list saved`);
|
|
95
82
|
|
|
83
|
+
const devices = [];
|
|
96
84
|
for (const building of buildingsList) {
|
|
97
85
|
if (!building.Structure) {
|
|
98
86
|
this.emit('warn', `Building missing structure: ${building.BuildingName || 'Unnamed'}`);
|
|
@@ -113,35 +101,37 @@ class MelCloud extends EventEmitter {
|
|
|
113
101
|
// Zamiana ID na string
|
|
114
102
|
allDevices.forEach(device => {
|
|
115
103
|
device.DeviceID = String(device.DeviceID);
|
|
116
|
-
device.Headers = headers;
|
|
104
|
+
device.Headers = this.headers;
|
|
117
105
|
});
|
|
118
106
|
|
|
119
107
|
if (this.logDebug) this.emit('debug', `Found ${allDevices.length} devices in building: ${building.Name || 'Unnamed'}`);
|
|
120
|
-
|
|
108
|
+
devices.push(...allDevices);
|
|
121
109
|
}
|
|
122
110
|
|
|
123
|
-
const devicesCount =
|
|
111
|
+
const devicesCount = devices.length;
|
|
124
112
|
if (devicesCount === 0) {
|
|
125
113
|
devicesList.Info = 'No devices found'
|
|
126
114
|
return devicesList;
|
|
127
115
|
}
|
|
128
116
|
|
|
129
|
-
await this.functions.saveData(this.devicesFile, devicesList.Devices);
|
|
130
|
-
if (this.logDebug) this.emit('debug', `${devicesCount} devices saved`);
|
|
131
|
-
|
|
132
117
|
devicesList.State = true;
|
|
133
118
|
devicesList.Info = `Found ${devicesCount} devices`;
|
|
119
|
+
devicesList.Devices = devices;
|
|
120
|
+
|
|
121
|
+
await this.functions.saveData(this.devicesFile, devicesList);
|
|
122
|
+
if (this.logDebug) this.emit('debug', `${devicesCount} devices saved`);
|
|
123
|
+
|
|
134
124
|
return devicesList;
|
|
135
125
|
} catch (error) {
|
|
136
126
|
throw new Error(`Check devices list error: ${error.message}`);
|
|
137
127
|
}
|
|
138
128
|
}
|
|
139
129
|
|
|
140
|
-
async
|
|
130
|
+
async connect() {
|
|
141
131
|
if (this.logDebug) this.emit('debug', `Connecting to MELCloud`);
|
|
142
132
|
|
|
143
133
|
try {
|
|
144
|
-
const accountInfo = { State: false, Info: '', LoginData: null,
|
|
134
|
+
const accountInfo = { State: false, Info: '', LoginData: null, Headers: {}, UseFahrenheit: false }
|
|
145
135
|
|
|
146
136
|
const payload = {
|
|
147
137
|
Email: this.user,
|
|
@@ -177,12 +167,16 @@ class MelCloud extends EventEmitter {
|
|
|
177
167
|
accountInfo.Info = 'Context key missing'
|
|
178
168
|
return accountInfo;
|
|
179
169
|
}
|
|
180
|
-
|
|
170
|
+
|
|
171
|
+
this.headers = {
|
|
172
|
+
'X-MitsContextKey': contextKey,
|
|
173
|
+
'Content-Type': 'application/json'
|
|
174
|
+
};
|
|
181
175
|
|
|
182
176
|
accountInfo.State = true;
|
|
183
177
|
accountInfo.Info = 'Connect to MELCloud Success';
|
|
184
178
|
accountInfo.LoginData = loginData;
|
|
185
|
-
accountInfo.
|
|
179
|
+
accountInfo.Headers = this.headers;
|
|
186
180
|
await this.functions.saveData(this.accountFile, accountInfo);
|
|
187
181
|
|
|
188
182
|
return accountInfo
|
|
@@ -191,345 +185,16 @@ class MelCloud extends EventEmitter {
|
|
|
191
185
|
}
|
|
192
186
|
}
|
|
193
187
|
|
|
194
|
-
// MELCloud Home
|
|
195
|
-
async checkMelcloudHomeDevicesList() {
|
|
196
|
-
try {
|
|
197
|
-
const devicesList = { State: false, Info: null, Devices: [] }
|
|
198
|
-
const headers = {
|
|
199
|
-
'Accept': '*/*',
|
|
200
|
-
'Accept-Language': 'en-US,en;q=0.9',
|
|
201
|
-
'Cookie': this.contextKey,
|
|
202
|
-
'User-Agent': 'homebridge-melcloud-control/4.0.0',
|
|
203
|
-
'DNT': '1',
|
|
204
|
-
'Origin': 'https://melcloudhome.com',
|
|
205
|
-
'Referer': 'https://melcloudhome.com/dashboard',
|
|
206
|
-
'Sec-Fetch-Dest': 'empty',
|
|
207
|
-
'Sec-Fetch-Mode': 'cors',
|
|
208
|
-
'Sec-Fetch-Site': 'same-origin',
|
|
209
|
-
'X-CSRF': '1'
|
|
210
|
-
};
|
|
211
|
-
|
|
212
|
-
if (this.logDebug) this.emit('debug', `Scanning for devices`);
|
|
213
|
-
const listDevicesData = await axios(ApiUrlsHome.GetUserContext, {
|
|
214
|
-
method: 'GET',
|
|
215
|
-
baseURL: ApiUrlsHome.BaseURL,
|
|
216
|
-
timeout: 25000,
|
|
217
|
-
headers: headers
|
|
218
|
-
});
|
|
219
|
-
const buildingsList = listDevicesData.data.buildings;
|
|
220
|
-
if (this.logDebug) this.emit('debug', `Buildings: ${JSON.stringify(buildingsList, null, 2)}`);
|
|
221
|
-
|
|
222
|
-
if (!buildingsList) {
|
|
223
|
-
devicesList.Info = 'No building found'
|
|
224
|
-
return devicesList;
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
await this.functions.saveData(this.buildingsFile, buildingsList);
|
|
228
|
-
if (this.logDebug) this.emit('debug', `Buildings list saved`);
|
|
229
|
-
|
|
230
|
-
const devices = buildingsList.flatMap(building => {
|
|
231
|
-
// Funkcja kapitalizująca klucze obiektu
|
|
232
|
-
const capitalizeKeys = obj =>
|
|
233
|
-
Object.fromEntries(
|
|
234
|
-
Object.entries(obj).map(([key, value]) => [
|
|
235
|
-
key.charAt(0).toUpperCase() + key.slice(1),
|
|
236
|
-
value
|
|
237
|
-
])
|
|
238
|
-
);
|
|
239
|
-
|
|
240
|
-
// Rekurencyjna kapitalizacja kluczy w obiekcie lub tablicy
|
|
241
|
-
const capitalizeKeysDeep = obj => {
|
|
242
|
-
if (Array.isArray(obj)) return obj.map(capitalizeKeysDeep);
|
|
243
|
-
if (obj && typeof obj === 'object') {
|
|
244
|
-
return Object.fromEntries(
|
|
245
|
-
Object.entries(obj).map(([key, value]) => [
|
|
246
|
-
key.charAt(0).toUpperCase() + key.slice(1),
|
|
247
|
-
capitalizeKeysDeep(value)
|
|
248
|
-
])
|
|
249
|
-
);
|
|
250
|
-
}
|
|
251
|
-
return obj;
|
|
252
|
-
};
|
|
253
|
-
|
|
254
|
-
// Funkcja tworząca finalny obiekt Device
|
|
255
|
-
const createDevice = (device, type) => {
|
|
256
|
-
// Settings już kapitalizowane w nazwach
|
|
257
|
-
const settingsArray = device.Settings || [];
|
|
258
|
-
|
|
259
|
-
const settingsObject = Object.fromEntries(
|
|
260
|
-
settingsArray.map(({ name, value }) => {
|
|
261
|
-
let parsedValue = value;
|
|
262
|
-
if (value === "True") parsedValue = true;
|
|
263
|
-
else if (value === "False") parsedValue = false;
|
|
264
|
-
else if (!isNaN(value) && value !== "") parsedValue = Number(value);
|
|
265
|
-
|
|
266
|
-
const key = name.charAt(0).toUpperCase() + name.slice(1);
|
|
267
|
-
return [key, parsedValue];
|
|
268
|
-
})
|
|
269
|
-
);
|
|
270
|
-
|
|
271
|
-
// Scal Capabilities + Settings + DeviceType w Device
|
|
272
|
-
const deviceObject = {
|
|
273
|
-
...capitalizeKeys(device.Capabilities || {}),
|
|
274
|
-
...settingsObject,
|
|
275
|
-
DeviceType: type,
|
|
276
|
-
IsConnected: device.IsConnected
|
|
277
|
-
};
|
|
278
|
-
|
|
279
|
-
// Kapitalizacja brakujących obiektów/tablic
|
|
280
|
-
if (device.FrostProtection) device.FrostProtection = { ...capitalizeKeys(device.FrostProtection || {}) };
|
|
281
|
-
if (device.OverheatProtection) device.OverheatProtection = { ...capitalizeKeys(device.OverheatProtection || {}) };
|
|
282
|
-
if (device.HolidayMode) device.HolidayMode = { ...capitalizeKeys(device.HolidayMode || {}) };
|
|
283
|
-
if (Array.isArray(device.Schedule)) device.Schedule = device.Schedule.map(capitalizeKeysDeep);
|
|
284
|
-
|
|
285
|
-
// Usuń stare pola Settings i Capabilities
|
|
286
|
-
const { Settings, Capabilities, Id, GivenDisplayName, ...rest } = device;
|
|
287
|
-
|
|
288
|
-
return {
|
|
289
|
-
...rest,
|
|
290
|
-
Type: type,
|
|
291
|
-
DeviceID: Id,
|
|
292
|
-
DeviceName: GivenDisplayName,
|
|
293
|
-
Device: deviceObject,
|
|
294
|
-
Headers: headers
|
|
295
|
-
};
|
|
296
|
-
};
|
|
297
|
-
|
|
298
|
-
return [
|
|
299
|
-
...(building.airToAirUnits || []).map(d => createDevice(capitalizeKeys(d), 0)),
|
|
300
|
-
...(building.airToWaterUnits || []).map(d => createDevice(capitalizeKeys(d), 1)),
|
|
301
|
-
...(building.airToVentilationUnits || []).map(d => createDevice(capitalizeKeys(d), 3))
|
|
302
|
-
];
|
|
303
|
-
});
|
|
304
|
-
|
|
305
|
-
const devicesCount = devices.length;
|
|
306
|
-
if (devicesCount === 0) {
|
|
307
|
-
devicesList.Info = 'No devices found'
|
|
308
|
-
return devicesList;
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
await this.functions.saveData(this.devicesFile, devices);
|
|
312
|
-
if (this.logDebug) this.emit('debug', `${devicesCount} devices saved`);
|
|
313
|
-
|
|
314
|
-
devicesList.State = true;
|
|
315
|
-
devicesList.Info = `Found ${devicesCount} devices`;
|
|
316
|
-
devicesList.Devices = devices;
|
|
317
|
-
return devicesList;
|
|
318
|
-
} catch (error) {
|
|
319
|
-
if (error.response?.status === 401) {
|
|
320
|
-
await connectToMelCloudHome();
|
|
321
|
-
if (this.logWarn) this.emit('warn', 'Check devices list not possible, cookies expired, trying to get new.');
|
|
322
|
-
return;
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
throw new Error(`Check devices list error: ${error.message}`);
|
|
326
|
-
}
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
async connectToMelCloudHome() {
|
|
330
|
-
if (this.logDebug) this.emit('debug', 'Connecting to MELCloud Home');
|
|
331
|
-
const GLOBAL_TIMEOUT = 90000;
|
|
332
|
-
|
|
333
|
-
let browser;
|
|
334
|
-
try {
|
|
335
|
-
const accountInfo = { State: false, Info: '', ContextKey: null, UseFahrenheit: false };
|
|
336
|
-
let chromiumPath = await this.functions.ensureChromiumInstalled();
|
|
337
|
-
|
|
338
|
-
// === Fallback to Puppeteer's built-in Chromium ===
|
|
339
|
-
if (!chromiumPath) {
|
|
340
|
-
try {
|
|
341
|
-
const puppeteerPath = puppeteer.executablePath();
|
|
342
|
-
if (puppeteerPath && fs.existsSync(puppeteerPath)) {
|
|
343
|
-
chromiumPath = puppeteerPath;
|
|
344
|
-
if (this.logDebug) this.emit('debug', `Using puppeteer Chromium at ${chromiumPath}`);
|
|
345
|
-
}
|
|
346
|
-
} catch { }
|
|
347
|
-
} else {
|
|
348
|
-
if (this.logDebug) this.emit('debug', `Using system Chromium at ${chromiumPath}`);
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
if (!chromiumPath) {
|
|
352
|
-
accountInfo.Info = 'Chromium not found on Your device, please install it manually and try again';
|
|
353
|
-
return accountInfo;
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
// Verify executable works
|
|
357
|
-
try {
|
|
358
|
-
const { stdout } = await execPromise(`"${chromiumPath}" --version`);
|
|
359
|
-
if (this.logDebug) this.emit('debug', `Chromium detected: ${stdout.trim()}`);
|
|
360
|
-
} catch (error) {
|
|
361
|
-
accountInfo.Info = `Chromium found at ${chromiumPath}, but cannot be executed: ${error.message}`;
|
|
362
|
-
return accountInfo;
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
if (this.logDebug) this.emit('debug', `Launching Chromium...`);
|
|
366
|
-
browser = await puppeteer.launch({
|
|
367
|
-
headless: true,
|
|
368
|
-
executablePath: chromiumPath,
|
|
369
|
-
timeout: GLOBAL_TIMEOUT,
|
|
370
|
-
args: [
|
|
371
|
-
'--no-sandbox',
|
|
372
|
-
'--disable-setuid-sandbox',
|
|
373
|
-
'--disable-dev-shm-usage',
|
|
374
|
-
'--single-process',
|
|
375
|
-
'--disable-gpu',
|
|
376
|
-
'--no-zygote'
|
|
377
|
-
]
|
|
378
|
-
});
|
|
379
|
-
browser.on('disconnected', () => this.emit('debug', 'Browser disconnected'));
|
|
380
|
-
|
|
381
|
-
const page = await browser.newPage();
|
|
382
|
-
page.on('error', error => this.emit('error', `Page crashed: ${error.message}`));
|
|
383
|
-
page.on('pageerror', error => this.emit('error', `Browser error: ${error.message}`));
|
|
384
|
-
page.setDefaultTimeout(GLOBAL_TIMEOUT);
|
|
385
|
-
page.setDefaultNavigationTimeout(GLOBAL_TIMEOUT);
|
|
386
|
-
|
|
387
|
-
// Clear cookies before navigation
|
|
388
|
-
try {
|
|
389
|
-
const client = await page.createCDPSession();
|
|
390
|
-
await client.send('Network.clearBrowserCookies');
|
|
391
|
-
} catch (error) {
|
|
392
|
-
if (this.logError) this.emit('error', `Clear cookies error: ${error.message}`);
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
try {
|
|
396
|
-
await page.goto(ApiUrlsHome.BaseURL, { waitUntil: ['domcontentloaded', 'networkidle2'], timeout: GLOBAL_TIMEOUT });
|
|
397
|
-
} catch (error) {
|
|
398
|
-
accountInfo.Info = `Navigation to ${ApiUrlsHome.BaseURL} failed: ${error.message}`;
|
|
399
|
-
return accountInfo;
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
// Wait extra to ensure UI is rendered
|
|
403
|
-
await new Promise(r => setTimeout(r, 3000));
|
|
404
|
-
const loginBtn = await page.waitForSelector('button.btn--blue', { timeout: GLOBAL_TIMEOUT / 4 });
|
|
405
|
-
const loginText = await page.evaluate(el => el.textContent.trim(), loginBtn);
|
|
406
|
-
|
|
407
|
-
if (!['Zaloguj', 'Sign In', 'Login'].includes(loginText)) {
|
|
408
|
-
accountInfo.Info = `Login button ${loginText} not found`;
|
|
409
|
-
return accountInfo;
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
await loginBtn.click();
|
|
413
|
-
await page.waitForNavigation({ waitUntil: 'networkidle2', timeout: GLOBAL_TIMEOUT / 3 });
|
|
414
|
-
|
|
415
|
-
const usernameInput = await page.$('input[name="username"]');
|
|
416
|
-
const passwordInput = await page.$('input[name="password"]');
|
|
417
|
-
if (!usernameInput || !passwordInput) {
|
|
418
|
-
accountInfo.Info = 'Username or password input not found';
|
|
419
|
-
return accountInfo;
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
await page.type('input[name="username"]', this.user, { delay: 50 });
|
|
423
|
-
await page.type('input[name="password"]', this.passwd, { delay: 50 });
|
|
424
|
-
|
|
425
|
-
const submitButton = await page.$('input[type="submit"], button[type="submit"]');
|
|
426
|
-
if (!submitButton) {
|
|
427
|
-
accountInfo.Info = 'Submit button not found';
|
|
428
|
-
return accountInfo;
|
|
429
|
-
}
|
|
430
|
-
await Promise.race([Promise.all([submitButton.click(), page.waitForNavigation({ waitUntil: ['domcontentloaded', 'networkidle2'], timeout: GLOBAL_TIMEOUT / 4 })]), new Promise(r => setTimeout(r, GLOBAL_TIMEOUT / 3))]);
|
|
431
|
-
|
|
432
|
-
// Extract cookies
|
|
433
|
-
let c1 = null, c2 = null;
|
|
434
|
-
const start = Date.now();
|
|
435
|
-
while ((!c1 || !c2) && Date.now() - start < GLOBAL_TIMEOUT / 2) {
|
|
436
|
-
const cookies = await page.browserContext().cookies();
|
|
437
|
-
c1 = cookies.find(c => c.name === '__Secure-monitorandcontrolC1')?.value || c1;
|
|
438
|
-
c2 = cookies.find(c => c.name === '__Secure-monitorandcontrolC2')?.value || c2;
|
|
439
|
-
if (!c1 || !c2) await new Promise(r => setTimeout(r, 500));
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
if (!c1 || !c2) {
|
|
443
|
-
accountInfo.Info = 'Cookies C1/C2 missing';
|
|
444
|
-
return accountInfo;
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
const contextKey = [
|
|
448
|
-
'__Secure-monitorandcontrol=chunks-2',
|
|
449
|
-
`__Secure-monitorandcontrolC1=${c1}`,
|
|
450
|
-
`__Secure-monitorandcontrolC2=${c2}`
|
|
451
|
-
].join('; ');
|
|
452
|
-
this.contextKey = contextKey;
|
|
453
|
-
|
|
454
|
-
accountInfo.State = true;
|
|
455
|
-
accountInfo.Info = 'Connect to MELCloud Home Success';
|
|
456
|
-
accountInfo.ContextKey = contextKey;
|
|
457
|
-
await this.functions.saveData(this.accountFile, accountInfo);
|
|
458
|
-
|
|
459
|
-
return accountInfo;
|
|
460
|
-
} catch (error) {
|
|
461
|
-
throw new Error(`Connect error: ${error.message}`);
|
|
462
|
-
} finally {
|
|
463
|
-
if (browser) {
|
|
464
|
-
try { await browser.close(); }
|
|
465
|
-
catch (closeErr) {
|
|
466
|
-
if (this.logError) this.emit('error', `Failed to close Puppeteer: ${closeErr.message}`);
|
|
467
|
-
}
|
|
468
|
-
}
|
|
469
|
-
}
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
async checkDevicesList() {
|
|
473
|
-
const TIMEOUT_MS = 30000; // 30 seconds timeout
|
|
474
|
-
try {
|
|
475
|
-
const devicesList = await Promise.race([
|
|
476
|
-
(async () => {
|
|
477
|
-
switch (this.accountType) {
|
|
478
|
-
case "melcloud":
|
|
479
|
-
return await this.checkMelcloudDevicesList();
|
|
480
|
-
case "melcloudhome":
|
|
481
|
-
return await this.checkMelcloudHomeDevicesList();
|
|
482
|
-
default:
|
|
483
|
-
return [];
|
|
484
|
-
}
|
|
485
|
-
})(),
|
|
486
|
-
new Promise((_, reject) => setTimeout(() => reject(new Error('Device list timeout (30s)')), TIMEOUT_MS))
|
|
487
|
-
]);
|
|
488
|
-
|
|
489
|
-
return devicesList;
|
|
490
|
-
} catch (error) {
|
|
491
|
-
throw new Error(error);
|
|
492
|
-
}
|
|
493
|
-
}
|
|
494
|
-
|
|
495
|
-
async connect() {
|
|
496
|
-
const TIMEOUT_MS = 120000;
|
|
497
|
-
|
|
498
|
-
try {
|
|
499
|
-
const response = await Promise.race([
|
|
500
|
-
(async () => {
|
|
501
|
-
switch (this.accountType) {
|
|
502
|
-
case "melcloud":
|
|
503
|
-
return await this.connectToMelCloud();
|
|
504
|
-
case "melcloudhome":
|
|
505
|
-
return await this.connectToMelCloudHome();
|
|
506
|
-
default:
|
|
507
|
-
return {};
|
|
508
|
-
}
|
|
509
|
-
})(),
|
|
510
|
-
new Promise((_, reject) => setTimeout(() => reject(new Error('Connection timeout (90s)')), TIMEOUT_MS))
|
|
511
|
-
]);
|
|
512
|
-
|
|
513
|
-
return response;
|
|
514
|
-
} catch (error) {
|
|
515
|
-
throw new Error(error);
|
|
516
|
-
}
|
|
517
|
-
}
|
|
518
|
-
|
|
519
188
|
async send(accountInfo) {
|
|
520
189
|
try {
|
|
521
|
-
const
|
|
190
|
+
const payload = { data: accountInfo.LoginData };
|
|
191
|
+
await axios(ApiUrls.UpdateApplicationOptions, {
|
|
522
192
|
method: 'POST',
|
|
523
193
|
baseURL: ApiUrls.BaseURL,
|
|
524
194
|
timeout: 15000,
|
|
525
|
-
headers:
|
|
526
|
-
|
|
527
|
-
'content-type': 'application/json'
|
|
528
|
-
}
|
|
195
|
+
headers: accountInfo.Headers,
|
|
196
|
+
data: payload
|
|
529
197
|
});
|
|
530
|
-
|
|
531
|
-
const payload = { data: accountInfo.LoginData };
|
|
532
|
-
await axiosInstance(ApiUrls.UpdateApplicationOptions, payload);
|
|
533
198
|
await this.functions.saveData(this.accountFile, accountInfo);
|
|
534
199
|
return true;
|
|
535
200
|
} catch (error) {
|
package/src/melcloudata.js
CHANGED
|
@@ -24,7 +24,7 @@ class MelCloudAta extends EventEmitter {
|
|
|
24
24
|
//set default values
|
|
25
25
|
this.deviceData = {};
|
|
26
26
|
|
|
27
|
-
//lock
|
|
27
|
+
//lock flag
|
|
28
28
|
this.locks = true;
|
|
29
29
|
this.impulseGenerator = new ImpulseGenerator()
|
|
30
30
|
.on('checkState', () => this.handleWithLock(async () => {
|
|
@@ -52,11 +52,11 @@ class MelCloudAta extends EventEmitter {
|
|
|
52
52
|
try {
|
|
53
53
|
//read device info from file
|
|
54
54
|
const devicesData = await this.functions.readData(this.devicesFile, true);
|
|
55
|
-
const
|
|
55
|
+
const scenes = devicesData.Scenes ?? [];
|
|
56
|
+
const deviceData = devicesData.Devices.find(device => device.DeviceID === this.deviceId);
|
|
56
57
|
|
|
57
58
|
if (this.accountType === 'melcloudhome') {
|
|
58
|
-
deviceData.
|
|
59
|
-
deviceData.Device.FirmwareAppVersion = deviceData.ConnectedInterfaceIdentifier || '4.0.0';
|
|
59
|
+
deviceData.Scenes = scenes;
|
|
60
60
|
deviceData.Device.OperationMode = AirConditioner.OperationModeMapStringToEnum[deviceData.Device.OperationMode] ?? deviceData.Device.OperationMode;
|
|
61
61
|
deviceData.Device.ActualFanSpeed = AirConditioner.FanSpeedMapStringToEnum[deviceData.Device.ActualFanSpeed] ?? deviceData.Device.ActualFanSpeed;
|
|
62
62
|
deviceData.Device.SetFanSpeed = AirConditioner.FanSpeedMapStringToEnum[deviceData.Device.SetFanSpeed] ?? deviceData.Device.SetFanSpeed;
|
|
@@ -76,13 +76,12 @@ class MelCloudAta extends EventEmitter {
|
|
|
76
76
|
if (this.logDebug) this.emit('debug', `Device Data: ${JSON.stringify(safeConfig, null, 2)}`);
|
|
77
77
|
|
|
78
78
|
//device
|
|
79
|
-
const serialNumber = deviceData.SerialNumber;
|
|
80
|
-
const firmwareAppVersion = deviceData.Device?.FirmwareAppVersion;
|
|
79
|
+
const serialNumber = deviceData.SerialNumber || '4.0.0';
|
|
80
|
+
const firmwareAppVersion = deviceData.Device?.FirmwareAppVersion || '4.0.0';
|
|
81
81
|
|
|
82
82
|
//units
|
|
83
83
|
const units = Array.isArray(deviceData.Device?.Units) ? deviceData.Device?.Units : [];
|
|
84
84
|
const unitsCount = units.length;
|
|
85
|
-
const manufacturer = 'Mitsubishi';
|
|
86
85
|
|
|
87
86
|
const { indoor, outdoor } = units.reduce((acc, unit) => {
|
|
88
87
|
const target = unit.IsIndoor ? 'indoor' : 'outdoor';
|
|
@@ -118,7 +117,7 @@ class MelCloudAta extends EventEmitter {
|
|
|
118
117
|
this.deviceData = deviceData;
|
|
119
118
|
|
|
120
119
|
//emit info
|
|
121
|
-
this.emit('deviceInfo',
|
|
120
|
+
this.emit('deviceInfo', indoor.model, outdoor.model, serialNumber, firmwareAppVersion);
|
|
122
121
|
|
|
123
122
|
//emit state
|
|
124
123
|
this.emit('deviceState', deviceData);
|
|
@@ -129,7 +128,7 @@ class MelCloudAta extends EventEmitter {
|
|
|
129
128
|
};
|
|
130
129
|
};
|
|
131
130
|
|
|
132
|
-
async send(accountType, displayType, deviceData,
|
|
131
|
+
async send(accountType, displayType, deviceData, flag, flagData) {
|
|
133
132
|
try {
|
|
134
133
|
let method = null
|
|
135
134
|
let payload = {};
|
|
@@ -141,7 +140,7 @@ class MelCloudAta extends EventEmitter {
|
|
|
141
140
|
deviceData.Device.SetTemperature = (deviceData.Device.DefaultCoolingSetTemperature + deviceData.Device.DefaultHeatingSetTemperature) / 2;
|
|
142
141
|
}
|
|
143
142
|
|
|
144
|
-
deviceData.Device.EffectiveFlags =
|
|
143
|
+
deviceData.Device.EffectiveFlags = flag;
|
|
145
144
|
payload = {
|
|
146
145
|
DeviceID: deviceData.Device.DeviceID,
|
|
147
146
|
EffectiveFlags: deviceData.Device.EffectiveFlags,
|
|
@@ -184,7 +183,7 @@ class MelCloudAta extends EventEmitter {
|
|
|
184
183
|
}
|
|
185
184
|
}
|
|
186
185
|
|
|
187
|
-
switch (
|
|
186
|
+
switch (flag) {
|
|
188
187
|
case 'frostprotection':
|
|
189
188
|
payload = {
|
|
190
189
|
enabled: deviceData.FrostProtection.Enabled,
|
|
@@ -194,6 +193,7 @@ class MelCloudAta extends EventEmitter {
|
|
|
194
193
|
};
|
|
195
194
|
method = 'POST';
|
|
196
195
|
path = ApiUrlsHome.PostProtectionFrost;
|
|
196
|
+
deviceData.Headers.Referer = ApiUrlsHome.Referers.PostProtectionFrost.replace('deviceid', deviceData.DeviceID);
|
|
197
197
|
break;
|
|
198
198
|
case 'overheatprotection':
|
|
199
199
|
payload = {
|
|
@@ -204,6 +204,7 @@ class MelCloudAta extends EventEmitter {
|
|
|
204
204
|
};
|
|
205
205
|
method = 'POST';
|
|
206
206
|
path = ApiUrlsHome.PostProtectionOverheat;
|
|
207
|
+
deviceData.Headers.Referer = ApiUrlsHome.Referers.PostProtectionOverheat.replace('deviceid', deviceData.DeviceID);
|
|
207
208
|
break;
|
|
208
209
|
case 'holidaymode':
|
|
209
210
|
payload = {
|
|
@@ -214,27 +215,40 @@ class MelCloudAta extends EventEmitter {
|
|
|
214
215
|
};
|
|
215
216
|
method = 'POST';
|
|
216
217
|
path = ApiUrlsHome.PostHolidayMode;
|
|
218
|
+
deviceData.Headers.Referer = ApiUrlsHome.Referers.PostHolidayMode.replace('deviceid', deviceData.DeviceID);
|
|
217
219
|
break;
|
|
218
220
|
case 'schedule':
|
|
219
221
|
payload = { enabled: deviceData.ScheduleEnabled };
|
|
220
222
|
method = 'PUT';
|
|
221
|
-
path = ApiUrlsHome.
|
|
223
|
+
path = ApiUrlsHome.PutScheduleEnabled.replace('deviceid', deviceData.DeviceID);
|
|
224
|
+
deviceData.Headers.Referer = ApiUrlsHome.Referers.PutScheduleEnabled.replace('deviceid', deviceData.DeviceID);
|
|
225
|
+
break;
|
|
226
|
+
case 'scene':
|
|
227
|
+
method = 'PUT';
|
|
228
|
+
const state = flagData.Enabled ? 'Enable' : 'Disable';
|
|
229
|
+
path = ApiUrlsHome.PutScene[state].replace('sceneid', flagData.Id);
|
|
230
|
+
deviceData.Headers.Referer = ApiUrlsHome.Referers.GetPutScenes;
|
|
222
231
|
break;
|
|
223
232
|
default:
|
|
224
233
|
payload = {
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
234
|
+
power: deviceData.Device.Power,
|
|
235
|
+
setTemperature: deviceData.Device.SetTemperature,
|
|
236
|
+
setFanSpeed: String(deviceData.Device.SetFanSpeed),
|
|
237
|
+
operationMode: AirConditioner.OperationModeMapEnumToString[deviceData.Device.OperationMode],
|
|
238
|
+
vaneHorizontalDirection: AirConditioner.VaneHorizontalDirectionMapEnumToString[deviceData.Device.VaneHorizontalDirection],
|
|
239
|
+
vaneVerticalDirection: AirConditioner.VaneVerticalDirectionMapEnumToString[deviceData.Device.VaneVerticalDirection],
|
|
240
|
+
temperatureIncrementOverride: null,
|
|
241
|
+
inStandbyMode: null
|
|
231
242
|
};
|
|
232
243
|
method = 'PUT';
|
|
233
|
-
path = ApiUrlsHome.
|
|
244
|
+
path = ApiUrlsHome.PutAta.replace('deviceid', deviceData.DeviceID);
|
|
245
|
+
deviceData.Headers.Referer = ApiUrlsHome.Referers.PutDeviceSettings
|
|
234
246
|
break
|
|
235
247
|
}
|
|
236
248
|
|
|
237
|
-
|
|
249
|
+
deviceData.Headers['Content-Type'] = 'application/json; charset=utf-8';
|
|
250
|
+
deviceData.Headers.Origin = ApiUrlsHome.Origin;
|
|
251
|
+
if (!this.logDebug) this.emit('warn', `Send Data: ${JSON.stringify(payload, null, 2)}, Headers: ${JSON.stringify(deviceData.Headers, null, 2)}`);
|
|
238
252
|
await axios(path, {
|
|
239
253
|
method: method,
|
|
240
254
|
baseURL: ApiUrlsHome.BaseURL,
|
|
@@ -259,7 +273,7 @@ class MelCloudAta extends EventEmitter {
|
|
|
259
273
|
|
|
260
274
|
setTimeout(() => {
|
|
261
275
|
this.lock = false
|
|
262
|
-
},
|
|
276
|
+
}, 2500);
|
|
263
277
|
}
|
|
264
278
|
};
|
|
265
279
|
export default MelCloudAta;
|