homebridge-melcloud-control 4.0.0-beta.425 → 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.425",
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';
@@ -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
- 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 = {
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
- if (this.logWarn) this.emit('warn', 'Login button not found');
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
- if (this.logWarn) this.emit('warn', 'Username or password input not found');
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
- if (this.logWarn) this.emit('warn', 'Submit button not found on login form');
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
- const pageText = await page.content();
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
- if (this.logWarn) this.emit('warn', 'Cookies C1/C2 missing after login');
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
- // Only throw for real technical errors
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
- if (this.logError) this.emit('error', `Failed to close Puppeteer browser: ${closeErr.message}`);
447
+ this.emit('error', `Failed to close Puppeteer browser: ${closeErr.message}`);
489
448
  }
490
449
  }
491
450
  }