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 CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "displayName": "MELCloud Control",
3
3
  "name": "homebridge-melcloud-control",
4
- "version": "4.0.0-beta.412",
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: ['--no-sandbox', '--disable-setuid-sandbox']
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
- // Emit page errors to logs
340
- page.on('error', err => this.emit('warn', `Page crashed: ${err.message}`));
341
- page.on('pageerror', err => this.emit('warn', `Browser error: ${err.message}`));
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
- await page.goto(ApiUrlsHome.BaseURL, { waitUntil: ['domcontentloaded', 'networkidle2'] });
350
+ page.setDefaultTimeout(30000);
351
+ page.setDefaultNavigationTimeout(30000);
344
352
 
345
- // Ensure login button is visible
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 (!loginBtn) throw new Error('Login button not found (no matching text)');
362
-
363
- if (this.logDebug) this.emit('debug', `Clicking login button...`);
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.all([
366
- loginBtn.click(),
367
- page.waitForNavigation({ waitUntil: ['domcontentloaded', 'networkidle2'], timeout: 15000 })
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.waitForSelector('input[name="username"]', { timeout: 10000 });
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) throw new Error('Submit button not found on login form');
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 (this.logDebug) this.emit('debug', `Submitting credentials...`);
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.all([
381
- submitButton.click(),
382
- page.waitForNavigation({ waitUntil: ['domcontentloaded', 'networkidle2'], timeout: 20000 })
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
- throw new Error('Login failed: incorrect email or password');
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
- throw new Error('Login blocked by CAPTCHA. Manual verification required.');
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) throw new Error('Cookies C1/C2 missing after login');
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
- // Handle Puppeteer system dependency errors
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('warn', `Missing system libraries detected.`);
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
- // Generic Puppeteer error
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('warn', `Failed to close Puppeteer browser: ${closeErr.message}`);
489
+ if (this.logError) this.emit('error', `Failed to close Puppeteer browser: ${closeErr.message}`);
456
490
  }
457
491
  }
458
492
  }
@@ -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.restFulPort}`);
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}`);