homebridge-melcloud-control 4.0.0-beta.245 → 4.0.0-beta.247
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 -3
- package/src/melcloudhometoken.js +44 -26
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.247",
|
|
5
5
|
"description": "Homebridge plugin to control Mitsubishi Air Conditioner, Heat Pump and Energy Recovery Ventilation.",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"author": "grzegorz914",
|
|
@@ -40,8 +40,6 @@
|
|
|
40
40
|
"axios": "^1.12.2",
|
|
41
41
|
"express": "^5.1.0",
|
|
42
42
|
"puppeteer": "^24.26.1",
|
|
43
|
-
"tough-cookie": "^6.0.0",
|
|
44
|
-
"axios-cookiejar-support": "^6.0.4",
|
|
45
43
|
"jsdom": "^27.0.1"
|
|
46
44
|
},
|
|
47
45
|
"keywords": [
|
package/src/melcloudhometoken.js
CHANGED
|
@@ -13,13 +13,7 @@ const AUTHORIZE_ENDPOINT = 'https://auth.melcloudhome.com/connect/authorize';
|
|
|
13
13
|
|
|
14
14
|
class MelCloudHomeToken {
|
|
15
15
|
constructor(log) {
|
|
16
|
-
this.
|
|
17
|
-
this.axiosInstance = wrapper(axios.create({
|
|
18
|
-
jar: this.jar,
|
|
19
|
-
withCredentials: true,
|
|
20
|
-
headers: { 'User-Agent': MOBILE_USER_AGENT },
|
|
21
|
-
maxRedirects: 10
|
|
22
|
-
}));
|
|
16
|
+
this.log = log;
|
|
23
17
|
}
|
|
24
18
|
|
|
25
19
|
// --- PKCE ---
|
|
@@ -49,7 +43,7 @@ class MelCloudHomeToken {
|
|
|
49
43
|
if (cookies.length > 0) reqOptions.headers['Cookie'] = cookies.join('; ');
|
|
50
44
|
|
|
51
45
|
const req = https.request(reqOptions, (res) => {
|
|
52
|
-
//
|
|
46
|
+
// Collect Set-Cookie
|
|
53
47
|
const setCookie = res.headers['set-cookie'];
|
|
54
48
|
if (setCookie) {
|
|
55
49
|
setCookie.forEach(c => {
|
|
@@ -60,7 +54,7 @@ class MelCloudHomeToken {
|
|
|
60
54
|
});
|
|
61
55
|
}
|
|
62
56
|
|
|
63
|
-
//
|
|
57
|
+
// Follow redirects
|
|
64
58
|
if ([301, 302, 303].includes(res.statusCode) && res.headers.location) {
|
|
65
59
|
const redirectUrl = res.headers.location.startsWith('http')
|
|
66
60
|
? res.headers.location
|
|
@@ -81,7 +75,7 @@ class MelCloudHomeToken {
|
|
|
81
75
|
|
|
82
76
|
// --- Get login page ---
|
|
83
77
|
async getLoginPage(authUrl) {
|
|
84
|
-
|
|
78
|
+
this.log.info('Getting login page...');
|
|
85
79
|
const resp = await this.httpsRequest(authUrl, {
|
|
86
80
|
method: 'GET',
|
|
87
81
|
headers: {
|
|
@@ -94,14 +88,14 @@ class MelCloudHomeToken {
|
|
|
94
88
|
const form = dom.window.document.querySelector('form');
|
|
95
89
|
if (!form) throw new Error('Login form not found');
|
|
96
90
|
|
|
97
|
-
return {
|
|
91
|
+
return { form, cookies: resp.cookies, loginPageUrl: resp.finalUrl };
|
|
98
92
|
}
|
|
99
93
|
|
|
100
|
-
// --- Submit login ---
|
|
94
|
+
// --- Submit login and handle form_post ---
|
|
101
95
|
async submitLogin(form, email, password, cookies, loginPageUrl) {
|
|
102
|
-
|
|
96
|
+
this.log.info('Submitting login credentials...');
|
|
103
97
|
|
|
104
|
-
// Collect all hidden inputs
|
|
98
|
+
// Step 1: Collect all hidden inputs
|
|
105
99
|
const formData = new URLSearchParams();
|
|
106
100
|
form.querySelectorAll('input').forEach(input => {
|
|
107
101
|
const name = input.getAttribute('name');
|
|
@@ -118,11 +112,35 @@ class MelCloudHomeToken {
|
|
|
118
112
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
|
|
119
113
|
}, formData.toString(), cookies);
|
|
120
114
|
|
|
121
|
-
//
|
|
122
|
-
const
|
|
123
|
-
|
|
115
|
+
// Step 2: Check if body contains hidden form_post
|
|
116
|
+
const dom = new JSDOM(resp.html);
|
|
117
|
+
const postForm = dom.window.document.querySelector('form');
|
|
118
|
+
if (postForm) {
|
|
119
|
+
// Collect all hidden inputs from form_post
|
|
120
|
+
const postData = new URLSearchParams();
|
|
121
|
+
postForm.querySelectorAll('input').forEach(input => {
|
|
122
|
+
const name = input.getAttribute('name');
|
|
123
|
+
if (name) postData.append(name, input.getAttribute('value') || '');
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
const postAction = new URL(postForm.getAttribute('action'), resp.finalUrl).toString();
|
|
127
|
+
const postResp = await this.httpsRequest(postAction, {
|
|
128
|
+
method: 'POST',
|
|
129
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
|
|
130
|
+
}, postData.toString(), cookies);
|
|
131
|
+
|
|
132
|
+
// Try to extract code from redirect URL
|
|
133
|
+
const codeMatch = postResp.finalUrl.match(/[?&]code=([^&]+)/);
|
|
134
|
+
if (codeMatch) return codeMatch[1];
|
|
135
|
+
|
|
136
|
+
// Or from body (JS redirect)
|
|
137
|
+
const codeFromBody = postResp.html.match(/melcloudhome:\/\/[^"'\s]*[?&]code=([^&"'\s]+)/);
|
|
138
|
+
if (codeFromBody) return codeFromBody[1];
|
|
139
|
+
}
|
|
124
140
|
|
|
125
|
-
//
|
|
141
|
+
// Fallback: check resp finalUrl or body
|
|
142
|
+
const codeMatch = resp.finalUrl.match(/[?&]code=([^&]+)/);
|
|
143
|
+
if (codeMatch) return codeMatch[1];
|
|
126
144
|
const codeFromBody = resp.html.match(/melcloudhome:\/\/[^"'\s]*[?&]code=([^&"'\s]+)/);
|
|
127
145
|
if (codeFromBody) return codeFromBody[1];
|
|
128
146
|
|
|
@@ -131,7 +149,7 @@ class MelCloudHomeToken {
|
|
|
131
149
|
|
|
132
150
|
// --- Exchange code for tokens ---
|
|
133
151
|
async exchangeCodeForTokens(code, codeVerifier) {
|
|
134
|
-
|
|
152
|
+
this.log.info('Exchanging authorization code for tokens...');
|
|
135
153
|
|
|
136
154
|
const tokenData = new URLSearchParams({
|
|
137
155
|
grant_type: 'authorization_code',
|
|
@@ -157,7 +175,7 @@ class MelCloudHomeToken {
|
|
|
157
175
|
|
|
158
176
|
// --- Main flow ---
|
|
159
177
|
async getTokensWithCredentials(email, password) {
|
|
160
|
-
|
|
178
|
+
this.log.info('Obtaining OAuth tokens automatically...');
|
|
161
179
|
|
|
162
180
|
const pkce = this.generatePKCE();
|
|
163
181
|
const state = this.generateState();
|
|
@@ -171,15 +189,15 @@ class MelCloudHomeToken {
|
|
|
171
189
|
authUrl.searchParams.set('code_challenge_method', 'S256');
|
|
172
190
|
authUrl.searchParams.set('state', state);
|
|
173
191
|
|
|
174
|
-
// 1️⃣
|
|
175
|
-
const { form, cookies } = await this.getLoginPage(authUrl.toString());
|
|
192
|
+
// 1️⃣ Get login page
|
|
193
|
+
const { form, cookies, loginPageUrl } = await this.getLoginPage(authUrl.toString());
|
|
176
194
|
|
|
177
|
-
// 2️⃣
|
|
178
|
-
const authCode = await this.submitLogin(form, email, password, cookies,
|
|
195
|
+
// 2️⃣ Submit login + handle form_post
|
|
196
|
+
const authCode = await this.submitLogin(form, email, password, cookies, loginPageUrl);
|
|
179
197
|
|
|
180
|
-
// 3️⃣
|
|
198
|
+
// 3️⃣ Exchange code for tokens
|
|
181
199
|
const tokens = await this.exchangeCodeForTokens(authCode, pkce.verifier);
|
|
182
|
-
|
|
200
|
+
this.log.info('✓ Successfully obtained OAuth tokens');
|
|
183
201
|
return tokens;
|
|
184
202
|
}
|
|
185
203
|
}
|