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