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