homebridge-melcloud-control 4.0.0-beta.197 → 4.0.0-beta.198

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 +166 -52
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.197",
4
+ "version": "4.0.0-beta.198",
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
@@ -1,5 +1,7 @@
1
+ const fs = require('fs');
1
2
  import axios from 'axios';
2
3
  import puppeteer from 'puppeteer';
4
+ import { execSync } from 'child_process';
3
5
  import EventEmitter from 'events';
4
6
  import ImpulseGenerator from './impulsegenerator.js';
5
7
  import Functions from './functions.js';
@@ -55,61 +57,112 @@ class MelCloud extends EventEmitter {
55
57
  }
56
58
 
57
59
  async checkMelcloudDevicesList(contextKey) {
60
+ if (this.logDebug) this.emit('debug', `Connecting to MELCloud Home`);
61
+
58
62
  try {
59
- const axiosInstanceGet = axios.create({
60
- method: 'GET',
61
- baseURL: ApiUrls.BaseURL,
62
- timeout: 15000,
63
- headers: { 'X-MitsContextKey': contextKey }
63
+ const browser = await puppeteer.launch({
64
+ headless: true,
65
+ args: ['--no-sandbox', '--disable-setuid-sandbox']
64
66
  });
67
+ const page = await browser.newPage();
65
68
 
66
- if (this.logDebug) this.emit('debug', `Scanning for devices`);
67
- const listDevicesData = await axiosInstanceGet(ApiUrls.ListDevices);
68
- const buildingsList = listDevicesData.data;
69
- if (this.logDebug) this.emit('debug', `Buildings: ${JSON.stringify(buildingsList, null, 2)}`);
69
+ await page.goto(ApiUrlsHome.BaseURL, { waitUntil: 'networkidle2' });
70
70
 
71
- if (!buildingsList) {
72
- if (this.logWarn) this.emit('warn', `No building found`);
73
- return null;
71
+ const buttons = await page.$$('button.btn--blue');
72
+ let loginBtn = null;
73
+ for (const btn of buttons) {
74
+ const text = await page.evaluate(el => el.textContent, btn);
75
+ if (text.trim() === 'Zaloguj' || text.trim() === 'Log In') {
76
+ loginBtn = btn;
77
+ break;
78
+ }
74
79
  }
75
80
 
76
- await this.functions.saveData(this.buildingsFile, buildingsList);
77
- if (this.logDebug) this.emit('debug', `Buildings list saved`);
81
+ if (!loginBtn && this.logWarn) this.emit('warn', `Login button not found`);
78
82
 
79
- const devices = [];
80
- for (const building of buildingsList) {
81
- const buildingStructure = building.Structure;
82
- const allDevices = [
83
- ...buildingStructure.Floors.flatMap(floor => [
84
- ...floor.Areas.flatMap(area => area.Devices),
85
- ...floor.Devices
86
- ]),
87
- ...buildingStructure.Areas.flatMap(area => area.Devices),
88
- ...buildingStructure.Devices
89
- ];
83
+ await Promise.all([
84
+ loginBtn.click(),
85
+ page.waitForNavigation({ waitUntil: 'networkidle2', timeout: 10000 })
86
+ ]);
90
87
 
91
- // Zamiana DeviceID na string
92
- allDevices.forEach(device => {
93
- if (device.DeviceID !== undefined && device.DeviceID !== null) {
94
- device.DeviceID = device.DeviceID.toString();
95
- }
96
- });
88
+ await page.waitForSelector('input[name="username"]', { timeout: 5000 });
89
+ await page.type('input[name="username"]', this.user, { delay: 50 });
90
+ await page.type('input[name="password"]', this.passwd, { delay: 50 });
91
+
92
+ const button1 = await page.$('input[type="submit"]');
93
+ await Promise.all([
94
+ button1.click(),
95
+ page.waitForNavigation({ waitUntil: 'networkidle2', timeout: 10000 })
96
+ ]);
97
+
98
+ let c1 = null, c2 = null;
99
+ const start = Date.now();
97
100
 
98
- devices.push(...allDevices);
101
+ while ((!c1 || !c2) && Date.now() - start < 20000) {
102
+ const cookies = await page.cookies();
103
+ c1 = cookies.find(c => c.name === '__Secure-monitorandcontrolC1')?.value || c1;
104
+ c2 = cookies.find(c => c.name === '__Secure-monitorandcontrolC2')?.value || c2;
105
+ if (!c1 || !c2) await new Promise(r => setTimeout(r, 500));
99
106
  }
100
107
 
101
- const devicesCount = devices.length;
102
- if (devicesCount === 0) {
103
- if (this.logWarn) this.emit('warn', `No devices found`);
108
+ if (!c1 || !c2) {
109
+ if (this.logWarn) this.emit('warn', `Cookies C1/C2 missing`);
104
110
  return null;
105
111
  }
106
112
 
107
- await this.functions.saveData(this.devicesFile, devices);
108
- if (this.logDebug) this.emit('debug', `${devicesCount} devices saved`);
113
+ const contextKey = [
114
+ '__Secure-monitorandcontrol=chunks-2',
115
+ `__Secure-monitorandcontrolC1=${c1}`,
116
+ `__Secure-monitorandcontrolC2=${c2}`
117
+ ].join('; ');
118
+
119
+ const accountInfo = { ContextKey: contextKey, UseFahrenheit: false };
120
+ this.contextKey = contextKey;
121
+
122
+ await this.functions.saveData(this.accountFile, accountInfo);
123
+ if (!refresh) this.emit('success', `Connect to MELCloud Home Success`);
124
+
125
+ await browser.close();
126
+ return accountInfo;
109
127
 
110
- return devices;
111
128
  } catch (error) {
112
- throw new Error(`Check devices list error: ${error.message}`);
129
+ if (error.message.includes('libnspr4.so') || error.message.includes('Failed to launch the browser process')) {
130
+ const inDocker = isRunningInDocker();
131
+ this.emit('warn', `Missing system libraries detected.`);
132
+
133
+ if (inDocker) {
134
+ this.emit('warn', `Running in Docker — attempting automatic fix...`);
135
+
136
+ const installCmd =
137
+ 'apt-get update && apt-get install -y ' +
138
+ 'libnss3 libnspr4 libatk1.0-0 libatk-bridge2.0-0 libcups2 ' +
139
+ 'libxcomposite1 libxrandr2 libxdamage1 libxkbcommon0 libpango-1.0-0 ' +
140
+ 'libgbm1 libasound2 libxshmfence1 fonts-liberation libappindicator3-1 libu2f-udev';
141
+
142
+ try {
143
+ execSync(installCmd, { stdio: 'inherit' });
144
+ this.emit('debug', `Libraries installed. Retrying Puppeteer...`);
145
+
146
+ const browser = await puppeteer.launch({
147
+ headless: true,
148
+ args: ['--no-sandbox', '--disable-setuid-sandbox']
149
+ });
150
+ await browser.close();
151
+ this.emit('success', `Puppeteer repaired and running.`);
152
+ return true;
153
+
154
+ } catch (fixError) {
155
+ this.emit('error', `Automatic fix failed. You can run manually inside container:\n${installCmd}`);
156
+ throw fixError;
157
+ }
158
+
159
+ } else {
160
+ this.emit('error', `System libraries missing. Detected non-Docker environment — please install dependencies manually.`);
161
+ throw error;
162
+ }
163
+ } else {
164
+ throw new Error(`Puppeteer failed: ${error.message}`);
165
+ }
113
166
  }
114
167
  }
115
168
 
@@ -267,17 +320,32 @@ class MelCloud extends EventEmitter {
267
320
  }
268
321
  }
269
322
 
323
+ isRunningInDocker() {
324
+ try {
325
+ if (fs.existsSync('/.dockerenv')) return true;
326
+ const cgroup = fs.readFileSync('/proc/1/cgroup', 'utf8');
327
+ return cgroup.includes('docker') || cgroup.includes('kubepods');
328
+ } catch {
329
+ return false;
330
+ }
331
+ }
332
+
270
333
  async connectToMelCloudHome(refresh = false) {
271
334
  if (this.logDebug) this.emit('debug', `Connecting to MELCloud Home`);
272
335
 
273
- const browser = await puppeteer.launch({ headless: true, args: ['--no-sandbox', '--disable-setuid-sandbox'] });
274
- const page = await browser.newPage();
275
-
336
+ let browser;
276
337
  try {
277
- // Open MELCloud Home
338
+ browser = await puppeteer.launch({
339
+ headless: true,
340
+ args: ['--no-sandbox', '--disable-setuid-sandbox']
341
+ });
342
+
343
+ const page = await browser.newPage();
278
344
  await page.goto(ApiUrlsHome.BaseURL, { waitUntil: 'networkidle2' });
345
+
279
346
  const buttons = await page.$$('button.btn--blue');
280
347
  let loginBtn = null;
348
+
281
349
  for (const btn of buttons) {
282
350
  const text = await page.evaluate(el => el.textContent, btn);
283
351
  if (text.trim() === 'Zaloguj' || text.trim() === 'Log In') {
@@ -288,33 +356,43 @@ class MelCloud extends EventEmitter {
288
356
 
289
357
  if (!loginBtn && this.logWarn) this.emit('warn', `Login button not found`);
290
358
 
291
- // Set credentials and login
292
- await Promise.all([loginBtn.click(), page.waitForNavigation({ waitUntil: 'networkidle2', timeout: 5000 })]);
359
+ await Promise.all([
360
+ loginBtn.click(),
361
+ page.waitForNavigation({ waitUntil: 'networkidle2', timeout: 10000 })
362
+ ]);
363
+
293
364
  await page.waitForSelector('input[name="username"]', { timeout: 5000 });
294
365
  await page.type('input[name="username"]', this.user, { delay: 50 });
295
366
  await page.type('input[name="password"]', this.passwd, { delay: 50 });
296
367
 
297
368
  const button1 = await page.$('input[type="submit"]');
298
- await Promise.all([button1.click(), page.waitForNavigation({ waitUntil: 'networkidle2', timeout: 5000 })]);
369
+ await Promise.all([
370
+ button1.click(),
371
+ page.waitForNavigation({ waitUntil: 'networkidle2', timeout: 10000 })
372
+ ]);
299
373
 
300
- // Get cookies C1 and C2
301
374
  let c1 = null, c2 = null;
302
375
  const start = Date.now();
303
376
 
304
- // Loop max 20s
305
377
  while ((!c1 || !c2) && Date.now() - start < 20000) {
306
- const cookies = await page.cookies();
378
+ const cookies = await page.browserContext().cookies();
307
379
  c1 = cookies.find(c => c.name === '__Secure-monitorandcontrolC1')?.value || c1;
308
380
  c2 = cookies.find(c => c.name === '__Secure-monitorandcontrolC2')?.value || c2;
309
381
  if (!c1 || !c2) await new Promise(r => setTimeout(r, 500));
310
382
  }
311
383
 
384
+
312
385
  if (!c1 || !c2) {
313
386
  if (this.logWarn) this.emit('warn', `Cookies C1/C2 missing`);
314
387
  return null;
315
388
  }
316
389
 
317
- const contextKey = ['__Secure-monitorandcontrol=chunks-2', `__Secure-monitorandcontrolC1=${c1}`, `__Secure-monitorandcontrolC2=${c2}`,].join('; ');
390
+ const contextKey = [
391
+ '__Secure-monitorandcontrol=chunks-2',
392
+ `__Secure-monitorandcontrolC1=${c1}`,
393
+ `__Secure-monitorandcontrolC2=${c2}`
394
+ ].join('; ');
395
+
318
396
  const accountInfo = { ContextKey: contextKey, UseFahrenheit: false };
319
397
  this.contextKey = contextKey;
320
398
 
@@ -322,13 +400,49 @@ class MelCloud extends EventEmitter {
322
400
  if (!refresh) this.emit('success', `Connect to MELCloud Home Success`);
323
401
 
324
402
  return accountInfo;
403
+
325
404
  } catch (error) {
326
- throw new Error(`Connect to MELCloud Home error: ${error.message}`);
405
+ if (error.message.includes('libnspr4.so') || error.message.includes('Failed to launch the browser process')) {
406
+ const inDocker = this.isRunningInDocker();
407
+ if (this.logWarn) this.emit('warn', `Missing system libraries detected.`);
408
+
409
+ if (inDocker) {
410
+ this.emit('warn', `Running in Docker — attempting automatic fix...`);
411
+
412
+ const installCmd =
413
+ 'apt-get update && apt-get install -y ' +
414
+ 'libnss3 libnspr4 libatk1.0-0 libatk-bridge2.0-0 libcups2 ' +
415
+ 'libxcomposite1 libxrandr2 libxdamage1 libxkbcommon0 libpango-1.0-0 ' +
416
+ 'libgbm1 libasound2 libxshmfence1 fonts-liberation libappindicator3-1 libu2f-udev';
417
+
418
+ try {
419
+ execSync(installCmd, { stdio: 'inherit' });
420
+ if (this.logDebug) this.emit('debug', `Libraries installed. Retrying Puppeteer...`);
421
+
422
+ const testBrowser = await puppeteer.launch({
423
+ headless: true,
424
+ args: ['--no-sandbox', '--disable-setuid-sandbox']
425
+ });
426
+ await testBrowser.close();
427
+
428
+ this.emit('success', `Puppeteer repaired and running, try again.`);
429
+ return true;
430
+ } catch (fixError) {
431
+ throw new Error(`Automatic fix failed. Run manually inside container:\n${installCmd}`);
432
+ }
433
+
434
+ } else {
435
+ throw new Error(`System libraries missing. Non-Docker environment detected — install dependencies manually.`);
436
+ }
437
+ } else {
438
+ throw new Error(`Puppeteer failed: ${error.message}`);
439
+ }
327
440
  } finally {
328
- await browser.close();
441
+ if (browser) await browser.close().catch(() => { });
329
442
  }
330
443
  }
331
444
 
445
+
332
446
  async checkDevicesList(contextKey) {
333
447
  let devices = [];
334
448
  switch (this.accountType) {