homebridge-melcloud-control 4.0.0-beta.412 → 4.0.0-beta.414
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 +71 -37
- package/src/melcloudhometoken.js +15 -15
- package/src/restful.js +1 -1
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.414",
|
|
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
|
@@ -73,7 +73,7 @@ class MelCloud extends EventEmitter {
|
|
|
73
73
|
const listDevicesData = await axiosInstance.get(ApiUrls.ListDevices);
|
|
74
74
|
|
|
75
75
|
if (!listDevicesData || !listDevicesData.data) {
|
|
76
|
-
this.emit('warn', `Invalid or empty response from MELCloud API`);
|
|
76
|
+
if (this.logWarn) this.emit('warn', `Invalid or empty response from MELCloud API`);
|
|
77
77
|
return null;
|
|
78
78
|
}
|
|
79
79
|
|
|
@@ -83,7 +83,7 @@ class MelCloud extends EventEmitter {
|
|
|
83
83
|
this.emit('debug', `Buildings: ${JSON.stringify(buildingsList, null, 2)}`);
|
|
84
84
|
|
|
85
85
|
if (!Array.isArray(buildingsList) || buildingsList.length === 0) {
|
|
86
|
-
this.emit('warn', `No buildings found in MELCloud account`);
|
|
86
|
+
if (this.logWarn) this.emit('warn', `No buildings found in MELCloud account`);
|
|
87
87
|
return null;
|
|
88
88
|
}
|
|
89
89
|
|
|
@@ -129,7 +129,7 @@ class MelCloud extends EventEmitter {
|
|
|
129
129
|
}
|
|
130
130
|
|
|
131
131
|
if (devices.length === 0) {
|
|
132
|
-
this.emit('warn', `No devices found in any building`);
|
|
132
|
+
if (this.logWarn) this.emit('warn', `No devices found in any building`);
|
|
133
133
|
return null;
|
|
134
134
|
}
|
|
135
135
|
|
|
@@ -331,25 +331,29 @@ class MelCloud extends EventEmitter {
|
|
|
331
331
|
try {
|
|
332
332
|
browser = await puppeteer.launch({
|
|
333
333
|
headless: true,
|
|
334
|
-
args: [
|
|
334
|
+
args: [
|
|
335
|
+
'--no-sandbox',
|
|
336
|
+
'--disable-setuid-sandbox',
|
|
337
|
+
'--disable-dev-shm-usage',
|
|
338
|
+
'--single-process',
|
|
339
|
+
'--no-zygote'
|
|
340
|
+
]
|
|
335
341
|
});
|
|
336
342
|
|
|
337
343
|
const page = await browser.newPage();
|
|
338
344
|
|
|
339
|
-
|
|
340
|
-
page.on('
|
|
341
|
-
page.on('
|
|
345
|
+
page.on('error', err => this.emit('error', `Page crashed: ${err.message}`));
|
|
346
|
+
page.on('pageerror', err => this.emit('error', `Browser error: ${err.message}`));
|
|
347
|
+
page.on('close', () => this.emit('debug', 'Page was closed unexpectedly'));
|
|
348
|
+
browser.on('disconnected', () => this.emit('debug', 'Browser disconnected unexpectedly'));
|
|
342
349
|
|
|
343
|
-
|
|
350
|
+
page.setDefaultTimeout(30000);
|
|
351
|
+
page.setDefaultNavigationTimeout(30000);
|
|
344
352
|
|
|
345
|
-
|
|
346
|
-
await page.waitForSelector('button.btn--blue', { timeout: 10000 }).catch(() => {
|
|
347
|
-
throw new Error('Login button not found on MELCloud Home');
|
|
348
|
-
});
|
|
353
|
+
await page.goto(ApiUrlsHome.BaseURL, { waitUntil: ['domcontentloaded', 'networkidle2'] });
|
|
349
354
|
|
|
350
355
|
const buttons = await page.$$('button.btn--blue');
|
|
351
356
|
let loginBtn = null;
|
|
352
|
-
|
|
353
357
|
for (const btn of buttons) {
|
|
354
358
|
const text = await page.evaluate(el => el.textContent.trim(), btn);
|
|
355
359
|
if (['Zaloguj', 'Sign In', 'Login'].includes(text)) {
|
|
@@ -357,44 +361,70 @@ class MelCloud extends EventEmitter {
|
|
|
357
361
|
break;
|
|
358
362
|
}
|
|
359
363
|
}
|
|
364
|
+
if (!loginBtn) {
|
|
365
|
+
if (this.logWarn) this.emit('warn', 'Login button not found');
|
|
366
|
+
return null;
|
|
367
|
+
}
|
|
360
368
|
|
|
361
|
-
if (
|
|
362
|
-
|
|
363
|
-
|
|
369
|
+
if (page.isClosed()) {
|
|
370
|
+
if (this.logWarn) this.emit('warn', 'Page closed before login click');
|
|
371
|
+
return null;
|
|
372
|
+
}
|
|
364
373
|
|
|
365
|
-
await Promise.
|
|
366
|
-
|
|
367
|
-
|
|
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))
|
|
368
380
|
]);
|
|
369
381
|
|
|
370
|
-
await page
|
|
382
|
+
const usernameInput = await page.$('input[name="username"]');
|
|
383
|
+
const passwordInput = await page.$('input[name="password"]');
|
|
384
|
+
if (!usernameInput || !passwordInput) {
|
|
385
|
+
if (this.logWarn) this.emit('warn', 'Username or password input not found');
|
|
386
|
+
return null;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
if (page.isClosed()) {
|
|
390
|
+
if (this.logWarn) this.emit('warn', 'Page closed before typing credentials');
|
|
391
|
+
return null;
|
|
392
|
+
}
|
|
393
|
+
|
|
371
394
|
await page.type('input[name="username"]', this.user, { delay: 50 });
|
|
372
395
|
await page.type('input[name="password"]', this.passwd, { delay: 50 });
|
|
373
396
|
|
|
374
|
-
// Handle possible "Show password" overlays
|
|
375
397
|
const submitButton = await page.$('input[type="submit"], button[type="submit"]');
|
|
376
|
-
if (!submitButton)
|
|
398
|
+
if (!submitButton) {
|
|
399
|
+
if (this.logWarn) this.emit('warn', 'Submit button not found on login form');
|
|
400
|
+
return null;
|
|
401
|
+
}
|
|
377
402
|
|
|
378
|
-
if (
|
|
403
|
+
if (page.isClosed()) {
|
|
404
|
+
if (this.logWarn) this.emit('warn', 'Page closed before submitting form');
|
|
405
|
+
return null;
|
|
406
|
+
}
|
|
379
407
|
|
|
380
|
-
await Promise.
|
|
381
|
-
|
|
382
|
-
|
|
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))
|
|
383
414
|
]);
|
|
384
415
|
|
|
385
|
-
// Check for login errors
|
|
386
416
|
const pageText = await page.content();
|
|
387
417
|
if (pageText.includes('incorrect') || pageText.includes('Invalid') || pageText.includes('nieprawidłowe')) {
|
|
388
|
-
|
|
418
|
+
if (this.logWarn) this.emit('warn', 'Login failed: incorrect email or password');
|
|
419
|
+
return null;
|
|
389
420
|
}
|
|
390
421
|
|
|
391
|
-
// Check for CAPTCHA
|
|
392
422
|
const captchaPresent = await page.$('iframe[src*="captcha"], div[class*="captcha"]');
|
|
393
423
|
if (captchaPresent) {
|
|
394
|
-
|
|
424
|
+
if (this.logWarn) this.emit('warn', 'Login blocked by CAPTCHA. Manual verification required.');
|
|
425
|
+
return null;
|
|
395
426
|
}
|
|
396
427
|
|
|
397
|
-
// Extract cookies with retry mechanism
|
|
398
428
|
let c1 = null, c2 = null;
|
|
399
429
|
const start = Date.now();
|
|
400
430
|
while ((!c1 || !c2) && Date.now() - start < 20000) {
|
|
@@ -404,7 +434,10 @@ class MelCloud extends EventEmitter {
|
|
|
404
434
|
if (!c1 || !c2) await new Promise(r => setTimeout(r, 500));
|
|
405
435
|
}
|
|
406
436
|
|
|
407
|
-
if (!c1 || !c2)
|
|
437
|
+
if (!c1 || !c2) {
|
|
438
|
+
if (this.logWarn) this.emit('warn', 'Cookies C1/C2 missing after login');
|
|
439
|
+
return null;
|
|
440
|
+
}
|
|
408
441
|
|
|
409
442
|
const contextKey = [
|
|
410
443
|
'__Secure-monitorandcontrol=chunks-2',
|
|
@@ -420,10 +453,10 @@ class MelCloud extends EventEmitter {
|
|
|
420
453
|
|
|
421
454
|
return accountInfo;
|
|
422
455
|
} catch (error) {
|
|
423
|
-
//
|
|
456
|
+
// Puppeteer / system error handling
|
|
424
457
|
if (error.message.includes('libnspr4.so') || error.message.includes('Failed to launch the browser process')) {
|
|
425
458
|
const inDocker = await this.functions.isRunningInDocker();
|
|
426
|
-
this.emit('
|
|
459
|
+
if (this.logError) this.emit('error', `Missing system libraries detected.`);
|
|
427
460
|
|
|
428
461
|
const installCmd =
|
|
429
462
|
'apt-get update && apt-get install -y ' +
|
|
@@ -432,8 +465,9 @@ class MelCloud extends EventEmitter {
|
|
|
432
465
|
'libgbm1 libasound2 libxshmfence1 fonts-liberation libappindicator3-1 libu2f-udev';
|
|
433
466
|
|
|
434
467
|
if (inDocker) {
|
|
435
|
-
this.emit('warn', `Running in Docker — attempting automatic fix...`);
|
|
468
|
+
if (this.logWarn) this.emit('warn', `Running in Docker — attempting automatic fix...`);
|
|
436
469
|
try {
|
|
470
|
+
const { execSync } = require('child_process');
|
|
437
471
|
execSync(installCmd, { stdio: 'inherit' });
|
|
438
472
|
this.emit('success', `System libraries installed. Retry the connection.`);
|
|
439
473
|
return true;
|
|
@@ -445,14 +479,14 @@ class MelCloud extends EventEmitter {
|
|
|
445
479
|
}
|
|
446
480
|
}
|
|
447
481
|
|
|
448
|
-
//
|
|
482
|
+
// Only throw for real technical errors
|
|
449
483
|
throw new Error(`Connect error: ${error.message}`);
|
|
450
484
|
} finally {
|
|
451
485
|
if (browser) {
|
|
452
486
|
try {
|
|
453
487
|
await browser.close();
|
|
454
488
|
} catch (closeErr) {
|
|
455
|
-
this.emit('
|
|
489
|
+
if (this.logError) this.emit('error', `Failed to close Puppeteer browser: ${closeErr.message}`);
|
|
456
490
|
}
|
|
457
491
|
}
|
|
458
492
|
}
|
package/src/melcloudhometoken.js
CHANGED
|
@@ -59,7 +59,7 @@ class MelCloudHomeToken extends EventEmitter {
|
|
|
59
59
|
const csrf = dom.window.document.querySelector('input[name="_csrf"]')?.value;
|
|
60
60
|
|
|
61
61
|
if (!csrf) {
|
|
62
|
-
this.emit('warn', 'CSRF token not found');
|
|
62
|
+
if (this.logWarn) this.emit('warn', 'CSRF token not found');
|
|
63
63
|
return null;
|
|
64
64
|
}
|
|
65
65
|
|
|
@@ -85,7 +85,7 @@ class MelCloudHomeToken extends EventEmitter {
|
|
|
85
85
|
});
|
|
86
86
|
|
|
87
87
|
if (response.status === 400) {
|
|
88
|
-
this.emit('warn', `Login failed: ${response.data}`);
|
|
88
|
+
if (this.logWarn) this.emit('warn', `Login failed: ${response.data}`);
|
|
89
89
|
return null;
|
|
90
90
|
}
|
|
91
91
|
|
|
@@ -106,7 +106,7 @@ class MelCloudHomeToken extends EventEmitter {
|
|
|
106
106
|
return code || null;
|
|
107
107
|
|
|
108
108
|
} catch (err) {
|
|
109
|
-
this.emit('warn', `loginToMelCloudHome error: ${err}`);
|
|
109
|
+
if (this.logWarn) this.emit('warn', `loginToMelCloudHome error: ${err}`);
|
|
110
110
|
return null;
|
|
111
111
|
}
|
|
112
112
|
}
|
|
@@ -120,7 +120,7 @@ class MelCloudHomeToken extends EventEmitter {
|
|
|
120
120
|
if (locationHeader && locationHeader.startsWith('melcloudhome://')) {
|
|
121
121
|
const match = locationHeader.match(/[?&]code=([^&]+)/);
|
|
122
122
|
if (match) {
|
|
123
|
-
this.emit('warn', `Found code in Location header: ${match[1]}`);
|
|
123
|
+
if (this.logWarn) this.emit('warn', `Found code in Location header: ${match[1]}`);
|
|
124
124
|
resolve(match[1]);
|
|
125
125
|
return;
|
|
126
126
|
}
|
|
@@ -132,14 +132,14 @@ class MelCloudHomeToken extends EventEmitter {
|
|
|
132
132
|
const formActionMatch = data.match(/action="([^"]+)"/);
|
|
133
133
|
|
|
134
134
|
if (formCodeMatch && formStateMatch && formActionMatch) {
|
|
135
|
-
this.emit('warn', 'Found form_post, submitting...');
|
|
135
|
+
if (this.logWarn) this.emit('warn', 'Found form_post, submitting...');
|
|
136
136
|
try {
|
|
137
137
|
const code = await this.submitFormPost(formActionMatch[1], formCodeMatch[1], formStateMatch[1]);
|
|
138
|
-
this.emit('warn', `submitFormPost returned code: ${code}`);
|
|
138
|
+
if (this.logWarn) this.emit('warn', `submitFormPost returned code: ${code}`);
|
|
139
139
|
resolve(code);
|
|
140
140
|
return;
|
|
141
141
|
} catch (err) {
|
|
142
|
-
this.emit('warn', `submitFormPost failed: ${err}`);
|
|
142
|
+
if (this.logWarn) this.emit('warn', `submitFormPost failed: ${err}`);
|
|
143
143
|
reject(err);
|
|
144
144
|
return;
|
|
145
145
|
}
|
|
@@ -148,29 +148,29 @@ class MelCloudHomeToken extends EventEmitter {
|
|
|
148
148
|
// 3️⃣ JS redirect in body
|
|
149
149
|
const bodyCodeMatch = data.match(/melcloudhome:\/\/[^"'\s]*[?&]code=([^&"'\s]+)/);
|
|
150
150
|
if (bodyCodeMatch) {
|
|
151
|
-
this.emit('warn', `Found code in body: ${bodyCodeMatch[1]}`);
|
|
151
|
+
if (this.logWarn) this.emit('warn', `Found code in body: ${bodyCodeMatch[1]}`);
|
|
152
152
|
resolve(bodyCodeMatch[1]);
|
|
153
153
|
return;
|
|
154
154
|
}
|
|
155
155
|
|
|
156
156
|
// 4️⃣ Follow redirect
|
|
157
157
|
if (locationHeader && ['301', '302', '303'].includes(headers['status'] || '')) {
|
|
158
|
-
this.emit('warn', `Following redirect to ${locationHeader}`);
|
|
158
|
+
if (this.logWarn) this.emit('warn', `Following redirect to ${locationHeader}`);
|
|
159
159
|
try {
|
|
160
160
|
const code = await followRedirect(locationHeader);
|
|
161
161
|
resolve(code);
|
|
162
162
|
return;
|
|
163
163
|
} catch (err) {
|
|
164
|
-
this.emit('warn', `Follow redirect failed: ${err}`);
|
|
164
|
+
if (this.logWarn) this.emit('warn', `Follow redirect failed: ${err}`);
|
|
165
165
|
reject(err);
|
|
166
166
|
return;
|
|
167
167
|
}
|
|
168
168
|
}
|
|
169
169
|
|
|
170
|
-
this.emit('warn', 'Authorization code not found in response');
|
|
170
|
+
if (this.logWarn) this.emit('warn', 'Authorization code not found in response');
|
|
171
171
|
reject(new Error('Authorization code not found'));
|
|
172
172
|
} catch (err) {
|
|
173
|
-
this.emit('warn', `extractCodeFromResponse error: ${err}`);
|
|
173
|
+
if (this.logWarn) this.emit('warn', `extractCodeFromResponse error: ${err}`);
|
|
174
174
|
reject(err);
|
|
175
175
|
}
|
|
176
176
|
});
|
|
@@ -178,7 +178,7 @@ class MelCloudHomeToken extends EventEmitter {
|
|
|
178
178
|
|
|
179
179
|
async submitFormPost(actionUrl, code, state) {
|
|
180
180
|
const formData = new URLSearchParams({ code, state });
|
|
181
|
-
this.emit('warn', `Submitting form_post to ${actionUrl}`);
|
|
181
|
+
if (this.logWarn) this.emit('warn', `Submitting form_post to ${actionUrl}`);
|
|
182
182
|
const res = await this.client.post(actionUrl, formData.toString(), {
|
|
183
183
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
184
184
|
maxRedirects: 0,
|
|
@@ -191,7 +191,7 @@ class MelCloudHomeToken extends EventEmitter {
|
|
|
191
191
|
if (match) return match[1];
|
|
192
192
|
}
|
|
193
193
|
|
|
194
|
-
this.emit('warn', 'Code not found after form_post submission');
|
|
194
|
+
if (this.logWarn) this.emit('warn', 'Code not found after form_post submission');
|
|
195
195
|
throw new Error('Code not found after form_post submission');
|
|
196
196
|
}
|
|
197
197
|
|
|
@@ -213,7 +213,7 @@ class MelCloudHomeToken extends EventEmitter {
|
|
|
213
213
|
});
|
|
214
214
|
|
|
215
215
|
const tokens = tokenResponse.data;
|
|
216
|
-
this.emit('warn', `Token obtained: ${JSON.stringify(tokens)}`);
|
|
216
|
+
if (this.logWarn) this.emit('warn', `Token obtained: ${JSON.stringify(tokens)}`);
|
|
217
217
|
return tokens;
|
|
218
218
|
|
|
219
219
|
} catch (err) {
|
package/src/restful.js
CHANGED
|
@@ -65,7 +65,7 @@ class RestFul extends EventEmitter {
|
|
|
65
65
|
|
|
66
66
|
// Start the server
|
|
67
67
|
app.listen(this.port, () => {
|
|
68
|
-
this.emit('connected', `RESTful started on port: ${this.
|
|
68
|
+
this.emit('connected', `RESTful started on port: ${this.port}`);
|
|
69
69
|
});
|
|
70
70
|
} catch (error) {
|
|
71
71
|
if (this.logWarn) this.emit('warn', `RESTful Connect error: ${error}`);
|