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

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.21",
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,112 @@
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
+ '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';
9
10
 
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;
11
+ export class MELCloudHomeAuth {
12
+ constructor() {
17
13
  this.jar = new CookieJar();
18
- this.axios = wrapper(
14
+ this.client = wrapper(
19
15
  axios.create({
20
16
  jar: this.jar,
21
17
  withCredentials: true,
22
- maxRedirects: 10,
23
18
  headers: {
24
- "User-Agent": USER_AGENT,
19
+ 'User-Agent': USER_AGENT,
25
20
  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",
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',
28
23
  },
24
+ maxRedirects: 0,
25
+ validateStatus: (status) => status >= 200 && status < 400,
29
26
  })
30
27
  );
31
28
  this.authenticated = false;
32
29
  }
33
30
 
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;
31
+ async login(username, password) {
63
32
  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();
90
- }
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");
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
+ });
108
37
 
109
- const loginUrl = this.findCognitoLoginUrl(resp.data) || finalUrl;
110
- return { loginUrl, csrf };
111
- }
38
+ // Follow redirect to Cognito
39
+ let finalUrl = resp.request.res.responseUrl;
40
+ if (!finalUrl.includes('.amazoncognito.com')) {
41
+ throw new Error('Unexpected redirect URL: ' + finalUrl);
42
+ }
43
+
44
+ // Extract CSRF token from HTML
45
+ const csrfToken = this.extractCsrfToken(resp.data);
46
+ if (!csrfToken) throw new Error('CSRF token not found');
47
+
48
+ // 2. POST credentials to Cognito
49
+ const loginData = qs.stringify({
50
+ _csrf: csrfToken,
51
+ username,
52
+ password,
53
+ cognitoAsfData: '', // minimal value, może być potrzebne pełne ASF
54
+ });
112
55
 
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
- );
139
- }
56
+ resp = await this.client.post(finalUrl, loginData, {
57
+ headers: {
58
+ 'Content-Type': 'application/x-www-form-urlencoded',
59
+ Origin: 'https://live-melcloudhome.auth.eu-west-1.amazoncognito.com',
60
+ Referer: finalUrl,
61
+ },
62
+ maxRedirects: 5,
63
+ });
140
64
 
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
- );
65
+ finalUrl = resp.request.res.responseUrl;
66
+
67
+ // Sprawdzenie czy udało się zalogować
68
+ if (
69
+ finalUrl.includes('melcloudhome.com/dashboard') &&
70
+ resp.status < 400
71
+ ) {
72
+ console.log('Authentication successful');
73
+ this.authenticated = true;
74
+ return true;
75
+ }
76
+
77
+ throw new Error('Authentication failed, final URL: ' + finalUrl);
78
+ } catch (err) {
79
+ throw new Error('Login error: ' + err.message);
162
80
  }
163
-
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
81
  }
169
82
 
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;
83
+ extractCsrfToken(html) {
84
+ const dom = new JSDOM(html);
85
+ const input = dom.window.document.querySelector('input[name="_csrf"]');
86
+ return input?.value || null;
179
87
  }
180
88
 
181
89
  async checkSession() {
182
90
  if (!this.authenticated) return false;
91
+
183
92
  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,
93
+ const resp = await this.client.get(`${BASE_URL}/api/user/context`, {
94
+ headers: { 'x-csrf': '1', Referer: `${BASE_URL}/dashboard` },
191
95
  });
192
96
  return resp.status === 200;
193
97
  } catch {
98
+ this.authenticated = false;
194
99
  return false;
195
100
  }
196
101
  }
197
102
 
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;
221
- }
103
+ getCookies() {
104
+ return this.jar.toJSON();
222
105
  }
223
106
  }
224
107
 
225
108
 
109
+
226
110
  export default MELCloudHomeAuth;
227
111
 
228
112