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 CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "displayName": "MELCloud Control",
3
3
  "name": "homebridge-melcloud-control",
4
- "version": "4.0.0-beta.424",
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.12.2",
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
- return parseJson ? JSON.parse(data) : data;
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
- throw new Error(`Read data error: ${error}`);
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
- browser = await puppeteer.launch({
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
- if (this.logWarn) this.emit('warn', 'Login button not found');
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
- 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
+ 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
- if (this.logWarn) this.emit('warn', 'Submit button not found on login form');
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
- const pageText = await page.content();
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
- if (this.logWarn) this.emit('warn', 'Cookies C1/C2 missing after login');
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
- // Only throw for real technical errors
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
- if (this.logError) this.emit('error', `Failed to close Puppeteer browser: ${closeErr.message}`);
447
+ this.emit('error', `Failed to close Puppeteer browser: ${closeErr.message}`);
490
448
  }
491
449
  }
492
450
  }