homebridge-melcloud-control 4.0.0-beta.4 → 4.0.0-beta.41

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.
@@ -201,6 +201,34 @@
201
201
  }
202
202
  ]
203
203
  },
204
+ "displayType": {
205
+ "title": "Account Type",
206
+ "type": "string",
207
+ "minimum": 0,
208
+ "maximum": 2,
209
+ "default": 1,
210
+ "description": "Here select the language used in MELCloud account.",
211
+ "oneOf": [
212
+ {
213
+ "title": "None/Disabled",
214
+ "enum": [
215
+ "0"
216
+ ]
217
+ },
218
+ {
219
+ "title": "MELCLoud",
220
+ "enum": [
221
+ "1"
222
+ ]
223
+ },
224
+ {
225
+ "title": "MELCLoud Home",
226
+ "enum": [
227
+ "2"
228
+ ]
229
+ }
230
+ ]
231
+ },
204
232
  "ataDevices": {
205
233
  "title": "Devices ATA",
206
234
  "type": "array",
@@ -1798,7 +1826,8 @@
1798
1826
  "name",
1799
1827
  "user",
1800
1828
  "passwd",
1801
- "language"
1829
+ "language",
1830
+ "displayType"
1802
1831
  ]
1803
1832
  }
1804
1833
  }
@@ -1817,6 +1846,7 @@
1817
1846
  "type": "password"
1818
1847
  },
1819
1848
  "accounts[].language",
1849
+ "accounts[].displayType",
1820
1850
  {
1821
1851
  "key": "accounts[]",
1822
1852
  "type": "tabarray",
@@ -76,6 +76,15 @@
76
76
  </select>
77
77
  </div>
78
78
 
79
+ <div class="mb-2">
80
+ <label for="displayType" class="form-label">Account Type</label>
81
+ <select id="displayType" name="displayType" class="form-control">
82
+ <option value="0">None/Disabled</option>
83
+ <option value="1">MELCloud</option>
84
+ <option value="2">MELCloud Home</option>
85
+ </select>
86
+ </div>
87
+
79
88
  <div class="text-center">
80
89
  <button id="logIn" type="button" class="btn btn-secondary">Connect to MELCloud</button>
81
90
  <button id="configButton" type="button" class="btn btn-secondary"><i class="fas fa-gear"></i></button>
@@ -119,7 +128,8 @@
119
128
  document.getElementById('user').value = acc.user || '';
120
129
  document.getElementById('passwd').value = acc.passwd || '';
121
130
  document.getElementById('language').value = acc.language || '';
122
- document.getElementById('logIn').disabled = !(acc.name && acc.user && acc.passwd && acc.language);
131
+ document.getElementById('displayType').value = acc.displayType || '';
132
+ document.getElementById('logIn').disabled = !(acc.name && acc.user && acc.passwd && acc.language && acc.displayType);
123
133
  this.deviceIndex = i;
124
134
  });
125
135
 
@@ -134,8 +144,9 @@
134
144
  acc.user = document.querySelector('#user').value;
135
145
  acc.passwd = document.querySelector('#passwd').value;
136
146
  acc.language = document.querySelector('#language').value;
147
+ acc.displayType = document.querySelector('#displayType').value;
137
148
 
138
- document.getElementById('logIn').disabled = !(acc.name && acc.user && acc.passwd && acc.language);
149
+ document.getElementById('logIn').disabled = !(acc.name && acc.user && acc.passwd && acc.languaged && acc.displayType);
139
150
 
140
151
  await homebridge.updatePluginConfig(pluginConfig);
141
152
  await homebridge.savePluginConfig(pluginConfig);
@@ -192,7 +203,7 @@
192
203
 
