homebridge-melcloud-control 4.2.3-beta.4 → 4.2.3-beta.40

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.
@@ -52,16 +52,11 @@ 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
- const deviceData = devicesData.find(device => device.DeviceID === this.deviceId);
55
+ const deviceData = devicesData.Devices.find(device => device.DeviceID === this.deviceId);
60
56
 
61
57
  if (this.accountType === 'melcloudhome') {
62
- deviceData.SerialNumber = deviceData.DeviceID || '4.0.0';
63
- deviceData.Device.FirmwareAppVersion = deviceData.ConnectedInterfaceIdentifier || '4.0.0';
64
58
  }
59
+
65
60
  const safeConfig = {
66
61
  ...deviceData,
67
62
  Headers: 'removed',
@@ -69,15 +64,15 @@ class MelCloudAtw extends EventEmitter {
69
64
  if (this.logDebug) this.emit('debug', `Device Data: ${JSON.stringify(safeConfig, null, 2)}`);
70
65
 
71
66
  //device
72
- const serialNumber = deviceData.SerialNumber;
67
+ //device
68
+ const serialNumber = deviceData.SerialNumber || '4.0.0';
69
+ const firmwareAppVersion = deviceData.Device?.FirmwareAppVersion || '4.0.0';
73
70
  const hasHotWaterTank = deviceData.Device?.HasHotWaterTank;
74
- const firmwareAppVersion = deviceData.Device?.FirmwareAppVersion;
75
71
  const hasZone2 = deviceData.Device?.HasZone2;
76
72
 
77
73
  //units
78
74
  const units = Array.isArray(deviceData.Device?.Units) ? deviceData.Device?.Units : [];
79
75
  const unitsCount = units.length;
80
- const manufacturer = 'Mitsubishi';
81
76
 
82
77
  const { indoor, outdoor } = units.reduce((acc, unit) => {
83
78
  const target = unit.IsIndoor ? 'indoor' : 'outdoor';
@@ -109,14 +104,11 @@ class MelCloudAtw extends EventEmitter {
109
104
 
110
105
  //check state changes
111
106
  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
- }
107
+ if (deviceDataHasNotChanged) return;
116
108
  this.devicesData = devicesData;
117
109
 
118
110
  //emit info
119
- this.emit('deviceInfo', manufacturer, indoor.model, outdoor.model, serialNumber, firmwareAppVersion, hasHotWaterTank, hasZone2);
111
+ this.emit('deviceInfo', indoor.model, outdoor.model, serialNumber, firmwareAppVersion, hasHotWaterTank, hasZone2);
120
112
 
121
113
  //emit state
122
114
  this.emit('deviceState', deviceData);
@@ -185,14 +177,21 @@ class MelCloudAtw extends EventEmitter {
185
177
  case "melcloudhome":
186
178
  switch (effectiveFlags) {
187
179
  case 'holidaymode':
188
- payload = { enabled: deviceData.HolidayMode.Enabled, startDate: deviceData.HolidayMode.StartDate, endDate: deviceData.HolidayMode.EndDate, units: { "ATW": [deviceData.DeviceID] } };
180
+ payload = {
181
+ enabled: deviceData.HolidayMode.Enabled,
182
+ startDate: deviceData.HolidayMode.StartDate,
183
+ endDate: deviceData.HolidayMode.EndDate,
184
+ units: { "ATW": [deviceData.DeviceID] }
185
+ };
189
186
  method = 'POST';
190
187
  path = ApiUrlsHome.PostHolidayMode;
188
+ deviceData.Headers.Referer = ApiUrlsHome.Referers.PostHolidayMode.replace('deviceid', deviceData.DeviceID);
191
189
  break;
192
190
  case 'schedule':
193
191
  payload = { enabled: deviceData.ScheduleEnabled };
194
192
  method = 'PUT';
195
- path = ApiUrlsHome.PutScheduleEnable.replace('deviceid', deviceData.DeviceID);
193
+ path = ApiUrlsHome.PutScheduleEnabled.replace('deviceid', deviceData.DeviceID);
194
+ deviceData.Headers.Referer = ApiUrlsHome.Referers.PutScheduleEnabled.replace('deviceid', deviceData.DeviceID);
196
195
  break;
197
196
  default:
198
197
  payload = {
@@ -211,11 +210,14 @@ class MelCloudAtw extends EventEmitter {
211
210
  EcoHotWater: deviceData.Device.EcoHotWater,
212
211
  };
213
212
  method = 'PUT';
214
- path = ApiUrlsHome.SetAtw.replace('deviceid', deviceData.DeviceID);
213
+ path = ApiUrlsHome.PutAtw.replace('deviceid', deviceData.DeviceID);
214
+ deviceData.Headers.Referer = ApiUrlsHome.Referers.PutDeviceSettings
215
215
  break
216
216
  }
217
217
 
218
- if (this.logDebug) this.emit('debug', `Send Data: ${JSON.stringify(payload, null, 2)}`);
218
+ deviceData.Headers['Content-Type'] = 'application/json; charset=utf-8';
219
+ deviceData.Headers.Origin = ApiUrlsHome.Origin;
220
+ if (this.logDebug) this.emit('debug', `Send Data: ${JSON.stringify(payload, null, 2)}, Headers: ${JSON.stringify(deviceData.Headers, null, 2)}`);
219
221
  await axios(path, {
220
222
  method: method,
221
223
  baseURL: ApiUrlsHome.BaseURL,
@@ -240,7 +242,7 @@ class MelCloudAtw extends EventEmitter {
240
242
 
241
243
  setTimeout(() => {
242
244
  this.lock = false
243
- }, 3000);
245
+ }, 2500);
244
246
  }
245
247
  };
246
248
  export default MelCloudAtw;
@@ -52,35 +52,28 @@ 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
- const deviceData = devicesData.find(device => device.DeviceID === this.deviceId);
55
+ const deviceData = devicesData.Devices.find(device => device.DeviceID === this.deviceId);
60
56
 
61
57
  if (this.accountType === 'melcloudhome') {
62
- deviceData.SerialNumber = deviceData.DeviceID || '4.0.0';
63
- deviceData.Device.FirmwareAppVersion = deviceData.ConnectedInterfaceIdentifier || '4.0.0';
64
-
65
58
  //read default temps
66
59
  const temps = await this.functions.readData(this.defaultTempsFile, true);
67
60
  deviceData.Device.DefaultHeatingSetTemperature = temps?.defaultHeatingSetTemperature ?? 20;
68
61
  deviceData.Device.DefaultCoolingSetTemperature = temps?.defaultCoolingSetTemperature ?? 24;
69
62
  }
63
+
70
64
  const safeConfig = {
71
65
  ...deviceData,
72
66
  Headers: 'removed',
73
67
  };
74
68
  if (this.logDebug) this.emit('debug', `Device Data: ${JSON.stringify(safeConfig, null, 2)}`);
75
69
 
76
- //presets
77
- const serialNumber = deviceData.SerialNumber;
78
- const firmwareAppVersion = deviceData.Device?.FirmwareAppVersion;
70
+ //device
71
+ const serialNumber = deviceData.SerialNumber || '4.0.0';
72
+ const firmwareAppVersion = deviceData.Device?.FirmwareAppVersion || '4.0.0';
79
73
 
80
74
  //units
81
75
  const units = Array.isArray(deviceData.Device?.Units) ? deviceData.Device?.Units : [];
82
76
  const unitsCount = units.length;
83
- const manufacturer = 'Mitsubishi';
84
77
 
85
78
  const { indoor, outdoor } = units.reduce((acc, unit) => {
86
79
  const target = unit.IsIndoor ? 'indoor' : 'outdoor';
@@ -112,14 +105,11 @@ class MelCloudErv extends EventEmitter {
112
105
 
113
106
  //check state changes
114
107
  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
- }
108
+ if (deviceDataHasNotChanged) return;
119
109
  this.devicesData = devicesData;
120
110
 
121
111
  //emit info
122
- this.emit('deviceInfo', manufacturer, indoor.model, outdoor.model, serialNumber, firmwareAppVersion);
112
+ this.emit('deviceInfo', indoor.model, outdoor.model, serialNumber, firmwareAppVersion);
123
113
 
124
114
  //emit state
125
115
  this.emit('deviceState', deviceData);
@@ -201,14 +191,21 @@ class MelCloudErv extends EventEmitter {
201
191
 
202
192
  switch (effectiveFlags) {
203
193
  case 'holidaymode':
204
- payload = { enabled: deviceData.HolidayMode.Enabled, startDate: deviceData.HolidayMode.StartDate, endDate: deviceData.HolidayMode.EndDate, units: { "ERV": [deviceData.DeviceID] } };
194
+ payload = {
195
+ enabled: deviceData.HolidayMode.Enabled,
196
+ startDate: deviceData.HolidayMode.StartDate,
197
+ endDate: deviceData.HolidayMode.EndDate,
198
+ units: { "ERV": [deviceData.DeviceID] }
199
+ };
205
200
  method = 'POST';
206
201
  path = ApiUrlsHome.PostHolidayMode;
202
+ deviceData.Headers.Referer = ApiUrlsHome.Referers.PostHolidayMode.replace('deviceid', deviceData.DeviceID);
207
203
  break;
208
204
  case 'schedule':
209
205
  payload = { enabled: deviceData.ScheduleEnabled };
210
206
  method = 'PUT';
211
- path = ApiUrlsHome.PutScheduleEnable.replace('deviceid', deviceData.DeviceID);
207
+ path = ApiUrlsHome.PutScheduleEnabled.replace('deviceid', deviceData.DeviceID);
208
+ deviceData.Headers.Referer = ApiUrlsHome.Referers.PutScheduleEnabled.replace('deviceid', deviceData.DeviceID);
212
209
  break;
213
210
  default:
214
211
  payload = {
@@ -219,11 +216,14 @@ class MelCloudErv extends EventEmitter {
219
216
  VentilationMode: Ventilation.VentilationModeMapEnumToString[deviceData.Device.VentilationMode],
220
217
  };
221
218
  method = 'PUT';
222
- path = ApiUrlsHome.SetErv.replace('deviceid', deviceData.DeviceID);
219
+ path = ApiUrlsHome.PutErv.replace('deviceid', deviceData.DeviceID);
220
+ deviceData.Headers.Referer = ApiUrlsHome.Referers.PutDeviceSettings
223
221
  break
224
222
  }
225
223
 
226
- if (this.logDebug) this.emit('debug', `Send Data: ${JSON.stringify(payload, null, 2)}`);
224
+ deviceData.Headers['Content-Type'] = 'application/json; charset=utf-8';
225
+ deviceData.Headers.Origin = ApiUrlsHome.Origin;
226
+ if (this.logDebug) this.emit('debug', `Send Data: ${JSON.stringify(payload, null, 2)}, Headers: ${JSON.stringify(deviceData.Headers, null, 2)}`);
227
227
  await axios(path, {
228
228
  method: method,
229
229
  baseURL: ApiUrlsHome.BaseURL,
@@ -248,7 +248,7 @@ class MelCloudErv extends EventEmitter {
248
248
 
249
249
  setTimeout(() => {
250
250
  this.lock = false
251
- }, 3000);
251
+ }, 2500);
252
252
  }
253
253
  };
254
254
  export default MelCloudErv;
@@ -0,0 +1,403 @@
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, LanguageLocaleMap } 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.headers = {};
26
+
27
+ this.functions = new Functions(this.logWarn, this.logError, this.logDebug)
28
+ .on('warn', warn => this.emit('warn', warn))
29
+ .on('error', error => this.emit('error', error))
30
+ .on('debug', debug => this.emit('debug', debug));
31
+
32
+ if (pluginStart) {
33
+ //lock flags
34
+ this.locks = {
35
+ connect: false,
36
+ checkDevicesList: false
37
+ };
38
+ this.impulseGenerator = new ImpulseGenerator()
39
+ .on('connect', () => this.handleWithLock('connect', async () => {
40
+ await this.connect();
41
+ }))
42
+ .on('checkDevicesList', () => this.handleWithLock('checkDevicesList', async () => {
43
+ await this.checkDevicesList();
44
+ }))
45
+ .on('state', (state) => {
46
+ this.emit(state ? 'success' : 'warn', `Impulse generator ${state ? 'started' : 'stopped'}`);
47
+ });
48
+ }
49
+ }
50
+
51
+ async handleWithLock(lockKey, fn) {
52
+ if (this.locks[lockKey]) return;
53
+
54
+ this.locks[lockKey] = true;
55
+ try {
56
+ await fn();
57
+ } catch (error) {
58
+ this.emit('error', `Inpulse generator error: ${error}`);
59
+ } finally {
60
+ this.locks[lockKey] = false;
61
+ }
62
+ }
63
+
64
+ // MELCloud Home
65
+ async checkScenesList() {
66
+ try {
67
+ 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
+ });
74
+
75
+ const scenesList = listScenesData.data;
76
+ if (this.logDebug) this.emit('debug', `Scenes: ${JSON.stringify(scenesList, null, 2)}`);
77
+
78
+ const capitalizeKeysDeep = obj => {
79
+ if (Array.isArray(obj)) {
80
+ return obj.map(item => capitalizeKeysDeep(item));
81
+ }
82
+
83
+ if (obj && typeof obj === 'object') {
84
+ return Object.fromEntries(
85
+ Object.entries(obj).map(([key, value]) => [
86
+ key.charAt(0).toUpperCase() + key.slice(1),
87
+ capitalizeKeysDeep(value)
88
+ ])
89
+ );
90
+ }
91
+
92
+ return obj;
93
+ };
94
+
95
+ return capitalizeKeysDeep(scenesList);
96
+ } catch (error) {
97
+ throw new Error(`Check scenes list error: ${error.message}`);
98
+ }
99
+ }
100
+
101
+ async checkDevicesList() {
102
+ try {
103
+ const devicesList = { State: false, Info: null, Devices: [], Scenes: [] }
104
+ 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
+ });
111
+
112
+ const userContext = listDevicesData.data;
113
+ const buildings = userContext.buildings ?? [];
114
+ const guestBuildings = userContext.guestBuildings ?? [];
115
+ const buildingsList = [...buildings, ...guestBuildings];
116
+ if (this.logDebug) this.emit('debug', `Buildings: ${JSON.stringify(buildingsList, null, 2)}`);
117
+
118
+ if (!buildingsList) {
119
+ devicesList.Info = 'No building found'
120
+ return devicesList;
121
+ }
122
+
123
+ await this.functions.saveData(this.buildingsFile, userContext);
124
+ if (this.logDebug) this.emit('debug', `Buildings list saved`);
125
+
126
+ const devices = buildingsList.flatMap(building => {
127
+ // Funkcja kapitalizująca klucze obiektu
128
+ const capitalizeKeys = obj =>
129
+ Object.fromEntries(
130
+ Object.entries(obj).map(([key, value]) => [
131
+ key.charAt(0).toUpperCase() + key.slice(1),
132
+ value
133
+ ])
134
+ );
135
+
136
+ // Rekurencyjna kapitalizacja kluczy w obiekcie lub tablicy
137
+ const capitalizeKeysDeep = obj => {
138
+ if (Array.isArray(obj)) return obj.map(capitalizeKeysDeep);
139
+ if (obj && typeof obj === 'object') {
140
+ return Object.fromEntries(
141
+ Object.entries(obj).map(([key, value]) => [
142
+ key.charAt(0).toUpperCase() + key.slice(1),
143
+ capitalizeKeysDeep(value)
144
+ ])
145
+ );
146
+ }
147
+ return obj;
148
+ };
149
+
150
+ // Funkcja tworząca finalny obiekt Device
151
+ const createDevice = (device, type) => {
152
+ // Settings już kapitalizowane w nazwach
153
+ const settingsArray = device.Settings || [];
154
+
155
+ const settingsObject = Object.fromEntries(
156
+ settingsArray.map(({ name, value }) => {
157
+ let parsedValue = value;
158
+ if (value === "True") parsedValue = true;
159
+ else if (value === "False") parsedValue = false;
160
+ else if (!isNaN(value) && value !== "") parsedValue = Number(value);
161
+
162
+ const key = name.charAt(0).toUpperCase() + name.slice(1);
163
+ return [key, parsedValue];
164
+ })
165
+ );
166
+
167
+ // Scal Capabilities + Settings + DeviceType w Device
168
+ const deviceObject = {
169
+ ...capitalizeKeys(device.Capabilities || {}),
170
+ ...settingsObject,
171
+ DeviceType: type,
172
+ FirmwareAppVersion: device.ConnectedInterfaceIdentifier,
173
+ IsConnected: device.IsConnected
174
+ };
175
+
176
+ // Kapitalizacja brakujących obiektów/tablic
177
+ if (device.FrostProtection) device.FrostProtection = { ...capitalizeKeys(device.FrostProtection || {}) };
178
+ if (device.OverheatProtection) device.OverheatProtection = { ...capitalizeKeys(device.OverheatProtection || {}) };
179
+ if (device.HolidayMode) device.HolidayMode = { ...capitalizeKeys(device.HolidayMode || {}) };
180
+ if (Array.isArray(device.Schedule)) device.Schedule = device.Schedule.map(capitalizeKeysDeep || []);
181
+
182
+ // Usuń stare pola Settings i Capabilities
183
+ const { Settings, Capabilities, Id, GivenDisplayName, ...rest } = device;
184
+
185
+ return {
186
+ ...rest,
187
+ Type: type,
188
+ DeviceID: Id,
189
+ DeviceName: GivenDisplayName,
190
+ SerialNumber: Id,
191
+ Device: deviceObject,
192
+ Headers: this.headers
193
+ };
194
+ };
195
+
196
+ return [
197
+ ...(building.airToAirUnits || []).map(d => createDevice(capitalizeKeys(d), 0)),
198
+ ...(building.airToWaterUnits || []).map(d => createDevice(capitalizeKeys(d), 1)),
199
+ ...(building.airToVentilationUnits || []).map(d => createDevice(capitalizeKeys(d), 3))
200
+ ];
201
+ });
202
+
203
+ const devicesCount = devices.length;
204
+ if (devicesCount === 0) {
205
+ devicesList.Info = 'No devices found'
206
+ return devicesList;
207
+ }
208
+
209
+ const scenes = await this.checkScenesList();
210
+
211
+ devicesList.State = true;
212
+ devicesList.Info = `Found ${devicesCount} devices`;
213
+ devicesList.Devices = devices;
214
+ devicesList.Scenes = scenes;
215
+
216
+ await this.functions.saveData(this.devicesFile, devicesList);
217
+ if (this.logDebug) this.emit('debug', `${devicesCount} devices saved`);
218
+
219
+ return devicesList;
220
+ } 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
+ throw new Error(`Check devices list error: ${error.message}`);
228
+ }
229
+ }
230
+
231
+ async connect() {
232
+ if (this.logDebug) this.emit('debug', 'Connecting to MELCloud Home');
233
+ const GLOBAL_TIMEOUT = 90000;
234
+
235
+ let browser;
236
+ try {
237
+ const accountInfo = { State: false, Info: '', Headers: {}, UseFahrenheit: false };
238
+ let chromiumPath = await this.functions.ensureChromiumInstalled();
239
+
240
+ // === Fallback to Puppeteer's built-in Chromium ===
241
+ if (!chromiumPath) {
242
+ try {
243
+ const puppeteerPath = puppeteer.executablePath();
244
+ if (puppeteerPath && fs.existsSync(puppeteerPath)) {
245
+ chromiumPath = puppeteerPath;
246
+ if (this.logDebug) this.emit('debug', `Using puppeteer Chromium at ${chromiumPath}`);
247
+ }
248
+ } catch { }
249
+ } else {
250
+ if (this.logDebug) this.emit('debug', `Using system Chromium at ${chromiumPath}`);
251
+ }
252
+
253
+ if (!chromiumPath) {
254
+ accountInfo.Info = 'Chromium not found on Your device, please install it manually and try again';
255
+ return accountInfo;
256
+ }
257
+
258
+ // Verify executable works
259
+ try {
260
+ const { stdout } = await execPromise(`"${chromiumPath}" --version`);
261
+ if (this.logDebug) this.emit('debug', `Chromium detected: ${stdout.trim()}`);
262
+ } catch (error) {
263
+ accountInfo.Info = `Chromium found at ${chromiumPath}, but cannot be executed: ${error.message}`;
264
+ return accountInfo;
265
+ }
266
+
267
+ if (this.logDebug) this.emit('debug', `Launching Chromium...`);
268
+ browser = await puppeteer.launch({
269
+ headless: true,
270
+ executablePath: chromiumPath,
271
+ timeout: GLOBAL_TIMEOUT,
272
+ args: [
273
+ '--no-sandbox',
274
+ '--disable-setuid-sandbox',
275
+ '--disable-dev-shm-usage',
276
+ '--single-process',
277
+ '--disable-gpu',
278
+ '--no-zygote'
279
+ ]
280
+ });
281
+ browser.on('disconnected', () => this.emit('debug', 'Browser disconnected'));
282
+
283
+ const page = await browser.newPage();
284
+ page.on('error', error => this.emit('error', `Page crashed: ${error.message}`));
285
+ page.on('pageerror', error => this.emit('error', `Browser error: ${error.message}`));
286
+ page.setDefaultTimeout(GLOBAL_TIMEOUT);
287
+ page.setDefaultNavigationTimeout(GLOBAL_TIMEOUT);
288
+
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
+ try {
298
+ await page.goto(ApiUrlsHome.BaseURL, { waitUntil: ['domcontentloaded', 'networkidle2'], timeout: GLOBAL_TIMEOUT });
299
+ } catch (error) {
300
+ accountInfo.Info = `Navigation to ${ApiUrlsHome.BaseURL} failed: ${error.message}`;
301
+ return accountInfo;
302
+ }
303
+
304
+ // Wait extra to ensure UI is rendered
305
+ await new Promise(r => setTimeout(r, 3000));
306
+ const loginBtn = await page.waitForSelector('button.btn--blue', { timeout: GLOBAL_TIMEOUT / 4 });
307
+ const loginText = await page.evaluate(el => el.textContent.trim(), loginBtn);
308
+
309
+ if (!['Zaloguj', 'Sign In', 'Login'].includes(loginText)) {
310
+ accountInfo.Info = `Login button ${loginText} not found`;
311
+ return accountInfo;
312
+ }
313
+
314
+ await loginBtn.click();
315
+ await page.waitForNavigation({ waitUntil: 'networkidle2', timeout: GLOBAL_TIMEOUT / 3 });
316
+
317
+ const usernameInput = await page.$('input[name="username"]');
318
+ const passwordInput = await page.$('input[name="password"]');
319
+ if (!usernameInput || !passwordInput) {
320
+ accountInfo.Info = 'Username or password input not found';
321
+ return accountInfo;
322
+ }
323
+
324
+ await page.type('input[name="username"]', this.user, { delay: 50 });
325
+ await page.type('input[name="password"]', this.passwd, { delay: 50 });
326
+
327
+ const submitButton = await page.$('input[type="submit"], button[type="submit"]');
328
+ if (!submitButton) {
329
+ accountInfo.Info = 'Submit button not found';
330
+ return accountInfo;
331
+ }
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))]);
333
+
334
+ // Extract cookies
335
+ let c1 = null, c2 = null;
336
+ const start = Date.now();
337
+ while ((!c1 || !c2) && Date.now() - start < GLOBAL_TIMEOUT / 2) {
338
+ const cookies = await page.browserContext().cookies();
339
+ c1 = cookies.find(c => c.name === '__Secure-monitorandcontrolC1')?.value || c1;
340
+ c2 = cookies.find(c => c.name === '__Secure-monitorandcontrolC2')?.value || c2;
341
+ if (!c1 || !c2) await new Promise(r => setTimeout(r, 500));
342
+ }
343
+
344
+ if (!c1 || !c2) {
345
+ accountInfo.Info = 'Cookies C1/C2 missing';
346
+ return accountInfo;
347
+ }
348
+
349
+ const cookies = [
350
+ '__Secure-monitorandcontrol=chunks-2',
351
+ `__Secure-monitorandcontrolC1=${c1}`,
352
+ `__Secure-monitorandcontrolC2=${c2}`
353
+ ].join('; ');
354
+
355
+ this.headers = {
356
+ 'Accept': '*/*',
357
+ 'Accept-Encoding': 'gzip, deflate, br',
358
+ 'Accept-Language': LanguageLocaleMap[this.language],
359
+ 'Cookie': cookies,
360
+ 'Priority': 'u=3, i',
361
+ 'Referer': ApiUrlsHome.Dashboard,
362
+ 'Sec-Fetch-Dest': 'empty',
363
+ 'Sec-Fetch-Mode': 'cors',
364
+ 'Sec-Fetch-Site': 'same-origin',
365
+ 'x-csrf': '1'
366
+ };
367
+
368
+ accountInfo.State = true;
369
+ accountInfo.Info = 'Connect to MELCloud Home Success';
370
+ accountInfo.Headers = this.headers;
371
+ await this.functions.saveData(this.accountFile, accountInfo);
372
+
373
+ return accountInfo;
374
+ } catch (error) {
375
+ throw new Error(`Connect error: ${error.message}`);
376
+ } finally {
377
+ if (browser) {
378
+ try { await browser.close(); }
379
+ catch (closeErr) {
380
+ if (this.logError) this.emit('error', `Failed to close Puppeteer: ${closeErr.message}`);
381
+ }
382
+ }
383
+ }
384
+ }
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
+ }
401
+
402
+ export default MelCloud;
403
+