homebridge-melcloud-control 4.0.0-beta.410 → 4.0.0-beta.412

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.
Files changed (2) hide show
  1. package/package.json +2 -2
  2. package/src/melcloud.js +113 -65
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.410",
4
+ "version": "4.0.0-beta.412",
5
5
  "description": "Homebridge plugin to control Mitsubishi Air Conditioner, Heat Pump and Energy Recovery Ventilation.",
6
6
  "license": "MIT",
7
7
  "author": "grzegorz914",
@@ -39,7 +39,7 @@
39
39
  "async-mqtt": "^2.6.3",
40
40
  "axios": "^1.12.2",
41
41
  "express": "^5.1.0",
42
- "puppeteer-core": "^24.26.1",
42
+ "puppeteer": "^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/melcloud.js CHANGED
@@ -1,6 +1,6 @@
1
1
 
2
2
  import { execSync } from 'child_process';
3
- import puppeteer from 'puppeteer-core';
3
+ import puppeteer from 'puppeteer';
4
4
  import axios from 'axios';
5
5
  import EventEmitter from 'events';
6
6
  import MelCloudHomeToken from './melcloudhometoken.js';
@@ -62,20 +62,28 @@ class MelCloud extends EventEmitter {
62
62
  // MELCloud
63
63
  async checkMelcloudDevicesList(contextKey) {
64
64
  try {
65
- const axiosInstanceGet = axios.create({
66
- method: 'GET',
65
+ const axiosInstance = axios.create({
67
66
  baseURL: ApiUrls.BaseURL,
68
67
  timeout: 15000,
69
68
  headers: { 'X-MitsContextKey': contextKey }
70
69
  });
71
70
 
72
- if (this.logDebug) this.emit('debug', `Scanning for devices`);
73
- const listDevicesData = await axiosInstanceGet(ApiUrls.ListDevices);
71
+ if (this.logDebug) this.emit('debug', `Scanning for devices...`);
72
+
73
+ const listDevicesData = await axiosInstance.get(ApiUrls.ListDevices);
74
+
75
+ if (!listDevicesData || !listDevicesData.data) {
76
+ this.emit('warn', `Invalid or empty response from MELCloud API`);
77
+ return null;
78
+ }
79
+
74
80
  const buildingsList = listDevicesData.data;
75
- if (this.logDebug) this.emit('debug', `Buildings: ${JSON.stringify(buildingsList, null, 2)}`);
76
81
 
77
- if (!buildingsList) {
78
- if (this.logWarn) this.emit('warn', `No building found`);
82
+ if (this.logDebug)
83
+ this.emit('debug', `Buildings: ${JSON.stringify(buildingsList, null, 2)}`);
84
+
85
+ if (!Array.isArray(buildingsList) || buildingsList.length === 0) {
86
+ this.emit('warn', `No buildings found in MELCloud account`);
79
87
  return null;
80
88
  }
81
89
 
@@ -83,42 +91,59 @@ class MelCloud extends EventEmitter {
83
91
  if (this.logDebug) this.emit('debug', `Buildings list saved`);
84
92
 
85
93
  const devices = [];
94
+
86
95
  for (const building of buildingsList) {
87
- const buildingStructure = building.Structure;
88
- const allDevices = [
89
- ...buildingStructure.Floors.flatMap(floor => [
90
- ...floor.Areas.flatMap(area => area.Devices),
91
- ...floor.Devices
92
- ]),
93
- ...buildingStructure.Areas.flatMap(area => area.Devices),
94
- ...buildingStructure.Devices
95
- ];
96
+ if (!building.Structure) {
97
+ this.emit(
98
+ 'warn',
99
+ `Building missing structure: ${building.BuildingName || 'Unnamed'}`
100
+ );
101
+ continue;
102
+ }
103
+
104
+ const { Structure } = building;
96
105
 
97
- // Zamiana DeviceID na string
106
+ const allDevices = [
107
+ ...(Structure.Floors?.flatMap(floor => [
108
+ ...(floor.Areas?.flatMap(area => area.Devices || []) || []),
109
+ ...(floor.Devices || [])
110
+ ]) || []),
111
+ ...(Structure.Areas?.flatMap(area => area.Devices || []) || []),
112
+ ...(Structure.Devices || [])
113
+ ].filter(d => d != null);
114
+
115
+ // Zamiana ID na string
98
116
  allDevices.forEach(device => {
99
- if (device.DeviceID !== undefined && device.DeviceID !== null) {
100
- device.DeviceID = device.DeviceID.toString();
101
- }
117
+ if (device.DeviceID != null) device.DeviceID = String(device.DeviceID);
102
118
  });
103
119
 
120
+ if (this.logDebug) {
121
+ const count = allDevices.length;
122
+ this.emit(
123
+ 'debug',
124
+ `Found ${count} devices in building: ${building.BuildingName || 'Unnamed'}`
125
+ );
126
+ }
127
+
104
128
  devices.push(...allDevices);
105
129
  }
106
130
 
107
- const devicesCount = devices.length;
108
- if (devicesCount === 0) {
109
- if (this.logWarn) this.emit('warn', `No devices found`);
131
+ if (devices.length === 0) {
132
+ this.emit('warn', `No devices found in any building`);
110
133
  return null;
111
134
  }
112
135
 
113
136
  await this.functions.saveData(this.devicesFile, devices);
114
- if (this.logDebug) this.emit('debug', `${devicesCount} devices saved`);
137
+ if (this.logDebug) this.emit('debug', `${devices.length} devices saved`);
115
138
 
116
139
  return devices;
117
140
  } catch (error) {
118
- throw new Error(`Check devices list error: ${error.message}`);
141
+ const msg = error.response ? `HTTP ${error.response.status}: ${error.response.statusText}` : error.message;
142
+ throw new Error(`Check devices list error: ${msg}`);
119
143
  }
120
144
  }
121
145
 
146
+
122
147
  async connectToMelCloud() {
123
148
  if (this.logDebug) this.emit('debug', `Connecting to MELCloud`);
124
149
 
@@ -166,7 +191,7 @@ class MelCloud extends EventEmitter {
166
191
 
167
192
  return accountInfo
168
193
  } catch (error) {
169
- throw new Error(`Connect to MELCloud error: ${error.message}`);
194
+ throw new Error(`Connect error: ${error.message}`);
170
195
  }
171
196
  }
172
197
 
@@ -270,7 +295,7 @@ class MelCloud extends EventEmitter {
270
295
 
271
296
  return devices;
272
297
  } catch (error) {
273
- throw new Error(`Connect to MELCloud Home error: ${error.message}`);
298
+ throw new Error(`Check devices list error: ${error.message}`);
274
299
  }
275
300
  }
276
301
 
@@ -310,39 +335,68 @@ class MelCloud extends EventEmitter {
310
335
  });
311
336
 
312
337
  const page = await browser.newPage();
313
- await page.goto(ApiUrlsHome.BaseURL, { waitUntil: 'networkidle2' });
338
+
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}`));
342
+
343
+ await page.goto(ApiUrlsHome.BaseURL, { waitUntil: ['domcontentloaded', 'networkidle2'] });
344
+
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
+ });
314
349
 
315
350
  const buttons = await page.$$('button.btn--blue');
316
351
  let loginBtn = null;
317
352
 
318
353
  for (const btn of buttons) {
319
- const text = await page.evaluate(el => el.textContent, btn);
320
- if (text.trim() === 'Zaloguj' || text.trim() === 'Sign In') {
354
+ const text = await page.evaluate(el => el.textContent.trim(), btn);
355
+ if (['Zaloguj', 'Sign In', 'Login'].includes(text)) {
321
356
  loginBtn = btn;
322
357
  break;
323
358
  }
324
359
  }
325
360
 
326
- if (!loginBtn && this.logWarn) this.emit('warn', `Login button not found`);
361
+ if (!loginBtn) throw new Error('Login button not found (no matching text)');
362
+
363
+ if (this.logDebug) this.emit('debug', `Clicking login button...`);
327
364
 
328
365
  await Promise.all([
329
366
  loginBtn.click(),
330
- page.waitForNavigation({ waitUntil: 'networkidle2', timeout: 10000 })
367
+ page.waitForNavigation({ waitUntil: ['domcontentloaded', 'networkidle2'], timeout: 15000 })
331
368
  ]);
332
369
 
333
- await page.waitForSelector('input[name="username"]', { timeout: 5000 });
370
+ await page.waitForSelector('input[name="username"]', { timeout: 10000 });
334
371
  await page.type('input[name="username"]', this.user, { delay: 50 });
335
372
  await page.type('input[name="password"]', this.passwd, { delay: 50 });
336
373
 
337
- const button1 = await page.$('input[type="submit"]');
374
+ // Handle possible "Show password" overlays
375
+ const submitButton = await page.$('input[type="submit"], button[type="submit"]');
376
+ if (!submitButton) throw new Error('Submit button not found on login form');
377
+
378
+ if (this.logDebug) this.emit('debug', `Submitting credentials...`);
379
+
338
380
  await Promise.all([
339
- button1.click(),
340
- page.waitForNavigation({ waitUntil: 'networkidle2', timeout: 10000 })
381
+ submitButton.click(),
382
+ page.waitForNavigation({ waitUntil: ['domcontentloaded', 'networkidle2'], timeout: 20000 })
341
383
  ]);
342
384
 
385
+ // Check for login errors
386
+ const pageText = await page.content();
387
+ if (pageText.includes('incorrect') || pageText.includes('Invalid') || pageText.includes('nieprawidłowe')) {
388
+ throw new Error('Login failed: incorrect email or password');
389
+ }
390
+
391
+ // Check for CAPTCHA
392
+ const captchaPresent = await page.$('iframe[src*="captcha"], div[class*="captcha"]');
393
+ if (captchaPresent) {
394
+ throw new Error('Login blocked by CAPTCHA. Manual verification required.');
395
+ }
396
+
397
+ // Extract cookies with retry mechanism
343
398
  let c1 = null, c2 = null;
344
399
  const start = Date.now();
345
-
346
400
  while ((!c1 || !c2) && Date.now() - start < 20000) {
347
401
  const cookies = await page.browserContext().cookies();
348
402
  c1 = cookies.find(c => c.name === '__Secure-monitorandcontrolC1')?.value || c1;
@@ -350,11 +404,7 @@ class MelCloud extends EventEmitter {
350
404
  if (!c1 || !c2) await new Promise(r => setTimeout(r, 500));
351
405
  }
352
406
 
353
-
354
- if (!c1 || !c2) {
355
- if (this.logWarn) this.emit('warn', `Cookies C1/C2 missing`);
356
- return null;
357
- }
407
+ if (!c1 || !c2) throw new Error('Cookies C1/C2 missing after login');
358
408
 
359
409
  const contextKey = [
360
410
  '__Secure-monitorandcontrol=chunks-2',
@@ -370,43 +420,41 @@ class MelCloud extends EventEmitter {
370
420
 
371
421
  return accountInfo;
372
422
  } catch (error) {
423
+ // Handle Puppeteer system dependency errors
373
424
  if (error.message.includes('libnspr4.so') || error.message.includes('Failed to launch the browser process')) {
374
425
  const inDocker = await this.functions.isRunningInDocker();
375
- if (this.logWarn) this.emit('warn', `Missing system libraries detected.`);
426
+ this.emit('warn', `Missing system libraries detected.`);
427
+
428
+ const installCmd =
429
+ 'apt-get update && apt-get install -y ' +
430
+ 'libnss3 libnspr4 libatk1.0-0 libatk-bridge2.0-0 libcups2 ' +
431
+ 'libxcomposite1 libxrandr2 libxdamage1 libxkbcommon0 libpango-1.0-0 ' +
432
+ 'libgbm1 libasound2 libxshmfence1 fonts-liberation libappindicator3-1 libu2f-udev';
376
433
 
377
434
  if (inDocker) {
378
435
  this.emit('warn', `Running in Docker — attempting automatic fix...`);
379
-
380
- const installCmd =
381
- 'apt-get update && apt-get install -y ' +
382
- 'libnss3 libnspr4 libatk1.0-0 libatk-bridge2.0-0 libcups2 ' +
383
- 'libxcomposite1 libxrandr2 libxdamage1 libxkbcommon0 libpango-1.0-0 ' +
384
- 'libgbm1 libasound2 libxshmfence1 fonts-liberation libappindicator3-1 libu2f-udev';
385
-
386
436
  try {
387
437
  execSync(installCmd, { stdio: 'inherit' });
388
- if (this.logDebug) this.emit('debug', `Libraries installed. Retrying Puppeteer...`);
389
-
390
- const testBrowser = await puppeteer.launch({
391
- headless: true,
392
- args: ['--no-sandbox', '--disable-setuid-sandbox']
393
- });
394
- await testBrowser.close();
395
-
396
- this.emit('success', `Puppeteer repaired and running, try again.`);
438
+ this.emit('success', `System libraries installed. Retry the connection.`);
397
439
  return true;
398
440
  } catch (fixError) {
399
441
  throw new Error(`Automatic fix failed. Run manually inside container:\n${installCmd}`);
400
442
  }
401
-
402
443
  } else {
403
- throw new Error(`System libraries missing. Non-Docker environment detected — install dependencies manually.`);
444
+ throw new Error(`Missing system libraries. Please install manually:\n${installCmd}`);
404
445
  }
405
- } else {
406
- throw new Error(`Puppeteer failed: ${error.message}`);
407
446
  }
447
+
448
+ // Generic Puppeteer error
449
+ throw new Error(`Connect error: ${error.message}`);
408
450
  } finally {
409
- if (browser) await browser.close().catch(() => { });
451
+ if (browser) {
452
+ try {
453
+ await browser.close();
454
+ } catch (closeErr) {
455
+ this.emit('warn', `Failed to close Puppeteer browser: ${closeErr.message}`);
456
+ }
457
+ }
410
458
  }
411
459
  }
412
460