homebridge-melcloud-control 4.0.0-beta.251 → 4.0.0-beta.253
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 +1 -1
- package/src/melcloudhometoken.js +39 -189
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.253",
|
|
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
|
@@ -177,7 +177,7 @@ class MelCloud extends EventEmitter {
|
|
|
177
177
|
// MELCloud Home
|
|
178
178
|
async connectToMelCloudHome() {
|
|
179
179
|
const oauth = new MelCloudHomeToken();
|
|
180
|
-
this.tokens = await oauth.
|
|
180
|
+
this.tokens = await oauth.getTokensWithPassword(this.user, this.passwd);
|
|
181
181
|
this.emit('warn', `Token ${JSON.stringify(this.tokens, null, 2)}`);
|
|
182
182
|
//this.axiosInstance.defaults.headers['Authorization'] = `Bearer ${this.tokens.accessToken}`;
|
|
183
183
|
return false;
|
package/src/melcloudhometoken.js
CHANGED
|
@@ -1,211 +1,61 @@
|
|
|
1
1
|
import https from 'https';
|
|
2
|
-
import
|
|
3
|
-
import { URL, URLSearchParams } from 'url';
|
|
4
|
-
import { JSDOM } from 'jsdom';
|
|
2
|
+
import { URLSearchParams } from 'url';
|
|
5
3
|
|
|
6
|
-
const
|
|
4
|
+
const TOKEN_ENDPOINT = 'https://auth.melcloudhome.com/connect/token';
|
|
7
5
|
const CLIENT_ID = 'homemobile';
|
|
8
|
-
// ⚠ Zmieniamy na redirect URI, który faktycznie akceptuje Cognito
|
|
9
6
|
const REDIRECT_URI = 'melcloudhome://signin-oidc-meu';
|
|
10
7
|
const SCOPE = 'openid profile email offline_access IdentityServerApi';
|
|
11
|
-
const TOKEN_ENDPOINT = 'https://auth.melcloudhome.com/connect/token';
|
|
12
|
-
const AUTHORIZE_ENDPOINT = 'https://auth.melcloudhome.com/connect/authorize';
|
|
13
8
|
|
|
14
9
|
class MelCloudHomeToken {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
httpsRequest(url, options = {}, body = undefined, cookies = [], redirectCount = 0) {
|
|
28
|
-
if (redirectCount > 10) throw new Error('Too many redirects');
|
|
29
|
-
|
|
30
|
-
return new Promise((resolve, reject) => {
|
|
31
|
-
const urlObj = new URL(url);
|
|
32
|
-
const reqOptions = {
|
|
33
|
-
...options,
|
|
34
|
-
hostname: urlObj.hostname,
|
|
35
|
-
path: urlObj.pathname + urlObj.search,
|
|
36
|
-
};
|
|
37
|
-
|
|
38
|
-
if (!reqOptions.headers) reqOptions.headers = {};
|
|
39
|
-
if (cookies.length > 0) reqOptions.headers['Cookie'] = cookies.join('; ');
|
|
40
|
-
|
|
41
|
-
const req = https.request(reqOptions, (res) => {
|
|
42
|
-
const setCookie = res.headers['set-cookie'];
|
|
43
|
-
if (setCookie) {
|
|
44
|
-
setCookie.forEach(c => {
|
|
45
|
-
const name = c.split('=')[0];
|
|
46
|
-
const idx = cookies.findIndex(x => x.startsWith(name + '='));
|
|
47
|
-
if (idx >= 0) cookies[idx] = c.split(';')[0];
|
|
48
|
-
else cookies.push(c.split(';')[0]);
|
|
49
|
-
});
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
if ([301, 302, 303].includes(res.statusCode) && res.headers.location) {
|
|
53
|
-
const redirectUrl = res.headers.location.startsWith('http')
|
|
54
|
-
? res.headers.location
|
|
55
|
-
: `https://${urlObj.hostname}${res.headers.location}`;
|
|
56
|
-
return resolve(this.httpsRequest(redirectUrl, options, undefined, cookies, redirectCount + 1));
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
let data = '';
|
|
60
|
-
res.on('data', chunk => data += chunk);
|
|
61
|
-
res.on('end', () => resolve({ html: data, cookies, finalUrl: url }));
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
req.on('error', reject);
|
|
65
|
-
if (body) req.write(body);
|
|
66
|
-
req.end();
|
|
67
|
-
});
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
async getLoginPage(authUrl) {
|
|
71
|
-
console.log('Getting login page...');
|
|
72
|
-
const resp = await this.httpsRequest(authUrl, {
|
|
73
|
-
method: 'GET',
|
|
74
|
-
headers: {
|
|
75
|
-
'User-Agent': MOBILE_USER_AGENT,
|
|
76
|
-
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
|
|
77
|
-
}
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
const dom = new JSDOM(resp.html);
|
|
81
|
-
const form = dom.window.document.querySelector('form');
|
|
82
|
-
if (!form) throw new Error('Login form not found');
|
|
83
|
-
|
|
84
|
-
return { form, cookies: resp.cookies, loginPageUrl: resp.finalUrl };
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
async submitLogin(form, email, password, cookies, loginPageUrl) {
|
|
88
|
-
console.log('Submitting login credentials...');
|
|
89
|
-
|
|
90
|
-
const formData = new URLSearchParams();
|
|
91
|
-
form.querySelectorAll('input').forEach(input => {
|
|
92
|
-
const name = input.getAttribute('name');
|
|
93
|
-
if (name) formData.append(name, input.getAttribute('value') || '');
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
// Overwrite username/password
|
|
97
|
-
formData.set('username', email);
|
|
98
|
-
formData.set('password', password);
|
|
99
|
-
|
|
100
|
-
console.log('Login form data (hidden + credentials):');
|
|
101
|
-
for (const [key, value] of formData.entries()) console.log(` ${key} = ${value}`);
|
|
102
|
-
|
|
103
|
-
const actionUrl = new URL(form.getAttribute('action'), loginPageUrl).toString();
|
|
104
|
-
const resp = await this.httpsRequest(actionUrl, {
|
|
105
|
-
method: 'POST',
|
|
106
|
-
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
|
|
107
|
-
}, formData.toString(), cookies);
|
|
108
|
-
|
|
109
|
-
// Form_post auto-submit handling
|
|
110
|
-
const domPost = new JSDOM(resp.html);
|
|
111
|
-
const postForm = domPost.window.document.querySelector('form');
|
|
112
|
-
if (postForm) {
|
|
113
|
-
const postData = new URLSearchParams();
|
|
114
|
-
postForm.querySelectorAll('input').forEach(input => {
|
|
115
|
-
const name = input.getAttribute('name');
|
|
116
|
-
if (name) postData.append(name, input.getAttribute('value') || '');
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
console.log('Form_post hidden inputs:');
|
|
120
|
-
for (const [key, value] of postData.entries()) console.log(` ${key} = ${value}`);
|
|
121
|
-
|
|
122
|
-
const postAction = new URL(postForm.getAttribute('action'), resp.finalUrl).toString();
|
|
123
|
-
const postResp = await this.httpsRequest(postAction, {
|
|
124
|
-
method: 'POST',
|
|
125
|
-
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
|
|
126
|
-
}, postData.toString(), cookies);
|
|
127
|
-
|
|
128
|
-
let codeMatch = postResp.finalUrl.match(/[?&]code=([^&]+)/);
|
|
129
|
-
if (!codeMatch) codeMatch = postResp.html.match(/melcloudhome:\/\/[^"'\s]*[?&]code=([^&"'\s]+)/);
|
|
130
|
-
|
|
131
|
-
if (codeMatch) {
|
|
132
|
-
const code = codeMatch[1];
|
|
133
|
-
console.log(`Authorization code obtained: ${code}`);
|
|
134
|
-
return code;
|
|
135
|
-
}
|
|
10
|
+
httpsRequest(url, options = {}, body = undefined) {
|
|
11
|
+
return new Promise((resolve, reject) => {
|
|
12
|
+
const req = https.request(url, options, (res) => {
|
|
13
|
+
let data = '';
|
|
14
|
+
res.on('data', chunk => data += chunk);
|
|
15
|
+
res.on('end', () => resolve({ statusCode: res.statusCode, body: data }));
|
|
16
|
+
});
|
|
17
|
+
req.on('error', reject);
|
|
18
|
+
if (body) req.write(body);
|
|
19
|
+
req.end();
|
|
20
|
+
});
|
|
136
21
|
}
|
|
137
22
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
return code;
|
|
145
|
-
}
|
|
23
|
+
/**
|
|
24
|
+
* Get OAuth tokens using username/password directly (ROPC)
|
|
25
|
+
* Works in Node.js / Docker, no Puppeteer needed
|
|
26
|
+
*/
|
|
27
|
+
async getTokensWithPassword(username, password) {
|
|
28
|
+
console.log('Obtaining OAuth tokens using password grant...');
|
|
146
29
|
|
|
147
|
-
|
|
148
|
-
|
|
30
|
+
const params = new URLSearchParams({
|
|
31
|
+
grant_type: 'password',
|
|
32
|
+
client_id: CLIENT_ID,
|
|
33
|
+
username,
|
|
34
|
+
password,
|
|
35
|
+
scope: SCOPE,
|
|
36
|
+
});
|
|
149
37
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
38
|
+
const headers = {
|
|
39
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
40
|
+
'Content-Length': params.toString().length,
|
|
41
|
+
};
|
|
154
42
|
|
|
155
|
-
|
|
156
|
-
grant_type: 'authorization_code',
|
|
157
|
-
code,
|
|
158
|
-
redirect_uri: REDIRECT_URI,
|
|
159
|
-
client_id: CLIENT_ID,
|
|
160
|
-
code_verifier: codeVerifier,
|
|
161
|
-
});
|
|
43
|
+
const resp = await this.httpsRequest(TOKEN_ENDPOINT, { method: 'POST', headers }, params.toString());
|
|
162
44
|
|
|
163
|
-
|
|
164
|
-
method: 'POST',
|
|
165
|
-
headers: {
|
|
166
|
-
'Content-Type': 'application/x-www-form-urlencoded',
|
|
167
|
-
'Content-Length': tokenData.toString().length,
|
|
168
|
-
}
|
|
169
|
-
}, tokenData.toString());
|
|
45
|
+
console.log('Token endpoint response:', resp.body);
|
|
170
46
|
|
|
171
|
-
|
|
47
|
+
const tokens = JSON.parse(resp.body);
|
|
172
48
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
49
|
+
if (tokens.error) {
|
|
50
|
+
console.error('Token response error:', tokens);
|
|
51
|
+
throw new Error(`Token request failed: ${tokens.error_description || tokens.error}`);
|
|
52
|
+
}
|
|
176
53
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
throw new Error(`Token exchange failed: ${tokens.error_description || tokens.error}`);
|
|
54
|
+
console.log('✓ Successfully obtained OAuth tokens');
|
|
55
|
+
return tokens;
|
|
180
56
|
}
|
|
181
|
-
|
|
182
|
-
return tokens;
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
async getTokensWithCredentials(email, password) {
|
|
186
|
-
console.log('Obtaining OAuth tokens automatically...');
|
|
187
|
-
const pkce = this.generatePKCE();
|
|
188
|
-
const state = this.generateState();
|
|
189
|
-
|
|
190
|
-
const authUrl = new URL(AUTHORIZE_ENDPOINT);
|
|
191
|
-
authUrl.searchParams.set('client_id', CLIENT_ID);
|
|
192
|
-
authUrl.searchParams.set('redirect_uri', REDIRECT_URI);
|
|
193
|
-
authUrl.searchParams.set('response_type', 'code');
|
|
194
|
-
authUrl.searchParams.set('scope', SCOPE);
|
|
195
|
-
authUrl.searchParams.set('code_challenge', pkce.challenge);
|
|
196
|
-
authUrl.searchParams.set('code_challenge_method', 'S256');
|
|
197
|
-
authUrl.searchParams.set('state', state);
|
|
198
|
-
|
|
199
|
-
const { form, cookies, loginPageUrl } = await this.getLoginPage(authUrl.toString());
|
|
200
|
-
const authCode = await this.submitLogin(form, email, password, cookies, loginPageUrl);
|
|
201
|
-
const tokens = await this.exchangeCodeForTokens(authCode, pkce.verifier);
|
|
202
|
-
|
|
203
|
-
console.log('✓ Successfully obtained OAuth tokens');
|
|
204
|
-
return tokens;
|
|
205
|
-
}
|
|
206
57
|
}
|
|
207
58
|
|
|
208
|
-
|
|
209
59
|
export default MelCloudHomeToken;
|
|
210
60
|
|
|
211
61
|
|