homebridge-melcloud-control 4.0.0-beta.196 → 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.
- package/package.json +1 -1
- package/src/deviceata.js +4 -2
- package/src/melcloud.js +166 -52
- package/src/melcloudata.js +11 -69
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.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/deviceata.js
CHANGED
|
@@ -414,7 +414,8 @@ class DeviceAta extends EventEmitter {
|
|
|
414
414
|
})
|
|
415
415
|
.onSet(async (value) => {
|
|
416
416
|
try {
|
|
417
|
-
|
|
417
|
+
const tempKey = this.accessory.operationMode === 8 ? 'DefaultCoolingSetTemperature' : 'SetTemperature';
|
|
418
|
+
deviceData.Device[tempKey] = value;
|
|
418
419
|
deviceData.Device.EffectiveFlags = AirConditioner.EffectiveFlags.SetTemperature;
|
|
419
420
|
await this.melCloudAta.send(this.accountType, this.displayMode, deviceData);
|
|
420
421
|
if (this.logInfo) this.emit('info', `Set cooling threshold temperature: ${value}${this.accessory.temperatureUnit}`);
|
|
@@ -435,7 +436,8 @@ class DeviceAta extends EventEmitter {
|
|
|
435
436
|
})
|
|
436
437
|
.onSet(async (value) => {
|
|
437
438
|
try {
|
|
438
|
-
|
|
439
|
+
const tempKey = this.accessory.operationMode === 8 ? 'DefaultHeatingSetTemperature' : 'SetTemperature';
|
|
440
|
+
deviceData.Device[tempKey] = value;
|
|
439
441
|
deviceData.Device.EffectiveFlags = AirConditioner.EffectiveFlags.SetTemperature;
|
|
440
442
|
await this.melCloudAta.send(this.accountType, this.displayMode, deviceData);
|
|
441
443
|
if (this.logInfo) this.emit('info', `Set heating threshold temperature: ${value}${this.accessory.temperatureUnit}`);
|
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
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
|
|
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
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
-
|
|
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
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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
|
-
|
|
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
|
-
|
|
102
|
-
|
|
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
|
-
|
|
108
|
-
|
|
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
|
-
|
|
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
|
-
|
|
274
|
-
const page = await browser.newPage();
|
|
275
|
-
|
|
336
|
+
let browser;
|
|
276
337
|
try {
|
|
277
|
-
|
|
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
|
-
|
|
292
|
-
|
|
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([
|
|
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 = [
|
|
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
|
-
|
|
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) {
|
package/src/melcloudata.js
CHANGED
|
@@ -189,35 +189,8 @@ class MelCloudAta extends EventEmitter {
|
|
|
189
189
|
withCredentials: true
|
|
190
190
|
});
|
|
191
191
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
case 1: // Heater/Cooler
|
|
195
|
-
switch (deviceData.Device.OperationMode) {
|
|
196
|
-
case 1: // HEAT
|
|
197
|
-
case 9: // ISEE HEAT
|
|
198
|
-
deviceData.Device.SetTemperature = deviceData.Device.DefaultHeatingSetTemperature;
|
|
199
|
-
break;
|
|
200
|
-
case 2: // DRY
|
|
201
|
-
case 3: // COOL
|
|
202
|
-
case 10: // ISEE DRY
|
|
203
|
-
case 11: // ISEE COOL
|
|
204
|
-
deviceData.Device.SetTemperature = deviceData.Device.DefaultCoolingSetTemperature;
|
|
205
|
-
break;
|
|
206
|
-
case 7: // FAN
|
|
207
|
-
break;
|
|
208
|
-
case 8: // AUTO
|
|
209
|
-
deviceData.Device.SetTemperature = (deviceData.Device.DefaultCoolingSetTemperature + deviceData.Device.DefaultHeatingSetTemperature) / 2;
|
|
210
|
-
break;
|
|
211
|
-
default:
|
|
212
|
-
if (this.logWarn) this.emit('warn', `Unknown operation mode: ${deviceData.Device.OperationMode}`);
|
|
213
|
-
return;
|
|
214
|
-
}
|
|
215
|
-
break;
|
|
216
|
-
case 2: // Thermostat
|
|
217
|
-
break;
|
|
218
|
-
default:
|
|
219
|
-
if (this.logWarn) this.emit('warn', `Unknown display mode: ${displayMode}`);
|
|
220
|
-
return;
|
|
192
|
+
if (displayMode === 1 && deviceData.Device.OperationMode === 8) {
|
|
193
|
+
deviceData.Device.SetTemperature = (deviceData.Device.DefaultCoolingSetTemperature + deviceData.Device.DefaultHeatingSetTemperature) / 2;
|
|
221
194
|
}
|
|
222
195
|
|
|
223
196
|
const payload = {
|
|
@@ -265,47 +238,16 @@ class MelCloudAta extends EventEmitter {
|
|
|
265
238
|
}
|
|
266
239
|
});
|
|
267
240
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
case 1: //Heather/Cooler
|
|
271
|
-
switch (deviceData.Device.OperationMode) {
|
|
272
|
-
case 1: //HEAT
|
|
273
|
-
case 9: //ISEE HEAT
|
|
274
|
-
deviceData.Device.SetTemperature = deviceData.Device.DefaultHeatingSetTemperature;
|
|
275
|
-
break;
|
|
276
|
-
case 2: //DRY
|
|
277
|
-
case 3: //COOL
|
|
278
|
-
case 10: //ISEE DRY
|
|
279
|
-
case 11: //ISEE COOL
|
|
280
|
-
deviceData.Device.SetTemperature = deviceData.Device.DefaultCoolingSetTemperature;
|
|
281
|
-
break;
|
|
282
|
-
case 7: //FAN
|
|
283
|
-
deviceData.Device.SetTemperature = deviceData.Device.SetTemperature;
|
|
284
|
-
break;
|
|
285
|
-
case 8: //AUTO
|
|
286
|
-
deviceData.Device.SetTemperature = (deviceData.Device.DefaultCoolingSetTemperature + deviceData.Device.DefaultHeatingSetTemperature) / 2;
|
|
287
|
-
break;
|
|
288
|
-
default:
|
|
289
|
-
if (this.logWarn) this.emit('warn', `Unknown operation mode: ${deviceData.Device.OperationMode}`);
|
|
290
|
-
return;
|
|
291
|
-
};
|
|
292
|
-
|
|
293
|
-
break
|
|
294
|
-
case 2: //Thermostat
|
|
295
|
-
deviceData.Device.SetTemperature = deviceData.Device.SetTemperature;
|
|
296
|
-
break;
|
|
297
|
-
default:
|
|
298
|
-
if (this.logWarn) this.emit('warn', `Unknown display mode: ${displayMode}`);
|
|
299
|
-
return;
|
|
300
|
-
};
|
|
241
|
+
if (displayMode === 1 && deviceData.Device.OperationMode === 8) {
|
|
242
|
+
deviceData.Device.SetTemperature = (deviceData.Device.DefaultCoolingSetTemperature + deviceData.Device.DefaultHeatingSetTemperature) / 2;
|
|
301
243
|
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
244
|
+
if (this.deviceState.DefaultCoolingSetTemperature !== deviceData.Device.DefaultCoolingSetTemperature || this.deviceState.DefaultHeatingSetTemperature !== deviceData.Device.DefaultHeatingSetTemperature) {
|
|
245
|
+
const temps = {
|
|
246
|
+
defaultCoolingSetTemperature: deviceData.Device.DefaultCoolingSetTemperature,
|
|
247
|
+
defaultHeatingSetTemperature: deviceData.Device.DefaultHeatingSetTemperature
|
|
248
|
+
};
|
|
249
|
+
await this.functions.saveData(this.defaultTempsFile, temps);
|
|
250
|
+
}
|
|
309
251
|
}
|
|
310
252
|
|
|
311
253
|
const settings = {
|