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.
- package/package.json +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.
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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 (
|
|
78
|
-
|
|
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
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
108
|
-
|
|
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', `${
|
|
137
|
+
if (this.logDebug) this.emit('debug', `${devices.length} devices saved`);
|
|
115
138
|
|
|
116
139
|
return devices;
|
|
117
140
|
} catch (error) {
|
|
118
|
-
|
|
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
|
|
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(`
|
|
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
|
-
|
|
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 (
|
|
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
|
|
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:
|
|
367
|
+
page.waitForNavigation({ waitUntil: ['domcontentloaded', 'networkidle2'], timeout: 15000 })
|
|
331
368
|
]);
|
|
332
369
|
|
|
333
|
-
await page.waitForSelector('input[name="username"]', { timeout:
|
|
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
|
-
|
|
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
|
-
|
|
340
|
-
page.waitForNavigation({ waitUntil: 'networkidle2', timeout:
|
|
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
|
-
|
|
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
|
-
|
|
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(`
|
|
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)
|
|
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
|
|