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