homebridge-melcloud-control 4.0.0-beta.289 → 4.0.0-beta.290
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/melcloud.js +11 -0
- package/src/melcloudhometoken.js +102 -23
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"displayName": "MELCloud Control",
|
|
3
3
|
"name": "homebridge-melcloud-control",
|
|
4
|
-
"version": "4.0.0-beta.
|
|
4
|
+
"version": "4.0.0-beta.290",
|
|
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/melcloud.js
CHANGED
|
@@ -281,6 +281,17 @@ class MelCloud extends EventEmitter {
|
|
|
281
281
|
}
|
|
282
282
|
|
|
283
283
|
async connectToMelCloudHome() {
|
|
284
|
+
const mel = new MelCloudHomeToken(console);
|
|
285
|
+
try {
|
|
286
|
+
const tokens = await mel.loginWithCredentials(this.user, this.passwd);
|
|
287
|
+
console.log('✅ Access Token:', tokens.access_token);
|
|
288
|
+
console.log('🔁 Refresh Token:', tokens.refresh_token);
|
|
289
|
+
} catch (err) {
|
|
290
|
+
console.error('❌ Login failed:', err.message);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
async connectToMelCloudHome2() {
|
|
284
295
|
let browser;
|
|
285
296
|
try {
|
|
286
297
|
browser = await puppeteer.launch({
|
package/src/melcloudhometoken.js
CHANGED
|
@@ -10,6 +10,7 @@ const REDIRECT_URI = 'melcloudhome://';
|
|
|
10
10
|
const SCOPE = 'openid profile email offline_access IdentityServerApi';
|
|
11
11
|
const TOKEN_ENDPOINT = 'https://auth.melcloudhome.com/connect/token';
|
|
12
12
|
const AUTHORIZE_ENDPOINT = 'https://auth.melcloudhome.com/connect/authorize';
|
|
13
|
+
const LOGIN_ENDPOINT = 'https://auth.melcloudhome.com/Account/Login';
|
|
13
14
|
|
|
14
15
|
class MelCloudHomeToken {
|
|
15
16
|
constructor(log = console) {
|
|
@@ -29,37 +30,115 @@ class MelCloudHomeToken {
|
|
|
29
30
|
return crypto.randomBytes(32).toString('hex');
|
|
30
31
|
}
|
|
31
32
|
|
|
32
|
-
async
|
|
33
|
-
const
|
|
34
|
-
|
|
33
|
+
async buildAuthorizeUrl() {
|
|
34
|
+
const pkce = this.generatePKCE();
|
|
35
|
+
this.pkce = pkce;
|
|
36
|
+
const state = this.generateState();
|
|
37
|
+
this.state = state;
|
|
38
|
+
|
|
39
|
+
const authUrl = new URL(AUTHORIZE_ENDPOINT);
|
|
40
|
+
authUrl.searchParams.set('client_id', CLIENT_ID);
|
|
41
|
+
authUrl.searchParams.set('redirect_uri', REDIRECT_URI);
|
|
42
|
+
authUrl.searchParams.set('response_type', 'code');
|
|
43
|
+
authUrl.searchParams.set('scope', SCOPE);
|
|
44
|
+
authUrl.searchParams.set('code_challenge', pkce.challenge);
|
|
45
|
+
authUrl.searchParams.set('code_challenge_method', 'S256');
|
|
46
|
+
authUrl.searchParams.set('state', state);
|
|
47
|
+
|
|
48
|
+
return authUrl.toString();
|
|
35
49
|
}
|
|
36
50
|
|
|
37
|
-
async
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
return loginPage.url;
|
|
53
|
-
} catch (error) {
|
|
54
|
-
console.error('Failed to obtain OAuth tokens:', error);
|
|
55
|
-
throw error;
|
|
51
|
+
async extractCodeFromHtml(html) {
|
|
52
|
+
const $ = cheerio.load(html);
|
|
53
|
+
let code = null;
|
|
54
|
+
|
|
55
|
+
const formCode = html.match(/name="code"\s+value="([^"]+)"/);
|
|
56
|
+
if (formCode) return formCode[1];
|
|
57
|
+
|
|
58
|
+
const deepLink = html.match(/melcloudhome:\/\/[^"'\s]*[?&]code=([^&"'\s]+)/);
|
|
59
|
+
if (deepLink) return deepLink[1];
|
|
60
|
+
|
|
61
|
+
const dataUrl = $('[data-url]').attr('data-url');
|
|
62
|
+
if (dataUrl && dataUrl.includes('code=')) {
|
|
63
|
+
const parsedUrl = new URL(dataUrl);
|
|
64
|
+
return parsedUrl.searchParams.get('code');
|
|
56
65
|
}
|
|
66
|
+
|
|
67
|
+
$('a[href]').each((_, el) => {
|
|
68
|
+
const href = $(el).attr('href');
|
|
69
|
+
if (href && href.includes('code=')) {
|
|
70
|
+
const parsedUrl = new URL(href);
|
|
71
|
+
code = parsedUrl.searchParams.get('code');
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
return code;
|
|
57
76
|
}
|
|
58
|
-
}
|
|
59
77
|
|
|
78
|
+
async exchangeCodeForTokens(code) {
|
|
79
|
+
const body = new URLSearchParams({
|
|
80
|
+
client_id: CLIENT_ID,
|
|
81
|
+
grant_type: 'authorization_code',
|
|
82
|
+
redirect_uri: REDIRECT_URI,
|
|
83
|
+
code_verifier: this.pkce.verifier,
|
|
84
|
+
code,
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
const res = await this.client.post(TOKEN_ENDPOINT, body, {
|
|
88
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
return res.data;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Main method: performs full login flow using email and password
|
|
96
|
+
*/
|
|
97
|
+
async loginWithCredentials(email, password) {
|
|
98
|
+
const authorizeUrl = await this.buildAuthorizeUrl();
|
|
99
|
+
this.log.debug('Fetching login page...');
|
|
100
|
+
const res = await this.client.get(authorizeUrl);
|
|
101
|
+
|
|
102
|
+
const $ = cheerio.load(res.data);
|
|
103
|
+
const form = $('form');
|
|
104
|
+
const action = form.attr('action') || '/Account/Login';
|
|
105
|
+
const requestVerificationToken = $('input[name="__RequestVerificationToken"]').val();
|
|
106
|
+
const returnUrl = $('input[name="ReturnUrl"]').val() || '';
|
|
107
|
+
|
|
108
|
+
this.log.debug('Submitting credentials...');
|
|
109
|
+
const loginRes = await this.client.post(
|
|
110
|
+
new URL(action, LOGIN_ENDPOINT).toString(),
|
|
111
|
+
new URLSearchParams({
|
|
112
|
+
Username: email,
|
|
113
|
+
Password: password,
|
|
114
|
+
__RequestVerificationToken: requestVerificationToken,
|
|
115
|
+
ReturnUrl: returnUrl,
|
|
116
|
+
}),
|
|
117
|
+
{
|
|
118
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
119
|
+
maxRedirects: 0,
|
|
120
|
+
validateStatus: status => status < 400 || status === 302 || status === 500,
|
|
121
|
+
}
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
let html = loginRes.data;
|
|
125
|
+
if (typeof html !== 'string' && loginRes.headers.location) {
|
|
126
|
+
const redirect = await this.client.get(loginRes.headers.location);
|
|
127
|
+
html = redirect.data;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const code = await this.extractCodeFromHtml(html);
|
|
131
|
+
if (!code) throw new Error('Authorization code not found in HTML');
|
|
60
132
|
|
|
133
|
+
this.log.debug(`Found authorization code: ${code}`);
|
|
134
|
+
|
|
135
|
+
return await this.exchangeCodeForTokens(code);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
61
138
|
|
|
62
139
|
export default MelCloudHomeToken;
|
|
63
140
|
|
|
64
141
|
|
|
65
142
|
|
|
143
|
+
|
|
144
|
+
|