homebridge-melcloud-control 4.3.11-beta.20 → 4.3.11-beta.22

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.3.11-beta.20",
4
+ "version": "4.3.11-beta.22",
5
5
  "description": "Homebridge plugin to control Mitsubishi Air Conditioner, Heat Pump and Energy Recovery Ventilation.",
6
6
  "license": "MIT",
7
7
  "author": "grzegorz914",
@@ -457,24 +457,17 @@ class MelCloudHome extends EventEmitter {
457
457
 
458
458
  async connect() {
459
459
  if (this.logDebug) this.emit('debug', 'Connecting to MELCloud Home');
460
- const client = new MELCloudHomeAuth({ debug: true });
460
+ const auth = new MELCloudHomeAuth();
461
461
  try {
462
- await client.login(this.user, this.passwd);
463
- console.log("Logged in!");
462
+ await auth.login(this.user, this.passwd);
463
+ console.log('Logged in!');
464
464
 
465
- const ctx = await client.getUserContext();
466
- console.log("User context:", JSON.stringify(ctx, null, 2));
465
+ const valid = await auth.checkSession();
466
+ console.log('Session valid:', valid);
467
467
 
468
- const devices = await client.getDevices();
469
- console.log("Devices:", JSON.stringify(devices, null, 2));
470
-
471
- // If you need a token-like object:
472
- const tokens = await client.getAuthTokenCandidates();
473
- console.log("Token candidates:", tokens);
468
+ console.log('Cookies:', auth.getCookies());
474
469
  } catch (err) {
475
- console.error("Error:", err.message || err);
476
- } finally {
477
- await client.logout();
470
+ console.error(err.message);
478
471
  }
479
472
  }
480
473
  }
@@ -1,228 +1,113 @@
1
- import axios from "axios";
2
- import { wrapper } from "axios-cookiejar-support";
3
- import { CookieJar } from "tough-cookie";
4
- import { JSDOM } from "jsdom";
1
+ import axios from 'axios';
2
+ import { CookieJar } from 'tough-cookie';
3
+ import { wrapper } from 'axios-cookiejar-support';
4
+ import { JSDOM } from 'jsdom';
5
+ import qs from 'qs';
5
6
 
6
- // Constants
7
+ const BASE_URL = 'https://melcloudhome.com'; // lub właściwe dla Home
7
8
  const USER_AGENT =
