homebridge-melcloud-control 4.0.0-beta.391 → 4.0.0-beta.392

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "displayName": "MELCloud Control",
3
3
  "name": "homebridge-melcloud-control",
4
- "version": "4.0.0-beta.391",
4
+ "version": "4.0.0-beta.392",
5
5
  "description": "Homebridge plugin to control Mitsubishi Air Conditioner, Heat Pump and Energy Recovery Ventilation.",
6
6
  "license": "MIT",
7
7
  "author": "grzegorz914",
@@ -35,39 +35,79 @@ class MelCloudHomeToken extends EventEmitter {
35
35
  return crypto.randomBytes(32).toString('hex');
36
36
  }
37
37
 
38
- async getLoginPage(authUrl) {
39
- const res = await this.client.get(authUrl, { maxRedirects: 10 });
40
- return { url: res.request.res.responseUrl };
38
+ async buildAuthorizeUrl() {
39
+ const pkce = this.generatePKCE();
40
+ const state = this.generateState();
41
+
42
+ const authUrl = new URL(AUTHORIZE_ENDPOINT);
43
+ authUrl.searchParams.set('client_id', CLIENT_ID);
44
+ authUrl.searchParams.set('redirect_uri', REDIRECT_URI);
45
+ authUrl.searchParams.set('response_type', 'code');
46
+ authUrl.searchParams.set('scope', SCOPE);
47
+ authUrl.searchParams.set('code_challenge', pkce.challenge);
48
+ authUrl.searchParams.set('code_challenge_method', 'S256');
49
+ authUrl.searchParams.set('state', state);
50
+
51
+ return { url: authUrl.toString(), codeVerifier: pkce.verifier };
41
52
  }
42
53
 
43
- async getTokens(code, codeVerifier) {
54
+ async loginToMelCloudHome(authUrl) {
44
55
  try {
45
- const tokenData = new URLSearchParams({
46
- grant_type: 'authorization_code',
47
- code: code,
48
- redirect_uri: REDIRECT_URI,
49
- client_id: CLIENT_ID,
50
- code_verifier: codeVerifier,
51
- });
56
+ const getResp = await this.client.get(authUrl, { headers: { 'Accept': 'text/html' } });
57
+ const cookies = getResp.headers['set-cookie'] || [];
58
+ const dom = new JSDOM(getResp.data);
59
+ const csrf = dom.window.document.querySelector('input[name="_csrf"]')?.value;
52
60
 
53
- this.emit('warn', `Code used: ${code}`);
54
- this.emit('warn', `Verifier: ${codeVerifier}`);
55
- this.emit('warn', `Redirect URI: ${REDIRECT_URI}`);
61
+ if (!csrf) {
62
+ this.emit('warn', 'CSRF token not found');
63
+ return null;
64
+ }
56
65
 
57
- const tokenResponse = await this.client.post(TOKEN_ENDPOINT, tokenData.toString(), {
66
+ const formData = new URLSearchParams({
67
+ _csrf: csrf,
68
+ username: this.user,
69
+ password: this.passwd
70
+ });
71
+
72
+ const response = await this.client.post(authUrl, formData.toString(), {
58
73
  headers: {
59
- 'Content-Type': 'application/x-www-form-urlencoded'
74
+ 'User-Agent': MOBILE_USER_AGENT,
75
+ 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
76
+ 'Accept-Language': 'en-US,en;q=0.9',
77
+ 'Content-Type': 'application/x-www-form-urlencoded',
78
+ 'Content-Length': formData.toString().length,
79
+ 'Cookie': cookies.join('; '),
80
+ 'Origin': 'https://live-melcloudhome.auth.eu-west-1.amazoncognito.com',
81
+ 'Referer': authUrl
60
82
  },
83
+ maxRedirects: 0,
84
+ validateStatus: status => [200, 302, 400].includes(status)
61
85
  });
62
86
 
63
- const tokens = tokenResponse.data;
64
- this.emit('warn', `Token ${JSON.stringify(tokens)}`);
65
- return tokens;
66
- } catch (error) {
67
- if (error.response) {
68
- this.emit('warn', `Token error response: ${JSON.stringify(error.response.data)}`);
87
+ if (response.status === 400) {
88
+ this.emit('warn', `Login failed: ${response.data}`);
89
+ return null;
69
90
  }
70
- throw new Error(`Failed to obtain OAuth token: ${error}`);
91
+
92
+ // Extract authorization code
93
+ const code = await this.extractCodeFromResponse(
94
+ response.data,
95
+ response.headers,
96
+ async (url) => {
97
+ const r = await this.client.get(url, {
98
+ maxRedirects: 0,
99
+ validateStatus: status => [200, 302, 303].includes(status)
100
+ });
101
+ return this.extractCodeFromResponse(r.data, r.headers, async u => this.extractCodeFromResponse(r.data, r.headers, u));
102
+ }
103
+ );
104
+
105
+ if (code) this.emit('warn', `Authorization code obtained: ${code}`);
106
+ return code || null;
107
+
108
+ } catch (err) {
109
+ this.emit('warn', `loginToMelCloudHome error: ${err}`);
110
+ return null;
71
111
  }
72
112
  }
73
113
 
@@ -92,7 +132,7 @@ class MelCloudHomeToken extends EventEmitter {
92
132
  const formActionMatch = data.match(/action="([^"]+)"/);
93
133
 
94
134
  if (formCodeMatch && formStateMatch && formActionMatch) {
95
- this.emit('warn', 'Found form_post response, submitting to callback endpoint...');
135
+ this.emit('warn', 'Found form_post, submitting...');
96
136
  try {
97
137
  const code = await this.submitFormPost(formActionMatch[1], formCodeMatch[1], formStateMatch[1]);
98
138
  this.emit('warn', `submitFormPost returned code: ${code}`);
@@ -105,10 +145,10 @@ class MelCloudHomeToken extends EventEmitter {
105
145
  }
106
146
  }
107
147
 
108
- // 3️⃣ JS redirect w body
148
+ // 3️⃣ JS redirect in body
109
149
  const bodyCodeMatch = data.match(/melcloudhome:\/\/[^"'\s]*[?&]code=([^&"'\s]+)/);
110
150
  if (bodyCodeMatch) {
111
- this.emit('warn', `Found code in response body: ${bodyCodeMatch[1]}`);
151
+ this.emit('warn', `Found code in body: ${bodyCodeMatch[1]}`);
112
152
  resolve(bodyCodeMatch[1]);
113
153
  return;
114
154
  }
@@ -129,134 +169,55 @@ class MelCloudHomeToken extends EventEmitter {
129
169
 
130
170
  this.emit('warn', 'Authorization code not found in response');
131
171
  reject(new Error('Authorization code not found'));
132
- } catch (error) {
133
- this.emit('warn', `extractCodeFromResponse error: ${error}`);
134
- reject(error);
172
+ } catch (err) {
173
+ this.emit('warn', `extractCodeFromResponse error: ${err}`);
174
+ reject(err);
135
175
  }
136
176
  });
137
177
  }
138
178
 
139
- // submitFormPost w JS
140
179
  async submitFormPost(actionUrl, code, state) {
141
180
  const formData = new URLSearchParams({ code, state });
142
181
  this.emit('warn', `Submitting form_post to ${actionUrl}`);
143
182
  const res = await this.client.post(actionUrl, formData.toString(), {
144
183
  headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
145
184
  maxRedirects: 0,
146
- validateStatus: status => [200, 302, 303].includes(status),
185
+ validateStatus: status => [200, 302, 303].includes(status)
147
186
  });
148
187
 
149
188
  const location = res.headers['location'];
150
189
  if (location) {
151
190
  const match = location.match(/[?&]code=([^&]+)/);
152
- if (match) {
153
- this.emit('warn', `Form post redirect returned code: ${match[1]}`);
154
- return match[1];
155
- }
191
+ if (match) return match[1];
156
192
  }
157
193
 
158
194
  this.emit('warn', 'Code not found after form_post submission');
159
195
  throw new Error('Code not found after form_post submission');
160
196
  }
161
197
 
198
+ async getTokens(code, codeVerifier) {
199
+ const tokenData = new URLSearchParams({
200
+ grant_type: 'authorization_code',
201
+ code: code,
202
+ redirect_uri: REDIRECT_URI,
203
+ client_id: CLIENT_ID,
204
+ code_verifier: codeVerifier
205
+ });
162
206
 
163
- async loginToMelCloudHome(url) {
164
207
  try {
165
- // Step 1: Pobierz stronę logowania
166
- const getResp = await this.client.get(url, {
167
- headers: { 'Accept': 'text/html' }
168
- });
169
-
170
- const cookies = getResp.headers['set-cookie'] || [];
171
- const dom = new JSDOM(getResp.data);
172
- const csrf = dom.window.document.querySelector('input[name="_csrf"]')?.value;
173
- if (!csrf) {
174
- this.emit('warn', `CSRF token not found`);
175
- return null;
176
- }
177
-
178
- // Step 2: Przygotuj dane logowania
179
- const formData = new URLSearchParams({
180
- _csrf: csrf,
181
- username: this.user,
182
- password: this.passwd,
183
- });
184
-
185
- // Step 3: Wyślij POST z danymi logowania
186
- const response = await this.client.post(url, formData.toString(), {
208
+ const tokenResponse = await this.client.post(TOKEN_ENDPOINT, tokenData.toString(), {
187
209
  headers: {
188
- 'User-Agent': MOBILE_USER_AGENT,
189
- 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
190
- 'Accept-Language': 'en-US,en;q=0.9',
191
210
  'Content-Type': 'application/x-www-form-urlencoded',
192
- 'Content-Length': formData.toString().length,
193
- 'Cookie': cookies.join('; '),
194
- 'Origin': 'https://live-melcloudhome.auth.eu-west-1.amazoncognito.com',
195
- 'Referer': url,
196
- },
197
- maxRedirects: 0,
198
- validateStatus: status => [200, 302, 400].includes(status),
199
- });
200
-
201
- if (response.status === 400) {
202
- this.emit('warn', `Login failed (bad request or invalid credentials): ${response.data}`);
203
- return null;
204
- }
205
-
206
- // Step 4: Pobierz Authorization Code
207
- if (response.status === 200 || response.status === 302) {
208
- try {
209
- const code = await this.extractCodeFromResponse(
210
- response.data, // HTML lub body
211
- response.headers, // nagłówki
212
- async (url) => { // followRedirect
213
- const r = await this.client.get(url, {
214
- maxRedirects: 0,
215
- validateStatus: status => [200, 302, 303].includes(status),
216
- });
217
- return this.extractCodeFromResponse(r.data, r.headers, followRedirect);
218
- }
219
- );
220
-
221
- if (code) {
222
- this.emit('warn', `Authorization code obtained: ${code}`);
223
- return code;
224
- } else {
225
- this.emit('warn', 'Authorization code not found in response');
226
- return null;
227
- }
228
- } catch (err) {
229
- this.emit('warn', `extractCodeFromResponse error: ${err}`);
230
- return null;
211
+ 'Authorization': 'Basic aG9tZW1vYmlsZTo='
231
212
  }
232
- }
233
-
234
- return null;
235
-
236
- } catch (error) {
237
- throw new Error(`Login to MELCloud Home error: ${error}`);
238
- }
239
- }
213
+ });
240
214
 
215
+ const tokens = tokenResponse.data;
216
+ this.emit('warn', `Token obtained: ${JSON.stringify(tokens)}`);
217
+ return tokens;
241
218
 
242
- async buildAuthorizeUrl() {
243
- try {
244
- const pkce = this.generatePKCE();
245
- const state = this.generateState();
246
- const authUrl = new URL(AUTHORIZE_ENDPOINT);
247
- authUrl.searchParams.set('client_id', CLIENT_ID);
248
- authUrl.searchParams.set('redirect_uri', REDIRECT_URI);
249
- authUrl.searchParams.set('response_type', 'code');
250
- authUrl.searchParams.set('scope', SCOPE);
251
- authUrl.searchParams.set('code_challenge', pkce.challenge);
252
- authUrl.searchParams.set('code_challenge_method', 'S256');
253
- authUrl.searchParams.set('state', state);
254
-
255
- const loginPage = await this.getLoginPage(authUrl.toString());
256
- const data = { codeVerifier: pkce.verifier, url: loginPage.url }
257
- return data;
258
- } catch (error) {
259
- throw new Error(`Failed to obtain OAuth link: ${error}`);
219
+ } catch (err) {
220
+ throw new Error(`Failed to obtain OAuth token: ${err}`);
260
221
  }
261
222
  }
262
223
  }
@@ -267,3 +228,4 @@ export default MelCloudHomeToken;
267
228
 
268
229
 
269
230
 
231
+