homebridge-melcloud-control 4.0.0-beta.411 → 4.0.0-beta.413
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/package.json +1 -1
- package/src/melcloud.js +116 -60
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"displayName": "MELCloud Control",
|
|
3
3
|
"name": "homebridge-melcloud-control",
|
|
4
|
-
"version": "4.0.0-beta.
|
|
4
|
+
"version": "4.0.0-beta.413",
|
|
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
|
@@ -62,20 +62,28 @@ class MelCloud extends EventEmitter {
|
|
|
62
62
|
// MELCloud
|
|
63
63
|
async checkMelcloudDevicesList(contextKey) {
|
|
64
64
|
try {
|
|
65
|
-
const
|
|
66
|
-
method: 'GET',
|
|
65
|
+
const axiosInstance = axios.create({
|
|
67
66
|
baseURL: ApiUrls.BaseURL,
|
|
68
67
|
timeout: 15000,
|
|
69
68
|
headers: { 'X-MitsContextKey': contextKey }
|
|
70
69
|
});
|
|
71
70
|
|
|
72
|
-
if (this.logDebug) this.emit('debug', `Scanning for devices
|
|
73
|
-
|
|
71
|
+
if (this.logDebug) this.emit('debug', `Scanning for devices...`);
|
|
72
|
+
|
|
73
|
+
const listDevicesData = await axiosInstance.get(ApiUrls.ListDevices);
|
|
74
|
+
|
|
75
|
+
if (!listDevicesData || !listDevicesData.data) {
|
|
76
|
+
this.emit('warn', `Invalid or empty response from MELCloud API`);
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
|
|
74
80
|
const buildingsList = listDevicesData.data;
|
|
75
|
-
if (this.logDebug) this.emit('debug', `Buildings: ${JSON.stringify(buildingsList, null, 2)}`);
|
|
76
81
|
|
|
77
|
-
if (
|
|
78
|
-
|
|
82
|
+
if (this.logDebug)
|
|
83
|
+
this.emit('debug', `Buildings: ${JSON.stringify(buildingsList, null, 2)}`);
|
|
84
|
+
|
|
85
|
+
if (!Array.isArray(buildingsList) || buildingsList.length === 0) {
|
|
86
|
+
this.emit('warn', `No buildings found in MELCloud account`);
|
|
79
87
|
return null;
|
|
80
88
|
}
|
|
81
89
|
|
|
@@ -83,42 +91,59 @@ class MelCloud extends EventEmitter {
|
|
|
83
91
|
if (this.logDebug) this.emit('debug', `Buildings list saved`);
|
|
84
92
|
|
|
85
93
|
const devices = [];
|
|
94
|
+
|
|
86
95
|
for (const building of buildingsList) {
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
+
if (!building.Structure) {
|
|
97
|
+
this.emit(
|
|
98
|
+
'warn',
|
|
99
|
+
`Building missing structure: ${building.BuildingName || 'Unnamed'}`
|
|
100
|
+
);
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const { Structure } = building;
|
|
96
105
|
|
|
97
|
-
|
|
106
|
+
const allDevices = [
|
|
107
|
+
...(Structure.Floors?.flatMap(floor => [
|
|
108
|
+
...(floor.Areas?.flatMap(area => area.Devices || []) || []),
|
|
109
|
+
...(floor.Devices || [])
|
|
110
|
+
]) || []),
|
|
111
|
+
...(Structure.Areas?.flatMap(area => area.Devices || []) || []),
|
|
112
|
+
...(Structure.Devices || [])
|
|
113
|
+
].filter(d => d != null);
|
|
114
|
+
|
|
115
|
+
// Zamiana ID na string
|
|
98
116
|
allDevices.forEach(device => {
|
|
99
|
-
if (device.DeviceID
|
|
100
|
-
device.DeviceID = device.DeviceID.toString();
|
|
101
|
-
}
|
|
117
|
+
if (device.DeviceID != null) device.DeviceID = String(device.DeviceID);
|
|
102
118
|
});
|
|
103
119
|
|
|
120
|
+
if (this.logDebug) {
|
|
121
|
+
const count = allDevices.length;
|
|
122
|
+
this.emit(
|
|
123
|
+
'debug',
|
|
124
|
+
`Found ${count} devices in building: ${building.BuildingName || 'Unnamed'}`
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
104
128
|
devices.push(...allDevices);
|
|
105
129
|
}
|
|
106
130
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
if (this.logWarn) this.emit('warn', `No devices found`);
|
|
131
|
+
if (devices.length === 0) {
|
|
132
|
+
this.emit('warn', `No devices found in any building`);
|
|
110
133
|
return null;
|
|
111
134
|
}
|
|
112
135
|
|
|
113
136
|
await this.functions.saveData(this.devicesFile, devices);
|
|
114
|
-
if (this.logDebug) this.emit('debug', `${
|
|
137
|
+
if (this.logDebug) this.emit('debug', `${devices.length} devices saved`);
|
|
115
138
|
|
|
116
139
|
return devices;
|
|
117
140
|
} catch (error) {
|
|
118
|
-
|
|
141
|
+
const msg = error.response ? `HTTP ${error.response.status}: ${error.response.statusText}` : error.message;
|
|
142
|
+
throw new Error(`Check devices list error: ${msg}`);
|
|
119
143
|
}
|
|
120
144
|
}
|
|
121
145
|
|
|
146
|
+
|
|
122
147
|
async connectToMelCloud() {
|
|
123
148
|
if (this.logDebug) this.emit('debug', `Connecting to MELCloud`);
|
|
124
149
|
|
|
@@ -166,7 +191,7 @@ class MelCloud extends EventEmitter {
|
|
|
166
191
|
|
|
167
192
|
return accountInfo
|
|
168
193
|
} catch (error) {
|
|
169
|
-
throw new Error(`Connect
|
|
194
|
+
throw new Error(`Connect error: ${error.message}`);
|
|
170
195
|
}
|
|
171
196
|
}
|
|
172
197
|
|
|
@@ -270,7 +295,7 @@ class MelCloud extends EventEmitter {
|
|
|
270
295
|
|
|
271
296
|
return devices;
|
|
272
297
|
} catch (error) {
|
|
273
|
-
throw new Error(`
|
|
298
|
+
throw new Error(`Check devices list error: ${error.message}`);
|
|
274
299
|
}
|
|
275
300
|
}
|
|
276
301
|
|
|
@@ -306,25 +331,29 @@ class MelCloud extends EventEmitter {
|
|
|
306
331
|
try {
|
|
307
332
|
browser = await puppeteer.launch({
|
|
308
333
|
headless: true,
|
|
309
|
-
args: [
|
|
334
|
+
args: [
|
|
335
|
+
'--no-sandbox',
|
|
336
|
+
'--disable-setuid-sandbox',
|
|
337
|
+
'--disable-dev-shm-usage',
|
|
338
|
+
'--single-process',
|
|
339
|
+
'--no-zygote'
|
|
340
|
+
]
|
|
310
341
|
});
|
|
311
342
|
|
|
312
343
|
const page = await browser.newPage();
|
|
313
344
|
|
|
314
|
-
// Emit page errors to logs
|
|
315
345
|
page.on('error', err => this.emit('warn', `Page crashed: ${err.message}`));
|
|
316
346
|
page.on('pageerror', err => this.emit('warn', `Browser error: ${err.message}`));
|
|
347
|
+
page.on('close', () => this.emit('warn', 'Page was closed unexpectedly'));
|
|
348
|
+
browser.on('disconnected', () => this.emit('warn', 'Browser disconnected unexpectedly'));
|
|
317
349
|
|
|
318
|
-
|
|
350
|
+
page.setDefaultTimeout(30000);
|
|
351
|
+
page.setDefaultNavigationTimeout(30000);
|
|
319
352
|
|
|
320
|
-
|
|
321
|
-
await page.waitForSelector('button.btn--blue', { timeout: 10000 }).catch(() => {
|
|
322
|
-
throw new Error('Login button not found on MELCloud Home');
|
|
323
|
-
});
|
|
353
|
+
await page.goto(ApiUrlsHome.BaseURL, { waitUntil: ['domcontentloaded', 'networkidle2'] });
|
|
324
354
|
|
|
325
355
|
const buttons = await page.$$('button.btn--blue');
|
|
326
356
|
let loginBtn = null;
|
|
327
|
-
|
|
328
357
|
for (const btn of buttons) {
|
|
329
358
|
const text = await page.evaluate(el => el.textContent.trim(), btn);
|
|
330
359
|
if (['Zaloguj', 'Sign In', 'Login'].includes(text)) {
|
|
@@ -332,44 +361,70 @@ class MelCloud extends EventEmitter {
|
|
|
332
361
|
break;
|
|
333
362
|
}
|
|
334
363
|
}
|
|
364
|
+
if (!loginBtn) {
|
|
365
|
+
this.emit('warn', 'Login button not found');
|
|
366
|
+
return null;
|
|
367
|
+
}
|
|
335
368
|
|
|
336
|
-
if (
|
|
337
|
-
|
|
338
|
-
|
|
369
|
+
if (page.isClosed()) {
|
|
370
|
+
this.emit('warn', 'Page closed before login click');
|
|
371
|
+
return null;
|
|
372
|
+
}
|
|
339
373
|
|
|
340
|
-
await Promise.
|
|
341
|
-
|
|
342
|
-
|
|
374
|
+
await Promise.race([
|
|
375
|
+
Promise.all([
|
|
376
|
+
loginBtn.click(),
|
|
377
|
+
page.waitForNavigation({ waitUntil: ['domcontentloaded', 'networkidle2'], timeout: 15000 })
|
|
378
|
+
]),
|
|
379
|
+
new Promise(r => setTimeout(r, 12000))
|
|
343
380
|
]);
|
|
344
381
|
|
|
345
|
-
await page
|
|
382
|
+
const usernameInput = await page.$('input[name="username"]');
|
|
383
|
+
const passwordInput = await page.$('input[name="password"]');
|
|
384
|
+
if (!usernameInput || !passwordInput) {
|
|
385
|
+
this.emit('warn', 'Username or password input not found');
|
|
386
|
+
return null;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
if (page.isClosed()) {
|
|
390
|
+
this.emit('warn', 'Page closed before typing credentials');
|
|
391
|
+
return null;
|
|
392
|
+
}
|
|
393
|
+
|
|
346
394
|
await page.type('input[name="username"]', this.user, { delay: 50 });
|
|
347
395
|
await page.type('input[name="password"]', this.passwd, { delay: 50 });
|
|
348
396
|
|
|
349
|
-
// Handle possible "Show password" overlays
|
|
350
397
|
const submitButton = await page.$('input[type="submit"], button[type="submit"]');
|
|
351
|
-
if (!submitButton)
|
|
398
|
+
if (!submitButton) {
|
|
399
|
+
this.emit('warn', 'Submit button not found on login form');
|
|
400
|
+
return null;
|
|
401
|
+
}
|
|
352
402
|
|
|
353
|
-
if (
|
|
403
|
+
if (page.isClosed()) {
|
|
404
|
+
this.emit('warn', 'Page closed before submitting form');
|
|
405
|
+
return null;
|
|
406
|
+
}
|
|
354
407
|
|
|
355
|
-
await Promise.
|
|
356
|
-
|
|
357
|
-
|
|
408
|
+
await Promise.race([
|
|
409
|
+
Promise.all([
|
|
410
|
+
submitButton.click(),
|
|
411
|
+
page.waitForNavigation({ waitUntil: ['domcontentloaded', 'networkidle2'], timeout: 20000 })
|
|
412
|
+
]),
|
|
413
|
+
new Promise(r => setTimeout(r, 15000))
|
|
358
414
|
]);
|
|
359
415
|
|
|
360
|
-
// Check for login errors
|
|
361
416
|
const pageText = await page.content();
|
|
362
417
|
if (pageText.includes('incorrect') || pageText.includes('Invalid') || pageText.includes('nieprawidłowe')) {
|
|
363
|
-
|
|
418
|
+
this.emit('warn', 'Login failed: incorrect email or password');
|
|
419
|
+
return null;
|
|
364
420
|
}
|
|
365
421
|
|
|
366
|
-
// Check for CAPTCHA
|
|
367
422
|
const captchaPresent = await page.$('iframe[src*="captcha"], div[class*="captcha"]');
|
|
368
423
|
if (captchaPresent) {
|
|
369
|
-
|
|
424
|
+
this.emit('warn', 'Login blocked by CAPTCHA. Manual verification required.');
|
|
425
|
+
return null;
|
|
370
426
|
}
|
|
371
427
|
|
|
372
|
-
// Extract cookies with retry mechanism
|
|
373
428
|
let c1 = null, c2 = null;
|
|
374
429
|
const start = Date.now();
|
|
375
430
|
while ((!c1 || !c2) && Date.now() - start < 20000) {
|
|
@@ -379,7 +434,10 @@ class MelCloud extends EventEmitter {
|
|
|
379
434
|
if (!c1 || !c2) await new Promise(r => setTimeout(r, 500));
|
|
380
435
|
}
|
|
381
436
|
|
|
382
|
-
if (!c1 || !c2)
|
|
437
|
+
if (!c1 || !c2) {
|
|
438
|
+
this.emit('warn', 'Cookies C1/C2 missing after login');
|
|
439
|
+
return null;
|
|
440
|
+
}
|
|
383
441
|
|
|
384
442
|
const contextKey = [
|
|
385
443
|
'__Secure-monitorandcontrol=chunks-2',
|
|
@@ -394,9 +452,8 @@ class MelCloud extends EventEmitter {
|
|
|
394
452
|
if (!refresh) this.emit('success', `Connect to MELCloud Home Success`);
|
|
395
453
|
|
|
396
454
|
return accountInfo;
|
|
397
|
-
|
|
398
455
|
} catch (error) {
|
|
399
|
-
//
|
|
456
|
+
// Puppeteer / system error handling
|
|
400
457
|
if (error.message.includes('libnspr4.so') || error.message.includes('Failed to launch the browser process')) {
|
|
401
458
|
const inDocker = await this.functions.isRunningInDocker();
|
|
402
459
|
this.emit('warn', `Missing system libraries detected.`);
|
|
@@ -410,6 +467,7 @@ class MelCloud extends EventEmitter {
|
|
|
410
467
|
if (inDocker) {
|
|
411
468
|
this.emit('warn', `Running in Docker — attempting automatic fix...`);
|
|
412
469
|
try {
|
|
470
|
+
const { execSync } = require('child_process');
|
|
413
471
|
execSync(installCmd, { stdio: 'inherit' });
|
|
414
472
|
this.emit('success', `System libraries installed. Retry the connection.`);
|
|
415
473
|
return true;
|
|
@@ -421,9 +479,8 @@ class MelCloud extends EventEmitter {
|
|
|
421
479
|
}
|
|
422
480
|
}
|
|
423
481
|
|
|
424
|
-
//
|
|
425
|
-
throw new Error(`
|
|
426
|
-
|
|
482
|
+
// Only throw for real technical errors
|
|
483
|
+
throw new Error(`Connect error: ${error.message}`);
|
|
427
484
|
} finally {
|
|
428
485
|
if (browser) {
|
|
429
486
|
try {
|
|
@@ -435,7 +492,6 @@ class MelCloud extends EventEmitter {
|
|
|
435
492
|
}
|
|
436
493
|
}
|
|
437
494
|
|
|
438
|
-
|
|
439
495
|
async checkDevicesList(contextKey) {
|
|
440
496
|
let devices = [];
|
|
441
497
|
switch (this.accountType) {
|