homebridge-melcloud-control 4.2.3-beta.1 → 4.2.3-beta.11

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.
@@ -1,5 +1,6 @@
1
1
  import { HomebridgePluginUiServer } from '@homebridge/plugin-ui-utils';
2
2
  import MelCloud from '../src/melcloud.js';
3
+ import MelCloudHome from '../src/melcloudhome.js';
3
4
 
4
5
  class PluginUiServer extends HomebridgePluginUiServer {
5
6
  constructor() {
@@ -17,7 +18,7 @@ class PluginUiServer extends HomebridgePluginUiServer {
17
18
  const accountFile = `${this.homebridgeStoragePath}/melcloud/${accountName}_Account`;
18
19
  const buildingsFile = `${this.homebridgeStoragePath}/melcloud/${accountName}_Buildings`;
19
20
  const devicesFile = `${this.homebridgeStoragePath}/melcloud/${accountName}_Devices`;
20
- const melCloud = new MelCloud(account, accountFile, buildingsFile, devicesFile);
21
+ const melCloud = account.type === 'melcloud' ? new MelCloud(account, accountFile, buildingsFile, devicesFile) : new MelCloudHome(account, accountFile, buildingsFile, devicesFile);
21
22
 
22
23
  try {
23
24
  const accountInfo = await melCloud.connect();
package/index.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { join } from 'path';
2
2
  import { mkdirSync, existsSync, writeFileSync } from 'fs';
3
3
  import MelCloud from './src/melcloud.js';
4
+ import MelCloudHome from './src/melcloudhome.js';
4
5
  import DeviceAta from './src/deviceata.js';
5
6
  import DeviceAtw from './src/deviceatw.js';
6
7
  import DeviceErv from './src/deviceerv.js';
@@ -84,8 +85,8 @@ class MelCloudPlatform {
84
85
  .on('start', async () => {
85
86
  try {
86
87
  //melcloud account
87
- const melCloud = new MelCloud(account, accountFile, buildingsFile, devicesFile, true)
88
- .on('success', (msg) => logLevel.success && log.success(`${accountName}, ${msg}`))
88
+ const melCloud = account.type === 'melcloud' ? new MelCloud(account, accountFile, buildingsFile, devicesFile, true) : new MelCloudHome(account, accountFile, buildingsFile, devicesFile, true);
89
+ melCloud.on('success', (msg) => logLevel.success && log.success(`${accountName}, ${msg}`))
89
90
  .on('info', (msg) => logLevel.info && log.info(`${accountName}, ${msg}`))
90
91
  .on('debug', (msg) => logLevel.debug && log.info(`${accountName}, debug: ${msg}`))
91
92
  .on('warn', (msg) => logLevel.warn && log.warn(`${accountName}, ${msg}`))
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "displayName": "MELCloud Control",
3
3
  "name": "homebridge-melcloud-control",
4
- "version": "4.2.3-beta.1",
4
+ "version": "4.2.3-beta.11",
5
5
  "description": "Homebridge plugin to control Mitsubishi Air Conditioner, Heat Pump and Energy Recovery Ventilation.",
6
6
  "license": "MIT",
7
7
  "author": "grzegorz914",
package/src/deviceata.js CHANGED
@@ -334,8 +334,6 @@ class DeviceAta extends EventEmitter {
334
334
  return state;
335
335
  })
336
336
  .onSet(async (state) => {
337
- if (!!state === this.accessory.power) return;
338
-
339
337
  try {
340
338
  deviceData.Device.Power = state ? true : false;
341
339
  await this.melCloudAta.send(this.accountType, this.displayType, deviceData, AirConditioner.EffectiveFlags.Power);
@@ -1331,7 +1329,7 @@ class DeviceAta extends EventEmitter {
1331
1329
  maxTempHeat: maxTempHeat,
1332
1330
  minTempCoolDryAuto: minTempCoolDryAuto,
1333
1331
  maxTempCoolDryAuto: maxTempCoolDryAuto,
1334
- power: power ? 1 : 0,
1332
+ power: power,
1335
1333
  inStandbyMode: inStandbyMode,
1336
1334
  operationMode: operationMode,
1337
1335
  currentOperationMode: 0,
@@ -1427,7 +1425,7 @@ class DeviceAta extends EventEmitter {
1427
1425
 
1428
1426
  //update characteristics
1429
1427
  this.melCloudService
1430
- ?.updateCharacteristic(Characteristic.Active, power ? 1 : 0)
1428
+ ?.updateCharacteristic(Characteristic.Active, power)
1431
1429
  .updateCharacteristic(Characteristic.CurrentHeaterCoolerState, obj.currentOperationMode)
1432
1430
  .updateCharacteristic(Characteristic.TargetHeaterCoolerState, obj.targetOperationMode)
1433
1431
  .updateCharacteristic(Characteristic.CurrentTemperature, roomTemperature)
package/src/deviceatw.js CHANGED
@@ -340,8 +340,6 @@ class DeviceAtw extends EventEmitter {
340
340
  return state;
341
341
  })
342
342
  .onSet(async (state) => {
343
- if (!!state === this.accessory.power) return;
344
-
345
343
  try {
346
344
  switch (i) {
347
345
  case 0: //Heat Pump
@@ -1340,8 +1338,15 @@ class DeviceAtw extends EventEmitter {
1340
1338
  effectiveFlags = HeatPump.EffectiveFlags.Power + HeatPump.EffectiveFlags.OperationMode;
1341
1339
  break;
1342
1340
  case 3: //HOLIDAY
1343
- deviceData.Device.HolidayMode = state;
1344
- effectiveFlags = HeatPump.EffectiveFlags.HolidayMode;
1341
+ if (this.accountType === 'melcloud') {
1342
+ deviceData.Device.HolidayMode = state;
1343
+ effectiveFlags = HeatPump.EffectiveFlags.HolidayMode;
1344
+ }
1345
+
1346
+ if (this.accountType === 'melcloudhome') {
1347
+ deviceData.Device.HolidayMode.Enabled = state;
1348
+ effectiveFlags = 'holidaymode';
1349
+ }
1345
1350
  break;
1346
1351
  case 10: //ALL ZONES PHYSICAL LOCK CONTROL
1347
1352
  deviceData.Device.ProhibitZone1 = state;
@@ -1510,7 +1515,8 @@ class DeviceAtw extends EventEmitter {
1510
1515
  const scheduleEnabled = deviceData.ScheduleEnabled;
1511
1516
  const schedulesOnServer = deviceData.Schedule ?? [];
1512
1517
  const presetsOnServer = deviceData.Presets ?? [];
1513
- const holidayModeEnabled = deviceData.HolidayMode?.Enabled;
1518
+ const holidayMode = deviceData.Device.HolidayMode;
1519
+ const holidayModeEnabled = accountTypeMelcloud ? holidayMode : deviceData.HolidayMode?.Enabled;
1514
1520
  const holidayModeActive = deviceData.HolidayMode?.Active ?? false;
1515
1521
 
1516
1522
  //device info
@@ -1545,12 +1551,11 @@ class DeviceAtw extends EventEmitter {
1545
1551
 
1546
1552
  //heat pump
1547
1553
  const heatPumpName = 'Heat Pump';
1548
- const power = deviceData.Device.Power ?? false;
1554
+ const power = deviceData.Device.Power;
1549
1555
  const inStandbyMode = deviceData.Device.InStandbyMode;
1550
1556
  const unitStatus = deviceData.Device.UnitStatus ?? 0;
1551
1557
  const operationMode = deviceData.Device.OperationMode;
1552
1558
  const outdoorTemperature = deviceData.Device.OutdoorTemperature;
1553
- const holidayMode = deviceData.Device.HolidayMode ?? false;
1554
1559
  const flowTemperatureHeatPump = deviceData.Device.FlowTemperature;
1555
1560
  const returnTemperatureHeatPump = deviceData.Device.ReturnTemperature;
1556
1561
  const isConnected = accountTypeMelcloud ? !deviceData.Device[connectKey] : deviceData.Device[connectKey];
@@ -1594,7 +1599,7 @@ class DeviceAtw extends EventEmitter {
1594
1599
  const obj = {
1595
1600
  presets: presetsOnServer,
1596
1601
  schedules: schedulesOnServer,
1597
- power: power ? 1 : 0,
1602
+ power: power,
1598
1603
  inStandbyMode: inStandbyMode,
1599
1604
  unitStatus: unitStatus,
1600
1605
  idleZone1: idleZone1,
@@ -2065,7 +2070,7 @@ class DeviceAtw extends EventEmitter {
2065
2070
  button.state = power ? (operationMode === 1) : false;
2066
2071
  break;
2067
2072
  case 53: //HOLIDAY
2068
- button.state = power ? (holidayMode === true) : false;
2073
+ button.state = power ? (holidayModeEnabled === true) : false;
2069
2074
  break;
2070
2075
  case 10: //ALL ZONES PHYSICAL LOCK CONTROL
2071
2076
  button.state = power ? (prohibitZone1 === true && prohibitHotWater === true && prohibitZone2 === true) : false;
package/src/deviceerv.js CHANGED
@@ -305,8 +305,6 @@ class DeviceErv extends EventEmitter {
305
305
  return state;
306
306
  })
307
307
  .onSet(async (state) => {
308
- if (!!state === this.accessory.power) return;
309
-
310
308
  try {
311
309
  deviceData.Device.Power = state ? true : false;
312
310
  await this.melCloudErv.send(this.accountType, this.displayType, deviceData, Ventilation.EffectiveFlags.Power);
@@ -1136,7 +1134,7 @@ class DeviceErv extends EventEmitter {
1136
1134
  actualVentilationMode: actualVentilationMode,
1137
1135
  numberOfFanSpeeds: numberOfFanSpeeds,
1138
1136
  supportsFanSpeed: supportsFanSpeed,
1139
- power: power ? 1 : 0,
1137
+ power: power,
1140
1138
  inStandbyMode: inStandbyMode,
1141
1139
  operationMode: operationMode,
1142
1140
  currentOperationMode: 0,
package/src/melcloud.js CHANGED
@@ -1,13 +1,10 @@
1
- import fs from 'fs';
2
1
  import axios from 'axios';
3
2
  import { exec } from 'child_process';
4
3
  import { promisify } from 'util';
5
4
  import EventEmitter from 'events';
6
- import puppeteer from 'puppeteer';
7
5
  import ImpulseGenerator from './impulsegenerator.js';
8
6
  import Functions from './functions.js';
9
- import { ApiUrls, ApiUrlsHome } from './constants.js';
10
- const execPromise = promisify(exec);
7
+ import { ApiUrls } from './constants.js';
11
8
 
12
9
  class MelCloud extends EventEmitter {
13
10
  constructor(account, accountFile, buildingsFile, devicesFile, pluginStart = false) {
@@ -61,23 +58,22 @@ class MelCloud extends EventEmitter {
61
58
  }
62
59
 
63
60
  // MELCloud
64
- async checkMelcloudDevicesList() {
61
+ async checkDevicesList() {
65
62
  try {
66
63
  const devicesList = { State: false, Info: null, Devices: [] }
67
64
  const headers = {
68
65
  'X-MitsContextKey': this.contextKey,
69
66
  'Content-Type': 'application/json'
70
67
  }
71
- const axiosInstance = axios.create({
68
+
69
+ if (this.logDebug) this.emit('debug', `Scanning for devices...`);
70
+ const listDevicesData = await axios(ApiUrls.ListDevices, {
72
71
  method: 'GET',
73
72
  baseURL: ApiUrls.BaseURL,
74
73
  timeout: 15000,
75
74
  headers: headers
76
75
  });
77
76
 
78
- if (this.logDebug) this.emit('debug', `Scanning for devices...`);
79
- const listDevicesData = await axiosInstance(ApiUrls.ListDevices);
80
-
81
77
  if (!listDevicesData || !listDevicesData.data) {
82
78
  devicesList.Info = 'Invalid or empty response from MELCloud API'
83
79
  return devicesList;
@@ -138,28 +134,27 @@ class MelCloud extends EventEmitter {
138
134
  }
139
135
  }
140
136
 
141
- async connectToMelCloud() {
137
+ async connect() {
142
138
  if (this.logDebug) this.emit('debug', `Connecting to MELCloud`);
143
139
 
144
140
  try {
145
141
  const accountInfo = { State: false, Info: '', LoginData: null, ContextKey: null, UseFahrenheit: false }
146
- const axiosInstance = axios.create({
142
+
143
+ const payload = {
144
+ Email: this.user,
145
+ Password: this.passwd,
146
+ Language: this.language,
147
+ AppVersion: '1.34.12',
148
+ CaptchaChallenge: '',
149
+ CaptchaResponse: '',
150
+ Persist: true
151
+ };
152
+ const accountData = await axios(ApiUrls.ClientLogin, {
147
153
  method: 'POST',
148
154
  baseURL: ApiUrls.BaseURL,
149
155
  timeout: 15000,
150
- data: {
151
- Email: this.user,
152
- Password: this.passwd,
153
- Language: this.language,
154
- AppVersion: '1.34.12',
155
- CaptchaChallenge: '',
156
- CaptchaResponse: '',
157
- Persist: true
158
- }
156
+ data: payload
159
157
  });
160
-
161
-
162
- const accountData = await axiosInstance(ApiUrls.ClientLogin);
163
158
  const account = accountData.data;
164
159
  const loginData = account.LoginData ?? [];
165
160
  const contextKey = loginData.ContextKey;
@@ -193,332 +188,6 @@ class MelCloud extends EventEmitter {
193
188
  }
194
189
  }
195
190
 
196
- // MELCloud Home
197
- async checkMelcloudHomeDevicesList() {
198
- try {
199
- const devicesList = { State: false, Info: null, Devices: [] }
200
- const headers = {
201
- 'Accept': '*/*',
202
- 'Accept-Language': 'en-US,en;q=0.9',
203
- 'Cookie': this.contextKey,
204
- 'User-Agent': 'homebridge-melcloud-control/4.0.0',
205
- 'DNT': '1',
206
- 'Origin': 'https://melcloudhome.com',
207
- 'Referer': 'https://melcloudhome.com/dashboard',
208
- 'Sec-Fetch-Dest': 'empty',
209
- 'Sec-Fetch-Mode': 'cors',
210
- 'Sec-Fetch-Site': 'same-origin',
211
- 'X-CSRF': '1'
212
- };
213
- const axiosInstance = axios.create({
214
- method: 'GET',
215
- baseURL: ApiUrlsHome.BaseURL,
216
- timeout: 25000,
217
- headers: headers
218
- });
219
-
220
- if (this.logDebug) this.emit('debug', `Scanning for devices`);
221
- const listDevicesData = await axiosInstance(ApiUrlsHome.GetUserContext);
222
- const buildingsList = listDevicesData.data.buildings;
223
- if (this.logDebug) this.emit('debug', `Buildings: ${JSON.stringify(buildingsList, null, 2)}`);
224
-
225
- if (!buildingsList) {
226
- devicesList.Info = 'No building found'
227
- return devicesList;
228
- }
229
-
230
- await this.functions.saveData(this.buildingsFile, buildingsList);
231
- if (this.logDebug) this.emit('debug', `Buildings list saved`);
232
-
233
- const devices = buildingsList.flatMap(building => {
234
- // Funkcja kapitalizująca klucze obiektu
235
- const capitalizeKeys = obj =>
236
- Object.fromEntries(
237
- Object.entries(obj).map(([key, value]) => [
238
- key.charAt(0).toUpperCase() + key.slice(1),
239
- value
240
- ])
241
- );
242
-
243
- // Rekurencyjna kapitalizacja kluczy w obiekcie lub tablicy
244
- const capitalizeKeysDeep = obj => {
245
- if (Array.isArray(obj)) return obj.map(capitalizeKeysDeep);
246
- if (obj && typeof obj === 'object') {
247
- return Object.fromEntries(
248
- Object.entries(obj).map(([key, value]) => [
249
- key.charAt(0).toUpperCase() + key.slice(1),
250
- capitalizeKeysDeep(value)
251
- ])
252
- );
253
- }
254
- return obj;
255
- };
256
-
257
- // Funkcja tworząca finalny obiekt Device
258
- const createDevice = (device, type) => {
259
- // Settings już kapitalizowane w nazwach
260
- const settingsArray = device.Settings || [];
261
-
262
- const settingsObject = Object.fromEntries(
263
- settingsArray.map(({ name, value }) => {
264
- let parsedValue = value;
265
- if (value === "True") parsedValue = true;
266
- else if (value === "False") parsedValue = false;
267
- else if (!isNaN(value) && value !== "") parsedValue = Number(value);
268
-
269
- const key = name.charAt(0).toUpperCase() + name.slice(1);
270
- return [key, parsedValue];
271
- })
272
- );
273
-
274
- // Scal Capabilities + Settings + DeviceType w Device
275
- const deviceObject = {
276
- ...capitalizeKeys(device.Capabilities || {}),
277
- ...settingsObject,
278
- DeviceType: type,
279
- IsConnected: device.IsConnected
280
- };
281
-
282
- // Kapitalizacja brakujących obiektów/tablic
283
- if (device.FrostProtection) device.FrostProtection = { ...capitalizeKeys(device.FrostProtection || {}) };
284
- if (device.OverheatProtection) device.OverheatProtection = { ...capitalizeKeys(device.OverheatProtection || {}) };
285
- if (device.HolidayMode) device.HolidayMode = { ...capitalizeKeys(device.HolidayMode || {}) };
286
- if (Array.isArray(device.Schedule)) device.Schedule = device.Schedule.map(capitalizeKeysDeep);
287
-
288
- // Usuń stare pola Settings i Capabilities
289
- const { Settings, Capabilities, Id, GivenDisplayName, ...rest } = device;
290
-
291
- return {
292
- ...rest,
293
- Type: type,
294
- DeviceID: Id,
295
- DeviceName: GivenDisplayName,
296
- Device: deviceObject,
297
- Headers: headers
298
- };
299
- };
300
-
301
- return [
302
- ...(building.airToAirUnits || []).map(d => createDevice(capitalizeKeys(d), 0)),
303
- ...(building.airToWaterUnits || []).map(d => createDevice(capitalizeKeys(d), 1)),
304
- ...(building.airToVentilationUnits || []).map(d => createDevice(capitalizeKeys(d), 3))
305
- ];
306
- });
307
-
308
- const devicesCount = devices.length;
309
- if (devicesCount === 0) {
310
- devicesList.Info = 'No devices found'
311
- return devicesList;
312
- }
313
-
314
- await this.functions.saveData(this.devicesFile, devices);
315
- if (this.logDebug) this.emit('debug', `${devicesCount} devices saved`);
316
-
317
- devicesList.State = true;
318
- devicesList.Info = `Found ${devicesCount} devices`;
319
- devicesList.Devices = devices;
320
- return devicesList;
321
- } catch (error) {
322
- if (error.response?.status === 401) {
323
- await connectToMelCloudHome();
324
- if (this.logWarn) this.emit('warn', 'Check devices list not possible, cookies expired, trying to get new.');
325
- return;
326
- }
327
-
328
- throw new Error(`Check devices list error: ${error.message}`);
329
- }
330
- }
331
-
332
- async connectToMelCloudHome() {
333
- if (this.logDebug) this.emit('debug', 'Connecting to MELCloud Home');
334
- const GLOBAL_TIMEOUT = 90000;
335
-
336
- let browser;
337
- try {
338
- const accountInfo = { State: false, Info: '', ContextKey: null, UseFahrenheit: false };
339
- let chromiumPath = await this.functions.ensureChromiumInstalled();
340
-
341
- // === Fallback to Puppeteer's built-in Chromium ===
342
- if (!chromiumPath) {
343
- try {
344
- const puppeteerPath = puppeteer.executablePath();
345
- if (puppeteerPath && fs.existsSync(puppeteerPath)) {
346
- chromiumPath = puppeteerPath;
347
- if (this.logDebug) this.emit('debug', `Using puppeteer Chromium at ${chromiumPath}`);
348
- }
349
- } catch { }
350
- } else {
351
- if (this.logDebug) this.emit('debug', `Using system Chromium at ${chromiumPath}`);
352
- }
353
-
354
- if (!chromiumPath) {
355
- accountInfo.Info = 'Chromium not found on Your device, please install it manually and try again';
356
- return accountInfo;
357
- }
358
-
359
- // Verify executable works
360
- try {
361
- const { stdout } = await execPromise(`"${chromiumPath}" --version`);
362
- if (this.logDebug) this.emit('debug', `Chromium detected: ${stdout.trim()}`);
363
- } catch (error) {
364
- accountInfo.Info = `Chromium found at ${chromiumPath}, but cannot be executed: ${error.message}`;
365
- return accountInfo;
366
- }
367
-
368
- if (this.logDebug) this.emit('debug', `Launching Chromium...`);
369
- browser = await puppeteer.launch({
370
- headless: true,
371
- executablePath: chromiumPath,
372
- timeout: GLOBAL_TIMEOUT,
373
- args: [
374
- '--no-sandbox',
375
- '--disable-setuid-sandbox',
376
- '--disable-dev-shm-usage',
377
- '--single-process',
378
- '--disable-gpu',
379
- '--no-zygote'
380
- ]
381
- });
382
- browser.on('disconnected', () => this.emit('debug', 'Browser disconnected'));
383
-
384
- const page = await browser.newPage();
385
- page.on('error', error => this.emit('error', `Page crashed: ${error.message}`));
386
- page.on('pageerror', error => this.emit('error', `Browser error: ${error.message}`));
387
- page.setDefaultTimeout(GLOBAL_TIMEOUT);
388
- page.setDefaultNavigationTimeout(GLOBAL_TIMEOUT);
389
-
390
- // Clear cookies before navigation
391
- try {
392
- const client = await page.createCDPSession();
393
- await client.send('Network.clearBrowserCookies');
394
- } catch (error) {
395
- if (this.logError) this.emit('error', `Clear cookies error: ${error.message}`);
396
- }
397
-
398
- try {
399
- await page.goto(ApiUrlsHome.BaseURL, { waitUntil: ['domcontentloaded', 'networkidle2'], timeout: GLOBAL_TIMEOUT });
400
- } catch (error) {
401
- accountInfo.Info = `Navigation to ${ApiUrlsHome.BaseURL} failed: ${error.message}`;
402
- return accountInfo;
403
- }
404
-
405
- // Wait extra to ensure UI is rendered
406
- await new Promise(r => setTimeout(r, 3000));
407
- const loginBtn = await page.waitForSelector('button.btn--blue', { timeout: GLOBAL_TIMEOUT / 4 });
408
- const loginText = await page.evaluate(el => el.textContent.trim(), loginBtn);
409
-
410
- if (!['Zaloguj', 'Sign In', 'Login'].includes(loginText)) {
411
- accountInfo.Info = `Login button ${loginText} not found`;
412
- return accountInfo;
413
- }
414
-
415
- await loginBtn.click();
416
- await page.waitForNavigation({ waitUntil: 'networkidle2', timeout: GLOBAL_TIMEOUT / 3 });
417
-
418
- const usernameInput = await page.$('input[name="username"]');
419
- const passwordInput = await page.$('input[name="password"]');
420
- if (!usernameInput || !passwordInput) {
421
- accountInfo.Info = 'Username or password input not found';
422
- return accountInfo;
423
- }
424
-
425
- await page.type('input[name="username"]', this.user, { delay: 50 });
426
- await page.type('input[name="password"]', this.passwd, { delay: 50 });
427
-
428
- const submitButton = await page.$('input[type="submit"], button[type="submit"]');
429
- if (!submitButton) {
430
- accountInfo.Info = 'Submit button not found';
431
- return accountInfo;
432
- }
433
- await Promise.race([Promise.all([submitButton.click(), page.waitForNavigation({ waitUntil: ['domcontentloaded', 'networkidle2'], timeout: GLOBAL_TIMEOUT / 4 })]), new Promise(r => setTimeout(r, GLOBAL_TIMEOUT / 3))]);
434
-
435
- // Extract cookies
436
- let c1 = null, c2 = null;
437
- const start = Date.now();
438
- while ((!c1 || !c2) && Date.now() - start < GLOBAL_TIMEOUT / 2) {
439
- const cookies = await page.browserContext().cookies();
440
- c1 = cookies.find(c => c.name === '__Secure-monitorandcontrolC1')?.value || c1;
441
- c2 = cookies.find(c => c.name === '__Secure-monitorandcontrolC2')?.value || c2;
442
- if (!c1 || !c2) await new Promise(r => setTimeout(r, 500));
443
- }
444
-
445
- if (!c1 || !c2) {
446
- accountInfo.Info = 'Cookies C1/C2 missing';
447
- return accountInfo;
448
- }
449
-
450
- const contextKey = [
451
- '__Secure-monitorandcontrol=chunks-2',
452
- `__Secure-monitorandcontrolC1=${c1}`,
453
- `__Secure-monitorandcontrolC2=${c2}`
454
- ].join('; ');
455
- this.contextKey = contextKey;
456
-
457
- accountInfo.State = true;
458
- accountInfo.Info = 'Connect to MELCloud Home Success';
459
- accountInfo.ContextKey = contextKey;
460
- await this.functions.saveData(this.accountFile, accountInfo);
461
-
462
- return accountInfo;
463
- } catch (error) {
464
- throw new Error(`Connect error: ${error.message}`);
465
- } finally {
466
- if (browser) {
467
- try { await browser.close(); }
468
- catch (closeErr) {
469
- if (this.logError) this.emit('error', `Failed to close Puppeteer: ${closeErr.message}`);
470
- }
471
- }
472
- }
473
- }
474
-
475
- async checkDevicesList() {
476
- const TIMEOUT_MS = 30000; // 30 seconds timeout
477
- try {
478
- const devicesList = await Promise.race([
479
- (async () => {
480
- switch (this.accountType) {
481
- case "melcloud":
482
- return await this.checkMelcloudDevicesList();
483
- case "melcloudhome":
484
- return await this.checkMelcloudHomeDevicesList();
485
- default:
486
- return [];
487
- }
488
- })(),
489
- new Promise((_, reject) => setTimeout(() => reject(new Error('Device list timeout (30s)')), TIMEOUT_MS))
490
- ]);
491
-
492
- return devicesList;
493
- } catch (error) {
494
- throw new Error(error);
495
- }
496
- }
497
-
498
- async connect() {
499
- const TIMEOUT_MS = 120000;
500
-
501
- try {
502
- const response = await Promise.race([
503
- (async () => {
504
- switch (this.accountType) {
505
- case "melcloud":
506
- return await this.connectToMelCloud();
507
- case "melcloudhome":
508
- return await this.connectToMelCloudHome();
509
- default:
510
- return {};
511
- }
512
- })(),
513
- new Promise((_, reject) => setTimeout(() => reject(new Error('Connection timeout (90s)')), TIMEOUT_MS))
514
- ]);
515
-
516
- return response;
517
- } catch (error) {
518
- throw new Error(error);
519
- }
520
- }
521
-
522
191
  async send(accountInfo) {
523
192
  try {
524
193
  const axiosInstance = axios.create({
@@ -52,10 +52,6 @@ class MelCloudAta extends EventEmitter {
52
52
  try {
53
53
  //read device info from file
54
54
  const devicesData = await this.functions.readData(this.devicesFile, true);
55
- if (!Array.isArray(devicesData)) {
56
- if (this.logWarn) this.emit('warn', `Device data not found`);
57
- return null;
58
- }
59
55
  const deviceData = devicesData.find(device => device.DeviceID === this.deviceId);
60
56
 
61
57
  if (this.accountType === 'melcloudhome') {
@@ -118,10 +114,7 @@ class MelCloudAta extends EventEmitter {
118
114
 
119
115
  //check state changes
120
116
  const deviceDataHasNotChanged = JSON.stringify(deviceData) === JSON.stringify(this.deviceData);
121
- if (deviceDataHasNotChanged) {
122
- if (this.logDebug) this.emit('debug', `Device state not changed`);
123
- return;
124
- }
117
+ if (deviceDataHasNotChanged) return;
125
118
  this.deviceData = deviceData;
126
119
 
127
120
  //emit info
@@ -167,17 +160,15 @@ class MelCloudAta extends EventEmitter {
167
160
  HideDryModeControl: deviceData.HideDryModeControl,
168
161
  HasPendingCommand: true
169
162
  };
170
- if (this.logDebug) this.emit('debug', `Send Data: ${JSON.stringify(payload, null, 2)}`);
171
163
 
172
- const axiosInstancePost = axios.create({
164
+ if (this.logDebug) this.emit('debug', `Send Data: ${JSON.stringify(payload, null, 2)}`);
165
+ await axios(ApiUrls.SetAta, {
173
166
  method: 'POST',
174
167
  baseURL: ApiUrls.BaseURL,
175
168
  timeout: 10000,
176
169
  headers: deviceData.Headers,
177
170
  data: payload
178
171
  });
179
-
180
- await axiosInstancePost(ApiUrls.SetAta);
181
172
  this.updateData(deviceData);
182
173
  return true;
183
174
  case "melcloudhome":
@@ -243,16 +234,14 @@ class MelCloudAta extends EventEmitter {
243
234
  break
244
235
  }
245
236
 
246
- const axiosInstancePut = axios.create({
237
+ if (this.logDebug) this.emit('debug', `Send Data: ${JSON.stringify(payload, null, 2)}`);
238
+ await axios(path, {
247
239
  method: method,
248
240
  baseURL: ApiUrlsHome.BaseURL,
249
241
  timeout: 10000,
250
242
  headers: deviceData.Headers,
251
243
  data: payload
252
244
  });
253
-
254
- if (this.logDebug) this.emit('debug', `Send Data: ${JSON.stringify(payload, null, 2)}`);
255
- await axiosInstancePut(path);
256
245
  this.updateData(deviceData);
257
246
  return true;
258
247
  default:
@@ -272,6 +261,5 @@ class MelCloudAta extends EventEmitter {
272
261
  this.lock = false
273
262
  }, 3000);
274
263
  }
275
-
276
264
  };
277
265
  export default MelCloudAta;
@@ -52,10 +52,6 @@ class MelCloudAtw extends EventEmitter {
52
52
  try {
53
53
  //read device info from file
54
54
  const devicesData = await this.functions.readData(this.devicesFile, true);
55
- if (!Array.isArray(devicesData)) {
56
- if (this.logWarn) this.emit('warn', `Device data not found`);
57
- return null;
58
- }
59
55
  const deviceData = devicesData.find(device => device.DeviceID === this.deviceId);
60
56
 
61
57
  if (this.accountType === 'melcloudhome') {
@@ -109,10 +105,7 @@ class MelCloudAtw extends EventEmitter {
109
105
 
110
106
  //check state changes
111
107
  const deviceDataHasNotChanged = JSON.stringify(devicesData) === JSON.stringify(this.devicesData);
112
- if (deviceDataHasNotChanged) {
113
- if (this.logDebug) this.emit('debug', `Device state not changed`);
114
- return;
115
- }
108
+ if (deviceDataHasNotChanged) return;
116
109
  this.devicesData = devicesData;
117
110
 
118
111
  //emit info
@@ -148,93 +141,81 @@ class MelCloudAtw extends EventEmitter {
148
141
  let path = '';
149
142
  switch (accountType) {
150
143
  case "melcloud":
151
- const axiosInstancePost = axios.create({
144
+ deviceData.Device.EffectiveFlags = effectiveFlags;
145
+ payload = {
146
+ DeviceID: deviceData.Device.DeviceID,
147
+ EffectiveFlags: deviceData.Device.EffectiveFlags,
148
+ Power: deviceData.Device.Power,
149
+ SetTemperatureZone1: deviceData.Device.SetTemperatureZone1,
150
+ SetTemperatureZone2: deviceData.Device.SetTemperatureZone2,
151
+ OperationMode: deviceData.Device.OperationMode,
152
+ OperationModeZone1: deviceData.Device.OperationModeZone1,
153
+ OperationModeZone2: deviceData.Device.OperationModeZone2,
154
+ SetHeatFlowTemperatureZone1: deviceData.Device.SetHeatFlowTemperatureZone1,
155
+ SetHeatFlowTemperatureZone2: deviceData.Device.SetHeatFlowTemperatureZone2,
156
+ SetCoolFlowTemperatureZone1: deviceData.Device.SetCoolFlowTemperatureZone1,
157
+ SetCoolFlowTemperatureZone2: deviceData.Device.SetCoolFlowTemperatureZone2,
158
+ SetTankWaterTemperature: deviceData.Device.SetTankWaterTemperature,
159
+ ForcedHotWaterMode: deviceData.Device.ForcedHotWaterMode,
160
+ EcoHotWater: deviceData.Device.EcoHotWater,
161
+ HolidayMode: deviceData.Device.HolidayMode,
162
+ ProhibitZone1: deviceData.Device.ProhibitHeatingZone1,
163
+ ProhibitZone2: deviceData.Device.ProhibitHeatingZone2,
164
+ ProhibitHotWater: deviceData.Device.ProhibitHotWater,
165
+ HasPendingCommand: true
166
+ }
167
+
168
+ if (this.logDebug) this.emit('debug', `Send Data: ${JSON.stringify(payload, null, 2)}`);
169
+ await axios(ApiUrls.SetAtw, {
152
170
  method: 'POST',
153
171
  baseURL: ApiUrls.BaseURL,
154
172
  timeout: 10000,
155
173
  headers: deviceData.Headers,
156
- withCredentials: true
174
+ data: payload
157
175
  });
158
-
159
- deviceData.Device.EffectiveFlags = effectiveFlags;
160
- payload = {
161
- data: {
162
- DeviceID: deviceData.Device.DeviceID,
163
- EffectiveFlags: deviceData.Device.EffectiveFlags,
164
- Power: deviceData.Device.Power,
165
- SetTemperatureZone1: deviceData.Device.SetTemperatureZone1,
166
- SetTemperatureZone2: deviceData.Device.SetTemperatureZone2,
167
- OperationMode: deviceData.Device.OperationMode,
168
- OperationModeZone1: deviceData.Device.OperationModeZone1,
169
- OperationModeZone2: deviceData.Device.OperationModeZone2,
170
- SetHeatFlowTemperatureZone1: deviceData.Device.SetHeatFlowTemperatureZone1,
171
- SetHeatFlowTemperatureZone2: deviceData.Device.SetHeatFlowTemperatureZone2,
172
- SetCoolFlowTemperatureZone1: deviceData.Device.SetCoolFlowTemperatureZone1,
173
- SetCoolFlowTemperatureZone2: deviceData.Device.SetCoolFlowTemperatureZone2,
174
- SetTankWaterTemperature: deviceData.Device.SetTankWaterTemperature,
175
- ForcedHotWaterMode: deviceData.Device.ForcedHotWaterMode,
176
- EcoHotWater: deviceData.Device.EcoHotWater,
177
- HolidayMode: deviceData.Device.HolidayMode,
178
- ProhibitZone1: deviceData.Device.ProhibitHeatingZone1,
179
- ProhibitZone2: deviceData.Device.ProhibitHeatingZone2,
180
- ProhibitHotWater: deviceData.Device.ProhibitHotWater,
181
- HasPendingCommand: true
182
- }
183
- }
184
-
185
- await axiosInstancePost(ApiUrls.SetAtw, payload);
186
176
  this.updateData(deviceData);
187
177
  return true;
188
178
  case "melcloudhome":
189
179
  switch (effectiveFlags) {
190
180
  case 'holidaymode':
191
- payload = {
192
- data: { enabled: deviceData.HolidayMode.Enabled, startDate: deviceData.HolidayMode.StartDate, endDate: deviceData.HolidayMode.EndDate, units: { "ATW": [deviceData.DeviceID] } }
193
- };
181
+ payload = { enabled: deviceData.HolidayMode.Enabled, startDate: deviceData.HolidayMode.StartDate, endDate: deviceData.HolidayMode.EndDate, units: { "ATW": [deviceData.DeviceID] } };
194
182
  method = 'POST';
195
183
  path = ApiUrlsHome.PostHolidayMode;
196
184
  break;
197
185
  case 'schedule':
198
- payload = {
199
- data: {
200
- enabled: deviceData.ScheduleEnabled
201
- }
202
- };
186
+ payload = { enabled: deviceData.ScheduleEnabled };
203
187
  method = 'PUT';
204
188
  path = ApiUrlsHome.PutScheduleEnable.replace('deviceid', deviceData.DeviceID);
205
189
  break;
206
190
  default:
207
191
  payload = {
208
- data: {
209
- Power: deviceData.Device.Power,
210
- SetTemperatureZone1: deviceData.Device.SetTemperatureZone1,
211
- SetTemperatureZone2: deviceData.Device.SetTemperatureZone2,
212
- OperationMode: HeatPump.OperationModeMapEnumToString[deviceData.Device.OperationMode],
213
- OperationModeZone1: HeatPump.OperationModeMapEnumToString[deviceData.Device.OperationModeZone1],
214
- OperationModeZone2: HeatPump.OperationModeMapEnumToString[deviceData.Device.OperationModeZone2],
215
- SetHeatFlowTemperatureZone1: deviceData.Device.SetHeatFlowTemperatureZone1,
216
- SetHeatFlowTemperatureZone2: deviceData.Device.SetHeatFlowTemperatureZone2,
217
- SetCoolFlowTemperatureZone1: deviceData.Device.SetCoolFlowTemperatureZone1,
218
- SetCoolFlowTemperatureZone2: deviceData.Device.SetCoolFlowTemperatureZone2,
219
- SetTankWaterTemperature: deviceData.Device.SetTankWaterTemperature,
220
- ForcedHotWaterMode: deviceData.Device.ForcedHotWaterMode,
221
- EcoHotWater: deviceData.Device.EcoHotWater,
222
- }
192
+ Power: deviceData.Device.Power,
193
+ SetTemperatureZone1: deviceData.Device.SetTemperatureZone1,
194
+ SetTemperatureZone2: deviceData.Device.SetTemperatureZone2,
195
+ OperationMode: HeatPump.OperationModeMapEnumToString[deviceData.Device.OperationMode],
196
+ OperationModeZone1: HeatPump.OperationModeMapEnumToString[deviceData.Device.OperationModeZone1],
197
+ OperationModeZone2: HeatPump.OperationModeMapEnumToString[deviceData.Device.OperationModeZone2],
198
+ SetHeatFlowTemperatureZone1: deviceData.Device.SetHeatFlowTemperatureZone1,
199
+ SetHeatFlowTemperatureZone2: deviceData.Device.SetHeatFlowTemperatureZone2,
200
+ SetCoolFlowTemperatureZone1: deviceData.Device.SetCoolFlowTemperatureZone1,
201
+ SetCoolFlowTemperatureZone2: deviceData.Device.SetCoolFlowTemperatureZone2,
202
+ SetTankWaterTemperature: deviceData.Device.SetTankWaterTemperature,
203
+ ForcedHotWaterMode: deviceData.Device.ForcedHotWaterMode,
204
+ EcoHotWater: deviceData.Device.EcoHotWater,
223
205
  };
224
206
  method = 'PUT';
225
207
  path = ApiUrlsHome.SetAtw.replace('deviceid', deviceData.DeviceID);
226
208
  break
227
209
  }
228
210
 
229
- const axiosInstancePut = axios.create({
211
+ if (this.logDebug) this.emit('debug', `Send Data: ${JSON.stringify(payload, null, 2)}`);
212
+ await axios(path, {
230
213
  method: method,
231
214
  baseURL: ApiUrlsHome.BaseURL,
232
215
  timeout: 10000,
233
- headers: deviceData.Headers
216
+ headers: deviceData.Headers,
217
+ data: payload
234
218
  });
235
-
236
- if (this.logDebug) this.emit('debug', `Send Data: ${JSON.stringify(payload.data, null, 2)}`);
237
- await axiosInstancePut(path, payload);
238
219
  this.updateData(deviceData);
239
220
  return true;
240
221
  default:
@@ -52,10 +52,6 @@ class MelCloudErv extends EventEmitter {
52
52
  try {
53
53
  //read device info from file
54
54
  const devicesData = await this.functions.readData(this.devicesFile, true);
55
- if (!Array.isArray(devicesData)) {
56
- if (this.logWarn) this.emit('warn', `Device data not found`);
57
- return null;
58
- }
59
55
  const deviceData = devicesData.find(device => device.DeviceID === this.deviceId);
60
56
 
61
57
  if (this.accountType === 'melcloudhome') {
@@ -112,10 +108,7 @@ class MelCloudErv extends EventEmitter {
112
108
 
113
109
  //check state changes
114
110
  const deviceDataHasNotChanged = JSON.stringify(devicesData) === JSON.stringify(this.devicesData);
115
- if (deviceDataHasNotChanged) {
116
- if (this.logDebug) this.emit('debug', `Device state not changed`);
117
- return;
118
- }
111
+ if (deviceDataHasNotChanged) return;
119
112
  this.devicesData = devicesData;
120
113
 
121
114
  //emit info
@@ -137,14 +130,6 @@ class MelCloudErv extends EventEmitter {
137
130
  let path = '';
138
131
  switch (accountType) {
139
132
  case "melcloud":
140
- const axiosInstancePost = axios.create({
141
- method: 'POST',
142
- baseURL: ApiUrls.BaseURL,
143
- timeout: 10000,
144
- headers: deviceData.Headers,
145
- withCredentials: true
146
- });
147
-
148
133
  //set target temp based on display mode and ventilation mode
149
134
  switch (displayType) {
150
135
  case 1: //Heather/Cooler
@@ -168,25 +153,30 @@ class MelCloudErv extends EventEmitter {
168
153
  //device state
169
154
  deviceData.Device.EffectiveFlags = effectiveFlags;
170
155
  payload = {
171
- data: {
172
- DeviceID: deviceData.Device.DeviceID,
173
- EffectiveFlags: deviceData.Device.EffectiveFlags,
174
- Power: deviceData.Device.Power,
175
- SetTemperature: deviceData.Device.SetTemperature,
176
- SetFanSpeed: deviceData.Device.SetFanSpeed,
177
- OperationMode: deviceData.Device.OperationMode,
178
- VentilationMode: deviceData.Device.VentilationMode,
179
- DefaultCoolingSetTemperature: deviceData.Device.DefaultCoolingSetTemperature,
180
- DefaultHeatingSetTemperature: deviceData.Device.DefaultHeatingSetTemperature,
181
- HideRoomTemperature: deviceData.Device.HideRoomTemperature,
182
- HideSupplyTemperature: deviceData.Device.HideSupplyTemperature,
183
- HideOutdoorTemperature: deviceData.Device.HideOutdoorTemperature,
184
- NightPurgeMode: deviceData.Device.NightPurgeMode,
185
- HasPendingCommand: true
186
- }
156
+ DeviceID: deviceData.Device.DeviceID,
157
+ EffectiveFlags: deviceData.Device.EffectiveFlags,
158
+ Power: deviceData.Device.Power,
159
+ SetTemperature: deviceData.Device.SetTemperature,
160
+ SetFanSpeed: deviceData.Device.SetFanSpeed,
161
+ OperationMode: deviceData.Device.OperationMode,
162
+ VentilationMode: deviceData.Device.VentilationMode,
163
+ DefaultCoolingSetTemperature: deviceData.Device.DefaultCoolingSetTemperature,
164
+ DefaultHeatingSetTemperature: deviceData.Device.DefaultHeatingSetTemperature,
165
+ HideRoomTemperature: deviceData.Device.HideRoomTemperature,
166
+ HideSupplyTemperature: deviceData.Device.HideSupplyTemperature,
167
+ HideOutdoorTemperature: deviceData.Device.HideOutdoorTemperature,
168
+ NightPurgeMode: deviceData.Device.NightPurgeMode,
169
+ HasPendingCommand: true
187
170
  }
188
171
 
189
- await axiosInstancePost(ApiUrls.SetErv, payload);
172
+ if (this.logDebug) this.emit('debug', `Send Data: ${JSON.stringify(payload, null, 2)}`);
173
+ await axios(ApiUrls.SetErv, {
174
+ method: 'POST',
175
+ baseURL: ApiUrls.BaseURL,
176
+ timeout: 10000,
177
+ headers: deviceData.Headers,
178
+ data: payload
179
+ });
190
180
  this.updateData(deviceData);
191
181
  return true;
192
182
  case "melcloudhome":
@@ -204,45 +194,36 @@ class MelCloudErv extends EventEmitter {
204
194
 
205
195
  switch (effectiveFlags) {
206
196
  case 'holidaymode':
207
- payload = {
208
- data: { enabled: deviceData.HolidayMode.Enabled, startDate: deviceData.HolidayMode.StartDate, endDate: deviceData.HolidayMode.EndDate, units: { "ERV": [deviceData.DeviceID] } }
209
- };
197
+ payload = { enabled: deviceData.HolidayMode.Enabled, startDate: deviceData.HolidayMode.StartDate, endDate: deviceData.HolidayMode.EndDate, units: { "ERV": [deviceData.DeviceID] } };
210
198
  method = 'POST';
211
199
  path = ApiUrlsHome.PostHolidayMode;
212
200
  break;
213
201
  case 'schedule':
214
- payload = {
215
- data: {
216
- enabled: deviceData.ScheduleEnabled
217
- }
218
- };
202
+ payload = { enabled: deviceData.ScheduleEnabled };
219
203
  method = 'PUT';
220
204
  path = ApiUrlsHome.PutScheduleEnable.replace('deviceid', deviceData.DeviceID);
221
205
  break;
222
206
  default:
223
207
  payload = {
224
- data: {
225
- Power: deviceData.Device.Power,
226
- SetTemperature: deviceData.Device.SetTemperature,
227
- SetFanSpeed: String(deviceData.Device.SetFanSpeed),
228
- OperationMode: Ventilation.OperationModeMapEnumToString[deviceData.Device.OperationMode],
229
- VentilationMode: Ventilation.VentilationModeMapEnumToString[deviceData.Device.VentilationMode],
230
- }
208
+ Power: deviceData.Device.Power,
209
+ SetTemperature: deviceData.Device.SetTemperature,
210
+ SetFanSpeed: String(deviceData.Device.SetFanSpeed),
211
+ OperationMode: Ventilation.OperationModeMapEnumToString[deviceData.Device.OperationMode],
212
+ VentilationMode: Ventilation.VentilationModeMapEnumToString[deviceData.Device.VentilationMode],
231
213
  };
232
214
  method = 'PUT';
233
215
  path = ApiUrlsHome.SetErv.replace('deviceid', deviceData.DeviceID);
234
216
  break
235
217
  }
236
218
 
237
- const axiosInstancePut = axios.create({
219
+ if (this.logDebug) this.emit('debug', `Send Data: ${JSON.stringify(payload, null, 2)}`);
220
+ await axios(path, {
238
221
  method: method,
239
222
  baseURL: ApiUrlsHome.BaseURL,
240
223
  timeout: 10000,
241
- headers: deviceData.Headers
224
+ headers: deviceData.Headers,
225
+ data: payload
242
226
  });
243
-
244
- if (this.logDebug) this.emit('debug', `Send Data: ${JSON.stringify(payload.data, null, 2)}`);
245
- await axiosInstancePut(path, payload);
246
227
  this.updateData(deviceData);
247
228
  return true;
248
229
  default:
@@ -0,0 +1,364 @@
1
+ import fs from 'fs';
2
+ import axios from 'axios';
3
+ import { exec } from 'child_process';
4
+ import { promisify } from 'util';
5
+ import EventEmitter from 'events';
6
+ import puppeteer from 'puppeteer';
7
+ import ImpulseGenerator from './impulsegenerator.js';
8
+ import Functions from './functions.js';
9
+ import { ApiUrlsHome } from './constants.js';
10
+ const execPromise = promisify(exec);
11
+
12
+ class MelCloud extends EventEmitter {
13
+ constructor(account, accountFile, buildingsFile, devicesFile, pluginStart = false) {
14
+ super();
15
+ this.accountType = account.type;
16
+ this.user = account.user;
17
+ this.passwd = account.passwd;
18
+ this.language = account.language;
19
+ this.logWarn = account.log?.warn;
20
+ this.logError = account.log?.error;
21
+ this.logDebug = account.log?.debug;
22
+ this.accountFile = accountFile;
23
+ this.buildingsFile = buildingsFile;
24
+ this.devicesFile = devicesFile;
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
+ };
37
+ this.impulseGenerator = new ImpulseGenerator()
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
+ }))
44
+ .on('state', (state) => {
45
+ this.emit(state ? 'success' : 'warn', `Impulse generator ${state ? 'started' : 'stopped'}`);
46
+ });
47
+ }
48
+ }
49
+
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 Home
64
+ async checkDevicesList() {
65
+ try {
66
+ const devicesList = { State: false, Info: null, Devices: [] }
67
+ const headers = {
68
+ 'Accept': '*/*',
69
+ 'Accept-Language': 'en-US,en;q=0.9',
70
+ 'Cookie': this.contextKey,
71
+ 'User-Agent': 'homebridge-melcloud-control/4.0.0',
72
+ 'DNT': '1',
73
+ 'Origin': 'https://melcloudhome.com',
74
+ 'Referer': 'https://melcloudhome.com/dashboard',
75
+ 'Sec-Fetch-Dest': 'empty',
76
+ 'Sec-Fetch-Mode': 'cors',
77
+ 'Sec-Fetch-Site': 'same-origin',
78
+ 'X-CSRF': '1'
79
+ };
80
+
81
+ if (this.logDebug) this.emit('debug', `Scanning for devices`);
82
+ const listDevicesData = await axios(ApiUrlsHome.GetUserContext, {
83
+ method: 'GET',
84
+ baseURL: ApiUrlsHome.BaseURL,
85
+ timeout: 25000,
86
+ headers: headers
87
+ });
88
+ const buildingsList = listDevicesData.data.buildings;
89
+ if (this.logDebug) this.emit('debug', `Buildings: ${JSON.stringify(buildingsList, null, 2)}`);
90
+
91
+ if (!buildingsList) {
92
+ devicesList.Info = 'No building found'
93
+ return devicesList;
94
+ }
95
+
96
+ await this.functions.saveData(this.buildingsFile, buildingsList);
97
+ if (this.logDebug) this.emit('debug', `Buildings list saved`);
98
+
99
+ const devices = buildingsList.flatMap(building => {
100
+ // Funkcja kapitalizująca klucze obiektu
101
+ const capitalizeKeys = obj =>
102
+ Object.fromEntries(
103
+ Object.entries(obj).map(([key, value]) => [
104
+ key.charAt(0).toUpperCase() + key.slice(1),
105
+ value
106
+ ])
107
+ );
108
+
109
+ // Rekurencyjna kapitalizacja kluczy w obiekcie lub tablicy
110
+ const capitalizeKeysDeep = obj => {
111
+ if (Array.isArray(obj)) return obj.map(capitalizeKeysDeep);
112
+ if (obj && typeof obj === 'object') {
113
+ return Object.fromEntries(
114
+ Object.entries(obj).map(([key, value]) => [
115
+ key.charAt(0).toUpperCase() + key.slice(1),
116
+ capitalizeKeysDeep(value)
117
+ ])
118
+ );
119
+ }
120
+ return obj;
121
+ };
122
+
123
+ // Funkcja tworząca finalny obiekt Device
124
+ const createDevice = (device, type) => {
125
+ // Settings już kapitalizowane w nazwach
126
+ const settingsArray = device.Settings || [];
127
+
128
+ const settingsObject = Object.fromEntries(
129
+ settingsArray.map(({ name, value }) => {
130
+ let parsedValue = value;
131
+ if (value === "True") parsedValue = true;
132
+ else if (value === "False") parsedValue = false;
133
+ else if (!isNaN(value) && value !== "") parsedValue = Number(value);
134
+
135
+ const key = name.charAt(0).toUpperCase() + name.slice(1);
136
+ return [key, parsedValue];
137
+ })
138
+ );
139
+
140
+ // Scal Capabilities + Settings + DeviceType w Device
141
+ const deviceObject = {
142
+ ...capitalizeKeys(device.Capabilities || {}),
143
+ ...settingsObject,
144
+ DeviceType: type,
145
+ IsConnected: device.IsConnected
146
+ };
147
+
148
+ // Kapitalizacja brakujących obiektów/tablic
149
+ if (device.FrostProtection) device.FrostProtection = { ...capitalizeKeys(device.FrostProtection || {}) };
150
+ if (device.OverheatProtection) device.OverheatProtection = { ...capitalizeKeys(device.OverheatProtection || {}) };
151
+ if (device.HolidayMode) device.HolidayMode = { ...capitalizeKeys(device.HolidayMode || {}) };
152
+ if (Array.isArray(device.Schedule)) device.Schedule = device.Schedule.map(capitalizeKeysDeep);
153
+
154
+ // Usuń stare pola Settings i Capabilities
155
+ const { Settings, Capabilities, Id, GivenDisplayName, ...rest } = device;
156
+
157
+ return {
158
+ ...rest,
159
+ Type: type,
160
+ DeviceID: Id,
161
+ DeviceName: GivenDisplayName,
162
+ Device: deviceObject,
163
+ Headers: headers
164
+ };
165
+ };
166
+
167
+ return [
168
+ ...(building.airToAirUnits || []).map(d => createDevice(capitalizeKeys(d), 0)),
169
+ ...(building.airToWaterUnits || []).map(d => createDevice(capitalizeKeys(d), 1)),
170
+ ...(building.airToVentilationUnits || []).map(d => createDevice(capitalizeKeys(d), 3))
171
+ ];
172
+ });
173
+
174
+ const devicesCount = devices.length;
175
+ if (devicesCount === 0) {
176
+ devicesList.Info = 'No devices found'
177
+ return devicesList;
178
+ }
179
+
180
+ await this.functions.saveData(this.devicesFile, devices);
181
+ if (this.logDebug) this.emit('debug', `${devicesCount} devices saved`);
182
+
183
+ devicesList.State = true;
184
+ devicesList.Info = `Found ${devicesCount} devices`;
185
+ devicesList.Devices = devices;
186
+ return devicesList;
187
+ } catch (error) {
188
+ if (error.response?.status === 401) {
189
+ await connectToMelCloudHome();
190
+ if (this.logWarn) this.emit('warn', 'Check devices list not possible, cookies expired, trying to get new.');
191
+ return;
192
+ }
193
+
194
+ throw new Error(`Check devices list error: ${error.message}`);
195
+ }
196
+ }
197
+
198
+ async connect() {
199
+ if (this.logDebug) this.emit('debug', 'Connecting to MELCloud Home');
200
+ const GLOBAL_TIMEOUT = 90000;
201
+
202
+ let browser;
203
+ try {
204
+ const accountInfo = { State: false, Info: '', ContextKey: null, UseFahrenheit: false };
205
+ let chromiumPath = await this.functions.ensureChromiumInstalled();
206
+
207
+ // === Fallback to Puppeteer's built-in Chromium ===
208
+ if (!chromiumPath) {
209
+ try {
210
+ const puppeteerPath = puppeteer.executablePath();
211
+ if (puppeteerPath && fs.existsSync(puppeteerPath)) {
212
+ chromiumPath = puppeteerPath;
213
+ if (this.logDebug) this.emit('debug', `Using puppeteer Chromium at ${chromiumPath}`);
214
+ }
215
+ } catch { }
216
+ } else {
217
+ if (this.logDebug) this.emit('debug', `Using system Chromium at ${chromiumPath}`);
218
+ }
219
+
220
+ if (!chromiumPath) {
221
+ accountInfo.Info = 'Chromium not found on Your device, please install it manually and try again';
222
+ return accountInfo;
223
+ }
224
+
225
+ // Verify executable works
226
+ try {
227
+ const { stdout } = await execPromise(`"${chromiumPath}" --version`);
228
+ if (this.logDebug) this.emit('debug', `Chromium detected: ${stdout.trim()}`);
229
+ } catch (error) {
230
+ accountInfo.Info = `Chromium found at ${chromiumPath}, but cannot be executed: ${error.message}`;
231
+ return accountInfo;
232
+ }
233
+
234
+ if (this.logDebug) this.emit('debug', `Launching Chromium...`);
235
+ browser = await puppeteer.launch({
236
+ headless: true,
237
+ executablePath: chromiumPath,
238
+ timeout: GLOBAL_TIMEOUT,
239
+ args: [
240
+ '--no-sandbox',
241
+ '--disable-setuid-sandbox',
242
+ '--disable-dev-shm-usage',
243
+ '--single-process',
244
+ '--disable-gpu',
245
+ '--no-zygote'
246
+ ]
247
+ });
248
+ browser.on('disconnected', () => this.emit('debug', 'Browser disconnected'));
249
+
250
+ const page = await browser.newPage();
251
+ page.on('error', error => this.emit('error', `Page crashed: ${error.message}`));
252
+ page.on('pageerror', error => this.emit('error', `Browser error: ${error.message}`));
253
+ page.setDefaultTimeout(GLOBAL_TIMEOUT);
254
+ page.setDefaultNavigationTimeout(GLOBAL_TIMEOUT);
255
+
256
+ // Clear cookies before navigation
257
+ try {
258
+ const client = await page.createCDPSession();
259
+ await client.send('Network.clearBrowserCookies');
260
+ } catch (error) {
261
+ if (this.logError) this.emit('error', `Clear cookies error: ${error.message}`);
262
+ }
263
+
264
+ try {
265
+ await page.goto(ApiUrlsHome.BaseURL, { waitUntil: ['domcontentloaded', 'networkidle2'], timeout: GLOBAL_TIMEOUT });
266
+ } catch (error) {
267
+ accountInfo.Info = `Navigation to ${ApiUrlsHome.BaseURL} failed: ${error.message}`;
268
+ return accountInfo;
269
+ }
270
+
271
+ // Wait extra to ensure UI is rendered
272
+ await new Promise(r => setTimeout(r, 3000));
273
+ const loginBtn = await page.waitForSelector('button.btn--blue', { timeout: GLOBAL_TIMEOUT / 4 });
274
+ const loginText = await page.evaluate(el => el.textContent.trim(), loginBtn);
275
+
276
+ if (!['Zaloguj', 'Sign In', 'Login'].includes(loginText)) {
277
+ accountInfo.Info = `Login button ${loginText} not found`;
278
+ return accountInfo;
279
+ }
280
+
281
+ await loginBtn.click();
282
+ await page.waitForNavigation({ waitUntil: 'networkidle2', timeout: GLOBAL_TIMEOUT / 3 });
283
+
284
+ const usernameInput = await page.$('input[name="username"]');
285
+ const passwordInput = await page.$('input[name="password"]');
286
+ if (!usernameInput || !passwordInput) {
287
+ accountInfo.Info = 'Username or password input not found';
288
+ return accountInfo;
289
+ }
290
+
291
+ await page.type('input[name="username"]', this.user, { delay: 50 });
292
+ await page.type('input[name="password"]', this.passwd, { delay: 50 });
293
+
294
+ const submitButton = await page.$('input[type="submit"], button[type="submit"]');
295
+ if (!submitButton) {
296
+ accountInfo.Info = 'Submit button not found';
297
+ return accountInfo;
298
+ }
299
+ await Promise.race([Promise.all([submitButton.click(), page.waitForNavigation({ waitUntil: ['domcontentloaded', 'networkidle2'], timeout: GLOBAL_TIMEOUT / 4 })]), new Promise(r => setTimeout(r, GLOBAL_TIMEOUT / 3))]);
300
+
301
+ // Extract cookies
302
+ let c1 = null, c2 = null;
303
+ const start = Date.now();
304
+ while ((!c1 || !c2) && Date.now() - start < GLOBAL_TIMEOUT / 2) {
305
+ const cookies = await page.browserContext().cookies();
306
+ c1 = cookies.find(c => c.name === '__Secure-monitorandcontrolC1')?.value || c1;
307
+ c2 = cookies.find(c => c.name === '__Secure-monitorandcontrolC2')?.value || c2;
308
+ if (!c1 || !c2) await new Promise(r => setTimeout(r, 500));
309
+ }
310
+
311
+ if (!c1 || !c2) {
312
+ accountInfo.Info = 'Cookies C1/C2 missing';
313
+ return accountInfo;
314
+ }
315
+
316
+ const contextKey = [
317
+ '__Secure-monitorandcontrol=chunks-2',
318
+ `__Secure-monitorandcontrolC1=${c1}`,
319
+ `__Secure-monitorandcontrolC2=${c2}`
320
+ ].join('; ');
321
+ this.contextKey = contextKey;
322
+
323
+ accountInfo.State = true;
324
+ accountInfo.Info = 'Connect to MELCloud Home Success';
325
+ accountInfo.ContextKey = contextKey;
326
+ await this.functions.saveData(this.accountFile, accountInfo);
327
+
328
+ return accountInfo;
329
+ } catch (error) {
330
+ throw new Error(`Connect error: ${error.message}`);
331
+ } finally {
332
+ if (browser) {
333
+ try { await browser.close(); }
334
+ catch (closeErr) {
335
+ if (this.logError) this.emit('error', `Failed to close Puppeteer: ${closeErr.message}`);
336
+ }
337
+ }
338
+ }
339
+ }
340
+
341
+ async send(accountInfo) {
342
+ try {
343
+ const axiosInstance = axios.create({
344
+ method: 'POST',
345
+ baseURL: ApiUrlsHome.BaseURL,
346
+ timeout: 15000,
347
+ headers: {
348
+ 'X-MitsContextKey': accountInfo.ContextKey,
349
+ 'content-type': 'application/json'
350
+ }
351
+ });
352
+
353
+ const payload = { data: accountInfo.LoginData };
354
+ await axiosInstance(ApiUrlsHome.UpdateApplicationOptions, payload);
355
+ await this.functions.saveData(this.accountFile, accountInfo);
356
+ return true;
357
+ } catch (error) {
358
+ throw new Error(`Send data error: ${error.message}`);
359
+ }
360
+ }
361
+ }
362
+
363
+ export default MelCloud;
364
+