homebridge-melcloud-control 4.3.11-beta.10 → 4.3.11-beta.12

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.10",
4
+ "version": "4.3.11-beta.12",
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/constants.js CHANGED
@@ -23,7 +23,7 @@ export const ApiUrlsHome = {
23
23
  GetConfiguration: "https://melcloudhome.com/api/configuration",
24
24
  GetUserContext: "/api/user/context",
25
25
  GetUserScenes: "/api/user/scenes",
26
- PostSchedule: " /api/cloudschedule/deviceid", // POST {"days":[2],"time":"17:59:00","enabled":true,"id":"53c5e804-0663-47d0-85c2-2d8ccd2573de","power":false,"operationMode":null,"setPoint":null,"vaneVerticalDirection":null,"vaneHorizontalDirection":null,"setFanSpeed":null}
26
+ PostSchedule: "/api/cloudschedule/deviceid", // POST {"days":[2],"time":"17:59:00","enabled":true,"id":"53c5e804-0663-47d0-85c2-2d8ccd2573de","power":false,"operationMode":null,"setPoint":null,"vaneVerticalDirection":null,"vaneHorizontalDirection":null,"setFanSpeed":null}
27
27
  PostProtectionFrost: "/api/protection/frost", // POST {"enabled":true,"min":13,"max":16,"units":{"ATA":["ef333525-2699-4290-af5a-2922566676da"]}}
28
28
  PostProtectionOverheat: "/api/protection/overheat", // POST {"enabled":true,"min":32,"max":35,"units":{"ATA":["ef333525-2699-4290-af5a-2922566676da"]}}
29
29
  PostHolidayMode: " /api/holidaymode", // POST {"enabled":true,"startDate":"2025-11-11T17:42:24.913","endDate":"2026-06-01T09:18:00","units":{"ATA":["ef333525-2699-4290-af5a-2922566676da"]}}
@@ -35,6 +35,7 @@ export const ApiUrlsHome = {
35
35
  Enable: "/api/scene/sceneid/enable",
36
36
  Disable: "/api/scene/sceneid/disable",
37
37
  },
38
+ DeleteSchedule: "/api/cloudschedule/deviceid/scheduleid",
38
39
  Referers: {
39
40
  GetPutScenes: "https://melcloudhome.com/scenes",
40
41
  PostHolidayMode: "https://melcloudhome.com/ata/deviceid/holidaymode",
package/src/functions.js CHANGED
@@ -58,35 +58,52 @@ class Functions extends EventEmitter {
58
58
  let chromiumPath = '/usr/bin/chromium-browser';
59
59
 
60
60
  try {
61
+ // Detect OS
61
62
  const { stdout: osOut } = await execPromise('uname -s');
62
63
  const osName = osOut.trim();
63
64
  if (this.logDebug) this.emit('debug', `Detected OS: ${osName}`);
64
65
 
66
+ // Detect Architecture
65
67
  const { stdout: archOut } = await execPromise('uname -m');
66
68
  const arch = archOut.trim();
67
69
  if (this.logDebug) this.emit('debug', `Detected architecture: ${arch}`);
68
70
 
69
71
  // Docker detection
70
72
  let isDocker = false;
71
- try { await access('/.dockerenv', fs.constants.F_OK); isDocker = true; } catch { }
73
+ try {
74
+ await access('/.dockerenv', fs.constants.F_OK);
75
+ isDocker = true;
76
+ } catch { }
77
+
72
78
  try {
73
79
  const { stdout } = await execPromise('cat /proc/1/cgroup || true');
74
80
  if (stdout.includes('docker') || stdout.includes('containerd')) isDocker = true;
75
81
  } catch { }
76
- if (isDocker && this.logDebug) this.emit('debug', 'Running inside Docker container.');
82
+ if (isDocker && this.logDebug) this.emit('debug', 'Running inside Docker container');
77
83
 
78
84
  // macOS
79
85
  if (osName === 'Darwin') {
80
86
  chromiumPath = '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome';
81
- try { await access(chromiumPath, fs.constants.X_OK); return chromiumPath; } catch { return null; }
87
+ try {
88
+ await access(chromiumPath, fs.constants.X_OK);
89
+ return chromiumPath;
90
+ } catch {
91
+ return null;
92
+ }
82
93
  }
83
94
 
84
95
  // ARM
85
96
  if (arch.startsWith('arm') || arch.startsWith('aarch')) {
86
- try { await access(chromiumPath, fs.constants.X_OK); return chromiumPath; }
87
- catch {
88
- try { await execPromise('sudo apt-get update -y && sudo apt-get install -y chromium-browser chromium-codecs-ffmpeg'); return chromiumPath; }
89
- catch { return null; }
97
+ try {
98
+ await access(chromiumPath, fs.constants.X_OK);
99
+ return chromiumPath;
100
+ } catch {
101
+ try {
102
+ await execPromise('sudo apt-get update -y && sudo apt-get install -y chromium-browser chromium-codecs-ffmpeg');
103
+ return chromiumPath;
104
+ } catch {
105
+ return null;
106
+ }
90
107
  }
91
108
  }
92
109
 
@@ -100,7 +117,11 @@ class Functions extends EventEmitter {
100
117
 
101
118
  // Entware (QNAP)
102
119
  let entwareExists = false;
103
- try { await access('/opt/bin/opkg', fs.constants.X_OK); entwareExists = true; } catch { }
120
+ try {
121
+ await access('/opt/bin/opkg', fs.constants.X_OK);
122
+ entwareExists = true;
123
+ } catch { }
124
+
104
125
  if (entwareExists) {
105
126
  try {
106
127
  await execPromise('/opt/bin/opkg update');
@@ -115,7 +136,12 @@ class Functions extends EventEmitter {
115
136
  'apk add --no-cache nspr nss libx11 libxcomposite libxdamage libxrandr atk cups libdrm libgbm alsa-lib',
116
137
  'yum install -y nspr nss libX11 libXcomposite libXdamage libXrandr atk cups libdrm libgbm alsa-lib'
117
138
  ];
118
- for (const cmd of depCommands) { try { await execPromise(`sudo ${cmd}`); } catch { } }
139
+
140
+ for (const cmd of depCommands) {
141
+ try {
142
+ await execPromise(`sudo ${cmd}`);
143
+ } catch { }
144
+ }
119
145
 
120
146
  process.env.LD_LIBRARY_PATH = `/usr/lib:/usr/lib64:${process.env.LD_LIBRARY_PATH || ''}`;
121
147
  return systemChromium;
@@ -123,7 +149,6 @@ class Functions extends EventEmitter {
123
149
 
124
150
  if (this.logDebug) this.emit('debug', `Unsupported OS: ${osName}`);
125
151
  return null;
126
-
127
152
  } catch (error) {
128
153
  if (this.logError) this.emit('error', `Chromium detection/install error: ${error.message}`);
129
154
  return null;
@@ -324,7 +324,7 @@ class MelCloudAta extends EventEmitter {
324
324
  //sens payload
325
325
  headers['Content-Type'] = 'application/json; charset=utf-8';
326
326
  headers.Origin = ApiUrlsHome.Origin;
327
- if (this.logDebug) this.emit('debug', `Send data: ${JSON.stringify(payload, null, 2)}`);
327
+ if (!this.logDebug) this.emit('debug', `Send data: ${JSON.stringify(payload, null, 2)}`);
328
328
 
329
329
  await axios(path, {
330
330
  method: method,
@@ -339,7 +339,6 @@ class MelCloudAta extends EventEmitter {
339
339
  return;
340
340
  }
341
341
  } catch (error) {
342
- if (error.response?.status === 500) return true; // Return 500 for schedule hovewer working correct
343
342
  throw new Error(`Send data error: ${error.message}`);
344
343
  }
345
344
  }
@@ -311,7 +311,6 @@ class MelCloudAtw extends EventEmitter {
311
311
  return;
312
312
  }
313
313
  } catch (error) {
314
- if (error.response?.status === 500) return true; // Return 500 for schedule hovewer working correct
315
314
  throw new Error(`Send data error: ${error.message}`);
316
315
  }
317
316
  }
@@ -315,7 +315,6 @@ class MelCloudErv extends EventEmitter {
315
315
  return;
316
316
  }
317
317
  } catch (error) {
318
- if (error.response?.status === 500) return true; // Return 500 for schedule hovewer working correct
319
318
  throw new Error(`Send data error: ${error.message}`);
320
319
  }
321
320
  }
@@ -5,6 +5,7 @@ import { exec } from 'child_process';
5
5
  import { promisify } from 'util';
6
6
  import EventEmitter from 'events';
7
7
  import puppeteer from 'puppeteer';
8
+ import { MELCloudHomeAuth } from "./melcloudhomeauth.js";
8
9
  import ImpulseGenerator from './impulsegenerator.js';
9
10
  import Functions from './functions.js';
10
11
  import { ApiUrlsHome, LanguageLocaleMap } from './constants.js';
@@ -267,7 +268,7 @@ class MelCloudHome extends EventEmitter {
267
268
  }
268
269
  }
269
270
 
270
- async connect() {
271
+ async connect1() {
271
272
  if (this.logDebug) this.emit('debug', 'Connecting to MELCloud Home');
272
273
  const GLOBAL_TIMEOUT = 90000;
273
274
 
@@ -341,8 +342,8 @@ class MelCloudHome extends EventEmitter {
341
342
  hash = params.get('hash');
342
343
  if (this.logDebug) this.emit('debug', `MelCloudHome WS hash detected: ${hash}`);
343
344
  }
344
- } catch (err) {
345
- this.emit('error', `CDP WebSocketCreated handler error: ${err.message}`);
345
+ } catch (error) {
346
+ if (this.logError) this.emit('error', `CDP WebSocketCreated handler error: ${error.message}`);
346
347
  }
347
348
  });
348
349
 
@@ -453,6 +454,31 @@ class MelCloudHome extends EventEmitter {
453
454
  }
454
455
  }
455
456
  }
457
+
458
+ async connect() {
459
+ if (this.logDebug) this.emit('debug', 'Connecting to MELCloud Home');
460
+ try {
461
+ const auth = new MELCloudHomeAuth();
462
+
463
+ await auth.login(this.user, this.passwd);
464
+
465
+ console.log("Logged in!");
466
+
467
+ const client = auth.getClient();
468
+
469
+ const ctx = await client.get("https://melcloudhome.com/api/user/context", {
470
+ headers: {
471
+ Accept: "application/json",
472
+ "x-csrf": "1",
473
+ referer: "https://melcloudhome.com/dashboard",
474
+ },
475
+ });
476
+
477
+ console.log(ctx.data);
478
+ } catch (error) {
479
+ throw new Error(`Connect error: ${error.message}`);
480
+ }
481
+ }
456
482
  }
457
483
 
458
484
  export default MelCloudHome;
@@ -0,0 +1,180 @@
1
+ import axios from "axios";
2
+ import { wrapper } from "axios-cookiejar-support";
3
+ import { CookieJar } from "tough-cookie";
4
+ import { JSDOM } from "jsdom";
5
+
6
+ const USER_AGENT =
7
+ "Mozilla/5.0 (Macintosh; Intel Mac OS X 14_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36";
8
+
9
+ const BASE_URL = "https://melcloudhome.com";
10
+ const COGNITO_BASE =
11
+ "https://live-melcloudhome.auth.eu-west-1.amazoncognito.com";
12
+
13
+ export class MELCloudHomeAuth {
14
+ constructor() {
15
+ this.jar = new CookieJar();
16
+ this.client = wrapper(
17
+ axios.create({
18
+ jar: this.jar,
19
+ timeout: 30000,
20
+ maxRedirects: 10,
21
+ headers: {
22
+ "User-Agent": USER_AGENT,
23
+ Accept:
24
+ "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8",
25
+ "Accept-Language": "en-US,en;q=0.9",
26
+ "sec-ch-ua":
27
+ '"Chromium";v="124", "Google Chrome";v="124", "Not A(Brand";v="99"',
28
+ "sec-ch-ua-mobile": "?0",
29
+ "sec-ch-ua-platform": '"macOS"',
30
+ },
31
+ })
32
+ );
33
+
34
+ this.authenticated = false;
35
+ }
36
+
37
+ //
38
+ // 1. Start OAuth login → redirected to AWS Cognito login page
39
+ //
40
+ async startLoginFlow() {
41
+ const resp = await this.client.get(`${BASE_URL}/bff/login`, {
42
+ params: { returnUrl: "/dashboard" },
43
+ });
44
+
45
+ const finalUrl = resp.request.res.responseUrl;
46
+
47
+ if (!finalUrl.includes("amazoncognito.com/login")) {
48
+ throw new Error(`Unexpected redirect: ${finalUrl}`);
49
+ }
50
+
51
+ const html = resp.data;
52
+ const csrf = this.extractCsrf(html);
53
+
54
+ if (!csrf) {
55
+ throw new Error("Failed to extract CSRF token.");
56
+ }
57
+
58
+ return { loginUrl: finalUrl, csrf };
59
+ }
60
+
61
+ //
62
+ // 2. Submit credentials to Cognito
63
+ //
64
+ async submitCredentials(loginUrl, csrf, username, password) {
65
+ const payload = new URLSearchParams({
66
+ _csrf: csrf,
67
+ username,
68
+ password,
69
+ cognitoAsfData: "",
70
+ });
71
+
72
+ const resp = await this.client.post(loginUrl, payload, {
73
+ headers: {
74
+ "Content-Type": "application/x-www-form-urlencoded",
75
+ Origin: COGNITO_BASE,
76
+ Referer: loginUrl,
77
+ },
78
+ maxRedirects: 10,
79
+ validateStatus: () => true,
80
+ });
81
+
82
+ const finalUrl = resp.request.res.responseUrl;
83
+
84
+ if (finalUrl.includes("/error")) {
85
+ throw new Error("Authentication failed: error returned from Cognito");
86
+ }
87
+
88
+ if (finalUrl.includes("amazoncognito.com")) {
89
+ throw new Error("Authentication failed: invalid username or password");
90
+ }
91
+
92
+ // Success: redirected back to melcloudhome.com
93
+ if (finalUrl.includes("melcloudhome.com")) {
94
+ this.authenticated = true;
95
+ return true;
96
+ }
97
+
98
+ throw new Error(`Unexpected final redirect: ${finalUrl}`);
99
+ }
100
+
101
+ //
102
+ // 3. Validate session – equivalent to Python check_session()
103
+ //
104
+ async checkSession() {
105
+ if (!this.authenticated) return false;
106
+
107
+ try {
108
+ const resp = await this.client.get(`${BASE_URL}/api/user/context`, {
109
+ headers: {
110
+ Accept: "application/json",
111
+ "x-csrf": "1",
112
+ referer: `${BASE_URL}/dashboard`,
113
+ },
114
+ validateStatus: () => true,
115
+ });
116
+
117
+ if (resp.status === 200) return true;
118
+ if (resp.status === 401) return false;
119
+
120
+ return false;
121
+ } catch {
122
+ return false;
123
+ }
124
+ }
125
+
126
+ //
127
+ // 4. Main login() method
128
+ //
129
+ async login(username, password) {
130
+ const step1 = await this.startLoginFlow();
131
+
132
+ await this.submitCredentials(
133
+ step1.loginUrl,
134
+ step1.csrf,
135
+ username,
136
+ password
137
+ );
138
+
139
+ // Wait for Blazor session init – same as Python sleep(3)
140
+ await new Promise((r) => setTimeout(r, 3000));
141
+
142
+ const ok = await this.checkSession();
143
+ if (!ok) throw new Error("Session not valid after login");
144
+
145
+ return true;
146
+ }
147
+
148
+ //
149
+ // Extract CSRF from Cognito HTML
150
+ //
151
+ extractCsrf(html) {
152
+ const dom = new JSDOM(html);
153
+ const input = dom.window.document.querySelector('input[name="_csrf"]');
154
+ return input?.value || null;
155
+ }
156
+
157
+ //
158
+ // Return axios instance for authenticated calls
159
+ //
160
+ getClient() {
161
+ if (!this.authenticated) {
162
+ throw new Error("Not authenticated. Call login() first.");
163
+ }
164
+ return this.client;
165
+ }
166
+
167
+ //
168
+ // Logout
169
+ //
170
+ async logout() {
171
+ try {
172
+ await this.client.get(`${BASE_URL}/bff/logout`);
173
+ } catch (_) { }
174
+
175
+ this.authenticated = false;
176
+ }
177
+ }
178
+
179
+ export default MELCloudHomeAuth;
180
+