homebridge-melcloud-control 4.0.0-beta.411 → 4.0.0-beta.413

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 +1 -1
  2. package/src/melcloud.js +116 -60
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.411",
4
+ "version": "4.0.0-beta.413",
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
@@ -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
 
@@ -306,25 +331,29 @@ class MelCloud extends EventEmitter {
306
331
  try {
307
332
  browser = await puppeteer.launch({
308
333
  headless: true,
309
- 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
+ ]
310
341
  });
311
342
 
312
343
  const page = await browser.newPage();
313
344
 
314
- // Emit page errors to logs
315
345
  page.on('error', err => this.emit('warn', `Page crashed: ${err.message}`));
316
346
  page.on('pageerror', err => this.emit('warn', `Browser error: ${err.message}`));
347
+ page.on('close', () => this.emit('warn', 'Page was closed unexpectedly'));
348
+ browser.on('disconnected', () => this.emit('warn', 'Browser disconnected unexpectedly'));
317
349
 
318
- await page.goto(ApiUrlsHome.BaseURL, { waitUntil: ['domcontentloaded', 'networkidle2'] });
350
+ page.setDefaultTimeout(30000);
351
+ page.setDefaultNavigationTimeout(30000);
319
352
 
320
- // Ensure login button is visible
321
- await page.waitForSelector('button.btn--blue', { timeout: 10000 }).catch(() => {
322
- throw new Error('Login button not found on MELCloud Home');
323
- });
353
+ await page.goto(ApiUrlsHome.BaseURL, { waitUntil: ['domcontentloaded', 'networkidle2'] });
324
354
 
325
355
  const buttons = await page.$$('button.btn--blue');
326
356
  let loginBtn = null;
327
-
328
357
  for (const btn of buttons) {
329
358
  const text = await page.evaluate(el => el.textContent.trim(), btn);
330
359
  if (['Zaloguj', 'Sign In', 'Login'].includes(text)) {
@@ -332,44 +361,70 @@ class MelCloud extends EventEmitter {
332
361
  break;
333
362
  }
334
363
  }
364
+ if (!loginBtn) {
365
+ this.emit('warn', 'Login button not found');
366
+ return null;
367
+ }
335
368
 
336
- if (!loginBtn) throw new Error('Login button not found (no matching text)');
337
-
338
- if (this.logDebug) this.emit('debug', `Clicking login button...`);
369
+ if (page.isClosed()) {
370
+ this.emit('warn', 'Page closed before login click');
371
+ return null;
372
+ }
339
373
 
340
- await Promise.all([
341
- loginBtn.click(),
342
- 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))
343
380
  ]);
344
381
 
345
- 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
+ this.emit('warn', 'Username or password input not found');
386
+ return null;
387
+ }
388
+
389
+ if (page.isClosed()) {
390
+ this.emit('warn', 'Page closed before typing credentials');
391
+ return null;
392
+ }
393
+
346
394
  await page.type('input[name="username"]', this.user, { delay: 50 });
347
395
  await page.type('input[name="password"]', this.passwd, { delay: 50 });
348
396
 
349
- // Handle possible "Show password" overlays
350
397
  const submitButton = await page.$('input[type="submit"], button[type="submit"]');
351
- if (!submitButton) throw new Error('Submit button not found on login form');
398
+ if (!submitButton) {
399
+ this.emit('warn', 'Submit button not found on login form');
400
+ return null;
401
+ }
352
402
 
353
- if (this.logDebug) this.emit('debug', `Submitting credentials...`);
403
+ if (page.isClosed()) {
404
+ this.emit('warn', 'Page closed before submitting form');
405
+ return null;
406
+ }
354
407
 
355
- await Promise.all([
356
- submitButton.click(),
357
- 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))
358
414
  ]);
359
415
 
360
- // Check for login errors
361
416
  const pageText = await page.content();
362
417
  if (pageText.includes('incorrect') || pageText.includes('Invalid') || pageText.includes('nieprawidłowe')) {
363
- throw new Error('Login failed: incorrect email or password');
418
+ this.emit('warn', 'Login failed: incorrect email or password');
419
+ return null;
364
420
  }
365
421
 
366
- // Check for CAPTCHA
367
422
  const captchaPresent = await page.$('iframe[src*="captcha"], div[class*="captcha"]');
368
423
  if (captchaPresent) {
369
- throw new Error('Login blocked by CAPTCHA. Manual verification required.');
424
+ this.emit('warn', 'Login blocked by CAPTCHA. Manual verification required.');
425
+ return null;
370
426
  }
371
427
 
372
- // Extract cookies with retry mechanism
373
428
  let c1 = null, c2 = null;
374
429
  const start = Date.now();
375
430
  while ((!c1 || !c2) && Date.now() - start < 20000) {
@@ -379,7 +434,10 @@ class MelCloud extends EventEmitter {
379
434
  if (!c1 || !c2) await new Promise(r => setTimeout(r, 500));
380
435
  }
381
436
 
382
- if (!c1 || !c2) throw new Error('Cookies C1/C2 missing after login');
437
+ if (!c1 || !c2) {
438
+ this.emit('warn', 'Cookies C1/C2 missing after login');
439
+ return null;
440
+ }
383
441
 
384
442
  const contextKey = [
385
443
  '__Secure-monitorandcontrol=chunks-2',
@@ -394,9 +452,8 @@ class MelCloud extends EventEmitter {
394
452
  if (!refresh) this.emit('success', `Connect to MELCloud Home Success`);
395
453
 
396
454
  return accountInfo;
397
-
398
455
  } catch (error) {
399
- // Handle Puppeteer system dependency errors
456
+ // Puppeteer / system error handling
400
457
  if (error.message.includes('libnspr4.so') || error.message.includes('Failed to launch the browser process')) {
401
458
  const inDocker = await this.functions.isRunningInDocker();
402
459
  this.emit('warn', `Missing system libraries detected.`);
@@ -410,6 +467,7 @@ class MelCloud extends EventEmitter {
410
467
  if (inDocker) {
411
468
  this.emit('warn', `Running in Docker — attempting automatic fix...`);
412
469
  try {
470
+ const { execSync } = require('child_process');
413
471
  execSync(installCmd, { stdio: 'inherit' });
414
472
  this.emit('success', `System libraries installed. Retry the connection.`);
415
473
  return true;
@@ -421,9 +479,8 @@ class MelCloud extends EventEmitter {
421
479
  }
422
480
  }
423
481
 
424
- // Generic Puppeteer error
425
- throw new Error(`MELCloud connection failed: ${error.message}`);
426
-
482
+ // Only throw for real technical errors
483
+ throw new Error(`Connect error: ${error.message}`);
427
484
  } finally {
428
485
  if (browser) {
429
486
  try {
@@ -435,7 +492,6 @@ class MelCloud extends EventEmitter {
435
492
  }
436
493
  }
437
494
 
438
-
439
495
  async checkDevicesList(contextKey) {
440
496
  let devices = [];
441
497
  switch (this.accountType) {