homebridge-melcloud-control 4.2.5-beta.3 → 4.2.5-beta.31

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.
@@ -5,7 +5,7 @@ import Functions from './functions.js';
5
5
  import { ApiUrls, ApiUrlsHome, Ventilation } from './constants.js';
6
6
 
7
7
  class MelCloudErv extends EventEmitter {
8
- constructor(account, device, devicesFile, defaultTempsFile) {
8
+ constructor(account, device, devicesFile, defaultTempsFile, accountFile) {
9
9
  super();
10
10
  this.accountType = account.type;
11
11
  this.logWarn = account.log?.warn;
@@ -16,6 +16,7 @@ class MelCloudErv extends EventEmitter {
16
16
  this.deviceId = device.id;
17
17
  this.devicesFile = devicesFile;
18
18
  this.defaultTempsFile = defaultTempsFile;
19
+ this.accountFile = accountFile;
19
20
  this.functions = new Functions(this.logWarn, this.logError, this.logDebug)
20
21
  .on('warn', warn => this.emit('warn', warn))
21
22
  .on('error', error => this.emit('error', error))
@@ -26,7 +27,7 @@ class MelCloudErv extends EventEmitter {
26
27
  this.headers = {};
27
28
 
28
29
  //lock flags
29
- this.locks = true;
30
+ this.locks = false;
30
31
  this.impulseGenerator = new ImpulseGenerator()
31
32
  .on('checkState', () => this.handleWithLock(async () => {
32
33
  await this.checkState();
@@ -53,24 +54,19 @@ class MelCloudErv extends EventEmitter {
53
54
  try {
54
55
  //read device info from file
55
56
  const devicesData = await this.functions.readData(this.devicesFile, true);
57
+ if (!devicesData) return;
58
+
56
59
  this.headers = devicesData.Headers;
57
- const scenes = devicesData.Scenes ?? [];
58
60
  const deviceData = devicesData.Devices.find(device => device.DeviceID === this.deviceId);
59
-
60
61
  if (this.accountType === 'melcloudhome') {
61
- deviceData.Scenes = scenes;
62
+ deviceData.Scenes = devicesData.Scenes ?? [];
62
63
 
63
64
  //read default temps
64
65
  const temps = await this.functions.readData(this.defaultTempsFile, true);
65
66
  deviceData.Device.DefaultHeatingSetTemperature = temps?.defaultHeatingSetTemperature ?? 20;
66
67
  deviceData.Device.DefaultCoolingSetTemperature = temps?.defaultCoolingSetTemperature ?? 24;
67
68
  }
68
-
69
- const safeConfig = {
70
- ...deviceData,
71
- Headers: 'removed',
72
- };
73
- if (this.logDebug) this.emit('debug', `Device Data: ${JSON.stringify(safeConfig, null, 2)}`);
69
+ if (this.logDebug) this.emit('debug', `Device Data: ${JSON.stringify(deviceData, null, 2)}`);
74
70
 
75
71
  //device
76
72
  const serialNumber = deviceData.SerialNumber || '4.0.0';
@@ -132,50 +128,61 @@ class MelCloudErv extends EventEmitter {
132
128
  let path = '';
133
129
  switch (accountType) {
134
130
  case "melcloud":
135
- //set target temp based on display mode and ventilation mode
136
- switch (displayType) {
137
- case 1: //Heather/Cooler
138
- switch (deviceData.Device.VentilationMode) {
139
- case 0: //LOSNAY
140
- deviceData.Device.SetTemperature = deviceData.Device.DefaultHeatingSetTemperature;
141
- break;
142
- case 1: //BYPASS
143
- deviceData.Device.SetTemperature = deviceData.Device.DefaultCoolingSetTemperature;
144
- break;
145
- case 2: //AUTO
146
- const setTemperature = (deviceData.Device.DefaultCoolingSetTemperature + deviceData.Device.DefaultHeatingSetTemperature) / 2;
147
- deviceData.Device.SetTemperature = setTemperature;
131
+ switch (flag) {
132
+ case 'account':
133
+ flagData.Account.LoginData.UseFahrenheit = flagData.UseFahrenheit;
134
+ payload = { data: flagData.LoginData };
135
+ path = ApiUrls.UpdateApplicationOptions;
136
+ await this.functions.saveData(this.accountFile, flagData);
137
+ break;
138
+ default:
139
+ //set target temp based on display mode and ventilation mode
140
+ switch (displayType) {
141
+ case 1: //Heather/Cooler
142
+ switch (deviceData.Device.VentilationMode) {
143
+ case 0: //LOSNAY
144
+ deviceData.Device.SetTemperature = deviceData.Device.DefaultHeatingSetTemperature;
145
+ break;
146
+ case 1: //BYPASS
147
+ deviceData.Device.SetTemperature = deviceData.Device.DefaultCoolingSetTemperature;
148
+ break;
149
+ case 2: //AUTO
150
+ const setTemperature = (deviceData.Device.DefaultCoolingSetTemperature + deviceData.Device.DefaultHeatingSetTemperature) / 2;
151
+ deviceData.Device.SetTemperature = setTemperature;
152
+ break;
153
+ };
154
+ case 2: //Thermostat
155
+ deviceData.Device.SetTemperature = deviceData.Device.SetTemperature;
148
156
  break;
149
157
  };
150
- case 2: //Thermostat
151
- deviceData.Device.SetTemperature = deviceData.Device.SetTemperature;
152
- break;
153
- };
154
158
 
155
- //device state
156
- deviceData.Device.EffectiveFlags = flag;
157
- payload = {
158
- DeviceID: deviceData.Device.DeviceID,
159
- EffectiveFlags: deviceData.Device.EffectiveFlags,
160
- Power: deviceData.Device.Power,
161
- SetTemperature: deviceData.Device.SetTemperature,
162
- SetFanSpeed: deviceData.Device.SetFanSpeed,
163
- OperationMode: deviceData.Device.OperationMode,
164
- VentilationMode: deviceData.Device.VentilationMode,
165
- DefaultCoolingSetTemperature: deviceData.Device.DefaultCoolingSetTemperature,
166
- DefaultHeatingSetTemperature: deviceData.Device.DefaultHeatingSetTemperature,
167
- HideRoomTemperature: deviceData.Device.HideRoomTemperature,
168
- HideSupplyTemperature: deviceData.Device.HideSupplyTemperature,
169
- HideOutdoorTemperature: deviceData.Device.HideOutdoorTemperature,
170
- NightPurgeMode: deviceData.Device.NightPurgeMode,
171
- HasPendingCommand: true
159
+ //device state
160
+ deviceData.Device.EffectiveFlags = flag;
161
+ payload = {
162
+ DeviceID: deviceData.Device.DeviceID,
163
+ EffectiveFlags: deviceData.Device.EffectiveFlags,
164
+ Power: deviceData.Device.Power,
165
+ SetTemperature: deviceData.Device.SetTemperature,
166
+ SetFanSpeed: deviceData.Device.SetFanSpeed,
167
+ OperationMode: deviceData.Device.OperationMode,
168
+ VentilationMode: deviceData.Device.VentilationMode,
169
+ DefaultCoolingSetTemperature: deviceData.Device.DefaultCoolingSetTemperature,
170
+ DefaultHeatingSetTemperature: deviceData.Device.DefaultHeatingSetTemperature,
171
+ HideRoomTemperature: deviceData.Device.HideRoomTemperature,
172
+ HideSupplyTemperature: deviceData.Device.HideSupplyTemperature,
173
+ HideOutdoorTemperature: deviceData.Device.HideOutdoorTemperature,
174
+ NightPurgeMode: deviceData.Device.NightPurgeMode,
175
+ HasPendingCommand: true
176
+ }
177
+ path = ApiUrls.SetErv;
178
+ break;
172
179
  }
173
180
 
174
181
  if (this.logDebug) this.emit('debug', `Send Data: ${JSON.stringify(payload, null, 2)}`);
175
- await axios(ApiUrls.SetErv, {
182
+ await axios(path, {
176
183
  method: 'POST',
177
184
  baseURL: ApiUrls.BaseURL,
178
- timeout: 10000,
185
+ timeout: 30000,
179
186
  headers: this.headers,
180
187
  data: payload
181
188
  });
@@ -228,7 +235,7 @@ class MelCloudErv extends EventEmitter {
228
235
  };
229
236
  method = 'PUT';
230
237
  path = ApiUrlsHome.PutErv.replace('deviceid', deviceData.DeviceID);
231
- this.headers.Referer = ApiUrlsHome.Referers.PutDeviceSettings
238
+ this.headers.Referer = ApiUrlsHome.Referers.PutDeviceSettings;
232
239
  break
233
240
  }
234
241
 
@@ -238,7 +245,7 @@ class MelCloudErv extends EventEmitter {
238
245
  await axios(path, {
239
246
  method: method,
240
247
  baseURL: ApiUrlsHome.BaseURL,
241
- timeout: 10000,
248
+ timeout: 30000,
242
249
  headers: this.headers,
243
250
  data: payload
244
251
  });
@@ -254,12 +261,12 @@ class MelCloudErv extends EventEmitter {
254
261
  }
255
262
 
256
263
  updateData(deviceData) {
257
- this.lock = true;
264
+ this.locks = true;
258
265
  this.emit('deviceState', deviceData);
259
266
 
260
267
  setTimeout(() => {
261
- this.lock = false
262
- }, 2500);
268
+ this.locks = false
269
+ }, 5000);
263
270
  }
264
271
  };
265
272
  export default MelCloudErv;
@@ -9,7 +9,7 @@ import Functions from './functions.js';
9
9
  import { ApiUrlsHome, LanguageLocaleMap } from './constants.js';
10
10
  const execPromise = promisify(exec);
11
11
 
12
- class MelCloud extends EventEmitter {
12
+ class MelCloudHome extends EventEmitter {
13
13
  constructor(account, accountFile, buildingsFile, devicesFile, pluginStart = false) {
14
14
  super();
15
15
  this.accountType = account.type;
@@ -61,16 +61,10 @@ class MelCloud extends EventEmitter {
61
61
  }
62
62
  }
63
63
 
64
- // MELCloud Home
65
64
  async checkScenesList() {
66
65
  try {
67
66
  if (this.logDebug) this.emit('debug', `Scanning for scenes`);
68
- const listScenesData = await axios(ApiUrlsHome.GetUserScenes, {
69
- method: 'GET',
70
- baseURL: ApiUrlsHome.BaseURL,
71
- timeout: 25000,
72
- headers: this.headers
73
- });
67
+ const listScenesData = await this.axiosInstance(ApiUrlsHome.GetUserScenes, { method: 'GET', });
74
68
 
75
69
  const scenesList = listScenesData.data;
76
70
  if (this.logDebug) this.emit('debug', `Scenes: ${JSON.stringify(scenesList, null, 2)}`);
@@ -102,12 +96,7 @@ class MelCloud extends EventEmitter {
102
96
  try {
103
97
  const devicesList = { State: false, Info: null, Devices: [], Scenes: [] }
104
98
  if (this.logDebug) this.emit('debug', `Scanning for devices`);
105
- const listDevicesData = await axios(ApiUrlsHome.GetUserContext, {
106
- method: 'GET',
107
- baseURL: ApiUrlsHome.BaseURL,
108
- timeout: 25000,
109
- headers: this.headers
110
- });
99
+ const listDevicesData = await this.axiosInstance(ApiUrlsHome.GetUserContext, { method: 'GET' });
111
100
 
112
101
  const userContext = listDevicesData.data;
113
102
  const buildings = userContext.buildings ?? [];
@@ -205,10 +194,17 @@ class MelCloud extends EventEmitter {
205
194
  return devicesList;
206
195
  }
207
196
 
208
- const scenes = await this.checkScenesList();
197
+ // Get scenes
198
+ let scenes = [];
199
+ try {
200
+ scenes = await this.checkScenesList();
201
+ if (this.logDebug) this.emit('debug', `Found ${scenes.length} svenes`);
202
+ } catch (error) {
203
+ if (this.logDebug) this.emit('debug', `Get scenes error: ${error} `);
204
+ }
209
205
 
210
206
  devicesList.State = true;
211
- devicesList.Info = `Found ${devicesCount} devices`;
207
+ devicesList.Info = `Found ${devicesCount} devices and ${scenes.length} scenes`;
212
208
  devicesList.Devices = devices;
213
209
  devicesList.Scenes = scenes;
214
210
  devicesList.Headers = this.headers;
@@ -218,12 +214,6 @@ class MelCloud extends EventEmitter {
218
214
 
219
215
  return devicesList;
220
216
  } catch (error) {
221
- if (error.response?.status === 401) {
222
- if (this.logWarn) this.emit('warn', 'Check devices list not possible, cookies expired, trying to get new.');
223
- await this.connect();
224
- return;
225
- }
226
-
227
217
  throw new Error(`Check devices list error: ${error.message}`);
228
218
  }
229
219
  }
@@ -234,7 +224,7 @@ class MelCloud extends EventEmitter {
234
224
 
235
225
  let browser;
236
226
  try {
237
- const accountInfo = { State: false, Info: '', Headers: {}, UseFahrenheit: false };
227
+ const accountInfo = { State: false, Info: '', Account: {}, UseFahrenheit: false };
238
228
  let chromiumPath = await this.functions.ensureChromiumInstalled();
239
229
 
240
230
  // === Fallback to Puppeteer's built-in Chromium ===
@@ -286,14 +276,6 @@ class MelCloud extends EventEmitter {
286
276
  page.setDefaultTimeout(GLOBAL_TIMEOUT);
287
277
  page.setDefaultNavigationTimeout(GLOBAL_TIMEOUT);
288
278
 
289
- // Clear cookies before navigation
290
- try {
291
- const client = await page.createCDPSession();
292
- await client.send('Network.clearBrowserCookies');
293
- } catch (error) {
294
- if (this.logError) this.emit('error', `Clear cookies error: ${error.message}`);
295
- }
296
-
297
279
  try {
298
280
  await page.goto(ApiUrlsHome.BaseURL, { waitUntil: ['domcontentloaded', 'networkidle2'], timeout: GLOBAL_TIMEOUT });
299
281
  } catch (error) {
@@ -303,7 +285,7 @@ class MelCloud extends EventEmitter {
303
285
 
304
286
  // Wait extra to ensure UI is rendered
305
287
  await new Promise(r => setTimeout(r, 3000));
306
- const loginBtn = await page.waitForSelector('button.btn--blue', { timeout: GLOBAL_TIMEOUT / 4 });
288
+ const loginBtn = await page.waitForSelector('button.btn--blue', { timeout: GLOBAL_TIMEOUT / 3 });
307
289
  const loginText = await page.evaluate(el => el.textContent.trim(), loginBtn);
308
290
 
309
291
  if (!['Zaloguj', 'Sign In', 'Login'].includes(loginText)) {
@@ -329,13 +311,13 @@ class MelCloud extends EventEmitter {
329
311
  accountInfo.Info = 'Submit button not found';
330
312
  return accountInfo;
331
313
  }
332
- await Promise.race([Promise.all([submitButton.click(), page.waitForNavigation({ waitUntil: ['domcontentloaded', 'networkidle2'], timeout: GLOBAL_TIMEOUT / 4 })]), new Promise(r => setTimeout(r, GLOBAL_TIMEOUT / 3))]);
314
+ await Promise.race([Promise.all([submitButton.click(), page.waitForNavigation({ waitUntil: ['domcontentloaded', 'networkidle2'], timeout: GLOBAL_TIMEOUT / 3 })]), new Promise(r => setTimeout(r, GLOBAL_TIMEOUT / 3))]);
333
315
 
334
316
  // Extract cookies
335
317
  let c1 = null, c2 = null;
336
318
  const start = Date.now();
337
319
  while ((!c1 || !c2) && Date.now() - start < GLOBAL_TIMEOUT / 2) {
338
- const cookies = await page.browserContext().cookies();
320
+ const cookies = await browser.cookies();
339
321
  c1 = cookies.find(c => c.name === '__Secure-monitorandcontrolC1')?.value || c1;
340
322
  c2 = cookies.find(c => c.name === '__Secure-monitorandcontrolC2')?.value || c2;
341
323
  if (!c1 || !c2) await new Promise(r => setTimeout(r, 500));
@@ -352,7 +334,9 @@ class MelCloud extends EventEmitter {
352
334
  `__Secure-monitorandcontrolC2=${c2}`
353
335
  ].join('; ');
354
336
 
355
- this.headers = {
337
+
338
+ const userAgent = await page.evaluate(() => navigator.userAgent);
339
+ const headers = {
356
340
  'Accept': '*/*',
357
341
  'Accept-Encoding': 'gzip, deflate, br',
358
342
  'Accept-Language': LanguageLocaleMap[this.language],
@@ -362,12 +346,18 @@ class MelCloud extends EventEmitter {
362
346
  'Sec-Fetch-Dest': 'empty',
363
347
  'Sec-Fetch-Mode': 'cors',
364
348
  'Sec-Fetch-Site': 'same-origin',
349
+ 'User-Agent': userAgent,
365
350
  'x-csrf': '1'
366
351
  };
352
+ this.headers = headers;
353
+ this.axiosInstance = axios.create({
354
+ baseURL: ApiUrlsHome.BaseURL,
355
+ timeout: 30000,
356
+ headers: headers
357
+ })
367
358
 
368
359
  accountInfo.State = true;
369
360
  accountInfo.Info = 'Connect to MELCloud Home Success';
370
- accountInfo.Headers = this.headers;
371
361
  await this.functions.saveData(this.accountFile, accountInfo);
372
362
 
373
363
  return accountInfo;
@@ -382,22 +372,7 @@ class MelCloud extends EventEmitter {
382
372
  }
383
373
  }
384
374
  }
385
-
386
- async send(accountInfo) {
387
- try {
388
- await axios(ApiUrlsHome.UpdateApplicationOptions, {
389
- method: 'POST',
390
- baseURL: ApiUrlsHome.BaseURL,
391
- timeout: 15000,
392
- headers: accountInfo.Headers
393
- });
394
- await this.functions.saveData(this.accountFile, accountInfo);
395
- return true;
396
- } catch (error) {
397
- throw new Error(`Send data error: ${error.message}`);
398
- }
399
- }
400
375
  }
401
376
 
402
- export default MelCloud;
377
+ export default MelCloudHome;
403
378