homebridge-melcloud-control 4.0.0-beta.21 → 4.0.0-beta.211
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/CHANGELOG.md +9 -0
- package/README.md +12 -11
- package/config.schema.json +62 -34
- package/homebridge-ui/public/index.html +72 -35
- package/homebridge-ui/server.js +4 -3
- package/index.js +40 -24
- package/package.json +2 -2
- package/src/constants.js +15 -22
- package/src/deviceata.js +199 -188
- package/src/deviceatw.js +7 -5
- package/src/deviceerv.js +28 -26
- package/src/functions.js +18 -3
- package/src/melcloud.js +205 -82
- package/src/melcloudata.js +139 -308
- package/src/melcloudatw.js +3 -228
- package/src/melclouderv.js +5 -162
package/src/melcloud.js
CHANGED
|
@@ -1,21 +1,22 @@
|
|
|
1
|
-
|
|
2
|
-
import
|
|
1
|
+
|
|
2
|
+
import { execSync } from 'child_process';
|
|
3
3
|
import puppeteer from 'puppeteer';
|
|
4
|
+
import axios from 'axios';
|
|
4
5
|
import EventEmitter from 'events';
|
|
5
6
|
import ImpulseGenerator from './impulsegenerator.js';
|
|
6
7
|
import Functions from './functions.js';
|
|
7
8
|
import { ApiUrls, ApiUrlsHome } from './constants.js';
|
|
8
9
|
|
|
9
10
|
class MelCloud extends EventEmitter {
|
|
10
|
-
constructor(user, passwd, language, accountFile, buildingsFile, devicesFile,
|
|
11
|
+
constructor(accountType, user, passwd, language, accountFile, buildingsFile, devicesFile, logWarn, logDebug, requestConfig) {
|
|
11
12
|
super();
|
|
13
|
+
this.accountType = accountType;
|
|
12
14
|
this.user = user;
|
|
13
15
|
this.passwd = passwd;
|
|
14
16
|
this.language = language;
|
|
15
17
|
this.accountFile = accountFile;
|
|
16
18
|
this.buildingsFile = buildingsFile;
|
|
17
19
|
this.devicesFile = devicesFile;
|
|
18
|
-
this.cookiesFile = cookiesFile;
|
|
19
20
|
this.logWarn = logWarn;
|
|
20
21
|
this.logDebug = logDebug;
|
|
21
22
|
this.requestConfig = requestConfig;
|
|
@@ -33,18 +34,15 @@ class MelCloud extends EventEmitter {
|
|
|
33
34
|
Persist: true
|
|
34
35
|
};
|
|
35
36
|
|
|
36
|
-
this.axiosDefaults = {
|
|
37
|
-
timeout: 15000,
|
|
38
|
-
maxContentLength: 100000000,
|
|
39
|
-
maxBodyLength: 1000000000,
|
|
40
|
-
httpsAgent: new Agent({
|
|
41
|
-
keepAlive: false,
|
|
42
|
-
rejectUnauthorized: false
|
|
43
|
-
})
|
|
44
|
-
};
|
|
45
|
-
|
|
46
37
|
if (!requestConfig) {
|
|
47
38
|
this.impulseGenerator = new ImpulseGenerator()
|
|
39
|
+
.on('connect', async () => {
|
|
40
|
+
try {
|
|
41
|
+
await this.connect(true);
|
|
42
|
+
} catch (error) {
|
|
43
|
+
this.emit('error', `Impulse generator error: ${error}`);
|
|
44
|
+
}
|
|
45
|
+
})
|
|
48
46
|
.on('checkDevicesList', async () => {
|
|
49
47
|
try {
|
|
50
48
|
await this.checkDevicesList(this.contextKey);
|
|
@@ -58,13 +56,14 @@ class MelCloud extends EventEmitter {
|
|
|
58
56
|
}
|
|
59
57
|
}
|
|
60
58
|
|
|
61
|
-
|
|
59
|
+
// MELCloud
|
|
60
|
+
async checkMelcloudDevicesList(contextKey) {
|
|
62
61
|
try {
|
|
63
62
|
const axiosInstanceGet = axios.create({
|
|
64
63
|
method: 'GET',
|
|
65
64
|
baseURL: ApiUrls.BaseURL,
|
|
66
|
-
|
|
67
|
-
|
|
65
|
+
timeout: 15000,
|
|
66
|
+
headers: { 'X-MitsContextKey': contextKey }
|
|
68
67
|
});
|
|
69
68
|
|
|
70
69
|
if (this.logDebug) this.emit('debug', `Scanning for devices`);
|
|
@@ -91,6 +90,14 @@ class MelCloud extends EventEmitter {
|
|
|
91
90
|
...buildingStructure.Areas.flatMap(area => area.Devices),
|
|
92
91
|
...buildingStructure.Devices
|
|
93
92
|
];
|
|
93
|
+
|
|
94
|
+
// Zamiana DeviceID na string
|
|
95
|
+
allDevices.forEach(device => {
|
|
96
|
+
if (device.DeviceID !== undefined && device.DeviceID !== null) {
|
|
97
|
+
device.DeviceID = device.DeviceID.toString();
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
|
|
94
101
|
devices.push(...allDevices);
|
|
95
102
|
}
|
|
96
103
|
|
|
@@ -109,21 +116,20 @@ class MelCloud extends EventEmitter {
|
|
|
109
116
|
}
|
|
110
117
|
}
|
|
111
118
|
|
|
112
|
-
async
|
|
119
|
+
async connectToMelCloud() {
|
|
113
120
|
if (this.logDebug) this.emit('debug', `Connecting to MELCloud`);
|
|
114
121
|
|
|
115
122
|
try {
|
|
116
123
|
const axiosInstanceLogin = axios.create({
|
|
117
124
|
method: 'POST',
|
|
118
125
|
baseURL: ApiUrls.BaseURL,
|
|
119
|
-
|
|
126
|
+
timeout: 15000,
|
|
120
127
|
});
|
|
121
128
|
|
|
122
129
|
const accountData = await axiosInstanceLogin(ApiUrls.ClientLogin, { data: this.loginData });
|
|
123
130
|
const account = accountData.data;
|
|
124
131
|
const accountInfo = account.LoginData;
|
|
125
132
|
const contextKey = accountInfo?.ContextKey;
|
|
126
|
-
const useFahrenheit = accountInfo?.UseFahrenheit ?? false;
|
|
127
133
|
this.contextKey = contextKey;
|
|
128
134
|
|
|
129
135
|
const debugData = {
|
|
@@ -145,52 +151,33 @@ class MelCloud extends EventEmitter {
|
|
|
145
151
|
this.axiosInstancePost = axios.create({
|
|
146
152
|
method: 'POST',
|
|
147
153
|
baseURL: ApiUrls.BaseURL,
|
|
154
|
+
timeout: 15000,
|
|
148
155
|
headers: {
|
|
149
156
|
'X-MitsContextKey': contextKey,
|
|
150
157
|
'content-type': 'application/json'
|
|
151
|
-
}
|
|
152
|
-
...this.axiosDefaults
|
|
158
|
+
}
|
|
153
159
|
});
|
|
154
160
|
|
|
155
161
|
await this.functions.saveData(this.accountFile, accountInfo);
|
|
156
|
-
|
|
157
162
|
this.emit('success', `Connect to MELCloud Success`);
|
|
158
163
|
|
|
159
|
-
return
|
|
160
|
-
accountInfo,
|
|
161
|
-
contextKey,
|
|
162
|
-
useFahrenheit
|
|
163
|
-
};
|
|
164
|
+
return accountInfo
|
|
164
165
|
} catch (error) {
|
|
165
166
|
throw new Error(`Connect to MELCloud error: ${error.message}`);
|
|
166
167
|
}
|
|
167
168
|
}
|
|
168
169
|
|
|
169
|
-
async
|
|
170
|
-
if (this.logDebug) this.emit('debug', `Connecting to MELCloud Home`);
|
|
171
|
-
|
|
170
|
+
async checkMelcloudHomeDevicesList(contextKey) {
|
|
172
171
|
try {
|
|
173
|
-
const c1 = cookieC1.trim();
|
|
174
|
-
const c2 = cookieC2.trim();
|
|
175
|
-
|
|
176
|
-
const cookieString = [
|
|
177
|
-
'__Secure-monitorandcontrol=chunks-2',
|
|
178
|
-
`__Secure-monitorandcontrolC1=${c1}`,
|
|
179
|
-
`__Secure-monitorandcontrolC2=${c2}`,
|
|
180
|
-
].join('; ');
|
|
181
|
-
|
|
182
172
|
const axiosInstance = axios.create({
|
|
173
|
+
method: 'GET',
|
|
183
174
|
baseURL: ApiUrlsHome.BaseURL,
|
|
184
|
-
timeout:
|
|
185
|
-
httpsAgent: new Agent({
|
|
186
|
-
keepAlive: false,
|
|
187
|
-
rejectUnauthorized: false
|
|
188
|
-
}),
|
|
175
|
+
timeout: 25000,
|
|
189
176
|
headers: {
|
|
190
177
|
'Accept': '*/*',
|
|
191
178
|
'Accept-Language': 'en-US,en;q=0.9',
|
|
192
|
-
'Cookie':
|
|
193
|
-
'User-Agent': 'homebridge-melcloud-
|
|
179
|
+
'Cookie': contextKey,
|
|
180
|
+
'User-Agent': 'homebridge-melcloud-control/4.0.0',
|
|
194
181
|
'DNT': '1',
|
|
195
182
|
'Origin': 'https://melcloudhome.com',
|
|
196
183
|
'Referer': 'https://melcloudhome.com/dashboard',
|
|
@@ -201,91 +188,227 @@ class MelCloud extends EventEmitter {
|
|
|
201
188
|
}
|
|
202
189
|
});
|
|
203
190
|
|
|
204
|
-
|
|
205
|
-
const
|
|
206
|
-
|
|
191
|
+
if (this.logDebug) this.emit('debug', `Scanning for devices`);
|
|
192
|
+
const listDevicesData = await axiosInstance(ApiUrlsHome.GetUserContext);
|
|
193
|
+
const buildingsList = listDevicesData.data.buildings;
|
|
194
|
+
if (this.logDebug) this.emit('debug', `Buildings: ${JSON.stringify(buildingsList, null, 2)}`);
|
|
207
195
|
|
|
208
|
-
|
|
196
|
+
if (!buildingsList) {
|
|
197
|
+
if (this.logWarn) this.emit('warn', `No building found`);
|
|
198
|
+
return null;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
await this.functions.saveData(this.buildingsFile, buildingsList);
|
|
202
|
+
if (this.logDebug) this.emit('debug', `Buildings list saved`);
|
|
209
203
|
|
|
210
|
-
|
|
204
|
+
const devices = buildingsList.flatMap(building => {
|
|
205
|
+
// Funkcja kapitalizująca klucze obiektu
|
|
206
|
+
const capitalizeKeys = obj =>
|
|
207
|
+
Object.fromEntries(
|
|
208
|
+
Object.entries(obj).map(([key, value]) => [
|
|
209
|
+
key.charAt(0).toUpperCase() + key.slice(1),
|
|
210
|
+
value
|
|
211
|
+
])
|
|
212
|
+
);
|
|
213
|
+
|
|
214
|
+
// Funkcja tworząca finalny obiekt Device
|
|
215
|
+
const createDevice = (device, type, contextKey) => {
|
|
216
|
+
// Settings już kapitalizowane w nazwach
|
|
217
|
+
const settingsArray = device.Settings || [];
|
|
218
|
+
|
|
219
|
+
const settingsObject = Object.fromEntries(
|
|
220
|
+
settingsArray.map(({ name, value }) => {
|
|
221
|
+
let parsedValue = value;
|
|
222
|
+
if (value === "True") parsedValue = true;
|
|
223
|
+
else if (value === "False") parsedValue = false;
|
|
224
|
+
else if (!isNaN(value) && value !== "") parsedValue = Number(value);
|
|
225
|
+
|
|
226
|
+
const key = name.charAt(0).toUpperCase() + name.slice(1);
|
|
227
|
+
return [key, parsedValue];
|
|
228
|
+
})
|
|
229
|
+
);
|
|
230
|
+
|
|
231
|
+
// Scal Capabilities + Settings + DeviceType w Device
|
|
232
|
+
const deviceObject = {
|
|
233
|
+
...capitalizeKeys(device.Capabilities || {}),
|
|
234
|
+
...settingsObject,
|
|
235
|
+
DeviceType: type
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
// Usuń stare pola Settings i Capabilities
|
|
239
|
+
const { Settings, Capabilities, Id, GivenDisplayName, ...rest } = device;
|
|
240
|
+
|
|
241
|
+
return {
|
|
242
|
+
...rest,
|
|
243
|
+
ContextKey: contextKey,
|
|
244
|
+
Type: type,
|
|
245
|
+
DeviceID: Id,
|
|
246
|
+
DeviceName: GivenDisplayName,
|
|
247
|
+
Device: deviceObject
|
|
248
|
+
};
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
return [
|
|
252
|
+
...(building.airToAirUnits || []).map(d => createDevice(capitalizeKeys(d), 0, this.contextKey)),
|
|
253
|
+
...(building.airToWaterUnits || []).map(d => createDevice(capitalizeKeys(d), 1, this.contextKey)),
|
|
254
|
+
...(building.airToVentilationUnits || []).map(d => createDevice(capitalizeKeys(d), 3, this.contextKey))
|
|
255
|
+
];
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
const devicesCount = devices.length;
|
|
259
|
+
if (devicesCount === 0) {
|
|
260
|
+
if (this.logWarn) this.emit('warn', `No devices found`);
|
|
261
|
+
return null;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
await this.functions.saveData(this.devicesFile, devices);
|
|
265
|
+
if (this.logDebug) this.emit('debug', `${devicesCount} devices saved`);
|
|
266
|
+
|
|
267
|
+
return devices;
|
|
211
268
|
} catch (error) {
|
|
212
269
|
throw new Error(`Connect to MELCloud Home error: ${error.message}`);
|
|
213
270
|
}
|
|
214
271
|
}
|
|
215
272
|
|
|
216
|
-
|
|
273
|
+
// MELCloud Home
|
|
274
|
+
async connectToMelCloudHome(refresh = false) {
|
|
275
|
+
if (this.logDebug) this.emit('debug', `Connecting to MELCloud Home`);
|
|
276
|
+
|
|
277
|
+
let browser;
|
|
217
278
|
try {
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
279
|
+
browser = await puppeteer.launch({
|
|
280
|
+
headless: true,
|
|
281
|
+
args: ['--no-sandbox', '--disable-setuid-sandbox']
|
|
282
|
+
});
|
|
221
283
|
|
|
222
|
-
|
|
223
|
-
await page.goto(
|
|
284
|
+
const page = await browser.newPage();
|
|
285
|
+
await page.goto(ApiUrlsHome.BaseURL, { waitUntil: 'networkidle2' });
|
|
224
286
|
|
|
225
|
-
|
|
226
|
-
const buttons = await page.$$('button.btn--blue'); // wszystkie buttony z klasą btn--blue
|
|
287
|
+
const buttons = await page.$$('button.btn--blue');
|
|
227
288
|
let loginBtn = null;
|
|
289
|
+
|
|
228
290
|
for (const btn of buttons) {
|
|
229
291
|
const text = await page.evaluate(el => el.textContent, btn);
|
|
230
|
-
if (text.trim() === 'Zaloguj') {
|
|
292
|
+
if (text.trim() === 'Zaloguj' || text.trim() === 'Log In') {
|
|
231
293
|
loginBtn = btn;
|
|
232
294
|
break;
|
|
233
295
|
}
|
|
234
296
|
}
|
|
235
297
|
|
|
236
|
-
if (!loginBtn
|
|
298
|
+
if (!loginBtn && this.logWarn) this.emit('warn', `Login button not found`);
|
|
237
299
|
|
|
238
300
|
await Promise.all([
|
|
239
301
|
loginBtn.click(),
|
|
240
|
-
page.waitForNavigation({ waitUntil: 'networkidle2', timeout:
|
|
302
|
+
page.waitForNavigation({ waitUntil: 'networkidle2', timeout: 10000 })
|
|
241
303
|
]);
|
|
242
304
|
|
|
243
|
-
|
|
244
|
-
await page.waitForSelector('input[name="username"]', { timeout: 15000 });
|
|
245
|
-
|
|
246
|
-
this.emit('warn', 'Typing credentials...');
|
|
305
|
+
await page.waitForSelector('input[name="username"]', { timeout: 5000 });
|
|
247
306
|
await page.type('input[name="username"]', this.user, { delay: 50 });
|
|
248
307
|
await page.type('input[name="password"]', this.passwd, { delay: 50 });
|
|
249
308
|
|
|
250
|
-
// Kliknij przycisk logowania
|
|
251
309
|
const button1 = await page.$('input[type="submit"]');
|
|
252
310
|
await Promise.all([
|
|
253
311
|
button1.click(),
|
|
254
|
-
page.waitForNavigation({ waitUntil: 'networkidle2', timeout:
|
|
312
|
+
page.waitForNavigation({ waitUntil: 'networkidle2', timeout: 10000 })
|
|
255
313
|
]);
|
|
256
314
|
|
|
257
315
|
let c1 = null, c2 = null;
|
|
258
316
|
const start = Date.now();
|
|
259
317
|
|
|
260
|
-
// Loop max 20s, czekamy aż pojawią się cookies
|
|
261
318
|
while ((!c1 || !c2) && Date.now() - start < 20000) {
|
|
262
|
-
const cookies = await page.cookies();
|
|
319
|
+
const cookies = await page.browserContext().cookies();
|
|
263
320
|
c1 = cookies.find(c => c.name === '__Secure-monitorandcontrolC1')?.value || c1;
|
|
264
321
|
c2 = cookies.find(c => c.name === '__Secure-monitorandcontrolC2')?.value || c2;
|
|
265
322
|
if (!c1 || !c2) await new Promise(r => setTimeout(r, 500));
|
|
266
323
|
}
|
|
267
324
|
|
|
325
|
+
|
|
268
326
|
if (!c1 || !c2) {
|
|
269
|
-
|
|
270
|
-
|
|
327
|
+
if (this.logWarn) this.emit('warn', `Cookies C1/C2 missing`);
|
|
328
|
+
return null;
|
|
271
329
|
}
|
|
272
330
|
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
331
|
+
const contextKey = [
|
|
332
|
+
'__Secure-monitorandcontrol=chunks-2',
|
|
333
|
+
`__Secure-monitorandcontrolC1=${c1}`,
|
|
334
|
+
`__Secure-monitorandcontrolC2=${c2}`
|
|
335
|
+
].join('; ');
|
|
336
|
+
|
|
337
|
+
const accountInfo = { ContextKey: contextKey, UseFahrenheit: false };
|
|
338
|
+
this.contextKey = contextKey;
|
|
339
|
+
|
|
340
|
+
await this.functions.saveData(this.accountFile, accountInfo);
|
|
341
|
+
if (!refresh) this.emit('success', `Connect to MELCloud Home Success`);
|
|
342
|
+
|
|
343
|
+
return accountInfo;
|
|
344
|
+
} catch (error) {
|
|
345
|
+
if (error.message.includes('libnspr4.so') || error.message.includes('Failed to launch the browser process')) {
|
|
346
|
+
const inDocker = await this.functions.isRunningInDocker();
|
|
347
|
+
if (this.logWarn) this.emit('warn', `Missing system libraries detected.`);
|
|
276
348
|
|
|
277
|
-
|
|
278
|
-
|
|
349
|
+
if (inDocker) {
|
|
350
|
+
this.emit('warn', `Running in Docker — attempting automatic fix...`);
|
|
279
351
|
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
352
|
+
const installCmd =
|
|
353
|
+
'apt-get update && apt-get install -y ' +
|
|
354
|
+
'libnss3 libnspr4 libatk1.0-0 libatk-bridge2.0-0 libcups2 ' +
|
|
355
|
+
'libxcomposite1 libxrandr2 libxdamage1 libxkbcommon0 libpango-1.0-0 ' +
|
|
356
|
+
'libgbm1 libasound2 libxshmfence1 fonts-liberation libappindicator3-1 libu2f-udev';
|
|
357
|
+
|
|
358
|
+
try {
|
|
359
|
+
execSync(installCmd, { stdio: 'inherit' });
|
|
360
|
+
if (this.logDebug) this.emit('debug', `Libraries installed. Retrying Puppeteer...`);
|
|
361
|
+
|
|
362
|
+
const testBrowser = await puppeteer.launch({
|
|
363
|
+
headless: true,
|
|
364
|
+
args: ['--no-sandbox', '--disable-setuid-sandbox']
|
|
365
|
+
});
|
|
366
|
+
await testBrowser.close();
|
|
367
|
+
|
|
368
|
+
this.emit('success', `Puppeteer repaired and running, try again.`);
|
|
369
|
+
return true;
|
|
370
|
+
} catch (fixError) {
|
|
371
|
+
throw new Error(`Automatic fix failed. Run manually inside container:\n${installCmd}`);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
} else {
|
|
375
|
+
throw new Error(`System libraries missing. Non-Docker environment detected — install dependencies manually.`);
|
|
376
|
+
}
|
|
377
|
+
} else {
|
|
378
|
+
throw new Error(`Puppeteer failed: ${error.message}`);
|
|
379
|
+
}
|
|
283
380
|
} finally {
|
|
284
|
-
await browser.close();
|
|
381
|
+
if (browser) await browser.close().catch(() => { });
|
|
285
382
|
}
|
|
286
383
|
}
|
|
287
384
|
|
|
385
|
+
async checkDevicesList(contextKey) {
|
|
386
|
+
let devices = [];
|
|
387
|
+
switch (this.accountType) {
|
|
388
|
+
case "melcloud":
|
|
389
|
+
devices = await this.checkMelcloudDevicesList(contextKey);
|
|
390
|
+
return devices
|
|
391
|
+
case "melcloudhome":
|
|
392
|
+
devices = await this.checkMelcloudHomeDevicesList(contextKey);
|
|
393
|
+
return devices;
|
|
394
|
+
default:
|
|
395
|
+
return devices;
|
|
396
|
+
}
|
|
397
|
+
}
|
|
288
398
|
|
|
399
|
+
async connect(refresh) {
|
|
400
|
+
let response = {};
|
|
401
|
+
switch (this.accountType) {
|
|
402
|
+
case "melcloud":
|
|
403
|
+
response = await this.connectToMelCloud();
|
|
404
|
+
return response
|
|
405
|
+
case "melcloudhome":
|
|
406
|
+
response = await this.connectToMelCloudHome(refresh);
|
|
407
|
+
return response
|
|
408
|
+
default:
|
|
409
|
+
return response
|
|
410
|
+
}
|
|
411
|
+
}
|
|
289
412
|
|
|
290
413
|
async send(accountInfo) {
|
|
291
414
|
try {
|