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/src/melcloud.js CHANGED
@@ -1,21 +1,22 @@
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';
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, cookiesFile, logWarn, logDebug, requestConfig) {
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
- async checkDevicesList(contextKey) {
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
- headers: { 'X-MitsContextKey': contextKey },
67
- ...this.axiosDefaults
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 connect() {
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
- ...this.axiosDefaults
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 connectHome(cookieC1, cookieC2) {
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: 10000,
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': cookieString,
193
- 'User-Agent': 'homebridge-melcloud-home/0.2.0',
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
- const response = await axiosInstance.get(ApiUrlsHome.GetUserContext);
205
- const homeData = response.data;
206
- if (this.logDebug) this.emit('debug', `[MELCloudHome] Response data: ${JSON.stringify(homeData, null, 2)}`);
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
- this.emit('success', `Connect to MELCloud Home Success`);
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
- return
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
- async connectHomeCookies() {
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
- const loginUrl = 'https://melcloudhome.com';
219
- const browser = await puppeteer.launch({ headless: true, args: ['--no-sandbox', '--disable-setuid-sandbox'] });
220
- const page = await browser.newPage();
279
+ browser = await puppeteer.launch({
280
+ headless: true,
281
+ args: ['--no-sandbox', '--disable-setuid-sandbox']
282
+ });
221
283
 
222
- this.emit('warn', 'Opening login page...');
223
- await page.goto(loginUrl, { waitUntil: 'networkidle2' });
284
+ const page = await browser.newPage();
285
+ await page.goto(ApiUrlsHome.BaseURL, { waitUntil: 'networkidle2' });
224
286
 
225
- // Kliknij przycisk logowania
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) throw new Error('Login button not found on homepage');
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: 20000 })
302
+ page.waitForNavigation({ waitUntil: 'networkidle2', timeout: 10000 })
241
303
  ]);
242
304
 
243
- // Poczekaj, strona przekieruje do Cognito login
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: 20000 })
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
- await browser.close();
270
- throw new Error('❌ Cookies C1/C2 not found after login');
327
+ if (this.logWarn) this.emit('warn', `Cookies C1/C2 missing`);
328
+ return null;
271
329
  }
272
330
 
273
- // Zapis do pliku
274
- const data = { C1: c1, C2: c2, date: new Date().toISOString() };
275
- await this.functions.saveData(this.cookiesFile, data);
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
- this.emit('warn', 'Login successful.');
278
- return data;
349
+ if (inDocker) {
350
+ this.emit('warn', `Running in Docker — attempting automatic fix...`);
279
351
 
280
- } catch (err) {
281
- this.emit('error', `Login failed: ${err.message}`);
282
- return null;
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 {