homebridge-melcloud-control 4.0.0-beta.424 → 4.0.0-beta.426
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 +3 -3
- package/src/functions.js +56 -2
- package/src/melcloud.js +26 -68
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.426",
|
|
5
5
|
"description": "Homebridge plugin to control Mitsubishi Air Conditioner, Heat Pump and Energy Recovery Ventilation.",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"author": "grzegorz914",
|
|
@@ -37,9 +37,9 @@
|
|
|
37
37
|
"dependencies": {
|
|
38
38
|
"@homebridge/plugin-ui-utils": "^2.1.0",
|
|
39
39
|
"async-mqtt": "^2.6.3",
|
|
40
|
-
"axios": "^1.
|
|
40
|
+
"axios": "^1.13.0",
|
|
41
41
|
"express": "^5.1.0",
|
|
42
|
-
"puppeteer": "^24.26.1",
|
|
42
|
+
"puppeteer-core": "^24.26.1",
|
|
43
43
|
"axios-cookiejar-support": "^6.0.4",
|
|
44
44
|
"tough-cookie": "^6.0.0",
|
|
45
45
|
"jsdom": "^27.0.1"
|
package/src/functions.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
2
|
import { promises as fsPromises } from 'fs';
|
|
3
|
+
import { promisify } from 'util';
|
|
4
|
+
const execAsync = promisify(exec);
|
|
3
5
|
|
|
4
6
|
class Functions {
|
|
5
7
|
constructor() {
|
|
@@ -18,13 +20,65 @@ class Functions {
|
|
|
18
20
|
async readData(path, parseJson = false) {
|
|
19
21
|
try {
|
|
20
22
|
const data = await fsPromises.readFile(path, 'utf8');
|
|
21
|
-
|
|
23
|
+
|
|
24
|
+
if (parseJson) {
|
|
25
|
+
if (!data.trim()) {
|
|
26
|
+
// Empty file when expecting JSON
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
try {
|
|
30
|
+
return JSON.parse(data);
|
|
31
|
+
} catch (jsonError) {
|
|
32
|
+
throw new Error(`JSON parse error in file "${path}": ${jsonError.message}`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// For non-JSON, just return file content (can be empty string)
|
|
37
|
+
return data;
|
|
22
38
|
} catch (error) {
|
|
23
39
|
if (error.code === 'ENOENT') {
|
|
24
40
|
// File does not exist
|
|
25
41
|
return null;
|
|
26
42
|
}
|
|
27
|
-
|
|
43
|
+
// Preserve original error details
|
|
44
|
+
const wrappedError = new Error(`Read data error for "${path}": ${error.message}`);
|
|
45
|
+
wrappedError.original = error;
|
|
46
|
+
throw wrappedError;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async ensureChromiumInstalled(logInfo, logWarn, logError) {
|
|
51
|
+
try {
|
|
52
|
+
const { stdout: arch } = await execAsync('uname -m');
|
|
53
|
+
const { stdout: osRelease } = await execAsync('cat /etc/os-release');
|
|
54
|
+
logInfo(`Detected architecture: ${arch.trim()}`);
|
|
55
|
+
logInfo(`Detected OS: ${osRelease.split('\n')[0]}`);
|
|
56
|
+
|
|
57
|
+
let chromiumPath = '/usr/bin/chromium-browser';
|
|
58
|
+
const { stdout: chromiumCheck } = await execAsync('which chromium || which chromium-browser || true');
|
|
59
|
+
|
|
60
|
+
if (chromiumCheck.trim()) {
|
|
61
|
+
chromiumPath = chromiumCheck.trim();
|
|
62
|
+
logInfo(`Found system Chromium: ${chromiumPath}`);
|
|
63
|
+
} else {
|
|
64
|
+
logWarn('Chromium not found. Installing...');
|
|
65
|
+
try {
|
|
66
|
+
await execAsync('sudo apt-get update -y && sudo apt-get install -y chromium-browser chromium-codecs-ffmpeg');
|
|
67
|
+
logInfo('Chromium installed successfully.');
|
|
68
|
+
} catch (aptErr) {
|
|
69
|
+
logWarn('apt-get failed, trying apk/yum fallback...');
|
|
70
|
+
try {
|
|
71
|
+
await execAsync('sudo apk add chromium ffmpeg');
|
|
72
|
+
} catch {
|
|
73
|
+
await execAsync('sudo yum install -y chromium chromium-codecs-ffmpeg');
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return chromiumPath;
|
|
79
|
+
} catch (err) {
|
|
80
|
+
logError(`Chromium detection error: ${err.message}`);
|
|
81
|
+
throw err;
|
|
28
82
|
}
|
|
29
83
|
}
|
|
30
84
|
|
package/src/melcloud.js
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
import { execSync } from 'child_process';
|
|
3
|
-
import puppeteer from 'puppeteer';
|
|
4
1
|
import axios from 'axios';
|
|
5
2
|
import EventEmitter from 'events';
|
|
3
|
+
import puppeteer from 'puppeteer-core';
|
|
6
4
|
import MelCloudHomeToken from './melcloudhometoken.js';
|
|
7
5
|
import ImpulseGenerator from './impulsegenerator.js';
|
|
8
6
|
import Functions from './functions.js';
|
|
@@ -21,7 +19,6 @@ class MelCloud extends EventEmitter {
|
|
|
21
19
|
this.accountFile = accountFile;
|
|
22
20
|
this.buildingsFile = buildingsFile;
|
|
23
21
|
this.devicesFile = devicesFile;
|
|
24
|
-
this.requestConfig = requestConfig;
|
|
25
22
|
this.devicesId = [];
|
|
26
23
|
this.contextKey = '';
|
|
27
24
|
this.functions = new Functions();
|
|
@@ -327,11 +324,19 @@ class MelCloud extends EventEmitter {
|
|
|
327
324
|
|
|
328
325
|
async connectToMelCloudHome(refresh = false) {
|
|
329
326
|
if (this.logDebug) this.emit('debug', `Connecting to MELCloud Home`);
|
|
330
|
-
|
|
331
327
|
let browser;
|
|
328
|
+
|
|
332
329
|
try {
|
|
333
|
-
|
|
330
|
+
// --- System Detection & Chromium install ---
|
|
331
|
+
const chromiumPath = await this.functions.ensureChromiumInstalled(
|
|
332
|
+
msg => this.emit('debug', msg),
|
|
333
|
+
msg => this.emit('warn', msg),
|
|
334
|
+
msg => this.emit('error', msg)
|
|
335
|
+
);
|
|
336
|
+
|
|
337
|
+
const launchOptions = {
|
|
334
338
|
headless: true,
|
|
339
|
+
executablePath: chromiumPath,
|
|
335
340
|
args: [
|
|
336
341
|
'--no-sandbox',
|
|
337
342
|
'--disable-setuid-sandbox',
|
|
@@ -339,9 +344,12 @@ class MelCloud extends EventEmitter {
|
|
|
339
344
|
'--single-process',
|
|
340
345
|
'--no-zygote'
|
|
341
346
|
]
|
|
342
|
-
}
|
|
347
|
+
};
|
|
343
348
|
|
|
349
|
+
browser = await puppeteer.launch(launchOptions);
|
|
344
350
|
const page = await browser.newPage();
|
|
351
|
+
|
|
352
|
+
// --- Event handlers ---
|
|
345
353
|
page.on('error', err => this.emit('error', `Page crashed: ${err.message}`));
|
|
346
354
|
page.on('pageerror', err => this.emit('error', `Browser error: ${err.message}`));
|
|
347
355
|
page.on('close', () => this.emit('debug', 'Page was closed unexpectedly'));
|
|
@@ -350,8 +358,10 @@ class MelCloud extends EventEmitter {
|
|
|
350
358
|
page.setDefaultTimeout(30000);
|
|
351
359
|
page.setDefaultNavigationTimeout(30000);
|
|
352
360
|
|
|
361
|
+
// --- Go to login page ---
|
|
353
362
|
await page.goto(ApiUrlsHome.BaseURL, { waitUntil: ['domcontentloaded', 'networkidle2'] });
|
|
354
363
|
|
|
364
|
+
// --- Login flow ---
|
|
355
365
|
const buttons = await page.$$('button.btn--blue');
|
|
356
366
|
let loginBtn = null;
|
|
357
367
|
for (const btn of buttons) {
|
|
@@ -362,12 +372,7 @@ class MelCloud extends EventEmitter {
|
|
|
362
372
|
}
|
|
363
373
|
}
|
|
364
374
|
if (!loginBtn) {
|
|
365
|
-
|
|
366
|
-
return null;
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
if (page.isClosed()) {
|
|
370
|
-
if (this.logWarn) this.emit('warn', 'Page closed before login click');
|
|
375
|
+
this.emit('warn', 'Login button not found');
|
|
371
376
|
return null;
|
|
372
377
|
}
|
|
373
378
|
|
|
@@ -379,15 +384,11 @@ class MelCloud extends EventEmitter {
|
|
|
379
384
|
new Promise(r => setTimeout(r, 12000))
|
|
380
385
|
]);
|
|
381
386
|
|
|
387
|
+
// --- Credentials ---
|
|
382
388
|
const usernameInput = await page.$('input[name="username"]');
|
|
383
389
|
const passwordInput = await page.$('input[name="password"]');
|
|
384
390
|
if (!usernameInput || !passwordInput) {
|
|
385
|
-
|
|
386
|
-
return null;
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
if (page.isClosed()) {
|
|
390
|
-
if (this.logWarn) this.emit('warn', 'Page closed before typing credentials');
|
|
391
|
+
this.emit('warn', 'Username or password input not found');
|
|
391
392
|
return null;
|
|
392
393
|
}
|
|
393
394
|
|
|
@@ -396,12 +397,7 @@ class MelCloud extends EventEmitter {
|
|
|
396
397
|
|
|
397
398
|
const submitButton = await page.$('input[type="submit"], button[type="submit"]');
|
|
398
399
|
if (!submitButton) {
|
|
399
|
-
|
|
400
|
-
return null;
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
if (page.isClosed()) {
|
|
404
|
-
if (this.logWarn) this.emit('warn', 'Page closed before submitting form');
|
|
400
|
+
this.emit('warn', 'Submit button not found on login form');
|
|
405
401
|
return null;
|
|
406
402
|
}
|
|
407
403
|
|
|
@@ -413,18 +409,7 @@ class MelCloud extends EventEmitter {
|
|
|
413
409
|
new Promise(r => setTimeout(r, 15000))
|
|
414
410
|
]);
|
|
415
411
|
|
|
416
|
-
|
|
417
|
-
if (pageText.includes('incorrect') || pageText.includes('Invalid') || pageText.includes('nieprawidłowe')) {
|
|
418
|
-
if (this.logWarn) this.emit('warn', 'Login failed: incorrect email or password');
|
|
419
|
-
return null;
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
const captchaPresent = await page.$('iframe[src*="captcha"], div[class*="captcha"]');
|
|
423
|
-
if (captchaPresent) {
|
|
424
|
-
if (this.logWarn) this.emit('warn', 'Login blocked by CAPTCHA. Manual verification required.');
|
|
425
|
-
return null;
|
|
426
|
-
}
|
|
427
|
-
|
|
412
|
+
// --- Cookie extraction ---
|
|
428
413
|
let c1 = null, c2 = null;
|
|
429
414
|
const start = Date.now();
|
|
430
415
|
while ((!c1 || !c2) && Date.now() - start < 20000) {
|
|
@@ -435,7 +420,7 @@ class MelCloud extends EventEmitter {
|
|
|
435
420
|
}
|
|
436
421
|
|
|
437
422
|
if (!c1 || !c2) {
|
|
438
|
-
|
|
423
|
+
this.emit('warn', 'Cookies C1/C2 missing after login');
|
|
439
424
|
return null;
|
|
440
425
|
}
|
|
441
426
|
|
|
@@ -447,46 +432,19 @@ class MelCloud extends EventEmitter {
|
|
|
447
432
|
|
|
448
433
|
const accountInfo = { ContextKey: contextKey, UseFahrenheit: false };
|
|
449
434
|
this.contextKey = contextKey;
|
|
450
|
-
|
|
451
435
|
await this.functions.saveData(this.accountFile, accountInfo);
|
|
452
|
-
if (!refresh) this.emit('success', `Connect to MELCloud Home Success`);
|
|
453
436
|
|
|
437
|
+
if (!refresh) this.emit('success', `Connect to MELCloud Home Success`);
|
|
454
438
|
return accountInfo;
|
|
455
|
-
} catch (error) {
|
|
456
|
-
// Puppeteer / system error handling
|
|
457
|
-
if (error.message.includes('libnspr4.so') || error.message.includes('Failed to launch the browser process')) {
|
|
458
|
-
const inDocker = await this.functions.isRunningInDocker();
|
|
459
|
-
if (this.logError) this.emit('error', `Missing system libraries detected.`);
|
|
460
|
-
|
|
461
|
-
const installCmd =
|
|
462
|
-
'apt-get update && apt-get install -y ' +
|
|
463
|
-
'libnss3 libnspr4 libatk1.0-0 libatk-bridge2.0-0 libcups2 ' +
|
|
464
|
-
'libxcomposite1 libxrandr2 libxdamage1 libxkbcommon0 libpango-1.0-0 ' +
|
|
465
|
-
'libgbm1 libasound2 libxshmfence1 fonts-liberation libappindicator3-1 libu2f-udev';
|
|
466
|
-
|
|
467
|
-
if (inDocker) {
|
|
468
|
-
if (this.logWarn) this.emit('warn', `Running in Docker — attempting automatic fix...`);
|
|
469
|
-
try {
|
|
470
|
-
const { execSync } = require('child_process');
|
|
471
|
-
execSync(installCmd, { stdio: 'inherit' });
|
|
472
|
-
this.emit('success', `System libraries installed. Retry the connection.`);
|
|
473
|
-
return true;
|
|
474
|
-
} catch (fixError) {
|
|
475
|
-
throw new Error(`Automatic fix failed. Run manually inside container:\n${installCmd}`);
|
|
476
|
-
}
|
|
477
|
-
} else {
|
|
478
|
-
throw new Error(`Missing system libraries. Please install manually:\n${installCmd}`);
|
|
479
|
-
}
|
|
480
|
-
}
|
|
481
439
|
|
|
482
|
-
|
|
440
|
+
} catch (error) {
|
|
483
441
|
throw new Error(`Connect error: ${error.message}`);
|
|
484
442
|
} finally {
|
|
485
443
|
if (browser) {
|
|
486
444
|
try {
|
|
487
445
|
await browser.close();
|
|
488
446
|
} catch (closeErr) {
|
|
489
|
-
|
|
447
|
+
this.emit('error', `Failed to close Puppeteer browser: ${closeErr.message}`);
|
|
490
448
|
}
|
|
491
449
|
}
|
|
492
450
|
}
|