homebridge-melcloud-control 4.4.1-beta.26 → 4.4.1-beta.28
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/package.json +1 -1
- package/src/melcloudhome.js +1 -153
- package/src/helper.js +0 -70
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"displayName": "MELCloud Control",
|
|
3
3
|
"name": "homebridge-melcloud-control",
|
|
4
|
-
"version": "4.4.1-beta.
|
|
4
|
+
"version": "4.4.1-beta.28",
|
|
5
5
|
"description": "Homebridge plugin to control Mitsubishi Air Conditioner, Heat Pump and Energy Recovery Ventilation.",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"author": "grzegorz914",
|
package/src/melcloudhome.js
CHANGED
|
@@ -9,11 +9,6 @@ import Functions from './functions.js';
|
|
|
9
9
|
import { ApiUrls, LanguageLocaleMap } from './constants.js';
|
|
10
10
|
const execPromise = promisify(exec);
|
|
11
11
|
|
|
12
|
-
import CookieJar from './helper.js';
|
|
13
|
-
|
|
14
|
-
import crypto from 'crypto';
|
|
15
|
-
import { URL } from 'url';
|
|
16
|
-
|
|
17
12
|
class MelCloudHome extends EventEmitter {
|
|
18
13
|
constructor(account, accountFile, buildingsFile, pluginStart = false) {
|
|
19
14
|
super();
|
|
@@ -33,8 +28,6 @@ class MelCloudHome extends EventEmitter {
|
|
|
33
28
|
this.socketConnected = false;
|
|
34
29
|
this.heartbeat = null;
|
|
35
30
|
|
|
36
|
-
this.cookies = [];
|
|
37
|
-
|
|
38
31
|
this.functions = new Functions(this.logWarn, this.logError, this.logDebug)
|
|
39
32
|
.on('warn', warn => this.emit('warn', warn))
|
|
40
33
|
.on('error', error => this.emit('error', error))
|
|
@@ -222,7 +215,7 @@ class MelCloudHome extends EventEmitter {
|
|
|
222
215
|
}
|
|
223
216
|
}
|
|
224
217
|
|
|
225
|
-
async
|
|
218
|
+
async connect() {
|
|
226
219
|
if (this.logDebug) this.emit('debug', 'Connecting to MELCloud Home');
|
|
227
220
|
const GLOBAL_TIMEOUT = 120000;
|
|
228
221
|
|
|
@@ -447,151 +440,6 @@ class MelCloudHome extends EventEmitter {
|
|
|
447
440
|
}
|
|
448
441
|
}
|
|
449
442
|
}
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
async axiosRequest(url, options, jar, maxRedirects = 10) {
|
|
453
|
-
let currentUrl = url;
|
|
454
|
-
|
|
455
|
-
for (let i = 0; i < maxRedirects; i++) {
|
|
456
|
-
const response = await axios({
|
|
457
|
-
url: currentUrl,
|
|
458
|
-
method: options.method || 'GET',
|
|
459
|
-
data: options.body,
|
|
460
|
-
headers: {
|
|
461
|
-
...options.headers,
|
|
462
|
-
...(jar.headerFor(currentUrl) && { Cookie: jar.headerFor(currentUrl) }),
|
|
463
|
-
},
|
|
464
|
-
validateStatus: () => true,
|
|
465
|
-
maxRedirects: 0,
|
|
466
|
-
responseType: 'text',
|
|
467
|
-
});
|
|
468
|
-
|
|
469
|
-
jar.addFromHeader(response.headers['set-cookie'], currentUrl);
|
|
470
|
-
|
|
471
|
-
if ([301, 302, 303, 307, 308].includes(response.status)) {
|
|
472
|
-
const location = response.headers.location;
|
|
473
|
-
if (!location) break;
|
|
474
|
-
|
|
475
|
-
if (location.startsWith('melcloudhome://')) {
|
|
476
|
-
return { url: location, data: response.data };
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
currentUrl = location.startsWith('http')
|
|
480
|
-
? location
|
|
481
|
-
: new URL(location, currentUrl).toString();
|
|
482
|
-
|
|
483
|
-
options.method = 'GET';
|
|
484
|
-
options.body = undefined;
|
|
485
|
-
continue;
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
return { url: currentUrl, data: response.data };
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
throw new Error('Too many redirects');
|
|
492
|
-
}
|
|
493
|
-
|
|
494
|
-
async loginWithCredentials(email, password) {
|
|
495
|
-
const jar = new CookieJar();
|
|
496
|
-
|
|
497
|
-
const userAgent =
|
|
498
|
-
'Mozilla/5.0 (iPhone; CPU iPhone OS 18_7 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.1 Mobile Safari/604.1';
|
|
499
|
-
|
|
500
|
-
const codeVerifier = crypto.randomBytes(32).toString('base64url');
|
|
501
|
-
const codeChallenge = crypto
|
|
502
|
-
.createHash('sha256')
|
|
503
|
-
.update(codeVerifier)
|
|
504
|
-
.digest('base64url');
|
|
505
|
-
|
|
506
|
-
const authUrl =
|
|
507
|
-
'https://auth.melcloudhome.com/connect/authorize?' +
|
|
508
|
-
new URLSearchParams({
|
|
509
|
-
client_id: 'homemobile',
|
|
510
|
-
redirect_uri: 'melcloudhome://',
|
|
511
|
-
response_type: 'code',
|
|
512
|
-
scope: 'openid profile email offline_access IdentityServerApi',
|
|
513
|
-
code_challenge: codeChallenge,
|
|
514
|
-
code_challenge_method: 'S256',
|
|
515
|
-
}).toString();
|
|
516
|
-
|
|
517
|
-
// STEP 1 – get login page
|
|
518
|
-
const loginPage = await this.axiosRequest(authUrl, {
|
|
519
|
-
headers: { 'User-Agent': userAgent },
|
|
520
|
-
}, jar);
|
|
521
|
-
|
|
522
|
-
const csrfMatch =
|
|
523
|
-
loginPage.data.match(/name="(_csrf|__RequestVerificationToken)".*?value="([^"]+)"/);
|
|
524
|
-
|
|
525
|
-
if (!csrfMatch) throw new Error('CSRF token not found');
|
|
526
|
-
const csrfToken = csrfMatch[2];
|
|
527
|
-
|
|
528
|
-
// STEP 2 – submit credentials
|
|
529
|
-
const form = new URLSearchParams({
|
|
530
|
-
_csrf: csrfToken,
|
|
531
|
-
username: email,
|
|
532
|
-
password,
|
|
533
|
-
}).toString();
|
|
534
|
-
|
|
535
|
-
const loginResult = await this.axiosRequest(loginPage.url, {
|
|
536
|
-
method: 'POST',
|
|
537
|
-
headers: {
|
|
538
|
-
'User-Agent': userAgent,
|
|
539
|
-
'Content-Type': 'application/x-www-form-urlencoded',
|
|
540
|
-
'Content-Length': Buffer.byteLength(form),
|
|
541
|
-
},
|
|
542
|
-
body: form,
|
|
543
|
-
}, jar);
|
|
544
|
-
|
|
545
|
-
if (!loginResult.url.startsWith('melcloudhome://')) {
|
|
546
|
-
throw new Error('OAuth redirect not received');
|
|
547
|
-
}
|
|
548
|
-
|
|
549
|
-
const code = new URL(loginResult.url).searchParams.get('code');
|
|
550
|
-
if (!code) throw new Error('Authorization code missing');
|
|
551
|
-
|
|
552
|
-
// STEP 3 – exchange token
|
|
553
|
-
const tokenResponse = await axios.post(
|
|
554
|
-
'https://auth.melcloudhome.com/connect/token',
|
|
555
|
-
new URLSearchParams({
|
|
556
|
-
grant_type: 'authorization_code',
|
|
557
|
-
client_id: 'homemobile',
|
|
558
|
-
redirect_uri: 'melcloudhome://',
|
|
559
|
-
code,
|
|
560
|
-
code_verifier: codeVerifier,
|
|
561
|
-
}).toString(),
|
|
562
|
-
{
|
|
563
|
-
headers: {
|
|
564
|
-
'Content-Type': 'application/x-www-form-urlencoded',
|
|
565
|
-
'Authorization': 'Basic aG9tZW1vYmlsZTo=',
|
|
566
|
-
},
|
|
567
|
-
},
|
|
568
|
-
);
|
|
569
|
-
|
|
570
|
-
if (!tokenResponse.data.refresh_token) {
|
|
571
|
-
throw new Error('No refresh token returned');
|
|
572
|
-
}
|
|
573
|
-
|
|
574
|
-
return {
|
|
575
|
-
refreshToken: tokenResponse.data.refresh_token,
|
|
576
|
-
accessToken: tokenResponse.data.access_token,
|
|
577
|
-
expiresIn: tokenResponse.data.expires_in,
|
|
578
|
-
};
|
|
579
|
-
}
|
|
580
|
-
|
|
581
|
-
async connect() {
|
|
582
|
-
if (this.logDebug) this.emit('debug', 'Connecting to MELCloud Home');
|
|
583
|
-
|
|
584
|
-
try {
|
|
585
|
-
const accountInfo = { State: false, Info: '', Account: {}, UseFahrenheit: false };
|
|
586
|
-
|
|
587
|
-
const tokens = await this.loginWithCredentials(this.user, this.passwd);
|
|
588
|
-
this.emit('debug', `Tokens: ${JSON.stringify(tokens, null, 2)}`);
|
|
589
|
-
return accountInfo
|
|
590
|
-
} catch (error) {
|
|
591
|
-
throw new Error(`Connect error: ${error.message}`);
|
|
592
|
-
}
|
|
593
|
-
}
|
|
594
|
-
|
|
595
443
|
}
|
|
596
444
|
|
|
597
445
|
export default MelCloudHome;
|
package/src/helper.js
DELETED
|
@@ -1,70 +0,0 @@
|
|
|
1
|
-
class CookieJar {
|
|
2
|
-
constructor() {
|
|
3
|
-
this.cookies = [];
|
|
4
|
-
}
|
|
5
|
-
|
|
6
|
-
addFromHeader(setCookie, url) {
|
|
7
|
-
if (!setCookie) return;
|
|
8
|
-
|
|
9
|
-
const urlObj = new URL(url);
|
|
10
|
-
const headers = Array.isArray(setCookie) ? setCookie : [setCookie];
|
|
11
|
-
|
|
12
|
-
for (const header of headers) {
|
|
13
|
-
const parts = header.split(';').map(p => p.trim());
|
|
14
|
-
const [name, value] = parts[0].split('=');
|
|
15
|
-
|
|
16
|
-
const cookie = {
|
|
17
|
-
name,
|
|
18
|
-
value,
|
|
19
|
-
domain: urlObj.hostname,
|
|
20
|
-
path: '/',
|
|
21
|
-
expires: null,
|
|
22
|
-
};
|
|
23
|
-
|
|
24
|
-
for (const part of parts.slice(1)) {
|
|
25
|
-
const [key, val] = part.split('=').map(v => v?.trim());
|
|
26
|
-
if (!key) continue;
|
|
27
|
-
|
|
28
|
-
switch (key.toLowerCase()) {
|
|
29
|
-
case 'domain':
|
|
30
|
-
cookie.domain = val;
|
|
31
|
-
break;
|
|
32
|
-
case 'path':
|
|
33
|
-
cookie.path = val || '/';
|
|
34
|
-
break;
|
|
35
|
-
case 'expires':
|
|
36
|
-
cookie.expires = new Date(val);
|
|
37
|
-
break;
|
|
38
|
-
case 'max-age':
|
|
39
|
-
cookie.expires = new Date(Date.now() + Number(val) * 1000);
|
|
40
|
-
break;
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
this.cookies = this.cookies.filter(
|
|
45
|
-
c => !(c.name === cookie.name && c.domain === cookie.domain && c.path === cookie.path),
|
|
46
|
-
);
|
|
47
|
-
|
|
48
|
-
this.cookies.push(cookie);
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
headerFor(url) {
|
|
53
|
-
const urlObj = new URL(url);
|
|
54
|
-
const now = Date.now();
|
|
55
|
-
|
|
56
|
-
const validCookies = this.cookies.filter(cookie => {
|
|
57
|
-
if (cookie.expires && cookie.expires.getTime() <= now) return false;
|
|
58
|
-
if (!urlObj.hostname.endsWith(cookie.domain)) return false;
|
|
59
|
-
if (!urlObj.pathname.startsWith(cookie.path)) return false;
|
|
60
|
-
return true;
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
return validCookies.length
|
|
64
|
-
? validCookies.map(c => `${c.name}=${c.value}`).join('; ')
|
|
65
|
-
: null;
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
export default CookieJar
|