homebridge-melcloud-control 4.0.0-beta.52 → 4.0.0-beta.521

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,111 +1,140 @@
1
- import { Agent } from 'https';
2
1
  import axios from 'axios';
3
- import puppeteer from 'puppeteer';
2
+ import { exec } from 'child_process';
3
+ import { promisify } from 'util';
4
4
  import EventEmitter from 'events';
5
+ import puppeteer from 'puppeteer-core';
5
6
  import ImpulseGenerator from './impulsegenerator.js';
6
7
  import Functions from './functions.js';
7
8
  import { ApiUrls, ApiUrlsHome } from './constants.js';
9
+ const execPromise = promisify(exec);
8
10
 
9
11
  class MelCloud extends EventEmitter {
10
- constructor(displayType, user, passwd, language, accountFile, buildingsFile, devicesFile, logWarn, logDebug, requestConfig) {
12
+ constructor(account, accountFile, buildingsFile, devicesFile, pluginStart = false) {
11
13
  super();
12
- this.displayType = displayType;
13
- this.user = user;
14
- this.passwd = passwd;
15
- this.language = language;
14
+ this.accountType = account.type;
15
+ this.user = account.user;
16
+ this.passwd = account.passwd;
17
+ this.language = account.language;
18
+ this.logWarn = account.log?.warn;
19
+ this.logError = account.log?.error;
20
+ this.logDebug = account.log?.debug;
16
21
  this.accountFile = accountFile;
17
22
  this.buildingsFile = buildingsFile;
18
23
  this.devicesFile = devicesFile;
19
- this.logWarn = logWarn;
20
- this.logDebug = logDebug;
21
- this.requestConfig = requestConfig;
22
24
  this.devicesId = [];
23
- this.contextKey = '';
24
- this.functions = new Functions();
25
-
26
- this.loginData = {
27
- Email: user,
28
- Password: passwd,
29
- Language: language,
30
- AppVersion: '1.34.12',
31
- CaptchaChallenge: '',
32
- CaptchaResponse: '',
33
- Persist: true
34
- };
35
-
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
- if (!requestConfig) {
25
+ this.contextKey = null;
26
+ this.functions = new Functions(this.logWarn, this.logError, this.logDebug)
27
+ .on('warn', warn => this.emit('warn', warn))
28
+ .on('error', error => this.emit('error', error))
29
+ .on('debug', debug => this.emit('debug', debug));
30
+
31
+ if (pluginStart) {
32
+ //lock flags
33
+ this.locks = {
34
+ connect: false,
35
+ checkDevicesList: false
36
+ };
47
37
  this.impulseGenerator = new ImpulseGenerator()
48
- .on('checkDevicesList', async () => {
49
- try {
50
- await this.checkDevicesList(this.contextKey);
51
- } catch (error) {
52
- this.emit('error', `Impulse generator error: ${error}`);
53
- }
54
- })
38
+ .on('connect', () => this.handleWithLock('connect', async () => {
39
+ await this.connect(true);
40
+ }))
41
+ .on('checkDevicesList', () => this.handleWithLock('checkDevicesList', async () => {
42
+ await this.checkDevicesList();
43
+ }))
55
44
  .on('state', (state) => {
56
- this.emit('success', `Impulse generator ${state ? 'started' : 'stopped'}.`);
45
+ this.emit(state ? 'success' : 'warn', `Impulse generator ${state ? 'started' : 'stopped'}`);
57
46
  });
58
47
  }
59
48
  }
60
49
 
61
- async checkMelcloudDevicesList(contextKey) {
50
+ async handleWithLock(lockKey, fn) {
51
+ if (this.locks[lockKey]) return;
52
+
53
+ this.locks[lockKey] = true;
54
+ try {
55
+ await fn();
56
+ } catch (error) {
57
+ this.emit('error', `Inpulse generator error: ${error}`);
58
+ } finally {
59
+ this.locks[lockKey] = false;
60
+ }
61
+ }
62
+
63
+ // MELCloud
64
+ async checkMelcloudDevicesList() {
62
65
  try {
63
- const axiosInstanceGet = axios.create({
66
+ const devicesList = { State: false, Info: null, Devices: [] }
67
+ const axiosInstance = axios.create({
64
68
  method: 'GET',
65
69
  baseURL: ApiUrls.BaseURL,
66
- headers: { 'X-MitsContextKey': contextKey },
67
- ...this.axiosDefaults
70
+ timeout: 15000,
71
+ headers: { 'X-MitsContextKey': this.contextKey }
68
72
  });
69
73
 
70
- if (this.logDebug) this.emit('debug', `Scanning for devices`);
71
- const listDevicesData = await axiosInstanceGet(ApiUrls.ListDevices);
74
+ if (this.logDebug) this.emit('debug', `Scanning for devices...`);
75
+
76
+ const listDevicesData = await axiosInstance(ApiUrls.ListDevices);
77
+
78
+ if (!listDevicesData || !listDevicesData.data) {
79
+ if (this.logWarn) this.emit('warn', `Invalid or empty response from MELCloud API`);
80
+ return null;
81
+ }
82
+
72
83
  const buildingsList = listDevicesData.data;
73
- if (this.logDebug) this.emit('debug', `Buildings: ${JSON.stringify(buildingsList, null, 2)}`);
74
84
 
75
- if (!buildingsList) {
76
- if (this.logWarn) this.emit('warn', `No building found`);
85
+ if (this.logDebug)
86
+ this.emit('debug', `Buildings: ${JSON.stringify(buildingsList, null, 2)}`);
87
+
88
+ if (!Array.isArray(buildingsList) || buildingsList.length === 0) {
89
+ if (this.logWarn) this.emit('warn', `No buildings found in MELCloud account`);
77
90
  return null;
78
91
  }
79
92
 
80
93
  await this.functions.saveData(this.buildingsFile, buildingsList);
81
94
  if (this.logDebug) this.emit('debug', `Buildings list saved`);
82
95
 
83
- const devices = [];
84
96
  for (const building of buildingsList) {
85
- const buildingStructure = building.Structure;
97
+ if (!building.Structure) {
98
+ this.emit('warn', `Building missing structure: ${building.BuildingName || 'Unnamed'}`);
99
+ continue;
100
+ }
101
+
102
+ const { Structure } = building;
103
+
86
104
  const allDevices = [
87
- ...buildingStructure.Floors.flatMap(floor => [
88
- ...floor.Areas.flatMap(area => area.Devices),
89
- ...floor.Devices
90
- ]),
91
- ...buildingStructure.Areas.flatMap(area => area.Devices),
92
- ...buildingStructure.Devices
93
- ];
94
- devices.push(...allDevices);
105
+ ...(Structure.Floors?.flatMap(floor => [
106
+ ...(floor.Areas?.flatMap(area => area.Devices || []) || []),
107
+ ...(floor.Devices || [])
108
+ ]) || []),
109
+ ...(Structure.Areas?.flatMap(area => area.Devices || []) || []),
110
+ ...(Structure.Devices || [])
111
+ ].filter(d => d != null);
112
+
113
+ // Zamiana ID na string
114
+ allDevices.forEach(device => {
115
+ if (device.DeviceID != null) device.DeviceID = String(device.DeviceID);
116
+ });
117
+
118
+ if (this.logDebug) this.emit('debug', `Found ${allDevices.length} devices in building: ${building.Name || 'Unnamed'}`);
119
+ devicesList.Devices.push(...allDevices);
95
120
  }
96
121
 
97
- const devicesCount = devices.length;
122
+ const devicesCount = devicesList.Devices.length;
98
123
  if (devicesCount === 0) {
99
- if (this.logWarn) this.emit('warn', `No devices found`);
100
- return null;
124
+ devicesList.State = false;
125
+ devicesList.Info = 'No devices found'
126
+ return devicesList;
101
127
  }
102
128
 
103
- await this.functions.saveData(this.devicesFile, devices);
129
+ await this.functions.saveData(this.devicesFile, devicesList.Devices);
104
130
  if (this.logDebug) this.emit('debug', `${devicesCount} devices saved`);
105
131
 
106
- return devices;
132
+ devicesList.State = true;
133
+ devicesList.Info = `Found ${devicesCount} devices`;
134
+ return devicesList;
107
135
  } catch (error) {
108
- throw new Error(`Check devices list error: ${error.message}`);
136
+ const msg = error.response ? `HTTP ${error.response.status}: ${error.response.statusText}` : error.message;
137
+ throw new Error(`Check devices list error: ${msg}`);
109
138
  }
110
139
  }
111
140
 
@@ -113,20 +142,30 @@ class MelCloud extends EventEmitter {
113
142
  if (this.logDebug) this.emit('debug', `Connecting to MELCloud`);
114
143
 
115
144
  try {
116
- const axiosInstanceLogin = axios.create({
145
+ const accountInfo = { State: false, Info: '', LoginData: null, ContextKey: null, UseFahrenheit: false }
146
+ const axiosInstance = axios.create({
117
147
  method: 'POST',
118
148
  baseURL: ApiUrls.BaseURL,
119
- ...this.axiosDefaults
149
+ timeout: 15000,
120
150
  });
121
151
 
122
- const accountData = await axiosInstanceLogin(ApiUrls.ClientLogin, { data: this.loginData });
152
+ const data = {
153
+ Email: this.user,
154
+ Password: this.passwd,
155
+ Language: this.language,
156
+ AppVersion: '1.34.12',
157
+ CaptchaChallenge: '',
158
+ CaptchaResponse: '',
159
+ Persist: true
160
+ };
161
+
162
+ const accountData = await axiosInstance(ApiUrls.ClientLogin, { data: data });
123
163
  const account = accountData.data;
124
- const accountInfo = account.LoginData;
125
- const contextKey = accountInfo?.ContextKey;
126
- this.contextKey = contextKey;
164
+ const loginData = account.LoginData ?? [];
165
+ const contextKey = loginData.ContextKey;
127
166
 
128
167
  const debugData = {
129
- ...accountInfo,
168
+ ...loginData,
130
169
  ContextKey: 'removed',
131
170
  ClientId: 'removed',
132
171
  Client: 'removed',
@@ -137,38 +176,36 @@ class MelCloud extends EventEmitter {
137
176
  if (this.logDebug) this.emit('debug', `MELCloud Info: ${JSON.stringify(debugData, null, 2)}`);
138
177
 
139
178
  if (!contextKey) {
140
- if (this.logWarn) this.emit('warn', `Context key missing`);
141
- return null;
179
+ accountInfo.State = false;
180
+ accountInfo.Info = 'Context key missing'
181
+ return accountInfo;
142
182
  }
183
+ this.contextKey = contextKey;
143
184
 
144
- this.axiosInstancePost = axios.create({
145
- method: 'POST',
146
- baseURL: ApiUrls.BaseURL,
147
- headers: {
148
- 'X-MitsContextKey': contextKey,
149
- 'content-type': 'application/json'
150
- },
151
- ...this.axiosDefaults
152
- });
153
-
185
+ accountInfo.State = true;
186
+ accountInfo.Info = 'Connect to MELCloud Success';
187
+ accountInfo.LoginData = loginData;
188
+ accountInfo.ContextKey = contextKey;
154
189
  await this.functions.saveData(this.accountFile, accountInfo);
155
- this.emit('success', `Connect to MELCloud Success`);
156
190
 
157
191
  return accountInfo
158
192
  } catch (error) {
159
- throw new Error(`Connect to MELCloud error: ${error.message}`);
193
+ throw new Error(`Connect error: ${error.message}`);
160
194
  }
161
195
  }
162
196
 
163
- async checkMelcloudHomeDevicesList(contextKey) {
197
+ // MELCloud Home
198
+ async checkMelcloudHomeDevicesList() {
164
199
  try {
200
+ const devicesList = { State: false, Info: null, Devices: [] }
165
201
  const axiosInstance = axios.create({
166
202
  method: 'GET',
167
203
  baseURL: ApiUrlsHome.BaseURL,
204
+ timeout: 25000,
168
205
  headers: {
169
206
  'Accept': '*/*',
170
207
  'Accept-Language': 'en-US,en;q=0.9',
171
- 'Cookie': contextKey,
208
+ 'Cookie': this.contextKey,
172
209
  'User-Agent': 'homebridge-melcloud-control/4.0.0',
173
210
  'DNT': '1',
174
211
  'Origin': 'https://melcloudhome.com',
@@ -177,8 +214,7 @@ class MelCloud extends EventEmitter {
177
214
  'Sec-Fetch-Mode': 'cors',
178
215
  'Sec-Fetch-Site': 'same-origin',
179
216
  'X-CSRF': '1'
180
- },
181
- ...this.axiosDefaults
217
+ }
182
218
  });
183
219
 
184
220
  if (this.logDebug) this.emit('debug', `Scanning for devices`);
@@ -187,14 +223,16 @@ class MelCloud extends EventEmitter {
187
223
  if (this.logDebug) this.emit('debug', `Buildings: ${JSON.stringify(buildingsList, null, 2)}`);
188
224
 
189
225
  if (!buildingsList) {
190
- if (this.logWarn) this.emit('warn', `No building found`);
191
- return null;
226
+ devicesList.State = false;
227
+ devicesList.Info = 'No building found'
228
+ return devicesList;
192
229
  }
193
230
 
194
231
  await this.functions.saveData(this.buildingsFile, buildingsList);
195
232
  if (this.logDebug) this.emit('debug', `Buildings list saved`);
196
233
 
197
- const allDevices = buildingsList.flatMap(building => {
234
+ const devices = buildingsList.flatMap(building => {
235
+ // Funkcja kapitalizująca klucze obiektu
198
236
  const capitalizeKeys = obj =>
199
237
  Object.fromEntries(
200
238
  Object.entries(obj).map(([key, value]) => [
@@ -203,143 +241,270 @@ class MelCloud extends EventEmitter {
203
241
  ])
204
242
  );
205
243
 
244
+ // Funkcja tworząca finalny obiekt Device
245
+ const createDevice = (device, type) => {
246
+ // Settings już kapitalizowane w nazwach
247
+ const settingsArray = device.Settings || [];
248
+
249
+ const settingsObject = Object.fromEntries(
250
+ settingsArray.map(({ name, value }) => {
251
+ let parsedValue = value;
252
+ if (value === "True") parsedValue = true;
253
+ else if (value === "False") parsedValue = false;
254
+ else if (!isNaN(value) && value !== "") parsedValue = Number(value);
255
+
256
+ const key = name.charAt(0).toUpperCase() + name.slice(1);
257
+ return [key, parsedValue];
258
+ })
259
+ );
260
+
261
+ // Scal Capabilities + Settings + DeviceType w Device
262
+ const deviceObject = {
263
+ ...capitalizeKeys(device.Capabilities || {}),
264
+ ...settingsObject,
265
+ DeviceType: type
266
+ };
267
+
268
+ // Usuń stare pola Settings i Capabilities
269
+ const { Settings, Capabilities, Id, GivenDisplayName, ...rest } = device;
270
+
271
+ return {
272
+ ...rest,
273
+ ContextKey: this.contextKey,
274
+ Type: type,
275
+ DeviceID: Id,
276
+ DeviceName: GivenDisplayName,
277
+ Device: deviceObject
278
+ };
279
+ };
280
+
206
281
  return [
207
- ...(building.airToAirUnits || []).map(device => ({ ...capitalizeKeys(device), Type: 0 })),
208
- ...(building.airToWaterUnits || []).map(device => ({ ...capitalizeKeys(device), Type: 1 })),
209
- ...(building.airToVentilationUnits || []).map(device => ({ ...capitalizeKeys(device), Type: 3 }))
282
+ ...(building.airToAirUnits || []).map(d => createDevice(capitalizeKeys(d), 0)),
283
+ ...(building.airToWaterUnits || []).map(d => createDevice(capitalizeKeys(d), 1)),
284
+ ...(building.airToVentilationUnits || []).map(d => createDevice(capitalizeKeys(d), 3))
210
285
  ];
211
286
  });
212
287
 
213
- const devicesCount = allDevices.length;
288
+ const devicesCount = devices.length;
214
289
  if (devicesCount === 0) {
215
- if (this.logWarn) this.emit('warn', `No devices found`);
216
- return null;
290
+ devicesList.State = false;
291
+ devicesList.Info = 'No devices found'
292
+ return devicesList;
217
293
  }
218
294
 
219
- const devices = allDevices.map(device => {
220
- const settingsArray = device.Settings || device.settings || []; // obsługa różnych nazw
221
-
222
- // Konwersja tablicy [{ name, value }] → { Name: Value }
223
- const settingsObject = Object.fromEntries(
224
- settingsArray.map(({ name, value }) => [
225
- name.charAt(0).toUpperCase() + name.slice(1),
226
- value
227
- ])
228
- );
229
-
230
- return {
231
- ...device, // zachowuje wszystko z allDevices
232
- DeviceID: device.Id,
233
- DeviceName: device.GivenDisplayName,
234
- Settings: settingsObject,
235
- Device: device.Capabilities
236
- };
237
- });
238
-
239
-
240
295
  await this.functions.saveData(this.devicesFile, devices);
241
296
  if (this.logDebug) this.emit('debug', `${devicesCount} devices saved`);
242
297
 
243
- return devices;
298
+ devicesList.State = true;
299
+ devicesList.Info = `Found ${devicesCount} devices`;
300
+ devicesList.Devices = devices;
301
+ return devicesList;
244
302
  } catch (error) {
245
- throw new Error(`Connect to MELCloud Home error: ${error.message}`);
303
+ throw new Error(`Check devices list error: ${error.message}`);
246
304
  }
247
305
  }
248
306
 
249
307
  async connectToMelCloudHome() {
250
- if (this.logDebug) this.emit('debug', `Connecting to MELCloud Home`);
251
-
252
- const browser = await puppeteer.launch({ headless: true, args: ['--no-sandbox', '--disable-setuid-sandbox'] });
253
- const page = await browser.newPage();
308
+ if (this.logDebug) this.emit('debug', 'Connecting to MELCloud Home');
309
+ const GLOBAL_TIMEOUT = 90000;
254
310
 
311
+ let browser;
255
312
  try {
256
- // Open MELCloud Home
257
- await page.goto(ApiUrlsHome.BaseURL, { waitUntil: 'networkidle2' });
258
- const buttons = await page.$$('button.btn--blue');
259
- let loginBtn = null;
260
- for (const btn of buttons) {
261
- const text = await page.evaluate(el => el.textContent, btn);
262
- if (text.trim() === 'Zaloguj' || text.trim() === 'Log In') {
263
- loginBtn = btn;
264
- break;
265
- }
313
+ const accountInfo = { State: false, Info: '', ContextKey: null, UseFahrenheit: false };
314
+ const chromiumPath = await this.functions.ensureChromiumInstalled();
315
+ if (!chromiumPath) {
316
+ accountInfo.State = false;
317
+ accountInfo.Info = 'Chromium not found on Your device, please install it manually and try again';
318
+ return accountInfo;
266
319
  }
267
320
 
268
- if (!loginBtn && this.logWarn) this.emit('warn', `Login button not found`);
321
+ // Verify executable works
322
+ try {
323
+ const { stdout } = await execPromise(`"${chromiumPath}" --version`);
324
+ this.emit('warn', `Chromium detected: ${stdout.trim()}`);
325
+ if (this.logDebug) this.emit('debug', `Chromium detected: ${stdout.trim()}`);
326
+ } catch (error) {
327
+ accountInfo.State = false;
328
+ accountInfo.Info = `Chromium found at ${chromiumPath}, but cannot be executed: ${error.message}`;
329
+ return accountInfo;
330
+ }
331
+
332
+ this.emit('warn', `Launching Chromium...`);
333
+ browser = await puppeteer.launch({
334
+ headless: 'shell',
335
+ executablePath: chromiumPath,
336
+ timeout: GLOBAL_TIMEOUT,
337
+ args: [
338
+ '--no-sandbox',
339
+ '--disable-setuid-sandbox',
340
+ '--disable-dev-shm-usage',
341
+ '--single-process',
342
+ '--disable-gpu',
343
+ '--no-zygote'
344
+ ]
345
+ });
269
346
 
270
- // Set credentials and login
271
- await Promise.all([loginBtn.click(), page.waitForNavigation({ waitUntil: 'networkidle2', timeout: 20000 })]);
272
- await page.waitForSelector('input[name="username"]', { timeout: 15000 });
347
+ const [page] = await browser.pages();
348
+ page.setDefaultTimeout(GLOBAL_TIMEOUT);
349
+ page.setDefaultNavigationTimeout(GLOBAL_TIMEOUT);
350
+
351
+ page.on('error', err => this.emit('error', `Page crashed: ${err.message}`));
352
+ page.on('pageerror', err => this.emit('error', `Browser error: ${err.message}`));
353
+ browser.on('disconnected', () => this.emit('debug', 'Browser disconnected'));
354
+
355
+ this.emit('warn', `Navigating to MELCloud...`);
356
+ try {
357
+ await page.goto(ApiUrlsHome.BaseURL, { waitUntil: ['domcontentloaded', 'networkidle2'], timeout: GLOBAL_TIMEOUT });
358
+ } catch (error) {
359
+ accountInfo.State = false;
360
+ accountInfo.Info = `Navigation to ${ApiUrlsHome.BaseURL} failed: ${error.message}`;
361
+ return accountInfo;
362
+ }
363
+
364
+ // Wait extra to ensure UI is rendered
365
+ await new Promise(r => setTimeout(r, 3000));
366
+
367
+ this.emit('warn', `Looking for login button...`);
368
+ let loginBtn;
369
+ try {
370
+ loginBtn = await page.waitForFunction(() => {
371
+ const btns = Array.from(document.querySelectorAll('button.btn--blue'));
372
+ return btns.find(b => ['Zaloguj', 'Sign In', 'Login'].includes(b.textContent.trim()));
373
+ }, { timeout: GLOBAL_TIMEOUT / 6 });
374
+ } catch {
375
+ accountInfo.State = false;
376
+ accountInfo.Info = 'Login button not found';
377
+ return accountInfo;
378
+ }
379
+
380
+ this.emit('warn', `Found login button ${loginBtn}`);
381
+ await Promise.race([Promise.all([loginBtn.click(), page.waitForNavigation({ waitUntil: ['domcontentloaded', 'networkidle2'], timeout: GLOBAL_TIMEOUT / 4 })]), new Promise(r => setTimeout(r, GLOBAL_TIMEOUT / 3))]);
382
+
383
+ this.emit('warn', `Looking for credentials form...`);
384
+ const usernameInput = await page.$('input[name="username"]');
385
+ const passwordInput = await page.$('input[name="password"]');
386
+ if (!usernameInput || !passwordInput) {
387
+ accountInfo.State = false;
388
+ accountInfo.Info = 'Username or password input not found';
389
+ return accountInfo;
390
+ }
391
+
392
+ this.emit('warn', `Type credentials data..`);
273
393
  await page.type('input[name="username"]', this.user, { delay: 50 });
274
394
  await page.type('input[name="password"]', this.passwd, { delay: 50 });
275
395
 
276
- const button1 = await page.$('input[type="submit"]');
277
- await Promise.all([button1.click(), page.waitForNavigation({ waitUntil: 'networkidle2', timeout: 20000 })]);
396
+ this.emit('warn', `Looking for submit button...`);
397
+ const submitButton = await page.$('input[type="submit"], button[type="submit"]');
398
+ if (!submitButton) {
399
+ accountInfo.State = false;
400
+ accountInfo.Info = 'Submit button not found';
401
+ return accountInfo;
402
+ }
403
+
404
+ this.emit('warn', `Found submit button ${submitButton}`);
405
+ await Promise.race([Promise.all([submitButton.click(), page.waitForNavigation({ waitUntil: ['domcontentloaded', 'networkidle2'], timeout: GLOBAL_TIMEOUT / 4 })]), new Promise(r => setTimeout(r, GLOBAL_TIMEOUT / 3))]);
278
406
 
279
- // Get cookies C1 and C2
407
+ this.emit('warn', `Looking for cookies...`);
408
+ // Extract cookies
280
409
  let c1 = null, c2 = null;
281
410
  const start = Date.now();
282
-
283
- // Loop max 20s
284
- while ((!c1 || !c2) && Date.now() - start < 20000) {
285
- const cookies = await page.cookies();
411
+ while ((!c1 || !c2) && Date.now() - start < GLOBAL_TIMEOUT / 2) {
412
+ const cookies = await page.browserContext().cookies();
286
413
  c1 = cookies.find(c => c.name === '__Secure-monitorandcontrolC1')?.value || c1;
287
414
  c2 = cookies.find(c => c.name === '__Secure-monitorandcontrolC2')?.value || c2;
288
415
  if (!c1 || !c2) await new Promise(r => setTimeout(r, 500));
289
416
  }
290
417
 
291
418
  if (!c1 || !c2) {
292
- if (this.logWarn) this.emit('warn', `Cookies C1/C2 missing`);
293
- return null;
419
+ accountInfo.State = false;
420
+ accountInfo.Info = 'Cookies C1/C2 missing';
421
+ return accountInfo;
294
422
  }
295
423
 
296
- const contextKey = ['__Secure-monitorandcontrol=chunks-2', `__Secure-monitorandcontrolC1=${c1}`, `__Secure-monitorandcontrolC2=${c2}`,].join('; ');
297
- const accountInfo = { ContextKey: contextKey, UseFahrenheit: false };
424
+ this.emit('warn', `Found cookies`);
425
+ const contextKey = [
426
+ '__Secure-monitorandcontrol=chunks-2',
427
+ `__Secure-monitorandcontrolC1=${c1}`,
428
+ `__Secure-monitorandcontrolC2=${c2}`
429
+ ].join('; ');
298
430
  this.contextKey = contextKey;
299
431
 
432
+ accountInfo.State = true;
433
+ accountInfo.Info = 'Connect to MELCloud Home Success';
434
+ accountInfo.ContextKey = contextKey;
300
435
  await this.functions.saveData(this.accountFile, accountInfo);
301
- this.emit('success', `Connect to MELCloud Home Success`);
302
436
 
303
437
  return accountInfo;
304
438
  } catch (error) {
305
- throw new Error(`Connect to MELCloud Home error: ${error.message}`);
439
+ throw new Error(`Connect error: ${error.message}`);
306
440
  } finally {
307
- await browser.close();
441
+ if (browser) {
442
+ try { await browser.close(); }
443
+ catch (closeErr) { this.emit('error', `Failed to close Puppeteer: ${closeErr.message}`); }
444
+ }
308
445
  }
309
446
  }
310
447
 
311
- async connect() {
312
- let response = {};
313
- switch (this.displayType) {
314
- case "melcloud":
315
- response = await this.connectToMelCloud();
316
- return response
317
- case "melcloudhome":
318
- response = await this.connectToMelCloudHome();
319
- return response
320
- default:
321
- return response
448
+ async checkDevicesList() {
449
+ const TIMEOUT_MS = 30000; // 30 seconds timeout
450
+ try {
451
+ const devicesList = await Promise.race([
452
+ (async () => {
453
+ switch (this.accountType) {
454
+ case "melcloud":
455
+ return await this.checkMelcloudDevicesList();
456
+ case "melcloudhome":
457
+ return await this.checkMelcloudHomeDevicesList();
458
+ default:
459
+ return [];
460
+ }
461
+ })(),
462
+ new Promise((_, reject) => setTimeout(() => reject(new Error('Device list timeout (30s)')), TIMEOUT_MS))
463
+ ]);
464
+
465
+ return devicesList;
466
+ } catch (error) {
467
+ throw new Error(`Device list error: ${error.message}`);
322
468
  }
323
469
  }
324
470
 
325
- async checkDevicesList(contextKey) {
326
- let devices = [];
327
- switch (this.displayType) {
328
- case "melcloud":
329
- devices = await this.checkMelcloudDevicesList(contextKey);
330
- return devices
331
- case "melcloudhome":
332
- devices = await this.checkMelcloudHomeDevicesList(contextKey);
333
- return devices
334
- default:
335
- return devices;
471
+ async connect() {
472
+ const TIMEOUT_MS = 120000;
473
+
474
+ try {
475
+ const response = await Promise.race([
476
+ (async () => {
477
+ switch (this.accountType) {
478
+ case "melcloud":
479
+ return await this.connectToMelCloud();
480
+ case "melcloudhome":
481
+ return await this.connectToMelCloudHome();
482
+ default:
483
+ return {};
484
+ }
485
+ })(),
486
+ new Promise((_, reject) => setTimeout(() => reject(new Error('Connection timeout (90s)')), TIMEOUT_MS))
487
+ ]);
488
+
489
+ return response;
490
+ } catch (error) {
491
+ throw new Error(`Connect error: ${error.message}`);
336
492
  }
337
493
  }
338
494
 
339
495
  async send(accountInfo) {
340
496
  try {
341
- const options = { data: accountInfo };
342
- await this.axiosInstancePost(ApiUrls.UpdateApplicationOptions, options);
497
+ const axiosInstance = axios.create({
498
+ baseURL: ApiUrls.BaseURL,
499
+ timeout: 15000,
500
+ headers: {
501
+ 'X-MitsContextKey': accountInfo.ContextKey,
502
+ 'content-type': 'application/json'
503
+ }
504
+ });
505
+
506
+ const options = { data: accountInfo.LoginData };
507
+ await axiosInstance.post(ApiUrls.UpdateApplicationOptions, options);
343
508
  await this.functions.saveData(this.accountFile, accountInfo);
344
509
  return true;
345
510
  } catch (error) {