homebridge-melcloud-control 4.0.0-beta.31 → 4.0.0-beta.310

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/src/melcloud.js CHANGED
@@ -1,21 +1,26 @@
1
- import { Agent } from 'https';
2
- import axios from 'axios';
1
+
2
+ import { execSync } from 'child_process';
3
3
  import puppeteer from 'puppeteer';
4
+ import axios from 'axios';
4
5
  import EventEmitter from 'events';
6
+ import MelCloudHomeToken from './melcloudhometoken.js';
5
7
  import ImpulseGenerator from './impulsegenerator.js';
6
8
  import Functions from './functions.js';
7
9
  import { ApiUrls, ApiUrlsHome } from './constants.js';
10
+ import { URLSearchParams } from 'url';
11
+ const TOKEN_ENDPOINT = 'https://auth.melcloudhome.com/connect/token';
12
+ const REDIRECT_URI = 'melcloudhome://';
8
13
 
9
14
  class MelCloud extends EventEmitter {
10
- constructor(user, passwd, language, accountFile, buildingsFile, devicesFile, cookiesFile, logWarn, logDebug, requestConfig) {
15
+ constructor(accountType, user, passwd, language, accountFile, buildingsFile, devicesFile, logWarn, logDebug, requestConfig) {
11
16
  super();
17
+ this.accountType = accountType;
12
18
  this.user = user;
13
19
  this.passwd = passwd;
14
20
  this.language = language;
15
21
  this.accountFile = accountFile;
16
22
  this.buildingsFile = buildingsFile;
17
23
  this.devicesFile = devicesFile;
18
- this.cookiesFile = cookiesFile;
19
24
  this.logWarn = logWarn;
20
25
  this.logDebug = logDebug;
21
26
  this.requestConfig = requestConfig;
@@ -23,6 +28,12 @@ class MelCloud extends EventEmitter {
23
28
  this.contextKey = '';
24
29
  this.functions = new Functions();
25
30
 
31
+ this.axiosInstance = axios.create({
32
+ baseURL: 'https://app.melcloud.com/Mitsubishi.Wifi.Client',
33
+ headers: { 'User-Agent': 'MonitorAndControl.App.Mobile/35 CFNetwork/3860.100.1 Darwin/25.0.0' }
34
+ });
35
+ this.tokens = null;
36
+
26
37
  this.loginData = {
27
38
  Email: user,
28
39
  Password: passwd,
@@ -33,18 +44,15 @@ class MelCloud extends EventEmitter {
33
44
  Persist: true
34
45
  };
35
46
 
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
47
  if (!requestConfig) {
47
48
  this.impulseGenerator = new ImpulseGenerator()
49
+ .on('connect', async () => {
50
+ try {
51
+ await this.connect(true);
52
+ } catch (error) {
53
+ this.emit('error', `Impulse generator error: ${error}`);
54
+ }
55
+ })
48
56
  .on('checkDevicesList', async () => {
49
57
  try {
50
58
  await this.checkDevicesList(this.contextKey);
@@ -58,13 +66,14 @@ class MelCloud extends EventEmitter {
58
66
  }
59
67
  }
60
68
 
61
- async checkDevicesList(contextKey) {
69
+ // MELCloud
70
+ async checkMelcloudDevicesList(contextKey) {
62
71
  try {
63
72
  const axiosInstanceGet = axios.create({
64
73
  method: 'GET',
65
74
  baseURL: ApiUrls.BaseURL,
66
- headers: { 'X-MitsContextKey': contextKey },
67
- ...this.axiosDefaults
75
+ timeout: 15000,
76
+ headers: { 'X-MitsContextKey': contextKey }
68
77
  });
69
78
 
70
79
  if (this.logDebug) this.emit('debug', `Scanning for devices`);
@@ -91,6 +100,14 @@ class MelCloud extends EventEmitter {
91
100
  ...buildingStructure.Areas.flatMap(area => area.Devices),
92
101
  ...buildingStructure.Devices
93
102
  ];
103
+
104
+ // Zamiana DeviceID na string
105
+ allDevices.forEach(device => {
106
+ if (device.DeviceID !== undefined && device.DeviceID !== null) {
107
+ device.DeviceID = device.DeviceID.toString();
108
+ }
109
+ });
110
+
94
111
  devices.push(...allDevices);
95
112
  }
96
113
 
@@ -109,21 +126,20 @@ class MelCloud extends EventEmitter {
109
126
  }
110
127
  }
111
128
 
112
- async connectMelCloud() {
129
+ async connectToMelCloud() {
113
130
  if (this.logDebug) this.emit('debug', `Connecting to MELCloud`);
114
131
 
115
132
  try {
116
133
  const axiosInstanceLogin = axios.create({
117
134
  method: 'POST',
118
135
  baseURL: ApiUrls.BaseURL,
119
- ...this.axiosDefaults
136
+ timeout: 15000,
120
137
  });
121
138
 
122
139
  const accountData = await axiosInstanceLogin(ApiUrls.ClientLogin, { data: this.loginData });
123
140
  const account = accountData.data;
124
141
  const accountInfo = account.LoginData;
125
142
  const contextKey = accountInfo?.ContextKey;
126
- const useFahrenheit = accountInfo?.UseFahrenheit ?? false;
127
143
  this.contextKey = contextKey;
128
144
 
129
145
  const debugData = {
@@ -145,52 +161,33 @@ class MelCloud extends EventEmitter {
145
161
  this.axiosInstancePost = axios.create({
146
162
  method: 'POST',
147
163
  baseURL: ApiUrls.BaseURL,
164
+ timeout: 15000,
148
165
  headers: {
149
166
  'X-MitsContextKey': contextKey,
150
167
  'content-type': 'application/json'
151
- },
152
- ...this.axiosDefaults
168
+ }
153
169
  });
154
170
 
155
171
  await this.functions.saveData(this.accountFile, accountInfo);
156
-
157
172
  this.emit('success', `Connect to MELCloud Success`);
158
173
 
159
- return {
160
- accountInfo,
161
- contextKey,
162
- useFahrenheit
163
- };
174
+ return accountInfo
164
175
  } catch (error) {
165
176
  throw new Error(`Connect to MELCloud error: ${error.message}`);
166
177
  }
167
178
  }
168
179
 
169
- async connectMelCloudHome() {
170
- if (this.logDebug) this.emit('debug', `Connecting to MELCloud Home`);
171
-
180
+ // MELCloud Home
181
+ async checkMelcloudHomeDevicesList(contextKey) {
172
182
  try {
173
- const coocies = await this.getCookies();
174
- const c1 = coocies.C1.trim();
175
- const c2 = coocies.C2.trim();
176
-
177
- const cookies = [
178
- '__Secure-monitorandcontrol=chunks-2',
179
- `__Secure-monitorandcontrolC1=${c1}`,
180
- `__Secure-monitorandcontrolC2=${c2}`,
181
- ].join('; ');
182
-
183
183
  const axiosInstance = axios.create({
184
+ method: 'GET',
184
185
  baseURL: ApiUrlsHome.BaseURL,
185
- timeout: 10000,
186
- httpsAgent: new Agent({
187
- keepAlive: false,
188
- rejectUnauthorized: false
189
- }),
186
+ timeout: 25000,
190
187
  headers: {
191
188
  'Accept': '*/*',
192
189
  'Accept-Language': 'en-US,en;q=0.9',
193
- 'Cookie': cookies,
190
+ 'Cookie': contextKey,
194
191
  'User-Agent': 'homebridge-melcloud-control/4.0.0',
195
192
  'DNT': '1',
196
193
  'Origin': 'https://melcloudhome.com',
@@ -202,94 +199,277 @@ class MelCloud extends EventEmitter {
202
199
  }
203
200
  });
204
201
 
205
- const response = await axiosInstance.get(ApiUrlsHome.GetUserContext);
206
- const homeData = response.data;
207
- if (this.logDebug) this.emit('debug', `MELCloud Home data: ${JSON.stringify(homeData, null, 2)}`);
202
+ if (this.logDebug) this.emit('debug', `Scanning for devices`);
203
+ const listDevicesData = await axiosInstance(ApiUrlsHome.GetUserContext);
204
+ const buildingsList = listDevicesData.data.buildings;
205
+ if (this.logDebug) this.emit('debug', `Buildings: ${JSON.stringify(buildingsList, null, 2)}`);
206
+
207
+ if (!buildingsList) {
208
+ if (this.logWarn) this.emit('warn', `No building found`);
209
+ return null;
210
+ }
211
+
212
+ await this.functions.saveData(this.buildingsFile, buildingsList);
213
+ if (this.logDebug) this.emit('debug', `Buildings list saved`);
214
+
215
+ const devices = buildingsList.flatMap(building => {
216
+ // Funkcja kapitalizująca klucze obiektu
217
+ const capitalizeKeys = obj =>
218
+ Object.fromEntries(
219
+ Object.entries(obj).map(([key, value]) => [
220
+ key.charAt(0).toUpperCase() + key.slice(1),
221
+ value
222
+ ])
223
+ );
224
+
225
+ // Funkcja tworząca finalny obiekt Device
226
+ const createDevice = (device, type, contextKey) => {
227
+ // Settings już kapitalizowane w nazwach
228
+ const settingsArray = device.Settings || [];
229
+
230
+ const settingsObject = Object.fromEntries(
231
+ settingsArray.map(({ name, value }) => {
232
+ let parsedValue = value;
233
+ if (value === "True") parsedValue = true;
234
+ else if (value === "False") parsedValue = false;
235
+ else if (!isNaN(value) && value !== "") parsedValue = Number(value);
236
+
237
+ const key = name.charAt(0).toUpperCase() + name.slice(1);
238
+ return [key, parsedValue];
239
+ })
240
+ );
241
+
242
+ // Scal Capabilities + Settings + DeviceType w Device
243
+ const deviceObject = {
244
+ ...capitalizeKeys(device.Capabilities || {}),
245
+ ...settingsObject,
246
+ DeviceType: type
247
+ };
248
+
249
+ // Usuń stare pola Settings i Capabilities
250
+ const { Settings, Capabilities, Id, GivenDisplayName, ...rest } = device;
251
+
252
+ return {
253
+ ...rest,
254
+ ContextKey: contextKey,
255
+ Type: type,
256
+ DeviceID: Id,
257
+ DeviceName: GivenDisplayName,
258
+ Device: deviceObject
259
+ };
260
+ };
261
+
262
+ return [
263
+ ...(building.airToAirUnits || []).map(d => createDevice(capitalizeKeys(d), 0, this.contextKey)),
264
+ ...(building.airToWaterUnits || []).map(d => createDevice(capitalizeKeys(d), 1, this.contextKey)),
265
+ ...(building.airToVentilationUnits || []).map(d => createDevice(capitalizeKeys(d), 3, this.contextKey))
266
+ ];
267
+ });
268
+
269
+ const devicesCount = devices.length;
270
+ if (devicesCount === 0) {
271
+ if (this.logWarn) this.emit('warn', `No devices found`);
272
+ return null;
273
+ }
208
274
 
209
- this.emit('success', `Connect to MELCloud Home Success`);
275
+ await this.functions.saveData(this.devicesFile, devices);
276
+ if (this.logDebug) this.emit('debug', `${devicesCount} devices saved`);
210
277
 
211
- return
278
+ return devices;
212
279
  } catch (error) {
213
280
  throw new Error(`Connect to MELCloud Home error: ${error.message}`);
214
281
  }
215
282
  }
216
283
 
217
- async getCookies() {
218
- const browser = await puppeteer.launch({ headless: true, args: ['--no-sandbox', '--disable-setuid-sandbox'] });
219
- const page = await browser.newPage();
284
+ async connectToMelCloudHome() {
285
+ let browser;
286
+ try {
287
+
288
+ const melCloudHomeToken = new MelCloudHomeToken({
289
+ user: this.user,
290
+ passwd: this.passwd,
291
+ logWarn: this.logWarn,
292
+ logError: this.logError,
293
+ })
294
+ .on('success', message => this.emit('success', message))
295
+ .on('warn', warn => this.emit('warn', warn))
296
+ .on('error', error => this.emit('error', error));
297
+
298
+ const data = await melCloudHomeToken.buildAuthorizeUrl();
299
+ const code = await melCloudHomeToken.loginToMelCloudHome(data.url);
300
+
301
+ return false
302
+
303
+ browser = await puppeteer.launch({
304
+ headless: true,
305
+ args: ['--no-sandbox', '--disable-setuid-sandbox']
306
+ });
307
+
308
+ const page = await browser.newPage();
309
+ await page.goto(data.url, { waitUntil: 'networkidle2' });
310
+
311
+ await page.waitForSelector('input[name="username"]', { timeout: 5000 });
312
+ await page.type('input[name="username"]', this.user, { delay: 50 });
313
+ await page.type('input[name="password"]', this.passwd, { delay: 50 });
314
+ const button1 = await page.$('input[type="submit"]');
315
+ await Promise.all([button1.click(), page.waitForNavigation({ waitUntil: 'networkidle2', timeout: 10000 })]);
316
+ const html = await page.content();
317
+ const code1 = await melCloudHomeToken.extractCodeFromHtml(html);
318
+ this.emit('warn', code);
319
+
320
+ // 5. Wymień code na access_token
321
+ const tokenResponse = await melCloudHomeToken.getTokens(code, data.codeVerifier);
322
+ const tokenData = await tokenResponse.json();
323
+ this.emit('debug', `Token: ${JSON.stringify(tokenData, null, 2)}`);
324
+ const accountInfo = {
325
+ accessToken: tokenData.access_token,
326
+ refreshToken: tokenData.refresh_token,
327
+ expiresIn: tokenData.expires_in
328
+ };
329
+
330
+ return accountInfo;
331
+ } catch (error) {
332
+ if (browser) await browser.close().catch(() => { });
333
+ throw error;
334
+ }
335
+ }
336
+
337
+ async connectToMelCloudHome1(refresh = false) {
338
+ if (this.logDebug) this.emit('debug', `Connecting to MELCloud Home`);
220
339
 
340
+ let browser;
221
341
  try {
222
- this.emit('warn', 'Opening login page...');
342
+ browser = await puppeteer.launch({
343
+ headless: true,
344
+ args: ['--no-sandbox', '--disable-setuid-sandbox']
345
+ });
346
+
347
+ const page = await browser.newPage();
223
348
  await page.goto(ApiUrlsHome.BaseURL, { waitUntil: 'networkidle2' });
224
349
 
225
- // Kliknij przycisk logowania
226
350
  const buttons = await page.$$('button.btn--blue');
227
351
  let loginBtn = null;
352
+
228
353
  for (const btn of buttons) {
229
354
  const text = await page.evaluate(el => el.textContent, btn);
230
- if (text.trim() === 'Zaloguj') {
355
+ if (text.trim() === 'Zaloguj' || text.trim() === 'Sign In') {
231
356
  loginBtn = btn;
232
357
  break;
233
358
  }
234
359
  }
235
360
 
236
- if (!loginBtn) throw new Error('Login button not found on homepage');
361
+ if (!loginBtn && this.logWarn) this.emit('warn', `Login button not found`);
237
362
 
238
363
  await Promise.all([
239
364
  loginBtn.click(),
240
- page.waitForNavigation({ waitUntil: 'networkidle2', timeout: 20000 })
365
+ page.waitForNavigation({ waitUntil: 'networkidle2', timeout: 10000 })
241
366
  ]);
242
367
 
243
- // Poczekaj, strona przekieruje do Cognito login
244
- await page.waitForSelector('input[name="username"]', { timeout: 15000 });
245
-
246
- this.emit('warn', 'Typing credentials...');
368
+ await page.waitForSelector('input[name="username"]', { timeout: 5000 });
247
369
  await page.type('input[name="username"]', this.user, { delay: 50 });
248
370
  await page.type('input[name="password"]', this.passwd, { delay: 50 });
249
371
 
250
- // Kliknij przycisk logowania
251
372
  const button1 = await page.$('input[type="submit"]');
252
373
  await Promise.all([
253
374
  button1.click(),
254
- page.waitForNavigation({ waitUntil: 'networkidle2', timeout: 20000 })
375
+ page.waitForNavigation({ waitUntil: 'networkidle2', timeout: 10000 })
255
376
  ]);
256
377
 
257
378
  let c1 = null, c2 = null;
258
379
  const start = Date.now();
259
380
 
260
- // Loop max 20s, czekamy aż pojawią się cookies
261
381
  while ((!c1 || !c2) && Date.now() - start < 20000) {
262
- const cookies = await page.cookies();
382
+ const cookies = await page.browserContext().cookies();
263
383
  c1 = cookies.find(c => c.name === '__Secure-monitorandcontrolC1')?.value || c1;
264
384
  c2 = cookies.find(c => c.name === '__Secure-monitorandcontrolC2')?.value || c2;
265
385
  if (!c1 || !c2) await new Promise(r => setTimeout(r, 500));
266
386
  }
267
387
 
388
+
268
389
  if (!c1 || !c2) {
269
- await browser.close();
270
- throw new Error('Cookies C1/C2 not found after login');
390
+ if (this.logWarn) this.emit('warn', `Cookies C1/C2 missing`);
391
+ return null;
271
392
  }
272
393
 
273
- // Zapis do pliku
274
- const data = { C1: c1, C2: c2, date: new Date().toISOString() };
275
- await this.functions.saveData(this.cookiesFile, data);
394
+ const contextKey = [
395
+ '__Secure-monitorandcontrol=chunks-2',
396
+ `__Secure-monitorandcontrolC1=${c1}`,
397
+ `__Secure-monitorandcontrolC2=${c2}`
398
+ ].join('; ');
399
+
400
+ const accountInfo = { ContextKey: contextKey, UseFahrenheit: false };
401
+ this.contextKey = contextKey;
402
+
403
+ await this.functions.saveData(this.accountFile, accountInfo);
404
+ if (!refresh) this.emit('success', `Connect to MELCloud Home Success`);
405
+
406
+ return accountInfo;
407
+ } catch (error) {
408
+ if (error.message.includes('libnspr4.so') || error.message.includes('Failed to launch the browser process')) {
409
+ const inDocker = await this.functions.isRunningInDocker();
410
+ if (this.logWarn) this.emit('warn', `Missing system libraries detected.`);
411
+
412
+ if (inDocker) {
413
+ this.emit('warn', `Running in Docker — attempting automatic fix...`);
414
+
415
+ const installCmd =
416
+ 'apt-get update && apt-get install -y ' +
417
+ 'libnss3 libnspr4 libatk1.0-0 libatk-bridge2.0-0 libcups2 ' +
418
+ 'libxcomposite1 libxrandr2 libxdamage1 libxkbcommon0 libpango-1.0-0 ' +
419
+ 'libgbm1 libasound2 libxshmfence1 fonts-liberation libappindicator3-1 libu2f-udev';
420
+
421
+ try {
422
+ execSync(installCmd, { stdio: 'inherit' });
423
+ if (this.logDebug) this.emit('debug', `Libraries installed. Retrying Puppeteer...`);
424
+
425
+ const testBrowser = await puppeteer.launch({
426
+ headless: true,
427
+ args: ['--no-sandbox', '--disable-setuid-sandbox']
428
+ });
429
+ await testBrowser.close();
430
+
431
+ this.emit('success', `Puppeteer repaired and running, try again.`);
432
+ return true;
433
+ } catch (fixError) {
434
+ throw new Error(`Automatic fix failed. Run manually inside container:\n${installCmd}`);
435
+ }
276
436
 
277
- this.emit('warn', 'Login successful.');
278
- return data;
279
- } catch (err) {
280
- this.emit('error', `Login failed: ${err.message}`);
281
- return null;
437
+ } else {
438
+ throw new Error(`System libraries missing. Non-Docker environment detected — install dependencies manually.`);
439
+ }
440
+ } else {
441
+ throw new Error(`Puppeteer failed: ${error.message}`);
442
+ }
282
443
  } finally {
283
- await browser.close();
444
+ if (browser) await browser.close().catch(() => { });
445
+ }
446
+ }
447
+
448
+ async checkDevicesList(contextKey) {
449
+ let devices = [];
450
+ switch (this.accountType) {
451
+ case "melcloud":
452
+ devices = await this.checkMelcloudDevicesList(contextKey);
453
+ return devices
454
+ case "melcloudhome":
455
+ devices = await this.checkMelcloudHomeDevicesList(contextKey);
456
+ return devices;
457
+ default:
458
+ return devices;
284
459
  }
285
460
  }
286
461
 
287
- async connect(mode) {
288
- switch (mode) {
289
- case 'home':
290
- return await this.connectMelCloudHome();
462
+ async connect(refresh) {
463
+ let response = {};
464
+ switch (this.accountType) {
465
+ case "melcloud":
466
+ response = await this.connectToMelCloud();
467
+ return response
468
+ case "melcloudhome":
469
+ response = await this.connectToMelCloudHome(refresh);
470
+ return response
291
471
  default:
292
- return await this.connectMelCloud();
472
+ return response
293
473
  }
294
474
  }
295
475