8
- "Mozilla/5.0 (Macintosh; Intel Mac OS X 14_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36";
9
-
10
- const BASE_URL = "https://melcloudhome.com";
11
- const COGNITO_BASE =
12
- "https://live-melcloudhome.auth.eu-west-1.amazoncognito.com";
13
-
14
- class MELCloudHomeAuth {
15
- constructor({ debug = false } = {}) {
16
- this.debug = debug;
17
- this.jar = new CookieJar();
18
- this.axios = wrapper(
19
- axios.create({
20
- jar: this.jar,
21
- withCredentials: true,
22
- maxRedirects: 10,
23
- headers: {
24
- "User-Agent": USER_AGENT,
25
- Accept:
26
- "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8",
27
- "Accept-Language": "en-US,en;q=0.9",
28
- },
29
- })
30
- );
31
- this.authenticated = false;
32
- }
33
-
34
- log(...args) {
35
- if (this.debug) console.log("[MelcloudHomeClient]", ...args);
36
- }
37
-
38
- _delay(ms) {
39
- return new Promise((r) => setTimeout(r, ms));
40
- }
41
-
42
- // --- Helper: generate cognitoAsfData (simplified) ---
43
- generateAsfData() {
44
- const data = {
45
- deviceName: "Chrome",
46
- osName: "Macintosh",
47
- osVersion: "14_5",
48
- timezoneOffset: new Date().getTimezoneOffset(),
49
- userAgent: USER_AGENT,
50
- screen: { width: 1440, height: 900 },
51
- language: "en-US",
52
- plugins: [],
53
- fonts: [],
54
- canvasFingerprint: "simplified", // placeholder
55
- };
56
- const json = JSON.stringify(data);
57
- return Buffer.from(json).toString("base64");
58
- }
59
-
60
- // --- Extract CSRF token from HTML ---
61
- extractCsrf(html) {
62
- if (!html) return null;
63
- try {
64
- const dom = new JSDOM(html);
65
- const el = dom.window.document.querySelector('input[name="_csrf"]');
66
- if (el && el.value) return el.value;
67
- } catch {}
68
- const m = html.match(/<input[^>]+name="_csrf"[^>]+value="([^"]+)"/i);
69
- if (m) return m[1];
70
- return null;
71
- }
72
-
73
- findCognitoLoginUrl(html) {
74
- if (!html) return null;
75
- const m = html.match(/action="([^"]*amazoncognito\.com[^"]*)"/i);
76
- if (m) return m[1];
77
- return null;
78
- }
79
-
80
- extractErrorMessage(html) {
81
- if (!html) return null;
82
- const patterns = [
83
- /<div[^>]*class="[^"]*error[^"]*"[^>]*>([^<]+)<\/div>/i,
84
- /<span[^>]*class="[^"]*error[^"]*"[^>]*>([^<]+)<\/span>/i,
85
- /<p[^>]*class="[^"]*error[^"]*"[^>]*>([^<]+)<\/p>/i,
86
- ];
87
- for (const p of patterns) {
88
- const m = html.match(p);
89
- if (m) return m[1].trim();
9
+ 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36';
10
+
11
+ export class MELCloudHomeAuth {
12
+ constructor() {
13
+ this.jar = new CookieJar();
14
+ this.client = wrapper(
15
+ axios.create({
16
+ jar: this.jar,
17
+ withCredentials: true,
18
+ headers: {
19
+ 'User-Agent': USER_AGENT,
20
+ Accept:
21
+ 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8',
22
+ 'Accept-Language': 'en-US,en;q=0.9',
23
+ },
24
+ maxRedirects: 0,
25
+ validateStatus: (status) => status >= 200 && status < 400,
26
+ })
27
+ );
28
+ this.authenticated = false;
90
29
  }
91
- return null;
92
- }
93
-
94
- async startLoginFlow() {
95
- this.log("Starting login flow: GET /bff/login");
96
- const resp = await this.axios.get(`${BASE_URL}/bff/login`, {
97
- params: { returnUrl: "/dashboard" },
98
- validateStatus: () => true,
99
- });
100
-
101
- const finalUrl =
102
- resp.request?.res?.responseUrl || resp.config?.url || "";
103
-
104
- this.log("Received final redirect URL:", finalUrl);
105
-
106
- const csrf = this.extractCsrf(resp.data);
107
- if (!csrf) throw new Error("Failed to extract CSRF token");
108
-
109
- const loginUrl = this.findCognitoLoginUrl(resp.data) || finalUrl;
110
- return { loginUrl, csrf };
111
- }
112
-
113
- async submitCredentials(loginUrl, csrf, username, password) {
114
- this.log("Submitting credentials to Cognito...");
115
-
116
- const payload = new URLSearchParams({
117
- _csrf: csrf,
118
- username,
119
- password,
120
- cognitoAsfData: this.generateAsfData(),
121
- });
122
-
123
- // Step 1: POST credentials
124
- const resp = await this.axios.post(loginUrl, payload.toString(), {
125
- headers: {
126
- "Content-Type": "application/x-www-form-urlencoded",
127
- Origin: COGNITO_BASE,
128
- Referer: loginUrl,
129
- },
130
- validateStatus: () => true,
131
- maxRedirects: 0,
132
- });
133
-
134
- const redirectUrl = resp.headers.location || resp.request?.res?.responseUrl;
135
- if (!redirectUrl.includes("signin-oidc")) {
136
- throw new Error(
137
- "Authentication failed: signin-oidc callback not received"
138
- );
30
+
31
+ async login(username, password) {
32
+ try {
33
+ // 1. GET login page to get CSRF token
34
+ let resp = await this.client.get(`${BASE_URL}/bff/login`, {
35
+ params: { returnUrl: '/dashboard' },
36
+ maxRedirects: 5, // pozwala axios śledzić redirecty
37
+ });
38
+
39
+ // finalny URL po redirectach
40
+ let finalUrl = resp.request?.res?.responseUrl || resp.request?.path || '';
41
+ if (!finalUrl.includes('.amazoncognito.com')) {
42
+ throw new Error('Unexpected redirect URL: ' + finalUrl);
43
+ }
44
+
45
+ // Extract CSRF token from HTML
46
+ const csrfToken = this.extractCsrfToken(resp.data);
47
+ if (!csrfToken) throw new Error('CSRF token not found');
48
+
49
+ // 2. POST credentials to Cognito
50
+ const loginData = qs.stringify({
51
+ _csrf: csrfToken,
52
+ username,
53
+ password,
54
+ cognitoAsfData: '', // minimal value, może być potrzebne pełne ASF
55
+ });
56
+
57
+ resp = await this.client.post(finalUrl, loginData, {
58
+ headers: {
59
+ 'Content-Type': 'application/x-www-form-urlencoded',
60
+ Origin: 'https://live-melcloudhome.auth.eu-west-1.amazoncognito.com',
61
+ Referer: finalUrl,
62
+ },
63
+ maxRedirects: 5,
64
+ });
65
+
66
+ finalUrl = resp.request.res.responseUrl;
67
+
68
+ // Sprawdzenie czy udało się zalogować
69
+ if (
70
+ finalUrl.includes('melcloudhome.com/dashboard') &&
71
+ resp.status < 400
72
+ ) {
73
+ console.log('Authentication successful');
74
+ this.authenticated = true;
75
+ return true;
76
+ }
77
+
78
+ throw new Error('Authentication failed, final URL: ' + finalUrl);
79
+ } catch (err) {
80
+ throw new Error('Login error: ' + err.message);
81
+ }
139
82
  }
140
83
 
141
- this.log("Calling signin-oidc callback:", redirectUrl);
142
-
143
- // Step 2: GET callback to set cookies
144
- const callback = await this.axios.get(redirectUrl, {
145
- headers: {
146
- Referer: loginUrl,
147
- "User-Agent": USER_AGENT,
148
- Accept: "text/html",
149
- },
150
- validateStatus: () => true,
151
- });
152
-
153
- this.log("signin-oidc status:", callback.status);
154
-
155
- // Check if cookie meu.identity is set
156
- const cookies = await this.jar.getCookies(BASE_URL);
157
- const hasIdentity = cookies.some((c) => c.key.includes("meu.identity"));
158
- if (!hasIdentity) {
159
- throw new Error(
160
- "OpenID callback did not produce session cookie (cognitoAsfData missing or rejected)"
161
- );
84
+ extractCsrfToken(html) {
85
+ const dom = new JSDOM(html);
86
+ const input = dom.window.document.querySelector('input[name="_csrf"]');
87
+ return input?.value || null;
162
88
  }
163
89
 
164
- this.authenticated = true;
165
- this.log("Login successful, session cookie meu.identity is present");
166
- await this._delay(1000); // short wait for Blazor initialization
167
- return true;
168
- }
169
-
170
- async login(username, password) {
171
- const { loginUrl, csrf } = await this.startLoginFlow();
172
- await this.submitCredentials(loginUrl, csrf, username, password);
173
-
174
- const valid = await this.checkSession();
175
- if (!valid) throw new Error("Session not valid after login");
176
-
177
- this.log("Login flow complete");
178
- return true;
179
- }
180
-
181
- async checkSession() {
182
- if (!this.authenticated) return false;
183
- try {
184
- const resp = await this.axios.get(`${BASE_URL}/api/user/context`, {
185
- headers: {
186
- Accept: "application/json",
187
- "x-csrf": "1",
188
- Referer: `${BASE_URL}/dashboard`,
189
- },
190
- validateStatus: () => true,
191
- });
192
- return resp.status === 200;
193
- } catch {
194
- return false;
90
+ async checkSession() {
91
+ if (!this.authenticated) return false;
92
+
93
+ try {
94
+ const resp = await this.client.get(`${BASE_URL}/api/user/context`, {
95
+ headers: { 'x-csrf': '1', Referer: `${BASE_URL}/dashboard` },
96
+ });
97
+ return resp.status === 200;
98
+ } catch {
99
+ this.authenticated = false;
100
+ return false;
101
+ }
195
102
  }
196
- }
197
-
198
- async getUserContext() {
199
- if (!this.authenticated) throw new Error("Not authenticated");
200
- const resp = await this.axios.get(`${BASE_URL}/api/user/context`, {
201
- headers: { Accept: "application/json", "x-csrf": "1", Referer: `${BASE_URL}/dashboard` },
202
- });
203
- return resp.data;
204
- }
205
-
206
- async getDevices() {
207
- if (!this.authenticated) throw new Error("Not authenticated");
208
- const resp = await this.axios.get(`${BASE_URL}/api/devices/devices`, {
209
- headers: { Accept: "application/json", "x-csrf": "1", Referer: `${BASE_URL}/dashboard` },
210
- validateStatus: () => true,
211
- });
212
- if (resp.status !== 200) throw new Error(`Failed to get devices: ${resp.status}`);
213
- return resp.data;
214
- }
215
-
216
- async logout() {
217
- try {
218
- await this.axios.get(`${BASE_URL}/bff/logout`, { validateStatus: () => true });
219
- } finally {
220
- this.authenticated = false;
103
+
104
+ getCookies() {
105
+ return this.jar.toJSON();
221
106
  }
222
- }
223
107
  }
224
108
 
225
109
 
110
+
226
111
  export default MELCloudHomeAuth;
227
112
 
228
113