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 CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "displayName": "MELCloud Control",
3
3
  "name": "homebridge-melcloud-control",
4
- "version": "4.0.0-beta.289",
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({
@@ -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 getLoginPage(authUrl) {
33
- const res = await this.client.get(authUrl, { maxRedirects: 10 });
34
- return { url: res.request.res.responseUrl };
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 getUrl() {
38
- try {
39
- const pkce = this.generatePKCE();
40
- const state = this.generateState();
41
- const authUrl = new URL(AUTHORIZE_ENDPOINT);
42
- authUrl.searchParams.set('client_id', CLIENT_ID);
43
- authUrl.searchParams.set('redirect_uri', REDIRECT_URI);
44
- authUrl.searchParams.set('response_type', 'code');
45
- authUrl.searchParams.set('scope', SCOPE);
46
- authUrl.searchParams.set('code_challenge', pkce.challenge);
47
- authUrl.searchParams.set('code_challenge_method', 'S256');
48
- authUrl.searchParams.set('state', state);
49
-
50
- const loginPage = await this.getLoginPage(authUrl.toString());
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
+