homebridge-melcloud-control 4.0.0-beta.246 → 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 +45 -21
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
|
@@ -12,8 +12,8 @@ const TOKEN_ENDPOINT = 'https://auth.melcloudhome.com/connect/token';
|
|
|
12
12
|
const AUTHORIZE_ENDPOINT = 'https://auth.melcloudhome.com/connect/authorize';
|
|
13
13
|
|
|
14
14
|
class MelCloudHomeToken {
|
|
15
|
-
constructor() {
|
|
16
|
-
|
|
15
|
+
constructor(log) {
|
|
16
|
+
this.log = log;
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
// --- PKCE ---
|
|
@@ -43,7 +43,7 @@ class MelCloudHomeToken {
|
|
|
43
43
|
if (cookies.length > 0) reqOptions.headers['Cookie'] = cookies.join('; ');
|
|
44
44
|
|
|
45
45
|
const req = https.request(reqOptions, (res) => {
|
|
46
|
-
//
|
|
46
|
+
// Collect Set-Cookie
|
|
47
47
|
const setCookie = res.headers['set-cookie'];
|
|
48
48
|
if (setCookie) {
|
|
49
49
|
setCookie.forEach(c => {
|
|
@@ -54,7 +54,7 @@ class MelCloudHomeToken {
|
|
|
54
54
|
});
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
-
//
|
|
57
|
+
// Follow redirects
|
|
58
58
|
if ([301, 302, 303].includes(res.statusCode) && res.headers.location) {
|
|
59
59
|
const redirectUrl = res.headers.location.startsWith('http')
|
|
60
60
|
? res.headers.location
|
|
@@ -75,7 +75,7 @@ class MelCloudHomeToken {
|
|
|
75
75
|
|
|
76
76
|
// --- Get login page ---
|
|
77
77
|
async getLoginPage(authUrl) {
|
|
78
|
-
|
|
78
|
+
this.log.info('Getting login page...');
|
|
79
79
|
const resp = await this.httpsRequest(authUrl, {
|
|
80
80
|
method: 'GET',
|
|
81
81
|
headers: {
|
|
@@ -88,14 +88,14 @@ class MelCloudHomeToken {
|
|
|
88
88
|
const form = dom.window.document.querySelector('form');
|
|
89
89
|
if (!form) throw new Error('Login form not found');
|
|
90
90
|
|
|
91
|
-
return {
|
|
91
|
+
return { form, cookies: resp.cookies, loginPageUrl: resp.finalUrl };
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
-
// --- Submit login ---
|
|
94
|
+
// --- Submit login and handle form_post ---
|
|
95
95
|
async submitLogin(form, email, password, cookies, loginPageUrl) {
|
|
96
|
-
|
|
96
|
+
this.log.info('Submitting login credentials...');
|
|
97
97
|
|
|
98
|
-
// Collect all hidden inputs
|
|
98
|
+
// Step 1: Collect all hidden inputs
|
|
99
99
|
const formData = new URLSearchParams();
|
|
100
100
|
form.querySelectorAll('input').forEach(input => {
|
|
101
101
|
const name = input.getAttribute('name');
|
|
@@ -112,11 +112,35 @@ class MelCloudHomeToken {
|
|
|
112
112
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
|
|
113
113
|
}, formData.toString(), cookies);
|
|
114
114
|
|
|
115
|
-
//
|
|
116
|
-
const
|
|
117
|
-
|
|
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
|
+
}
|
|
118
140
|
|
|
119
|
-
//
|
|
141
|
+
// Fallback: check resp finalUrl or body
|
|
142
|
+
const codeMatch = resp.finalUrl.match(/[?&]code=([^&]+)/);
|
|
143
|
+
if (codeMatch) return codeMatch[1];
|
|
120
144
|
const codeFromBody = resp.html.match(/melcloudhome:\/\/[^"'\s]*[?&]code=([^&"'\s]+)/);
|
|
121
145
|
if (codeFromBody) return codeFromBody[1];
|
|
122
146
|
|
|
@@ -125,7 +149,7 @@ class MelCloudHomeToken {
|
|
|
125
149
|
|
|
126
150
|
// --- Exchange code for tokens ---
|
|
127
151
|
async exchangeCodeForTokens(code, codeVerifier) {
|
|
128
|
-
|
|
152
|
+
this.log.info('Exchanging authorization code for tokens...');
|
|
129
153
|
|
|
130
154
|
const tokenData = new URLSearchParams({
|
|
131
155
|
grant_type: 'authorization_code',
|
|
@@ -151,7 +175,7 @@ class MelCloudHomeToken {
|
|
|
151
175
|
|
|
152
176
|
// --- Main flow ---
|
|
153
177
|
async getTokensWithCredentials(email, password) {
|
|
154
|
-
|
|
178
|
+
this.log.info('Obtaining OAuth tokens automatically...');
|
|
155
179
|
|
|
156
180
|
const pkce = this.generatePKCE();
|
|
157
181
|
const state = this.generateState();
|
|
@@ -165,15 +189,15 @@ class MelCloudHomeToken {
|
|
|
165
189
|
authUrl.searchParams.set('code_challenge_method', 'S256');
|
|
166
190
|
authUrl.searchParams.set('state', state);
|
|
167
191
|
|
|
168
|
-
// 1️⃣
|
|
169
|
-
const { form, cookies } = await this.getLoginPage(authUrl.toString());
|
|
192
|
+
// 1️⃣ Get login page
|
|
193
|
+
const { form, cookies, loginPageUrl } = await this.getLoginPage(authUrl.toString());
|
|
170
194
|
|
|
171
|
-
// 2️⃣
|
|
172
|
-
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);
|
|
173
197
|
|
|
174
|
-
// 3️⃣
|
|
198
|
+
// 3️⃣ Exchange code for tokens
|
|
175
199
|
const tokens = await this.exchangeCodeForTokens(authCode, pkce.verifier);
|
|
176
|
-
|
|
200
|
+
this.log.info('✓ Successfully obtained OAuth tokens');
|
|
177
201
|
return tokens;
|
|
178
202
|
}
|
|
179
203
|
}
|