homebridge-melcloud-control 4.2.3-beta.5 → 4.2.3-beta.7
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/homebridge-ui/server.js +2 -1
- package/index.js +3 -2
- package/package.json +1 -1
- package/src/melcloud.js +2 -329
- package/src/melcloudata.js +1 -9
- package/src/melcloudatw.js +1 -8
- package/src/melclouderv.js +1 -8
- package/src/melcloudhome.js +364 -0
package/homebridge-ui/server.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { HomebridgePluginUiServer } from '@homebridge/plugin-ui-utils';
|
|
2
2
|
import MelCloud from '../src/melcloud.js';
|
|
3
|
+
import MelCloudHome from '../src/melcloudhome.js';
|
|
3
4
|
|
|
4
5
|
class PluginUiServer extends HomebridgePluginUiServer {
|
|
5
6
|
constructor() {
|
|
@@ -17,7 +18,7 @@ class PluginUiServer extends HomebridgePluginUiServer {
|
|
|
17
18
|
const accountFile = `${this.homebridgeStoragePath}/melcloud/${accountName}_Account`;
|
|
18
19
|
const buildingsFile = `${this.homebridgeStoragePath}/melcloud/${accountName}_Buildings`;
|
|
19
20
|
const devicesFile = `${this.homebridgeStoragePath}/melcloud/${accountName}_Devices`;
|
|
20
|
-
const melCloud = new MelCloud(account, accountFile, buildingsFile, devicesFile);
|
|
21
|
+
const melCloud = account.type === 'melcloud' ? new MelCloud(account, accountFile, buildingsFile, devicesFile) : new MelCloudHome(account, accountFile, buildingsFile, devicesFile);
|
|
21
22
|
|
|
22
23
|
try {
|
|
23
24
|
const accountInfo = await melCloud.connect();
|
package/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { join } from 'path';
|
|
2
2
|
import { mkdirSync, existsSync, writeFileSync } from 'fs';
|
|
3
3
|
import MelCloud from './src/melcloud.js';
|
|
4
|
+
import MelCloudHome from './src/melcloudhome.js';
|
|
4
5
|
import DeviceAta from './src/deviceata.js';
|
|
5
6
|
import DeviceAtw from './src/deviceatw.js';
|
|
6
7
|
import DeviceErv from './src/deviceerv.js';
|
|
@@ -84,8 +85,8 @@ class MelCloudPlatform {
|
|
|
84
85
|
.on('start', async () => {
|
|
85
86
|
try {
|
|
86
87
|
//melcloud account
|
|
87
|
-
const melCloud = new MelCloud(account, accountFile, buildingsFile, devicesFile, true)
|
|
88
|
-
|
|
88
|
+
const melCloud = account.type === 'melcloud' ? new MelCloud(account, accountFile, buildingsFile, devicesFile, true) : new MelCloudHome(account, accountFile, buildingsFile, devicesFile, true);
|
|
89
|
+
melCloud.on('success', (msg) => logLevel.success && log.success(`${accountName}, ${msg}`))
|
|
89
90
|
.on('info', (msg) => logLevel.info && log.info(`${accountName}, ${msg}`))
|
|
90
91
|
.on('debug', (msg) => logLevel.debug && log.info(`${accountName}, debug: ${msg}`))
|
|
91
92
|
.on('warn', (msg) => logLevel.warn && log.warn(`${accountName}, ${msg}`))
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"displayName": "MELCloud Control",
|
|
3
3
|
"name": "homebridge-melcloud-control",
|
|
4
|
-
"version": "4.2.3-beta.
|
|
4
|
+
"version": "4.2.3-beta.7",
|
|
5
5
|
"description": "Homebridge plugin to control Mitsubishi Air Conditioner, Heat Pump and Energy Recovery Ventilation.",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"author": "grzegorz914",
|
package/src/melcloud.js
CHANGED
|
@@ -1,9 +1,7 @@
|
|
|
1
|
-
import fs from 'fs';
|
|
2
1
|
import axios from 'axios';
|
|
3
2
|
import { exec } from 'child_process';
|
|
4
3
|
import { promisify } from 'util';
|
|
5
4
|
import EventEmitter from 'events';
|
|
6
|
-
import puppeteer from 'puppeteer';
|
|
7
5
|
import ImpulseGenerator from './impulsegenerator.js';
|
|
8
6
|
import Functions from './functions.js';
|
|
9
7
|
import { ApiUrls, ApiUrlsHome } from './constants.js';
|
|
@@ -61,7 +59,7 @@ class MelCloud extends EventEmitter {
|
|
|
61
59
|
}
|
|
62
60
|
|
|
63
61
|
// MELCloud
|
|
64
|
-
async
|
|
62
|
+
async checkDevicesList() {
|
|
65
63
|
try {
|
|
66
64
|
const devicesList = { State: false, Info: null, Devices: [] }
|
|
67
65
|
const headers = {
|
|
@@ -137,7 +135,7 @@ class MelCloud extends EventEmitter {
|
|
|
137
135
|
}
|
|
138
136
|
}
|
|
139
137
|
|
|
140
|
-
async
|
|
138
|
+
async connect() {
|
|
141
139
|
if (this.logDebug) this.emit('debug', `Connecting to MELCloud`);
|
|
142
140
|
|
|
143
141
|
try {
|
|
@@ -191,331 +189,6 @@ class MelCloud extends EventEmitter {
|
|
|
191
189
|
}
|
|
192
190
|
}
|
|
193
191
|
|
|
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
192
|
async send(accountInfo) {
|
|
520
193
|
try {
|
|
521
194
|
const axiosInstance = axios.create({
|
package/src/melcloudata.js
CHANGED
|
@@ -52,10 +52,6 @@ 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
|
-
if (!Array.isArray(devicesData)) {
|
|
56
|
-
if (this.logWarn) this.emit('warn', `Device data not found`);
|
|
57
|
-
return null;
|
|
58
|
-
}
|
|
59
55
|
const deviceData = devicesData.find(device => device.DeviceID === this.deviceId);
|
|
60
56
|
|
|
61
57
|
if (this.accountType === 'melcloudhome') {
|
|
@@ -118,10 +114,7 @@ class MelCloudAta extends EventEmitter {
|
|
|
118
114
|
|
|
119
115
|
//check state changes
|
|
120
116
|
const deviceDataHasNotChanged = JSON.stringify(deviceData) === JSON.stringify(this.deviceData);
|
|
121
|
-
if (deviceDataHasNotChanged)
|
|
122
|
-
if (this.logDebug) this.emit('debug', `Device state not changed`);
|
|
123
|
-
return;
|
|
124
|
-
}
|
|
117
|
+
if (deviceDataHasNotChanged) return;
|
|
125
118
|
this.deviceData = deviceData;
|
|
126
119
|
|
|
127
120
|
//emit info
|
|
@@ -268,6 +261,5 @@ class MelCloudAta extends EventEmitter {
|
|
|
268
261
|
this.lock = false
|
|
269
262
|
}, 3000);
|
|
270
263
|
}
|
|
271
|
-
|
|
272
264
|
};
|
|
273
265
|
export default MelCloudAta;
|
package/src/melcloudatw.js
CHANGED
|
@@ -52,10 +52,6 @@ class MelCloudAtw extends EventEmitter {
|
|
|
52
52
|
try {
|
|
53
53
|
//read device info from file
|
|
54
54
|
const devicesData = await this.functions.readData(this.devicesFile, true);
|
|
55
|
-
if (!Array.isArray(devicesData)) {
|
|
56
|
-
if (this.logWarn) this.emit('warn', `Device data not found`);
|
|
57
|
-
return null;
|
|
58
|
-
}
|
|
59
55
|
const deviceData = devicesData.find(device => device.DeviceID === this.deviceId);
|
|
60
56
|
|
|
61
57
|
if (this.accountType === 'melcloudhome') {
|
|
@@ -109,10 +105,7 @@ class MelCloudAtw extends EventEmitter {
|
|
|
109
105
|
|
|
110
106
|
//check state changes
|
|
111
107
|
const deviceDataHasNotChanged = JSON.stringify(devicesData) === JSON.stringify(this.devicesData);
|
|
112
|
-
if (deviceDataHasNotChanged)
|
|
113
|
-
if (this.logDebug) this.emit('debug', `Device state not changed`);
|
|
114
|
-
return;
|
|
115
|
-
}
|
|
108
|
+
if (deviceDataHasNotChanged) return;
|
|
116
109
|
this.devicesData = devicesData;
|
|
117
110
|
|
|
118
111
|
//emit info
|
package/src/melclouderv.js
CHANGED
|
@@ -52,10 +52,6 @@ class MelCloudErv extends EventEmitter {
|
|
|
52
52
|
try {
|
|
53
53
|
//read device info from file
|
|
54
54
|
const devicesData = await this.functions.readData(this.devicesFile, true);
|
|
55
|
-
if (!Array.isArray(devicesData)) {
|
|
56
|
-
if (this.logWarn) this.emit('warn', `Device data not found`);
|
|
57
|
-
return null;
|
|
58
|
-
}
|
|
59
55
|
const deviceData = devicesData.find(device => device.DeviceID === this.deviceId);
|
|
60
56
|
|
|
61
57
|
if (this.accountType === 'melcloudhome') {
|
|
@@ -112,10 +108,7 @@ class MelCloudErv extends EventEmitter {
|
|
|
112
108
|
|
|
113
109
|
//check state changes
|
|
114
110
|
const deviceDataHasNotChanged = JSON.stringify(devicesData) === JSON.stringify(this.devicesData);
|
|
115
|
-
if (deviceDataHasNotChanged)
|
|
116
|
-
if (this.logDebug) this.emit('debug', `Device state not changed`);
|
|
117
|
-
return;
|
|
118
|
-
}
|
|
111
|
+
if (deviceDataHasNotChanged) return;
|
|
119
112
|
this.devicesData = devicesData;
|
|
120
113
|
|
|
121
114
|
//emit info
|
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import axios from 'axios';
|
|
3
|
+
import { exec } from 'child_process';
|
|
4
|
+
import { promisify } from 'util';
|
|
5
|
+
import EventEmitter from 'events';
|
|
6
|
+
import puppeteer from 'puppeteer';
|
|
7
|
+
import ImpulseGenerator from './impulsegenerator.js';
|
|
8
|
+
import Functions from './functions.js';
|
|
9
|
+
import { ApiUrls, ApiUrlsHome } from './constants.js';
|
|
10
|
+
const execPromise = promisify(exec);
|
|
11
|
+
|
|
12
|
+
class MelCloud extends EventEmitter {
|
|
13
|
+
constructor(account, accountFile, buildingsFile, devicesFile, pluginStart = false) {
|
|
14
|
+
super();
|
|
15
|
+
this.accountType = account.type;
|
|
16
|
+
this.user = account.user;
|
|
17
|
+
this.passwd = account.passwd;
|
|
18
|
+
this.language = account.language;
|
|
19
|
+
this.logWarn = account.log?.warn;
|
|
20
|
+
this.logError = account.log?.error;
|
|
21
|
+
this.logDebug = account.log?.debug;
|
|
22
|
+
this.accountFile = accountFile;
|
|
23
|
+
this.buildingsFile = buildingsFile;
|
|
24
|
+
this.devicesFile = devicesFile;
|
|
25
|
+
this.contextKey = null;
|
|
26
|
+
this.functions = new Functions(this.logWarn, this.logError, this.logDebug)
|
|
27
|
+
.on('warn', warn => this.emit('warn', warn))
|
|
28
|
+
.on('error', error => this.emit('error', error))
|
|
29
|
+
.on('debug', debug => this.emit('debug', debug));
|
|
30
|
+
|
|
31
|
+
if (pluginStart) {
|
|
32
|
+
//lock flags
|
|
33
|
+
this.locks = {
|
|
34
|
+
connect: false,
|
|
35
|
+
checkDevicesList: false
|
|
36
|
+
};
|
|
37
|
+
this.impulseGenerator = new ImpulseGenerator()
|
|
38
|
+
.on('connect', () => this.handleWithLock('connect', async () => {
|
|
39
|
+
await this.connect(true);
|
|
40
|
+
}))
|
|
41
|
+
.on('checkDevicesList', () => this.handleWithLock('checkDevicesList', async () => {
|
|
42
|
+
await this.checkDevicesList();
|
|
43
|
+
}))
|
|
44
|
+
.on('state', (state) => {
|
|
45
|
+
this.emit(state ? 'success' : 'warn', `Impulse generator ${state ? 'started' : 'stopped'}`);
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async handleWithLock(lockKey, fn) {
|
|
51
|
+
if (this.locks[lockKey]) return;
|
|
52
|
+
|
|
53
|
+
this.locks[lockKey] = true;
|
|
54
|
+
try {
|
|
55
|
+
await fn();
|
|
56
|
+
} catch (error) {
|
|
57
|
+
this.emit('error', `Inpulse generator error: ${error}`);
|
|
58
|
+
} finally {
|
|
59
|
+
this.locks[lockKey] = false;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// MELCloud Home
|
|
64
|
+
async checkDevicesList() {
|
|
65
|
+
try {
|
|
66
|
+
const devicesList = { State: false, Info: null, Devices: [] }
|
|
67
|
+
const headers = {
|
|
68
|
+
'Accept': '*/*',
|
|
69
|
+
'Accept-Language': 'en-US,en;q=0.9',
|
|
70
|
+
'Cookie': this.contextKey,
|
|
71
|
+
'User-Agent': 'homebridge-melcloud-control/4.0.0',
|
|
72
|
+
'DNT': '1',
|
|
73
|
+
'Origin': 'https://melcloudhome.com',
|
|
74
|
+
'Referer': 'https://melcloudhome.com/dashboard',
|
|
75
|
+
'Sec-Fetch-Dest': 'empty',
|
|
76
|
+
'Sec-Fetch-Mode': 'cors',
|
|
77
|
+
'Sec-Fetch-Site': 'same-origin',
|
|
78
|
+
'X-CSRF': '1'
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
if (this.logDebug) this.emit('debug', `Scanning for devices`);
|
|
82
|
+
const listDevicesData = await axios(ApiUrlsHome.GetUserContext, {
|
|
83
|
+
method: 'GET',
|
|
84
|
+
baseURL: ApiUrlsHome.BaseURL,
|
|
85
|
+
timeout: 25000,
|
|
86
|
+
headers: headers
|
|
87
|
+
});
|
|
88
|
+
const buildingsList = listDevicesData.data.buildings;
|
|
89
|
+
if (this.logDebug) this.emit('debug', `Buildings: ${JSON.stringify(buildingsList, null, 2)}`);
|
|
90
|
+
|
|
91
|
+
if (!buildingsList) {
|
|
92
|
+
devicesList.Info = 'No building found'
|
|
93
|
+
return devicesList;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
await this.functions.saveData(this.buildingsFile, buildingsList);
|
|
97
|
+
if (this.logDebug) this.emit('debug', `Buildings list saved`);
|
|
98
|
+
|
|
99
|
+
const devices = buildingsList.flatMap(building => {
|
|
100
|
+
// Funkcja kapitalizująca klucze obiektu
|
|
101
|
+
const capitalizeKeys = obj =>
|
|
102
|
+
Object.fromEntries(
|
|
103
|
+
Object.entries(obj).map(([key, value]) => [
|
|
104
|
+
key.charAt(0).toUpperCase() + key.slice(1),
|
|
105
|
+
value
|
|
106
|
+
])
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
// Rekurencyjna kapitalizacja kluczy w obiekcie lub tablicy
|
|
110
|
+
const capitalizeKeysDeep = obj => {
|
|
111
|
+
if (Array.isArray(obj)) return obj.map(capitalizeKeysDeep);
|
|
112
|
+
if (obj && typeof obj === 'object') {
|
|
113
|
+
return Object.fromEntries(
|
|
114
|
+
Object.entries(obj).map(([key, value]) => [
|
|
115
|
+
key.charAt(0).toUpperCase() + key.slice(1),
|
|
116
|
+
capitalizeKeysDeep(value)
|
|
117
|
+
])
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
return obj;
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
// Funkcja tworząca finalny obiekt Device
|
|
124
|
+
const createDevice = (device, type) => {
|
|
125
|
+
// Settings już kapitalizowane w nazwach
|
|
126
|
+
const settingsArray = device.Settings || [];
|
|
127
|
+
|
|
128
|
+
const settingsObject = Object.fromEntries(
|
|
129
|
+
settingsArray.map(({ name, value }) => {
|
|
130
|
+
let parsedValue = value;
|
|
131
|
+
if (value === "True") parsedValue = true;
|
|
132
|
+
else if (value === "False") parsedValue = false;
|
|
133
|
+
else if (!isNaN(value) && value !== "") parsedValue = Number(value);
|
|
134
|
+
|
|
135
|
+
const key = name.charAt(0).toUpperCase() + name.slice(1);
|
|
136
|
+
return [key, parsedValue];
|
|
137
|
+
})
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
// Scal Capabilities + Settings + DeviceType w Device
|
|
141
|
+
const deviceObject = {
|
|
142
|
+
...capitalizeKeys(device.Capabilities || {}),
|
|
143
|
+
...settingsObject,
|
|
144
|
+
DeviceType: type,
|
|
145
|
+
IsConnected: device.IsConnected
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
// Kapitalizacja brakujących obiektów/tablic
|
|
149
|
+
if (device.FrostProtection) device.FrostProtection = { ...capitalizeKeys(device.FrostProtection || {}) };
|
|
150
|
+
if (device.OverheatProtection) device.OverheatProtection = { ...capitalizeKeys(device.OverheatProtection || {}) };
|
|
151
|
+
if (device.HolidayMode) device.HolidayMode = { ...capitalizeKeys(device.HolidayMode || {}) };
|
|
152
|
+
if (Array.isArray(device.Schedule)) device.Schedule = device.Schedule.map(capitalizeKeysDeep);
|
|
153
|
+
|
|
154
|
+
// Usuń stare pola Settings i Capabilities
|
|
155
|
+
const { Settings, Capabilities, Id, GivenDisplayName, ...rest } = device;
|
|
156
|
+
|
|
157
|
+
return {
|
|
158
|
+
...rest,
|
|
159
|
+
Type: type,
|
|
160
|
+
DeviceID: Id,
|
|
161
|
+
DeviceName: GivenDisplayName,
|
|
162
|
+
Device: deviceObject,
|
|
163
|
+
Headers: headers
|
|
164
|
+
};
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
return [
|
|
168
|
+
...(building.airToAirUnits || []).map(d => createDevice(capitalizeKeys(d), 0)),
|
|
169
|
+
...(building.airToWaterUnits || []).map(d => createDevice(capitalizeKeys(d), 1)),
|
|
170
|
+
...(building.airToVentilationUnits || []).map(d => createDevice(capitalizeKeys(d), 3))
|
|
171
|
+
];
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
const devicesCount = devices.length;
|
|
175
|
+
if (devicesCount === 0) {
|
|
176
|
+
devicesList.Info = 'No devices found'
|
|
177
|
+
return devicesList;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
await this.functions.saveData(this.devicesFile, devices);
|
|
181
|
+
if (this.logDebug) this.emit('debug', `${devicesCount} devices saved`);
|
|
182
|
+
|
|
183
|
+
devicesList.State = true;
|
|
184
|
+
devicesList.Info = `Found ${devicesCount} devices`;
|
|
185
|
+
devicesList.Devices = devices;
|
|
186
|
+
return devicesList;
|
|
187
|
+
} catch (error) {
|
|
188
|
+
if (error.response?.status === 401) {
|
|
189
|
+
await connectToMelCloudHome();
|
|
190
|
+
if (this.logWarn) this.emit('warn', 'Check devices list not possible, cookies expired, trying to get new.');
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
throw new Error(`Check devices list error: ${error.message}`);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
async connect() {
|
|
199
|
+
if (this.logDebug) this.emit('debug', 'Connecting to MELCloud Home');
|
|
200
|
+
const GLOBAL_TIMEOUT = 90000;
|
|
201
|
+
|
|
202
|
+
let browser;
|
|
203
|
+
try {
|
|
204
|
+
const accountInfo = { State: false, Info: '', ContextKey: null, UseFahrenheit: false };
|
|
205
|
+
let chromiumPath = await this.functions.ensureChromiumInstalled();
|
|
206
|
+
|
|
207
|
+
// === Fallback to Puppeteer's built-in Chromium ===
|
|
208
|
+
if (!chromiumPath) {
|
|
209
|
+
try {
|
|
210
|
+
const puppeteerPath = puppeteer.executablePath();
|
|
211
|
+
if (puppeteerPath && fs.existsSync(puppeteerPath)) {
|
|
212
|
+
chromiumPath = puppeteerPath;
|
|
213
|
+
if (this.logDebug) this.emit('debug', `Using puppeteer Chromium at ${chromiumPath}`);
|
|
214
|
+
}
|
|
215
|
+
} catch { }
|
|
216
|
+
} else {
|
|
217
|
+
if (this.logDebug) this.emit('debug', `Using system Chromium at ${chromiumPath}`);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (!chromiumPath) {
|
|
221
|
+
accountInfo.Info = 'Chromium not found on Your device, please install it manually and try again';
|
|
222
|
+
return accountInfo;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Verify executable works
|
|
226
|
+
try {
|
|
227
|
+
const { stdout } = await execPromise(`"${chromiumPath}" --version`);
|
|
228
|
+
if (this.logDebug) this.emit('debug', `Chromium detected: ${stdout.trim()}`);
|
|
229
|
+
} catch (error) {
|
|
230
|
+
accountInfo.Info = `Chromium found at ${chromiumPath}, but cannot be executed: ${error.message}`;
|
|
231
|
+
return accountInfo;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (this.logDebug) this.emit('debug', `Launching Chromium...`);
|
|
235
|
+
browser = await puppeteer.launch({
|
|
236
|
+
headless: true,
|
|
237
|
+
executablePath: chromiumPath,
|
|
238
|
+
timeout: GLOBAL_TIMEOUT,
|
|
239
|
+
args: [
|
|
240
|
+
'--no-sandbox',
|
|
241
|
+
'--disable-setuid-sandbox',
|
|
242
|
+
'--disable-dev-shm-usage',
|
|
243
|
+
'--single-process',
|
|
244
|
+
'--disable-gpu',
|
|
245
|
+
'--no-zygote'
|
|
246
|
+
]
|
|
247
|
+
});
|
|
248
|
+
browser.on('disconnected', () => this.emit('debug', 'Browser disconnected'));
|
|
249
|
+
|
|
250
|
+
const page = await browser.newPage();
|
|
251
|
+
page.on('error', error => this.emit('error', `Page crashed: ${error.message}`));
|
|
252
|
+
page.on('pageerror', error => this.emit('error', `Browser error: ${error.message}`));
|
|
253
|
+
page.setDefaultTimeout(GLOBAL_TIMEOUT);
|
|
254
|
+
page.setDefaultNavigationTimeout(GLOBAL_TIMEOUT);
|
|
255
|
+
|
|
256
|
+
// Clear cookies before navigation
|
|
257
|
+
try {
|
|
258
|
+
const client = await page.createCDPSession();
|
|
259
|
+
await client.send('Network.clearBrowserCookies');
|
|
260
|
+
} catch (error) {
|
|
261
|
+
if (this.logError) this.emit('error', `Clear cookies error: ${error.message}`);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
try {
|
|
265
|
+
await page.goto(ApiUrlsHome.BaseURL, { waitUntil: ['domcontentloaded', 'networkidle2'], timeout: GLOBAL_TIMEOUT });
|
|
266
|
+
} catch (error) {
|
|
267
|
+
accountInfo.Info = `Navigation to ${ApiUrlsHome.BaseURL} failed: ${error.message}`;
|
|
268
|
+
return accountInfo;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Wait extra to ensure UI is rendered
|
|
272
|
+
await new Promise(r => setTimeout(r, 3000));
|
|
273
|
+
const loginBtn = await page.waitForSelector('button.btn--blue', { timeout: GLOBAL_TIMEOUT / 4 });
|
|
274
|
+
const loginText = await page.evaluate(el => el.textContent.trim(), loginBtn);
|
|
275
|
+
|
|
276
|
+
if (!['Zaloguj', 'Sign In', 'Login'].includes(loginText)) {
|
|
277
|
+
accountInfo.Info = `Login button ${loginText} not found`;
|
|
278
|
+
return accountInfo;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
await loginBtn.click();
|
|
282
|
+
await page.waitForNavigation({ waitUntil: 'networkidle2', timeout: GLOBAL_TIMEOUT / 3 });
|
|
283
|
+
|
|
284
|
+
const usernameInput = await page.$('input[name="username"]');
|
|
285
|
+
const passwordInput = await page.$('input[name="password"]');
|
|
286
|
+
if (!usernameInput || !passwordInput) {
|
|
287
|
+
accountInfo.Info = 'Username or password input not found';
|
|
288
|
+
return accountInfo;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
await page.type('input[name="username"]', this.user, { delay: 50 });
|
|
292
|
+
await page.type('input[name="password"]', this.passwd, { delay: 50 });
|
|
293
|
+
|
|
294
|
+
const submitButton = await page.$('input[type="submit"], button[type="submit"]');
|
|
295
|
+
if (!submitButton) {
|
|
296
|
+
accountInfo.Info = 'Submit button not found';
|
|
297
|
+
return accountInfo;
|
|
298
|
+
}
|
|
299
|
+
await Promise.race([Promise.all([submitButton.click(), page.waitForNavigation({ waitUntil: ['domcontentloaded', 'networkidle2'], timeout: GLOBAL_TIMEOUT / 4 })]), new Promise(r => setTimeout(r, GLOBAL_TIMEOUT / 3))]);
|
|
300
|
+
|
|
301
|
+
// Extract cookies
|
|
302
|
+
let c1 = null, c2 = null;
|
|
303
|
+
const start = Date.now();
|
|
304
|
+
while ((!c1 || !c2) && Date.now() - start < GLOBAL_TIMEOUT / 2) {
|
|
305
|
+
const cookies = await page.browserContext().cookies();
|
|
306
|
+
c1 = cookies.find(c => c.name === '__Secure-monitorandcontrolC1')?.value || c1;
|
|
307
|
+
c2 = cookies.find(c => c.name === '__Secure-monitorandcontrolC2')?.value || c2;
|
|
308
|
+
if (!c1 || !c2) await new Promise(r => setTimeout(r, 500));
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
if (!c1 || !c2) {
|
|
312
|
+
accountInfo.Info = 'Cookies C1/C2 missing';
|
|
313
|
+
return accountInfo;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const contextKey = [
|
|
317
|
+
'__Secure-monitorandcontrol=chunks-2',
|
|
318
|
+
`__Secure-monitorandcontrolC1=${c1}`,
|
|
319
|
+
`__Secure-monitorandcontrolC2=${c2}`
|
|
320
|
+
].join('; ');
|
|
321
|
+
this.contextKey = contextKey;
|
|
322
|
+
|
|
323
|
+
accountInfo.State = true;
|
|
324
|
+
accountInfo.Info = 'Connect to MELCloud Home Success';
|
|
325
|
+
accountInfo.ContextKey = contextKey;
|
|
326
|
+
await this.functions.saveData(this.accountFile, accountInfo);
|
|
327
|
+
|
|
328
|
+
return accountInfo;
|
|
329
|
+
} catch (error) {
|
|
330
|
+
throw new Error(`Connect error: ${error.message}`);
|
|
331
|
+
} finally {
|
|
332
|
+
if (browser) {
|
|
333
|
+
try { await browser.close(); }
|
|
334
|
+
catch (closeErr) {
|
|
335
|
+
if (this.logError) this.emit('error', `Failed to close Puppeteer: ${closeErr.message}`);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
async send(accountInfo) {
|
|
342
|
+
try {
|
|
343
|
+
const axiosInstance = axios.create({
|
|
344
|
+
method: 'POST',
|
|
345
|
+
baseURL: ApiUrls.BaseURL,
|
|
346
|
+
timeout: 15000,
|
|
347
|
+
headers: {
|
|
348
|
+
'X-MitsContextKey': accountInfo.ContextKey,
|
|
349
|
+
'content-type': 'application/json'
|
|
350
|
+
}
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
const payload = { data: accountInfo.LoginData };
|
|
354
|
+
await axiosInstance(ApiUrls.UpdateApplicationOptions, payload);
|
|
355
|
+
await this.functions.saveData(this.accountFile, accountInfo);
|
|
356
|
+
return true;
|
|
357
|
+
} catch (error) {
|
|
358
|
+
throw new Error(`Send data error: ${error.message}`);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
export default MelCloud;
|
|
364
|
+
|