homebridge-melcloud-control 4.0.0-beta.235 → 4.0.0-beta.237

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.
@@ -151,7 +151,6 @@
151
151
  formElements.passwd.value = account.passwd || '';
152
152
  formElements.language.value = account.language || '0';
153
153
  formElements.accountType.value = account.type || 'disabled';
154
-
155
154
  formElements.logIn.disabled = !(account.name && account.user && account.passwd && account.language && account.type);
156
155
  });
157
156
  });
@@ -178,9 +177,7 @@
178
177
 
179
178
  document.getElementById('melCloudAccount').style.display = 'block';
180
179
 
181
-
182
-
183
- // --- Config Button Toggle ---
180
+ // Config Button Toggle
184
181
  const configButton = document.getElementById('configButton');
185
182
  let configButtonState = false;
186
183
  configButton.addEventListener('click', () => {
@@ -202,7 +199,7 @@
202
199
  }
203
200
  });
204
201
 
205
- // --- Device Handling & Login Logic ---
202
+ // Device Handling & Login Logic
206
203
  function removeStaleDevices(configDevices, melcloudDevices) {
207
204
  const melcloudIds = melcloudDevices.map(d => d.DeviceID);
208
205
  const removedDevices = [];
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.235",
4
+ "version": "4.0.0-beta.237",
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
@@ -3,6 +3,7 @@ import { execSync } from 'child_process';
3
3
  import puppeteer from 'puppeteer';
4
4
  import axios from 'axios';
5
5
  import EventEmitter from 'events';
6
+ import MelCloudHomeToken from './melcloudhometoken.js';
6
7
  import ImpulseGenerator from './impulsegenerator.js';
7
8
  import Functions from './functions.js';
8
9
  import { ApiUrls, ApiUrlsHome } from './constants.js';
@@ -24,6 +25,12 @@ class MelCloud extends EventEmitter {
24
25
  this.contextKey = '';
25
26
  this.functions = new Functions();
26
27
 
28
+ this.axiosInstance = axios.create({
29
+ baseURL: 'https://app.melcloud.com/Mitsubishi.Wifi.Client',
30
+ headers: { 'User-Agent': 'MonitorAndControl.App.Mobile/35 CFNetwork/3860.100.1 Darwin/25.0.0' }
31
+ });
32
+ this.tokens = null;
33
+
27
34
  this.loginData = {
28
35
  Email: user,
29
36
  Password: passwd,
@@ -167,6 +174,15 @@ class MelCloud extends EventEmitter {
167
174
  }
168
175
  }
169
176
 
177
+ // MELCloud Home
178
+ async connectToMelCloudHome() {
179
+ const oauth = new MelCloudHomeToken(this.log);
180
+ this.tokens = await oauth.getTokensWithCredentials(this.user, this.passwd);
181
+ this.emit('warn', `Token${JSON.stringify(this.tokens, null, 2)}`);
182
+ //this.axiosInstance.defaults.headers['Authorization'] = `Bearer ${this.tokens.accessToken}`;
183
+ return false;
184
+ }
185
+
170
186
  async checkMelcloudHomeDevicesList(contextKey) {
171
187
  try {
172
188
  const axiosInstance = axios.create({
@@ -270,8 +286,7 @@ class MelCloud extends EventEmitter {
270
286
  }
271
287
  }
272
288
 
273
- // MELCloud Home
274
- async connectToMelCloudHome(refresh = false) {
289
+ async connectToMelCloudHome1(refresh = false) {
275
290
  if (this.logDebug) this.emit('debug', `Connecting to MELCloud Home`);
276
291
 
277
292
  let browser;
@@ -0,0 +1,178 @@
1
+ // oauth-helper-axios.js
2
+ import axios from 'axios';
3
+ import { CookieJar } from 'tough-cookie';
4
+ import { wrapper } from 'axios-cookiejar-support';
5
+ import crypto from 'crypto';
6
+
7
+ const MOBILE_USER_AGENT = 'MonitorAndControl.App.Mobile/35 CFNetwork/3860.100.1 Darwin/25.0.0';
8
+ const CLIENT_ID = 'homemobile';
9
+ const REDIRECT_URI = 'melcloudhome://';
10
+ 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
+
14
+ export class MelCloudHomeToken {
15
+ constructor(log) {
16
+ this.log = log;
17
+ this.jar = new CookieJar();
18
+ this.axiosInstance = wrapper(axios.create({
19
+ jar: this.jar,
20
+ withCredentials: true,
21
+ headers: { 'User-Agent': MOBILE_USER_AGENT },
22
+ maxRedirects: 10
23
+ }));
24
+ }
25
+
26
+ generatePKCE() {
27
+ const verifier = crypto.randomBytes(32).toString('base64url');
28
+ const challenge = crypto.createHash('sha256').update(verifier).digest('base64url');
29
+ return { verifier, challenge };
30
+ }
31
+
32
+ generateState() {
33
+ return crypto.randomBytes(32).toString('hex');
34
+ }
35
+
36
+ async getLoginPage(authUrl) {
37
+ console.log('Getting login page...');
38
+ const resp = await this.axiosInstance.get(authUrl, {
39
+ headers: {
40
+ 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
41
+ 'Accept-Language': 'en-US,en;q=0.9',
42
+ },
43
+ validateStatus: () => true
44
+ });
45
+
46
+ const html = resp.data;
47
+ const formSnippet = html.match(/<form[^>]*>[\s\S]{0,800}/);
48
+ if (formSnippet) console.log(`Form HTML preview: ${formSnippet[0].replace(/\s+/g, ' ').substring(0, 500)}`);
49
+
50
+ const csrfMatch = html.match(/name="_csrf"\s+value="([^"]+)"/);
51
+ if (!csrfMatch) throw new Error('Could not extract CSRF token from login page');
52
+
53
+ const cookies = await this.jar.getCookieString(authUrl) || '';
54
+ return { html, loginUrl: resp.request.res.responseUrl, csrf: csrfMatch[1], cookies };
55
+ }
56
+
57
+ async submitLogin(loginUrl, csrf, email, password, cookies) {
58
+ console.log('Submitting login credentials...');
59
+
60
+ const formData = new URLSearchParams({ _csrf: csrf, username: email, password: password });
61
+
62
+ const resp = await this.axiosInstance.post(loginUrl, formData.toString(), {
63
+ headers: {
64
+ 'Content-Type': 'application/x-www-form-urlencoded',
65
+ 'Cookie': cookies,
66
+ 'Origin': 'https://live-melcloudhome.auth.eu-west-1.amazoncognito.com',
67
+ 'Referer': loginUrl
68
+ },
69
+ maxRedirects: 0,
70
+ validateStatus: status => status >= 200 && status < 400
71
+ }).catch(err => {
72
+ if (err.response && err.response.status >= 300 && err.response.status < 400) return err.response;
73
+ throw err;
74
+ });
75
+
76
+ const locationHeader = resp.headers.location;
77
+ if (locationHeader && locationHeader.startsWith('melcloudhome://')) {
78
+ const codeMatch = locationHeader.match(/[?&]code=([^&]+)/);
79
+ if (codeMatch) return codeMatch[1];
80
+ }
81
+
82
+ const formCodeMatch = resp.data.match(/name="code"\s+value="([^"]+)"/);
83
+ const formStateMatch = resp.data.match(/name="state"\s+value="([^"]+)"/);
84
+ const formActionMatch = resp.data.match(/action="([^"]+)"/);
85
+
86
+ if (formCodeMatch && formStateMatch && formActionMatch) {
87
+ return this.submitFormPost(formActionMatch[1], formCodeMatch[1], formStateMatch[1]);
88
+ }
89
+
90
+ const bodyCodeMatch = resp.data.match(/melcloudhome:\/\/[^"'\s]*[?&]code=([^&"'\s]+)/);
91
+ if (bodyCodeMatch) return bodyCodeMatch[1];
92
+
93
+ throw new Error('No authorization code found after login');
94
+ }
95
+
96
+ async submitFormPost(actionUrl, code, state) {
97
+ const formData = new URLSearchParams({ code, state });
98
+
99
+ const resp = await this.axiosInstance.post(actionUrl, formData.toString(), {
100
+ headers: {
101
+ 'Content-Type': 'application/x-www-form-urlencoded',
102
+ 'Origin': 'https://live-melcloudhome.auth.eu-west-1.amazoncognito.com',
103
+ 'Referer': actionUrl
104
+ },
105
+ maxRedirects: 0,
106
+ validateStatus: status => status >= 200 && status < 400
107
+ }).catch(err => {
108
+ if (err.response && err.response.status >= 300 && err.response.status < 400) return err.response;
109
+ throw err;
110
+ });
111
+
112
+ const locationHeader = resp.headers.location;
113
+ if (locationHeader && locationHeader.startsWith('melcloudhome://')) {
114
+ const codeMatch = locationHeader.match(/[?&]code=([^&]+)/);
115
+ if (codeMatch) return codeMatch[1];
116
+ }
117
+
118
+ const bodyCodeMatch = resp.data.match(/melcloudhome:\/\/[^"'\s]*[?&]code=([^&"'\s]+)/);
119
+ if (bodyCodeMatch) return bodyCodeMatch[1];
120
+
121
+ throw new Error('No authorization code found in form_post callback');
122
+ }
123
+
124
+ async exchangeCodeForTokens(code, codeVerifier) {
125
+ console.log('Exchanging authorization code for tokens...');
126
+
127
+ const tokenData = new URLSearchParams({
128
+ grant_type: 'authorization_code',
129
+ code,
130
+ redirect_uri: REDIRECT_URI,
131
+ client_id: CLIENT_ID,
132
+ code_verifier: codeVerifier
133
+ });
134
+
135
+ const resp = await this.axiosInstance.post(TOKEN_ENDPOINT, tokenData.toString(), {
136
+ headers: {
137
+ 'Content-Type': 'application/x-www-form-urlencoded',
138
+ 'Authorization': 'Basic aG9tZW1vYmlsZTo='
139
+ }
140
+ });
141
+
142
+ if (resp.data.error) throw new Error(`Token exchange failed: ${resp.data.error_description || resp.data.error}`);
143
+
144
+ return {
145
+ accessToken: resp.data.access_token,
146
+ refreshToken: resp.data.refresh_token,
147
+ expiresIn: resp.data.expires_in,
148
+ idToken: resp.data.id_token
149
+ };
150
+ }
151
+
152
+ async getTokensWithCredentials(email, password) {
153
+ this.log.info('Obtaining OAuth tokens automatically...');
154
+ const pkce = this.generatePKCE();
155
+ const state = this.generateState();
156
+
157
+ const authUrl = new URL(AUTHORIZE_ENDPOINT);
158
+ authUrl.searchParams.set('client_id', CLIENT_ID);
159
+ authUrl.searchParams.set('redirect_uri', REDIRECT_URI);
160
+ authUrl.searchParams.set('response_type', 'code');
161
+ authUrl.searchParams.set('scope', SCOPE);
162
+ authUrl.searchParams.set('code_challenge', pkce.challenge);
163
+ authUrl.searchParams.set('code_challenge_method', 'S256');
164
+ authUrl.searchParams.set('state', state);
165
+
166
+ const loginPage = await this.getLoginPage(authUrl.toString());
167
+ const authCode = await this.submitLogin(loginPage.loginUrl, loginPage.csrf, email, password, loginPage.cookies);
168
+ const tokens = await this.exchangeCodeForTokens(authCode, pkce.verifier);
169
+
170
+ this.log.info('✓ Successfully obtained OAuth tokens');
171
+ return tokens;
172
+ }
173
+ }
174
+
175
+ module.exports = MelCloudHomeToken;
176
+
177
+
178
+