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

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.40",
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,24 +156,18 @@ 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(cookies) {
172
166
  try {
173
- const c1 = cookieC1.trim();
174
- const c2 = cookieC2.trim();
167
+ const c1 = cookies.c1.trim();
168
+ const c2 = cookies.c2.trim();
175
169
 
176
- const cookieString = [
170
+ const cookie = [
177
171
  '__Secure-monitorandcontrol=chunks-2',
178
172
  `__Secure-monitorandcontrolC1=${c1}`,
179
173
  `__Secure-monitorandcontrolC2=${c2}`,
@@ -181,7 +175,7 @@ class MelCloud extends EventEmitter {
181
175
 
182
176
  const axiosInstance = axios.create({
183
177
  baseURL: ApiUrlsHome.BaseURL,
184
- timeout: 10000,
178
+ timeout: 20000,
185
179
  httpsAgent: new Agent({
186
180
  keepAlive: false,
187
181
  rejectUnauthorized: false
@@ -189,8 +183,8 @@ class MelCloud extends EventEmitter {
189
183
  headers: {
190
184
  'Accept': '*/*',
191
185
  'Accept-Language': 'en-US,en;q=0.9',
192
- 'Cookie': cookieString,
193
- 'User-Agent': 'homebridge-melcloud-home/0.2.0',
186
+ 'Cookie': cookie,
187
+ 'User-Agent': 'homebridge-melcloud-control/4.0.0',
194
188
  'DNT': '1',
195
189
  'Origin': 'https://melcloudhome.com',
196
190
  'Referer': 'https://melcloudhome.com/dashboard',
@@ -201,93 +195,127 @@ class MelCloud extends EventEmitter {
201
195
  }
202
196
  });
203
197
 
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)}`);
198
+ if (this.logDebug) this.emit('debug', `Scanning for devices`);
199
+ const listDevicesData = await axiosInstance.get(ApiUrlsHome.GetUserContext);
200
+ const buildingsList = listDevicesData.data.buildings;
201
+ if (this.logDebug) this.emit('debug', `Buildings: ${JSON.stringify(buildingsList, null, 2)}`);
202
+
203
+ if (!buildingsList) {
204
+ if (this.logWarn) this.emit('warn', `No building found`);
205
+ return null;
206
+ }
207
207
 
208
- this.emit('success', `Connect to MELCloud Home Success`);
208
+ await this.functions.saveData(this.buildingsFile, buildingsList);
209
+ if (this.logDebug) this.emit('debug', `Buildings list saved`);
210
+
211
+ const devices = buildingsList.flatMap(building => [
212
+ ...(building.airToAirUnits || []),
213
+ ...(building.airToWaterUnits || [])
214
+ ]);
215
+
216
+ const devicesCount = devices.length;
217
+ if (devicesCount === 0) {
218
+ if (this.logWarn) this.emit('warn', `No devices found`);
219
+ return null;
220
+ }
221
+
222
+ await this.functions.saveData(this.devicesFile, devices);
223
+ if (this.logDebug) this.emit('debug', `${devicesCount} devices saved`);
209
224
 
210
- return
225
+ return devices;
211
226
  } catch (error) {
212
227
  throw new Error(`Connect to MELCloud Home error: ${error.message}`);
213
228
  }
214
229
  }
215
230
 
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');
231
+ async connectToMelCloudHome() {
232
+ if (this.logDebug) this.emit('debug', `Connecting to MELCloud Home`);
223
233
 
234
+ const browser = await puppeteer.launch({ headless: true, args: ['--no-sandbox', '--disable-setuid-sandbox'] });
235
+ const page = await browser.newPage();
224
236
 
237
+ try {
238
+ // Open MELCloud Home
239
+ await page.goto(ApiUrlsHome.BaseURL, { waitUntil: 'networkidle2' });
240
+ const buttons = await page.$$('button.btn--blue');
241
+ let loginBtn = null;
242
+ for (const btn of buttons) {
243
+ const text = await page.evaluate(el => el.textContent, btn);
244
+ if (text.trim() === 'Zaloguj' || text.trim() === 'Log In') {
245
+ loginBtn = btn;
246
+ break;
247
+ }
248
+ }
225
249
 
226
- const browser = await puppeteer.launch({
227
- headless: true,
228
- args: ['--no-sandbox', '--disable-setuid-sandbox']
229
- });
250
+ if (!loginBtn && this.logWarn) this.emit('warn', `Login button not found`);
230
251
 
231
- const page = await browser.newPage();
252
+ // Set credentials and login
253
+ await Promise.all([loginBtn.click(), page.waitForNavigation({ waitUntil: 'networkidle2', timeout: 20000 })]);
254
+ await page.waitForSelector('input[name="username"]', { timeout: 15000 });
255
+ await page.type('input[name="username"]', this.user, { delay: 50 });
256
+ await page.type('input[name="password"]', this.passwd, { delay: 50 });
232
257
 
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;
264
- }
265
- }
258
+ const button1 = await page.$('input[type="submit"]');
259
+ await Promise.all([button1.click(), page.waitForNavigation({ waitUntil: 'networkidle2', timeout: 20000 })]);
266
260
 
267
- if (!buttonFound) {
268
- throw new Error('❌ Could not find login button on the page.');
269
- }
261
+ // Get cookies C1 and C2
262
+ let c1 = null, c2 = null;
263
+ const start = Date.now();
270
264
 
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));
265
+ // Loop max 20s
266
+ while ((!c1 || !c2) && Date.now() - start < 20000) {
267
+ const cookies = await page.cookies();
268
+ c1 = cookies.find(c => c.name === '__Secure-monitorandcontrolC1')?.value || c1;
269
+ c2 = cookies.find(c => c.name === '__Secure-monitorandcontrolC2')?.value || c2;
270
+ if (!c1 || !c2) await new Promise(r => setTimeout(r, 500));
271
+ }
274
272
 
275
- // Pobierz cookies dla domeny melcloudhome.com
276
- const cookies = await page.cookies();
273
+ if (!c1 || !c2) {
274
+ if (this.logWarn) this.emit('warn', `Cookies C1/C2 missing`);
275
+ return null;
276
+ }
277
277
 
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;
278
+ const accountInfo = {};
279
+ const contextKey = { c1, c2 };
280
+ const useFahrenheit = false;
281
+ this.contextKey = contextKey;
281
282
 
282
- // Zapisz do pliku
283
- const data = { C1: c1, C2: c2, date: new Date().toISOString() };
284
- await this.functions.saveData(this.coociesFile, data);
283
+ this.emit('success', `Connect to MELCloud Home Success`);
285
284
 
286
- await browser.close();
285
+ return { accountInfo, contextKey, useFahrenheit };
286
+ } catch (error) {
287
+ throw new Error(`Connect to MELCloud Home error: ${error.message}`);
288
+ } finally {
289
+ await browser.close();
290
+ }
291
+ }
287
292
 
288
- this.emit('warn', `Login successful.`);
293
+ async connect() {
294
+ let response = null;
295
+ switch (this.displayType) {
296
+ case "1":
297
+ response = await this.connectToMelCloud();
298
+ return response
299
+ case "2":
300
+ response = await this.connectToMelCloudHome();
301
+ return response
302
+ default:
303
+ return null
304
+ }
305
+ }
289
306
 
290
- return false;
307
+ async checkDevicesList(key) {
308
+ let devices = null;
309
+ switch (this.displayType) {
310
+ case "1":
311
+ devices = await this.checkMelcloudDevicesList(key);
312
+ return devices
313
+ case "2":
314
+ devices = await this.checkMelcloudHomeDevicesList(key);
315
+ return devices
316
+ default:
317
+ return null;
318
+ }
291
319
  }
292
320
 
293
321
  async send(accountInfo) {