homebridge-melcloud-control 4.2.3-beta.6 → 4.2.3-beta.61

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,12 +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
- const deviceData = devicesData.find(device => device.DeviceID === this.deviceId);
55
+ const deviceData = devicesData.Devices.find(device => device.DeviceID === this.deviceId);
56
56
 
57
57
  if (this.accountType === 'melcloudhome') {
58
- deviceData.SerialNumber = deviceData.DeviceID || '4.0.0';
59
- deviceData.Device.FirmwareAppVersion = deviceData.ConnectedInterfaceIdentifier || '4.0.0';
60
58
  }
59
+
61
60
  const safeConfig = {
62
61
  ...deviceData,
63
62
  Headers: 'removed',
@@ -65,15 +64,15 @@ class MelCloudAtw extends EventEmitter {
65
64
  if (this.logDebug) this.emit('debug', `Device Data: ${JSON.stringify(safeConfig, null, 2)}`);
66
65
 
67
66
  //device
68
- const serialNumber = deviceData.SerialNumber;
67
+ //device
68
+ const serialNumber = deviceData.SerialNumber || '4.0.0';
69
+ const firmwareAppVersion = deviceData.Device?.FirmwareAppVersion || '4.0.0';
69
70
  const hasHotWaterTank = deviceData.Device?.HasHotWaterTank;
70
- const firmwareAppVersion = deviceData.Device?.FirmwareAppVersion;
71
71
  const hasZone2 = deviceData.Device?.HasZone2;
72
72
 
73
73
  //units
74
74
  const units = Array.isArray(deviceData.Device?.Units) ? deviceData.Device?.Units : [];
75
75
  const unitsCount = units.length;
76
- const manufacturer = 'Mitsubishi';
77
76
 
78
77
  const { indoor, outdoor } = units.reduce((acc, unit) => {
79
78
  const target = unit.IsIndoor ? 'indoor' : 'outdoor';
@@ -105,11 +104,11 @@ class MelCloudAtw extends EventEmitter {
105
104
 
106
105
  //check state changes
107
106
  const deviceDataHasNotChanged = JSON.stringify(devicesData) === JSON.stringify(this.devicesData);
108
- if (deviceDataHasNotChanged) return;
107
+ if (deviceDataHasNotChanged) return;
109
108
  this.devicesData = devicesData;
110
109
 
111
110
  //emit info
112
- this.emit('deviceInfo', manufacturer, indoor.model, outdoor.model, serialNumber, firmwareAppVersion, hasHotWaterTank, hasZone2);
111
+ this.emit('deviceInfo', indoor.model, outdoor.model, serialNumber, firmwareAppVersion, hasHotWaterTank, hasZone2);
113
112
 
114
113
  //emit state
115
114
  this.emit('deviceState', deviceData);
@@ -120,7 +119,7 @@ class MelCloudAtw extends EventEmitter {
120
119
  };
121
120
  };
122
121
 
123
- async send(accountType, displayType, deviceData, effectiveFlags) {
122
+ async send(accountType, displayType, deviceData, flag) {
124
123
  try {
125
124
 
126
125
  //prevent to set out of range temp
@@ -141,7 +140,7 @@ class MelCloudAtw extends EventEmitter {
141
140
  let path = '';
142
141
  switch (accountType) {
143
142
  case "melcloud":
144
- deviceData.Device.EffectiveFlags = effectiveFlags;
143
+ deviceData.Device.EffectiveFlags = flag;
145
144
  payload = {
146
145
  DeviceID: deviceData.Device.DeviceID,
147
146
  EffectiveFlags: deviceData.Device.EffectiveFlags,
@@ -176,39 +175,49 @@ class MelCloudAtw extends EventEmitter {
176
175
  this.updateData(deviceData);
177
176
  return true;
178
177
  case "melcloudhome":
179
- switch (effectiveFlags) {
178
+ switch (flag) {
180
179
  case 'holidaymode':
181
- 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
+ };
182
186
  method = 'POST';
183
187
  path = ApiUrlsHome.PostHolidayMode;
188
+ deviceData.Headers.Referer = ApiUrlsHome.Referers.PostHolidayMode.replace('deviceid', deviceData.DeviceID);
184
189
  break;
185
190
  case 'schedule':
186
191
  payload = { enabled: deviceData.ScheduleEnabled };
187
192
  method = 'PUT';
188
- 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);
189
195
  break;
190
196
  default:
191
197
  payload = {
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,
198
+ power: deviceData.Device.Power,
199
+ setTemperatureZone1: deviceData.Device.SetTemperatureZone1,
200
+ setTemperatureZone2: deviceData.Device.SetTemperatureZone2,
201
+ operationMode: HeatPump.OperationModeMapEnumToString[deviceData.Device.OperationMode],
202
+ operationModeZone1: HeatPump.OperationModeMapEnumToString[deviceData.Device.OperationModeZone1],
203
+ operationModeZone2: HeatPump.OperationModeMapEnumToString[deviceData.Device.OperationModeZone2],
204
+ opetHeatFlowTemperatureZone1: deviceData.Device.SetHeatFlowTemperatureZone1,
205
+ setHeatFlowTemperatureZone2: deviceData.Device.SetHeatFlowTemperatureZone2,
206
+ setCoolFlowTemperatureZone1: deviceData.Device.SetCoolFlowTemperatureZone1,
207
+ setCoolFlowTemperatureZone2: deviceData.Device.SetCoolFlowTemperatureZone2,
208
+ setTankWaterTemperature: deviceData.Device.SetTankWaterTemperature,
209
+ forcedHotWaterMode: deviceData.Device.ForcedHotWaterMode,
210
+ ecoHotWater: deviceData.Device.EcoHotWater,
205
211
  };
206
212
  method = 'PUT';
207
- path = ApiUrlsHome.SetAtw.replace('deviceid', deviceData.DeviceID);
213
+ path = ApiUrlsHome.PutAtw.replace('deviceid', deviceData.DeviceID);
214
+ deviceData.Headers.Referer = ApiUrlsHome.Referers.PutDeviceSettings
208
215
  break
209
216
  }
210
217
 
211
- 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)}`);
212
221
  await axios(path, {
213
222
  method: method,
214
223
  baseURL: ApiUrlsHome.BaseURL,
@@ -233,7 +242,7 @@ class MelCloudAtw extends EventEmitter {
233
242
 
234
243
  setTimeout(() => {
235
244
  this.lock = false
236
- }, 3000);
245
+ }, 2500);
237
246
  }
238
247
  };
239
248
  export default MelCloudAtw;
@@ -52,31 +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
- const deviceData = devicesData.find(device => device.DeviceID === this.deviceId);
55
+ const deviceData = devicesData.Devices.find(device => device.DeviceID === this.deviceId);
56
56
 
57
57
  if (this.accountType === 'melcloudhome') {
58
- deviceData.SerialNumber = deviceData.DeviceID || '4.0.0';
59
- deviceData.Device.FirmwareAppVersion = deviceData.ConnectedInterfaceIdentifier || '4.0.0';
60
-
61
58
  //read default temps
62
59
  const temps = await this.functions.readData(this.defaultTempsFile, true);
63
60
  deviceData.Device.DefaultHeatingSetTemperature = temps?.defaultHeatingSetTemperature ?? 20;
64
61
  deviceData.Device.DefaultCoolingSetTemperature = temps?.defaultCoolingSetTemperature ?? 24;
65
62
  }
63
+
66
64
  const safeConfig = {
67
65
  ...deviceData,
68
66
  Headers: 'removed',
69
67
  };
70
68
  if (this.logDebug) this.emit('debug', `Device Data: ${JSON.stringify(safeConfig, null, 2)}`);
71
69
 
72
- //presets
73
- const serialNumber = deviceData.SerialNumber;
74
- const firmwareAppVersion = deviceData.Device?.FirmwareAppVersion;
70
+ //device
71
+ const serialNumber = deviceData.SerialNumber || '4.0.0';
72
+ const firmwareAppVersion = deviceData.Device?.FirmwareAppVersion || '4.0.0';
75
73
 
76
74
  //units
77
75
  const units = Array.isArray(deviceData.Device?.Units) ? deviceData.Device?.Units : [];
78
76
  const unitsCount = units.length;
79
- const manufacturer = 'Mitsubishi';
80
77
 
81
78
  const { indoor, outdoor } = units.reduce((acc, unit) => {
82
79
  const target = unit.IsIndoor ? 'indoor' : 'outdoor';
@@ -112,7 +109,7 @@ class MelCloudErv extends EventEmitter {
112
109
  this.devicesData = devicesData;
113
110
 
114
111
  //emit info
115
- this.emit('deviceInfo', manufacturer, indoor.model, outdoor.model, serialNumber, firmwareAppVersion);
112
+ this.emit('deviceInfo', indoor.model, outdoor.model, serialNumber, firmwareAppVersion);
116
113
 
117
114
  //emit state
118
115
  this.emit('deviceState', deviceData);
@@ -123,7 +120,7 @@ class MelCloudErv extends EventEmitter {
123
120
  };
124
121
  };
125
122
 
126
- async send(accountType, displayType, deviceData, effectiveFlags) {
123
+ async send(accountType, displayType, deviceData, flag) {
127
124
  try {
128
125
  let method = null
129
126
  let payload = {};
@@ -151,7 +148,7 @@ class MelCloudErv extends EventEmitter {
151
148
  };
152
149
 
153
150
  //device state
154
- deviceData.Device.EffectiveFlags = effectiveFlags;
151
+ deviceData.Device.EffectiveFlags = flag;
155
152
  payload = {
156
153
  DeviceID: deviceData.Device.DeviceID,
157
154
  EffectiveFlags: deviceData.Device.EffectiveFlags,
@@ -192,31 +189,41 @@ class MelCloudErv extends EventEmitter {
192
189
  }
193
190
  }
194
191
 
195
- switch (effectiveFlags) {
192
+ switch (flag) {
196
193
  case 'holidaymode':
197
- 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
+ };
198
200
  method = 'POST';
199
201
  path = ApiUrlsHome.PostHolidayMode;
202
+ deviceData.Headers.Referer = ApiUrlsHome.Referers.PostHolidayMode.replace('deviceid', deviceData.DeviceID);
200
203
  break;
201
204
  case 'schedule':
202
205
  payload = { enabled: deviceData.ScheduleEnabled };
203
206
  method = 'PUT';
204
- 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);
205
209
  break;
206
210
  default:
207
211
  payload = {
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],
212
+ power: deviceData.Device.Power,
213
+ setTemperature: deviceData.Device.SetTemperature,
214
+ setFanSpeed: String(deviceData.Device.SetFanSpeed),
215
+ operationMode: Ventilation.OperationModeMapEnumToString[deviceData.Device.OperationMode],
216
+ ventilationMode: Ventilation.VentilationModeMapEnumToString[deviceData.Device.VentilationMode],
213
217
  };
214
218
  method = 'PUT';
215
- path = ApiUrlsHome.SetErv.replace('deviceid', deviceData.DeviceID);
219
+ path = ApiUrlsHome.PutErv.replace('deviceid', deviceData.DeviceID);
220
+ deviceData.Headers.Referer = ApiUrlsHome.Referers.PutDeviceSettings
216
221
  break
217
222
  }
218
223
 
219
- 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)}`);
220
227
  await axios(path, {
221
228
  method: method,
222
229
  baseURL: ApiUrlsHome.BaseURL,
@@ -241,7 +248,7 @@ class MelCloudErv extends EventEmitter {
241
248
 
242
249
  setTimeout(() => {
243
250
  this.lock = false
244
- }, 3000);
251
+ }, 2500);
245
252
  }
246
253
  };
247
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
+