193
204
  try {
194
205
  const acc = pluginConfig[0].accounts[this.deviceIndex];
195
- const payload = { accountName: acc.name, user: acc.user, passwd: acc.passwd, language: acc.language };
206
+ const payload = { accountName: acc.name, user: acc.user, passwd: acc.passwd, language: acc.language, displayType: acc.displayType };
196
207
  const devicesInMelCloud = await homebridge.request('/connect', payload);
197
208
 
198
209
  // Initialize devices arrays
@@ -17,10 +17,11 @@ class PluginUiServer extends HomebridgePluginUiServer {
17
17
  const user = payload.user;
18
18
  const passwd = payload.passwd;
19
19
  const language = payload.language;
20
+ const displayType = payload.displayType;
20
21
  const accountFile = `${this.homebridgeStoragePath}/melcloud/${accountName}_Account`;
21
22
  const buildingsFile = `${this.homebridgeStoragePath}/melcloud/${accountName}_Buildings`;
22
23
  const devicesFile = `${this.homebridgeStoragePath}/melcloud/${accountName}_Devices`;
23
- const melCloud = new MelCloud(user, passwd, language, accountFile, buildingsFile, devicesFile, false, true);
24
+ const melCloud = new MelCloud(displayType, user, passwd, language, accountFile, buildingsFile, devicesFile, false, true);
24
25
 
25
26
  try {
26
27
  const response = await melCloud.connect();
package/index.js CHANGED
@@ -30,6 +30,9 @@ class MelCloudPlatform {
30
30
  api.on('didFinishLaunching', async () => {
31
31
  //loop through accounts
32
32
  for (const account of config.accounts) {
33
+ const displayType = account.displayType || 1;
34
+ if (displayType === 0) continue;
35
+
33
36
  const accountName = account.name;
34
37
  const user = account.user;
35
38
  const passwd = account.passwd;
@@ -64,7 +67,7 @@ class MelCloudPlatform {
64
67
  passwd: 'removed',
65
68
  mqtt: {
66
69
  auth: {
67
- ...device.mqtt?.auth,
70
+ ...account.mqtt?.auth,
68
71
  passwd: 'removed',
69
72
  }
70
73
  },
@@ -76,7 +79,6 @@ class MelCloudPlatform {
76
79
  const accountFile = `${prefDir}/${accountName}_Account`;
77
80
  const buildingsFile = `${prefDir}/${accountName}_Buildings`;
78
81
  const devicesFile = `${prefDir}/${accountName}_Devices`;
79
- const coociesFile = `${prefDir}/${accountName}_Coocies`;
80
82
 
81
83
 
82
84
  //set account refresh interval
@@ -88,17 +90,13 @@ class MelCloudPlatform {
88
90
  .on('start', async () => {
89
91
  try {
90
92
  //melcloud account
91
- const melCloud = new MelCloud(user, passwd, language, accountFile, buildingsFile, devicesFile, coociesFile, logLevel.warn, logLevel.debug, false)
93
+ const melCloud = new MelCloud(displayType, user, passwd, language, accountFile, buildingsFile, devicesFile, logLevel.warn, logLevel.debug, false)
92
94
  .on('success', (msg) => logLevel.success && log.success(`${accountName}, ${msg}`))
93
95
  .on('info', (msg) => logLevel.info && log.info(`${accountName}, ${msg}`))
94
96
  .on('debug', (msg) => logLevel.debug && log.info(`${accountName}, debug: ${msg}`))
95
97
  .on('warn', (msg) => logLevel.warn && log.warn(`${accountName}, ${msg}`))
96
98
  .on('error', (msg) => logLevel.error && log.error(`${accountName}, ${msg}`));
97
99
 
98
- await melCloud.connectHomeCoocies()
99
- return;
100
-
101
-
102
100
  //connect
103
101
  let response;
104
102
  try {
@@ -109,10 +107,10 @@ class MelCloudPlatform {
109
107
  }
110
108
 
111
109
  const accountInfo = response.accountInfo ?? false;
112
- const contextKey = response.contextKey ?? false;
110
+ const contextKey = response.contextKey;
113
111
  const useFahrenheit = response.useFahrenheit ?? false;
114
112
 
115
- if (contextKey === false) {
113
+ if (!contextKey) {
116
114
  return;
117
115
  }
118
116
 
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.4",
4
+ "version": "4.0.0-beta.41",
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
@@ -7,15 +7,15 @@ import Functions from './functions.js';
7
7
  import { ApiUrls, ApiUrlsHome } from './constants.js';
8
8
 
9
9
  class MelCloud extends EventEmitter {
10
- constructor(user, passwd, language, accountFile, buildingsFile, devicesFile, coociesFile, logWarn, logDebug, requestConfig) {
10
+ constructor(displayType, user, passwd, language, accountFile, buildingsFile, devicesFile, logWarn, logDebug, requestConfig) {
11
11
  super();
12
+ this.displayType = displayType;
12
13
  this.user = user;
13
14
  this.passwd = passwd;
14
15
  this.language = language;
15
16
  this.accountFile = accountFile;
16
17
  this.buildingsFile = buildingsFile;
17
18
  this.devicesFile = devicesFile;
18
- this.coociesFile = coociesFile;
19
19
  this.logWarn = logWarn;
20
20
  this.logDebug = logDebug;
21
21
  this.requestConfig = requestConfig;
@@ -58,7 +58,7 @@ class MelCloud extends EventEmitter {
58
58
  }
59
59
  }
60
60
 
61
- async checkDevicesList(contextKey) {
61
+ async checkMelcloudDevicesList(contextKey) {
62
62
  try {
63
63
  const axiosInstanceGet = axios.create({
64
64
  method: 'GET',
@@ -109,7 +109,7 @@ class MelCloud extends EventEmitter {
109
109
  }
110
110
  }
111
111
 
112
- async connect() {
112
+ async connectToMelCloud() {
113
113
  if (this.logDebug) this.emit('debug', `Connecting to MELCloud`);
114
114
 
115
115
  try {
@@ -156,41 +156,22 @@ class MelCloud extends EventEmitter {
156
156
 
157
157
  this.emit('success', `Connect to MELCloud Success`);
158
158
 
159
- return {
160
- accountInfo,
161
- contextKey,
162
- useFahrenheit
163
- };
159
+ return { accountInfo, contextKey, useFahrenheit };
164
160
  } catch (error) {
165
161
  throw new Error(`Connect to MELCloud error: ${error.message}`);
166
162
  }
167
163
  }
168
164
 
169
- async connectHome(cookieC1, cookieC2) {
170
- if (this.logDebug) this.emit('debug', `Connecting to MELCloud Home`);
171
-
165
+ async checkMelcloudHomeDevicesList(contextKey) {
172
166
  try {
173
- const c1 = cookieC1.trim();
174
- const c2 = cookieC2.trim();
175
-
176
- const cookieString = [
177
- '__Secure-monitorandcontrol=chunks-2',
178
- `__Secure-monitorandcontrolC1=${c1}`,
179
- `__Secure-monitorandcontrolC2=${c2}`,
180
- ].join('; ');
181
-
182
167
  const axiosInstance = axios.create({
168
+ method: 'GET',
183
169
  baseURL: ApiUrlsHome.BaseURL,
184
- timeout: 10000,
185
- httpsAgent: new Agent({
186
- keepAlive: false,
187
- rejectUnauthorized: false
188
- }),
189
170
  headers: {
190
171
  'Accept': '*/*',
191
172
  'Accept-Language': 'en-US,en;q=0.9',
192
- 'Cookie': cookieString,
193
- 'User-Agent': 'homebridge-melcloud-home/0.2.0',
173
+ 'Cookie': contextKey,
174
+ 'User-Agent': 'homebridge-melcloud-control/4.0.0',
194
175
  'DNT': '1',
195
176
  'Origin': 'https://melcloudhome.com',
196
177
  'Referer': 'https://melcloudhome.com/dashboard',
@@ -198,96 +179,132 @@ class MelCloud extends EventEmitter {
198
179
  'Sec-Fetch-Mode': 'cors',
199
180
  'Sec-Fetch-Site': 'same-origin',
200
181
  'X-CSRF': '1'
201
- }
182
+ },
183
+ ...this.axiosDefaults
202
184
  });
203
185
 
204
- const response = await axiosInstance.get(ApiUrlsHome.GetUserContext);
205
- const homeData = response.data;
206
- if (this.logDebug) this.emit('debug', `[MELCloudHome] Response data: ${JSON.stringify(homeData, null, 2)}`);
186
+ if (this.logDebug) this.emit('debug', `Scanning for devices`);
187
+ const listDevicesData = await axiosInstance(ApiUrlsHome.GetUserContext);
188
+ const buildingsList = listDevicesData.data.buildings;
189
+ if (this.logDebug) this.emit('debug', `Buildings: ${JSON.stringify(buildingsList, null, 2)}`);
207
190
 
208
- this.emit('success', `Connect to MELCloud Home Success`);
191
+ if (!buildingsList) {
192
+ if (this.logWarn) this.emit('warn', `No building found`);
193
+ return null;
194
+ }
195
+
196
+ await this.functions.saveData(this.buildingsFile, buildingsList);
197
+ if (this.logDebug) this.emit('debug', `Buildings list saved`);
198
+
199
+ const devices = buildingsList.flatMap(building => [
200
+ ...(building.airToAirUnits || []),
201
+ ...(building.airToWaterUnits || [])
202
+ ]);
209
203
 
210
- return
204
+ const devicesCount = devices.length;
205
+ if (devicesCount === 0) {
206
+ if (this.logWarn) this.emit('warn', `No devices found`);
207
+ return null;
208
+ }
209
+
210
+ await this.functions.saveData(this.devicesFile, devices);
211
+ if (this.logDebug) this.emit('debug', `${devicesCount} devices saved`);
212
+
213
+ return devices;
211
214
  } catch (error) {
212
215
  throw new Error(`Connect to MELCloud Home error: ${error.message}`);
213
216
  }
214
217
  }
215
218
 
216
- async connectHomeCoocies() {
217
- const loginUrl = new URL('https://live-melcloudhome.auth.eu-west-1.amazoncognito.com/login');
218
- loginUrl.searchParams.set('client_id', '3g4d5l5kivuqi7oia68gib7uso');
219
- loginUrl.searchParams.set('redirect_uri', 'https://auth.melcloudhome.com/signin-oidc-meu');
220
- loginUrl.searchParams.set('response_type', 'code');
221
- loginUrl.searchParams.set('scope', 'openid profile');
222
- loginUrl.searchParams.set('response_mode', 'form_post');
219
+ async connectToMelCloudHome() {
220
+ if (this.logDebug) this.emit('debug', `Connecting to MELCloud Home`);
221
+
222
+ const browser = await puppeteer.launch({ headless: true, args: ['--no-sandbox', '--disable-setuid-sandbox'] });
223
+ const page = await browser.newPage();
224
+
225
+ try {
226
+ // Open MELCloud Home
227
+ await page.goto(ApiUrlsHome.BaseURL, { waitUntil: 'networkidle2' });
228
+ const buttons = await page.$$('button.btn--blue');
229
+ let loginBtn = null;
230
+ for (const btn of buttons) {
231
+ const text = await page.evaluate(el => el.textContent, btn);
232
+ if (text.trim() === 'Zaloguj' || text.trim() === 'Log In') {
233
+ loginBtn = btn;
234
+ break;
235
+ }
236
+ }
223
237
 
238
+ if (!loginBtn && this.logWarn) this.emit('warn', `Login button not found`);
224
239
 
240
+ // Set credentials and login
241
+ await Promise.all([loginBtn.click(), page.waitForNavigation({ waitUntil: 'networkidle2', timeout: 20000 })]);
242
+ await page.waitForSelector('input[name="username"]', { timeout: 15000 });
243
+ await page.type('input[name="username"]', this.user, { delay: 50 });
244
+ await page.type('input[name="password"]', this.passwd, { delay: 50 });
225
245
 
226
- const browser = await puppeteer.launch({
227
- headless: true,
228
- args: ['--no-sandbox', '--disable-setuid-sandbox']
229
- });
246
+ const button1 = await page.$('input[type="submit"]');
247
+ await Promise.all([button1.click(), page.waitForNavigation({ waitUntil: 'networkidle2', timeout: 20000 })]);
230
248
 
231
- const page = await browser.newPage();
249
+ // Get cookies C1 and C2
250
+ let c1 = null, c2 = null;
251
+ const start = Date.now();
232
252
 
233
- // Otwórz stronę logowania
234
- this.emit('warn', `Opening login page...`);
235
- await page.goto(loginUrl, { waitUntil: 'networkidle2' });
236
-
237
- // Wpisz login i hasło
238
- this.emit('warn', `Typing credentials...`);
239
- await page.type('input[name="username"]', this.user, { delay: 50 });
240
- await page.type('input[name="password"]', this.passwd, { delay: 50 });
241
-
242
- // Sprawdź kilka możliwych wariantów
243
- const buttonSelectors = [
244
- 'button[type="submit"]',
245
- 'input[type="submit"]',
246
- 'button[name="signIn"]',
247
- 'button.btn-primary',
248
- 'button:has-text("Sign in")',
249
- 'button:has-text("Zaloguj")'
250
- ];
251
-
252
- // Kliknij "Sign in" (przycisk AWS Cognito)
253
- let buttonFound = false;
254
- for (const selector of buttonSelectors) {
255
- const button = await page.$(selector);
256
- if (button) {
257
- console.log(`Found submit button: ${selector}`);
258
- await Promise.all([
259
- button.click(),
260
- page.waitForNavigation({ waitUntil: 'networkidle2', timeout: 15000 })
261
- ]);
262
- buttonFound = true;
263
- break;
253
+ // Loop max 20s
254
+ while ((!c1 || !c2) && Date.now() - start < 20000) {
255
+ const cookies = await page.cookies();
256
+ c1 = cookies.find(c => c.name === '__Secure-monitorandcontrolC1')?.value || c1;
257
+ c2 = cookies.find(c => c.name === '__Secure-monitorandcontrolC2')?.value || c2;
258
+ if (!c1 || !c2) await new Promise(r => setTimeout(r, 500));
264
259
  }
265
- }
266
-
267
- if (!buttonFound) {
268
- throw new Error('❌ Could not find login button on the page.');
269
- }
270
260
 
271
- // Poczekaj załaduje się MELCloud Home
272
- this.emit('warn', `Waiting for redirect to melcloudhome.com...`);
273
- await new Promise(resolve => setTimeout(resolve, 5000));
261
+ if (!c1 || !c2) {
262
+ if (this.logWarn) this.emit('warn', `Cookies C1/C2 missing`);
263
+ return null;
264
+ }
274
265
 
275
- // Pobierz cookies dla domeny melcloudhome.com
276
- const cookies = await page.cookies();
266
+ const accountInfo = {};
267
+ const contextKey = ['__Secure-monitorandcontrol=chunks-2', `__Secure-monitorandcontrolC1=${c1}`, `__Secure-monitorandcontrolC2=${c2}`,].join('; ');
268
+ const useFahrenheit = false;
269
+ this.contextKey = contextKey;
277
270
 
278
- // Znajdź __Secure-monitorandcontrolC1 i C2
279
- const c1 = cookies.find(c => c.name === '__Secure-monitorandcontrolC1')?.value || null;
280
- const c2 = cookies.find(c => c.name === '__Secure-monitorandcontrolC2')?.value || null;
281
271
 
282
- // Zapisz do pliku
283
- const data = { C1: c1, C2: c2, date: new Date().toISOString() };
284
- await this.functions.saveData(this.coociesFile, data);
272
+ this.emit('success', `Connect to MELCloud Home Success`);
285
273
 
286
- await browser.close();
274
+ return { accountInfo, contextKey, useFahrenheit };
275
+ } catch (error) {
276
+ throw new Error(`Connect to MELCloud Home error: ${error.message}`);
277
+ } finally {
278
+ await browser.close();
279
+ }
280
+ }
287
281
 
288
- this.emit('warn', `Login successful.`);
282
+ async connect() {
283
+ let response = null;
284
+ switch (this.displayType) {
285
+ case "1":
286
+ response = await this.connectToMelCloud();
287
+ return response
288
+ case "2":
289
+ response = await this.connectToMelCloudHome();
290
+ return response
291
+ default:
292
+ return null
293
+ }
294
+ }
289
295
 
290
- return false;
296
+ async checkDevicesList(contextKey) {
297
+ let devices = null;
298
+ switch (this.displayType) {
299
+ case "1":
300
+ devices = await this.checkMelcloudDevicesList(contextKey);
301
+ return devices
302
+ case "2":
303
+ devices = await this.checkMelcloudHomeDevicesList(contextKey);
304
+ return devices
305
+ default:
306
+ return null;
307
+ }
291
308
  }
292
309
 
293
310
  async send(accountInfo) {