homebridge-melcloud-control 4.2.3-beta.1 → 4.2.3-beta.11
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/homebridge-ui/server.js +2 -1
- package/index.js +3 -2
- package/package.json +1 -1
- package/src/deviceata.js +2 -4
- package/src/deviceatw.js +14 -9
- package/src/deviceerv.js +1 -3
- package/src/melcloud.js +18 -349
- package/src/melcloudata.js +5 -17
- package/src/melcloudatw.js +47 -66
- package/src/melclouderv.js +34 -53
- package/src/melcloudhome.js +364 -0
package/homebridge-ui/server.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { HomebridgePluginUiServer } from '@homebridge/plugin-ui-utils';
|
|
2
2
|
import MelCloud from '../src/melcloud.js';
|
|
3
|
+
import MelCloudHome from '../src/melcloudhome.js';
|
|
3
4
|
|
|
4
5
|
class PluginUiServer extends HomebridgePluginUiServer {
|
|
5
6
|
constructor() {
|
|
@@ -17,7 +18,7 @@ class PluginUiServer extends HomebridgePluginUiServer {
|
|
|
17
18
|
const accountFile = `${this.homebridgeStoragePath}/melcloud/${accountName}_Account`;
|
|
18
19
|
const buildingsFile = `${this.homebridgeStoragePath}/melcloud/${accountName}_Buildings`;
|
|
19
20
|
const devicesFile = `${this.homebridgeStoragePath}/melcloud/${accountName}_Devices`;
|
|
20
|
-
const melCloud = new MelCloud(account, accountFile, buildingsFile, devicesFile);
|
|
21
|
+
const melCloud = account.type === 'melcloud' ? new MelCloud(account, accountFile, buildingsFile, devicesFile) : new MelCloudHome(account, accountFile, buildingsFile, devicesFile);
|
|
21
22
|
|
|
22
23
|
try {
|
|
23
24
|
const accountInfo = await melCloud.connect();
|
package/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { join } from 'path';
|
|
2
2
|
import { mkdirSync, existsSync, writeFileSync } from 'fs';
|
|
3
3
|
import MelCloud from './src/melcloud.js';
|
|
4
|
+
import MelCloudHome from './src/melcloudhome.js';
|
|
4
5
|
import DeviceAta from './src/deviceata.js';
|
|
5
6
|
import DeviceAtw from './src/deviceatw.js';
|
|
6
7
|
import DeviceErv from './src/deviceerv.js';
|
|
@@ -84,8 +85,8 @@ class MelCloudPlatform {
|
|
|
84
85
|
.on('start', async () => {
|
|
85
86
|
try {
|
|
86
87
|
//melcloud account
|
|
87
|
-
const melCloud = new MelCloud(account, accountFile, buildingsFile, devicesFile, true)
|
|
88
|
-
|
|
88
|
+
const melCloud = account.type === 'melcloud' ? new MelCloud(account, accountFile, buildingsFile, devicesFile, true) : new MelCloudHome(account, accountFile, buildingsFile, devicesFile, true);
|
|
89
|
+
melCloud.on('success', (msg) => logLevel.success && log.success(`${accountName}, ${msg}`))
|
|
89
90
|
.on('info', (msg) => logLevel.info && log.info(`${accountName}, ${msg}`))
|
|
90
91
|
.on('debug', (msg) => logLevel.debug && log.info(`${accountName}, debug: ${msg}`))
|
|
91
92
|
.on('warn', (msg) => logLevel.warn && log.warn(`${accountName}, ${msg}`))
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"displayName": "MELCloud Control",
|
|
3
3
|
"name": "homebridge-melcloud-control",
|
|
4
|
-
"version": "4.2.3-beta.
|
|
4
|
+
"version": "4.2.3-beta.11",
|
|
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/deviceata.js
CHANGED
|
@@ -334,8 +334,6 @@ class DeviceAta extends EventEmitter {
|
|
|
334
334
|
return state;
|
|
335
335
|
})
|
|
336
336
|
.onSet(async (state) => {
|
|
337
|
-
if (!!state === this.accessory.power) return;
|
|
338
|
-
|
|
339
337
|
try {
|
|
340
338
|
deviceData.Device.Power = state ? true : false;
|
|
341
339
|
await this.melCloudAta.send(this.accountType, this.displayType, deviceData, AirConditioner.EffectiveFlags.Power);
|
|
@@ -1331,7 +1329,7 @@ class DeviceAta extends EventEmitter {
|
|
|
1331
1329
|
maxTempHeat: maxTempHeat,
|
|
1332
1330
|
minTempCoolDryAuto: minTempCoolDryAuto,
|
|
1333
1331
|
maxTempCoolDryAuto: maxTempCoolDryAuto,
|
|
1334
|
-
power: power
|
|
1332
|
+
power: power,
|
|
1335
1333
|
inStandbyMode: inStandbyMode,
|
|
1336
1334
|
operationMode: operationMode,
|
|
1337
1335
|
currentOperationMode: 0,
|
|
@@ -1427,7 +1425,7 @@ class DeviceAta extends EventEmitter {
|
|
|
1427
1425
|
|
|
1428
1426
|
//update characteristics
|
|
1429
1427
|
this.melCloudService
|
|
1430
|
-
?.updateCharacteristic(Characteristic.Active, power
|
|
1428
|
+
?.updateCharacteristic(Characteristic.Active, power)
|
|
1431
1429
|
.updateCharacteristic(Characteristic.CurrentHeaterCoolerState, obj.currentOperationMode)
|
|
1432
1430
|
.updateCharacteristic(Characteristic.TargetHeaterCoolerState, obj.targetOperationMode)
|
|
1433
1431
|
.updateCharacteristic(Characteristic.CurrentTemperature, roomTemperature)
|
package/src/deviceatw.js
CHANGED
|
@@ -340,8 +340,6 @@ class DeviceAtw extends EventEmitter {
|
|
|
340
340
|
return state;
|
|
341
341
|
})
|
|
342
342
|
.onSet(async (state) => {
|
|
343
|
-
if (!!state === this.accessory.power) return;
|
|
344
|
-
|
|
345
343
|
try {
|
|
346
344
|
switch (i) {
|
|
347
345
|
case 0: //Heat Pump
|
|
@@ -1340,8 +1338,15 @@ class DeviceAtw extends EventEmitter {
|
|
|
1340
1338
|
effectiveFlags = HeatPump.EffectiveFlags.Power + HeatPump.EffectiveFlags.OperationMode;
|
|
1341
1339
|
break;
|
|
1342
1340
|
case 3: //HOLIDAY
|
|
1343
|
-
|
|
1344
|
-
|
|
1341
|
+
if (this.accountType === 'melcloud') {
|
|
1342
|
+
deviceData.Device.HolidayMode = state;
|
|
1343
|
+
effectiveFlags = HeatPump.EffectiveFlags.HolidayMode;
|
|
1344
|
+
}
|
|
1345
|
+
|
|
1346
|
+
if (this.accountType === 'melcloudhome') {
|
|
1347
|
+
deviceData.Device.HolidayMode.Enabled = state;
|
|
1348
|
+
effectiveFlags = 'holidaymode';
|
|
1349
|
+
}
|
|
1345
1350
|
break;
|
|
1346
1351
|
case 10: //ALL ZONES PHYSICAL LOCK CONTROL
|
|
1347
1352
|
deviceData.Device.ProhibitZone1 = state;
|
|
@@ -1510,7 +1515,8 @@ class DeviceAtw extends EventEmitter {
|
|
|
1510
1515
|
const scheduleEnabled = deviceData.ScheduleEnabled;
|
|
1511
1516
|
const schedulesOnServer = deviceData.Schedule ?? [];
|
|
1512
1517
|
const presetsOnServer = deviceData.Presets ?? [];
|
|
1513
|
-
const
|
|
1518
|
+
const holidayMode = deviceData.Device.HolidayMode;
|
|
1519
|
+
const holidayModeEnabled = accountTypeMelcloud ? holidayMode : deviceData.HolidayMode?.Enabled;
|
|
1514
1520
|
const holidayModeActive = deviceData.HolidayMode?.Active ?? false;
|
|
1515
1521
|
|
|
1516
1522
|
//device info
|
|
@@ -1545,12 +1551,11 @@ class DeviceAtw extends EventEmitter {
|
|
|
1545
1551
|
|
|
1546
1552
|
//heat pump
|
|
1547
1553
|
const heatPumpName = 'Heat Pump';
|
|
1548
|
-
const power = deviceData.Device.Power
|
|
1554
|
+
const power = deviceData.Device.Power;
|
|
1549
1555
|
const inStandbyMode = deviceData.Device.InStandbyMode;
|
|
1550
1556
|
const unitStatus = deviceData.Device.UnitStatus ?? 0;
|
|
1551
1557
|
const operationMode = deviceData.Device.OperationMode;
|
|
1552
1558
|
const outdoorTemperature = deviceData.Device.OutdoorTemperature;
|
|
1553
|
-
const holidayMode = deviceData.Device.HolidayMode ?? false;
|
|
1554
1559
|
const flowTemperatureHeatPump = deviceData.Device.FlowTemperature;
|
|
1555
1560
|
const returnTemperatureHeatPump = deviceData.Device.ReturnTemperature;
|
|
1556
1561
|
const isConnected = accountTypeMelcloud ? !deviceData.Device[connectKey] : deviceData.Device[connectKey];
|
|
@@ -1594,7 +1599,7 @@ class DeviceAtw extends EventEmitter {
|
|
|
1594
1599
|
const obj = {
|
|
1595
1600
|
presets: presetsOnServer,
|
|
1596
1601
|
schedules: schedulesOnServer,
|
|
1597
|
-
power: power
|
|
1602
|
+
power: power,
|
|
1598
1603
|
inStandbyMode: inStandbyMode,
|
|
1599
1604
|
unitStatus: unitStatus,
|
|
1600
1605
|
idleZone1: idleZone1,
|
|
@@ -2065,7 +2070,7 @@ class DeviceAtw extends EventEmitter {
|
|
|
2065
2070
|
button.state = power ? (operationMode === 1) : false;
|
|
2066
2071
|
break;
|
|
2067
2072
|
case 53: //HOLIDAY
|
|
2068
|
-
button.state = power ? (
|
|
2073
|
+
button.state = power ? (holidayModeEnabled === true) : false;
|
|
2069
2074
|
break;
|
|
2070
2075
|
case 10: //ALL ZONES PHYSICAL LOCK CONTROL
|
|
2071
2076
|
button.state = power ? (prohibitZone1 === true && prohibitHotWater === true && prohibitZone2 === true) : false;
|
package/src/deviceerv.js
CHANGED
|
@@ -305,8 +305,6 @@ class DeviceErv extends EventEmitter {
|
|
|
305
305
|
return state;
|
|
306
306
|
})
|
|
307
307
|
.onSet(async (state) => {
|
|
308
|
-
if (!!state === this.accessory.power) return;
|
|
309
|
-
|
|
310
308
|
try {
|
|
311
309
|
deviceData.Device.Power = state ? true : false;
|
|
312
310
|
await this.melCloudErv.send(this.accountType, this.displayType, deviceData, Ventilation.EffectiveFlags.Power);
|
|
@@ -1136,7 +1134,7 @@ class DeviceErv extends EventEmitter {
|
|
|
1136
1134
|
actualVentilationMode: actualVentilationMode,
|
|
1137
1135
|
numberOfFanSpeeds: numberOfFanSpeeds,
|
|
1138
1136
|
supportsFanSpeed: supportsFanSpeed,
|
|
1139
|
-
power: power
|
|
1137
|
+
power: power,
|
|
1140
1138
|
inStandbyMode: inStandbyMode,
|
|
1141
1139
|
operationMode: operationMode,
|
|
1142
1140
|
currentOperationMode: 0,
|
package/src/melcloud.js
CHANGED
|
@@ -1,13 +1,10 @@
|
|
|
1
|
-
import fs from 'fs';
|
|
2
1
|
import axios from 'axios';
|
|
3
2
|
import { exec } from 'child_process';
|
|
4
3
|
import { promisify } from 'util';
|
|
5
4
|
import EventEmitter from 'events';
|
|
6
|
-
import puppeteer from 'puppeteer';
|
|
7
5
|
import ImpulseGenerator from './impulsegenerator.js';
|
|
8
6
|
import Functions from './functions.js';
|
|
9
|
-
import { ApiUrls
|
|
10
|
-
const execPromise = promisify(exec);
|
|
7
|
+
import { ApiUrls } from './constants.js';
|
|
11
8
|
|
|
12
9
|
class MelCloud extends EventEmitter {
|
|
13
10
|
constructor(account, accountFile, buildingsFile, devicesFile, pluginStart = false) {
|
|
@@ -61,23 +58,22 @@ class MelCloud extends EventEmitter {
|
|
|
61
58
|
}
|
|
62
59
|
|
|
63
60
|
// MELCloud
|
|
64
|
-
async
|
|
61
|
+
async checkDevicesList() {
|
|
65
62
|
try {
|
|
66
63
|
const devicesList = { State: false, Info: null, Devices: [] }
|
|
67
64
|
const headers = {
|
|
68
65
|
'X-MitsContextKey': this.contextKey,
|
|
69
66
|
'Content-Type': 'application/json'
|
|
70
67
|
}
|
|
71
|
-
|
|
68
|
+
|
|
69
|
+
if (this.logDebug) this.emit('debug', `Scanning for devices...`);
|
|
70
|
+
const listDevicesData = await axios(ApiUrls.ListDevices, {
|
|
72
71
|
method: 'GET',
|
|
73
72
|
baseURL: ApiUrls.BaseURL,
|
|
74
73
|
timeout: 15000,
|
|
75
74
|
headers: headers
|
|
76
75
|
});
|
|
77
76
|
|
|
78
|
-
if (this.logDebug) this.emit('debug', `Scanning for devices...`);
|
|
79
|
-
const listDevicesData = await axiosInstance(ApiUrls.ListDevices);
|
|
80
|
-
|
|
81
77
|
if (!listDevicesData || !listDevicesData.data) {
|
|
82
78
|
devicesList.Info = 'Invalid or empty response from MELCloud API'
|
|
83
79
|
return devicesList;
|
|
@@ -138,28 +134,27 @@ class MelCloud extends EventEmitter {
|
|
|
138
134
|
}
|
|
139
135
|
}
|
|
140
136
|
|
|
141
|
-
async
|
|
137
|
+
async connect() {
|
|
142
138
|
if (this.logDebug) this.emit('debug', `Connecting to MELCloud`);
|
|
143
139
|
|
|
144
140
|
try {
|
|
145
141
|
const accountInfo = { State: false, Info: '', LoginData: null, ContextKey: null, UseFahrenheit: false }
|
|
146
|
-
|
|
142
|
+
|
|
143
|
+
const payload = {
|
|
144
|
+
Email: this.user,
|
|
145
|
+
Password: this.passwd,
|
|
146
|
+
Language: this.language,
|
|
147
|
+
AppVersion: '1.34.12',
|
|
148
|
+
CaptchaChallenge: '',
|
|
149
|
+
CaptchaResponse: '',
|
|
150
|
+
Persist: true
|
|
151
|
+
};
|
|
152
|
+
const accountData = await axios(ApiUrls.ClientLogin, {
|
|
147
153
|
method: 'POST',
|
|
148
154
|
baseURL: ApiUrls.BaseURL,
|
|
149
155
|
timeout: 15000,
|
|
150
|
-
data:
|
|
151
|
-
Email: this.user,
|
|
152
|
-
Password: this.passwd,
|
|
153
|
-
Language: this.language,
|
|
154
|
-
AppVersion: '1.34.12',
|
|
155
|
-
CaptchaChallenge: '',
|
|
156
|
-
CaptchaResponse: '',
|
|
157
|
-
Persist: true
|
|
158
|
-
}
|
|
156
|
+
data: payload
|
|
159
157
|
});
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
const accountData = await axiosInstance(ApiUrls.ClientLogin);
|
|
163
158
|
const account = accountData.data;
|
|
164
159
|
const loginData = account.LoginData ?? [];
|
|
165
160
|
const contextKey = loginData.ContextKey;
|
|
@@ -193,332 +188,6 @@ class MelCloud extends EventEmitter {
|
|
|
193
188
|
}
|
|
194
189
|
}
|
|
195
190
|
|
|
196
|
-
// MELCloud Home
|
|
197
|
-
async checkMelcloudHomeDevicesList() {
|
|
198
|
-
try {
|
|
199
|
-
const devicesList = { State: false, Info: null, Devices: [] }
|
|
200
|
-
const headers = {
|
|
201
|
-
'Accept': '*/*',
|
|
202
|
-
'Accept-Language': 'en-US,en;q=0.9',
|
|
203
|
-
'Cookie': this.contextKey,
|
|
204
|
-
'User-Agent': 'homebridge-melcloud-control/4.0.0',
|
|
205
|
-
'DNT': '1',
|
|
206
|
-
'Origin': 'https://melcloudhome.com',
|
|
207
|
-
'Referer': 'https://melcloudhome.com/dashboard',
|
|
208
|
-
'Sec-Fetch-Dest': 'empty',
|
|
209
|
-
'Sec-Fetch-Mode': 'cors',
|
|
210
|
-
'Sec-Fetch-Site': 'same-origin',
|
|
211
|
-
'X-CSRF': '1'
|
|
212
|
-
};
|
|
213
|
-
const axiosInstance = axios.create({
|
|
214
|
-
method: 'GET',
|
|
215
|
-
baseURL: ApiUrlsHome.BaseURL,
|
|
216
|
-
timeout: 25000,
|
|
217
|
-
headers: headers
|
|
218
|
-
});
|
|
219
|
-
|
|
220
|
-
if (this.logDebug) this.emit('debug', `Scanning for devices`);
|
|
221
|
-
const listDevicesData = await axiosInstance(ApiUrlsHome.GetUserContext);
|
|
222
|
-
const buildingsList = listDevicesData.data.buildings;
|
|
223
|
-
if (this.logDebug) this.emit('debug', `Buildings: ${JSON.stringify(buildingsList, null, 2)}`);
|
|
224
|
-
|
|
225
|
-
if (!buildingsList) {
|
|
226
|
-
devicesList.Info = 'No building found'
|
|
227
|
-
return devicesList;
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
await this.functions.saveData(this.buildingsFile, buildingsList);
|
|
231
|
-
if (this.logDebug) this.emit('debug', `Buildings list saved`);
|
|
232
|
-
|
|
233
|
-
const devices = buildingsList.flatMap(building => {
|
|
234
|
-
// Funkcja kapitalizująca klucze obiektu
|
|
235
|
-
const capitalizeKeys = obj =>
|
|
236
|
-
Object.fromEntries(
|
|
237
|
-
Object.entries(obj).map(([key, value]) => [
|
|
238
|
-
key.charAt(0).toUpperCase() + key.slice(1),
|
|
239
|
-
value
|
|
240
|
-
])
|
|
241
|
-
);
|
|
242
|
-
|
|
243
|
-
// Rekurencyjna kapitalizacja kluczy w obiekcie lub tablicy
|
|
244
|
-
const capitalizeKeysDeep = obj => {
|
|
245
|
-
if (Array.isArray(obj)) return obj.map(capitalizeKeysDeep);
|
|
246
|
-
if (obj && typeof obj === 'object') {
|
|
247
|
-
return Object.fromEntries(
|
|
248
|
-
Object.entries(obj).map(([key, value]) => [
|
|
249
|
-
key.charAt(0).toUpperCase() + key.slice(1),
|
|
250
|
-
capitalizeKeysDeep(value)
|
|
251
|
-
])
|
|
252
|
-
);
|
|
253
|
-
}
|
|
254
|
-
return obj;
|
|
255
|
-
};
|
|
256
|
-
|
|
257
|
-
// Funkcja tworząca finalny obiekt Device
|
|
258
|
-
const createDevice = (device, type) => {
|
|
259
|
-
// Settings już kapitalizowane w nazwach
|
|
260
|
-
const settingsArray = device.Settings || [];
|
|
261
|
-
|
|
262
|
-
const settingsObject = Object.fromEntries(
|
|
263
|
-
settingsArray.map(({ name, value }) => {
|
|
264
|
-
let parsedValue = value;
|
|
265
|
-
if (value === "True") parsedValue = true;
|
|
266
|
-
else if (value === "False") parsedValue = false;
|
|
267
|
-
else if (!isNaN(value) && value !== "") parsedValue = Number(value);
|
|
268
|
-
|
|
269
|
-
const key = name.charAt(0).toUpperCase() + name.slice(1);
|
|
270
|
-
return [key, parsedValue];
|
|
271
|
-
})
|
|
272
|
-
);
|
|
273
|
-
|
|
274
|
-
// Scal Capabilities + Settings + DeviceType w Device
|
|
275
|
-
const deviceObject = {
|
|
276
|
-
...capitalizeKeys(device.Capabilities || {}),
|
|
277
|
-
...settingsObject,
|
|
278
|
-
DeviceType: type,
|
|
279
|
-
IsConnected: device.IsConnected
|
|
280
|
-
};
|
|
281
|
-
|
|
282
|
-
// Kapitalizacja brakujących obiektów/tablic
|
|
283
|
-
if (device.FrostProtection) device.FrostProtection = { ...capitalizeKeys(device.FrostProtection || {}) };
|
|
284
|
-
if (device.OverheatProtection) device.OverheatProtection = { ...capitalizeKeys(device.OverheatProtection || {}) };
|
|
285
|
-
if (device.HolidayMode) device.HolidayMode = { ...capitalizeKeys(device.HolidayMode || {}) };
|
|
286
|
-
if (Array.isArray(device.Schedule)) device.Schedule = device.Schedule.map(capitalizeKeysDeep);
|
|
287
|
-
|
|
288
|
-
// Usuń stare pola Settings i Capabilities
|
|
289
|
-
const { Settings, Capabilities, Id, GivenDisplayName, ...rest } = device;
|
|
290
|
-
|
|
291
|
-
return {
|
|
292
|
-
...rest,
|
|
293
|
-
Type: type,
|
|
294
|
-
DeviceID: Id,
|
|
295
|
-
DeviceName: GivenDisplayName,
|
|
296
|
-
Device: deviceObject,
|
|
297
|
-
Headers: headers
|
|
298
|
-
};
|
|
299
|
-
};
|
|
300
|
-
|
|
301
|
-
return [
|
|
302
|
-
...(building.airToAirUnits || []).map(d => createDevice(capitalizeKeys(d), 0)),
|
|
303
|
-
...(building.airToWaterUnits || []).map(d => createDevice(capitalizeKeys(d), 1)),
|
|
304
|
-
...(building.airToVentilationUnits || []).map(d => createDevice(capitalizeKeys(d), 3))
|
|
305
|
-
];
|
|
306
|
-
});
|
|
307
|
-
|
|
308
|
-
const devicesCount = devices.length;
|
|
309
|
-
if (devicesCount === 0) {
|
|
310
|
-
devicesList.Info = 'No devices found'
|
|
311
|
-
return devicesList;
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
await this.functions.saveData(this.devicesFile, devices);
|
|
315
|
-
if (this.logDebug) this.emit('debug', `${devicesCount} devices saved`);
|
|
316
|
-
|
|
317
|
-
devicesList.State = true;
|
|
318
|
-
devicesList.Info = `Found ${devicesCount} devices`;
|
|
319
|
-
devicesList.Devices = devices;
|
|
320
|
-
return devicesList;
|
|
321
|
-
} catch (error) {
|
|
322
|
-
if (error.response?.status === 401) {
|
|
323
|
-
await connectToMelCloudHome();
|
|
324
|
-
if (this.logWarn) this.emit('warn', 'Check devices list not possible, cookies expired, trying to get new.');
|
|
325
|
-
return;
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
throw new Error(`Check devices list error: ${error.message}`);
|
|
329
|
-
}
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
async connectToMelCloudHome() {
|
|
333
|
-
if (this.logDebug) this.emit('debug', 'Connecting to MELCloud Home');
|
|
334
|
-
const GLOBAL_TIMEOUT = 90000;
|
|
335
|
-
|
|
336
|
-
let browser;
|
|
337
|
-
try {
|
|
338
|
-
const accountInfo = { State: false, Info: '', ContextKey: null, UseFahrenheit: false };
|
|
339
|
-
let chromiumPath = await this.functions.ensureChromiumInstalled();
|
|
340
|
-
|
|
341
|
-
// === Fallback to Puppeteer's built-in Chromium ===
|
|
342
|
-
if (!chromiumPath) {
|
|
343
|
-
try {
|
|
344
|
-
const puppeteerPath = puppeteer.executablePath();
|
|
345
|
-
if (puppeteerPath && fs.existsSync(puppeteerPath)) {
|
|
346
|
-
chromiumPath = puppeteerPath;
|
|
347
|
-
if (this.logDebug) this.emit('debug', `Using puppeteer Chromium at ${chromiumPath}`);
|
|
348
|
-
}
|
|
349
|
-
} catch { }
|
|
350
|
-
} else {
|
|
351
|
-
if (this.logDebug) this.emit('debug', `Using system Chromium at ${chromiumPath}`);
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
if (!chromiumPath) {
|
|
355
|
-
accountInfo.Info = 'Chromium not found on Your device, please install it manually and try again';
|
|
356
|
-
return accountInfo;
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
// Verify executable works
|
|
360
|
-
try {
|
|
361
|
-
const { stdout } = await execPromise(`"${chromiumPath}" --version`);
|
|
362
|
-
if (this.logDebug) this.emit('debug', `Chromium detected: ${stdout.trim()}`);
|
|
363
|
-
} catch (error) {
|
|
364
|
-
accountInfo.Info = `Chromium found at ${chromiumPath}, but cannot be executed: ${error.message}`;
|
|
365
|
-
return accountInfo;
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
if (this.logDebug) this.emit('debug', `Launching Chromium...`);
|
|
369
|
-
browser = await puppeteer.launch({
|
|
370
|
-
headless: true,
|
|
371
|
-
executablePath: chromiumPath,
|
|
372
|
-
timeout: GLOBAL_TIMEOUT,
|
|
373
|
-
args: [
|
|
374
|
-
'--no-sandbox',
|
|
375
|
-
'--disable-setuid-sandbox',
|
|
376
|
-
'--disable-dev-shm-usage',
|
|
377
|
-
'--single-process',
|
|
378
|
-
'--disable-gpu',
|
|
379
|
-
'--no-zygote'
|
|
380
|
-
]
|
|
381
|
-
});
|
|
382
|
-
browser.on('disconnected', () => this.emit('debug', 'Browser disconnected'));
|
|
383
|
-
|
|
384
|
-
const page = await browser.newPage();
|
|
385
|
-
page.on('error', error => this.emit('error', `Page crashed: ${error.message}`));
|
|
386
|
-
page.on('pageerror', error => this.emit('error', `Browser error: ${error.message}`));
|
|
387
|
-
page.setDefaultTimeout(GLOBAL_TIMEOUT);
|
|
388
|
-
page.setDefaultNavigationTimeout(GLOBAL_TIMEOUT);
|
|
389
|
-
|
|
390
|
-
// Clear cookies before navigation
|
|
391
|
-
try {
|
|
392
|
-
const client = await page.createCDPSession();
|
|
393
|
-
await client.send('Network.clearBrowserCookies');
|
|
394
|
-
} catch (error) {
|
|
395
|
-
if (this.logError) this.emit('error', `Clear cookies error: ${error.message}`);
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
try {
|
|
399
|
-
await page.goto(ApiUrlsHome.BaseURL, { waitUntil: ['domcontentloaded', 'networkidle2'], timeout: GLOBAL_TIMEOUT });
|
|
400
|
-
} catch (error) {
|
|
401
|
-
accountInfo.Info = `Navigation to ${ApiUrlsHome.BaseURL} failed: ${error.message}`;
|
|
402
|
-
return accountInfo;
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
// Wait extra to ensure UI is rendered
|
|
406
|
-
await new Promise(r => setTimeout(r, 3000));
|
|
407
|
-
const loginBtn = await page.waitForSelector('button.btn--blue', { timeout: GLOBAL_TIMEOUT / 4 });
|
|
408
|
-
const loginText = await page.evaluate(el => el.textContent.trim(), loginBtn);
|
|
409
|
-
|
|
410
|
-
if (!['Zaloguj', 'Sign In', 'Login'].includes(loginText)) {
|
|
411
|
-
accountInfo.Info = `Login button ${loginText} not found`;
|
|
412
|
-
return accountInfo;
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
await loginBtn.click();
|
|
416
|
-
await page.waitForNavigation({ waitUntil: 'networkidle2', timeout: GLOBAL_TIMEOUT / 3 });
|
|
417
|
-
|
|
418
|
-
const usernameInput = await page.$('input[name="username"]');
|
|
419
|
-
const passwordInput = await page.$('input[name="password"]');
|
|
420
|
-
if (!usernameInput || !passwordInput) {
|
|
421
|
-
accountInfo.Info = 'Username or password input not found';
|
|
422
|
-
return accountInfo;
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
await page.type('input[name="username"]', this.user, { delay: 50 });
|
|
426
|
-
await page.type('input[name="password"]', this.passwd, { delay: 50 });
|
|
427
|
-
|
|
428
|
-
const submitButton = await page.$('input[type="submit"], button[type="submit"]');
|
|
429
|
-
if (!submitButton) {
|
|
430
|
-
accountInfo.Info = 'Submit button not found';
|
|
431
|
-
return accountInfo;
|
|
432
|
-
}
|
|
433
|
-
await Promise.race([Promise.all([submitButton.click(), page.waitForNavigation({ waitUntil: ['domcontentloaded', 'networkidle2'], timeout: GLOBAL_TIMEOUT / 4 })]), new Promise(r => setTimeout(r, GLOBAL_TIMEOUT / 3))]);
|
|
434
|
-
|
|
435
|
-
// Extract cookies
|
|
436
|
-
let c1 = null, c2 = null;
|
|
437
|
-
const start = Date.now();
|
|
438
|
-
while ((!c1 || !c2) && Date.now() - start < GLOBAL_TIMEOUT / 2) {
|
|
439
|
-
const cookies = await page.browserContext().cookies();
|
|
440
|
-
c1 = cookies.find(c => c.name === '__Secure-monitorandcontrolC1')?.value || c1;
|
|
441
|
-
c2 = cookies.find(c => c.name === '__Secure-monitorandcontrolC2')?.value || c2;
|
|
442
|
-
if (!c1 || !c2) await new Promise(r => setTimeout(r, 500));
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
if (!c1 || !c2) {
|
|
446
|
-
accountInfo.Info = 'Cookies C1/C2 missing';
|
|
447
|
-
return accountInfo;
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
const contextKey = [
|
|
451
|
-
'__Secure-monitorandcontrol=chunks-2',
|
|
452
|
-
`__Secure-monitorandcontrolC1=${c1}`,
|
|
453
|
-
`__Secure-monitorandcontrolC2=${c2}`
|
|
454
|
-
].join('; ');
|
|
455
|
-
this.contextKey = contextKey;
|
|
456
|
-
|
|
457
|
-
accountInfo.State = true;
|
|
458
|
-
accountInfo.Info = 'Connect to MELCloud Home Success';
|
|
459
|
-
accountInfo.ContextKey = contextKey;
|
|
460
|
-
await this.functions.saveData(this.accountFile, accountInfo);
|
|
461
|
-
|
|
462
|
-
return accountInfo;
|
|
463
|
-
} catch (error) {
|
|
464
|
-
throw new Error(`Connect error: ${error.message}`);
|
|
465
|
-
} finally {
|
|
466
|
-
if (browser) {
|
|
467
|
-
try { await browser.close(); }
|
|
468
|
-
catch (closeErr) {
|
|
469
|
-
if (this.logError) this.emit('error', `Failed to close Puppeteer: ${closeErr.message}`);
|
|
470
|
-
}
|
|
471
|
-
}
|
|
472
|
-
}
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
async checkDevicesList() {
|
|
476
|
-
const TIMEOUT_MS = 30000; // 30 seconds timeout
|
|
477
|
-
try {
|
|
478
|
-
const devicesList = await Promise.race([
|
|
479
|
-
(async () => {
|
|
480
|
-
switch (this.accountType) {
|
|
481
|
-
case "melcloud":
|
|
482
|
-
return await this.checkMelcloudDevicesList();
|
|
483
|
-
case "melcloudhome":
|
|
484
|
-
return await this.checkMelcloudHomeDevicesList();
|
|
485
|
-
default:
|
|
486
|
-
return [];
|
|
487
|
-
}
|
|
488
|
-
})(),
|
|
489
|
-
new Promise((_, reject) => setTimeout(() => reject(new Error('Device list timeout (30s)')), TIMEOUT_MS))
|
|
490
|
-
]);
|
|
491
|
-
|
|
492
|
-
return devicesList;
|
|
493
|
-
} catch (error) {
|
|
494
|
-
throw new Error(error);
|
|
495
|
-
}
|
|
496
|
-
}
|
|
497
|
-
|
|
498
|
-
async connect() {
|
|
499
|
-
const TIMEOUT_MS = 120000;
|
|
500
|
-
|
|
501
|
-
try {
|
|
502
|
-
const response = await Promise.race([
|
|
503
|
-
(async () => {
|
|
504
|
-
switch (this.accountType) {
|
|
505
|
-
case "melcloud":
|
|
506
|
-
return await this.connectToMelCloud();
|
|
507
|
-
case "melcloudhome":
|
|
508
|
-
return await this.connectToMelCloudHome();
|
|
509
|
-
default:
|
|
510
|
-
return {};
|
|
511
|
-
}
|
|
512
|
-
})(),
|
|
513
|
-
new Promise((_, reject) => setTimeout(() => reject(new Error('Connection timeout (90s)')), TIMEOUT_MS))
|
|
514
|
-
]);
|
|
515
|
-
|
|
516
|
-
return response;
|
|
517
|
-
} catch (error) {
|
|
518
|
-
throw new Error(error);
|
|
519
|
-
}
|
|
520
|
-
}
|
|
521
|
-
|
|
522
191
|
async send(accountInfo) {
|
|
523
192
|
try {
|
|
524
193
|
const axiosInstance = axios.create({
|
package/src/melcloudata.js
CHANGED
|
@@ -52,10 +52,6 @@ class MelCloudAta extends EventEmitter {
|
|
|
52
52
|
try {
|
|
53
53
|
//read device info from file
|
|
54
54
|
const devicesData = await this.functions.readData(this.devicesFile, true);
|
|
55
|
-
if (!Array.isArray(devicesData)) {
|
|
56
|
-
if (this.logWarn) this.emit('warn', `Device data not found`);
|
|
57
|
-
return null;
|
|
58
|
-
}
|
|
59
55
|
const deviceData = devicesData.find(device => device.DeviceID === this.deviceId);
|
|
60
56
|
|
|
61
57
|
if (this.accountType === 'melcloudhome') {
|
|
@@ -118,10 +114,7 @@ class MelCloudAta extends EventEmitter {
|
|
|
118
114
|
|
|
119
115
|
//check state changes
|
|
120
116
|
const deviceDataHasNotChanged = JSON.stringify(deviceData) === JSON.stringify(this.deviceData);
|
|
121
|
-
if (deviceDataHasNotChanged)
|
|
122
|
-
if (this.logDebug) this.emit('debug', `Device state not changed`);
|
|
123
|
-
return;
|
|
124
|
-
}
|
|
117
|
+
if (deviceDataHasNotChanged) return;
|
|
125
118
|
this.deviceData = deviceData;
|
|
126
119
|
|
|
127
120
|
//emit info
|
|
@@ -167,17 +160,15 @@ class MelCloudAta extends EventEmitter {
|
|
|
167
160
|
HideDryModeControl: deviceData.HideDryModeControl,
|
|
168
161
|
HasPendingCommand: true
|
|
169
162
|
};
|
|
170
|
-
if (this.logDebug) this.emit('debug', `Send Data: ${JSON.stringify(payload, null, 2)}`);
|
|
171
163
|
|
|
172
|
-
|
|
164
|
+
if (this.logDebug) this.emit('debug', `Send Data: ${JSON.stringify(payload, null, 2)}`);
|
|
165
|
+
await axios(ApiUrls.SetAta, {
|
|
173
166
|
method: 'POST',
|
|
174
167
|
baseURL: ApiUrls.BaseURL,
|
|
175
168
|
timeout: 10000,
|
|
176
169
|
headers: deviceData.Headers,
|
|
177
170
|
data: payload
|
|
178
171
|
});
|
|
179
|
-
|
|
180
|
-
await axiosInstancePost(ApiUrls.SetAta);
|
|
181
172
|
this.updateData(deviceData);
|
|
182
173
|
return true;
|
|
183
174
|
case "melcloudhome":
|
|
@@ -243,16 +234,14 @@ class MelCloudAta extends EventEmitter {
|
|
|
243
234
|
break
|
|
244
235
|
}
|
|
245
236
|
|
|
246
|
-
|
|
237
|
+
if (this.logDebug) this.emit('debug', `Send Data: ${JSON.stringify(payload, null, 2)}`);
|
|
238
|
+
await axios(path, {
|
|
247
239
|
method: method,
|
|
248
240
|
baseURL: ApiUrlsHome.BaseURL,
|
|
249
241
|
timeout: 10000,
|
|
250
242
|
headers: deviceData.Headers,
|
|
251
243
|
data: payload
|
|
252
244
|
});
|
|
253
|
-
|
|
254
|
-
if (this.logDebug) this.emit('debug', `Send Data: ${JSON.stringify(payload, null, 2)}`);
|
|
255
|
-
await axiosInstancePut(path);
|
|
256
245
|
this.updateData(deviceData);
|
|
257
246
|
return true;
|
|
258
247
|
default:
|
|
@@ -272,6 +261,5 @@ class MelCloudAta extends EventEmitter {
|
|
|
272
261
|
this.lock = false
|
|
273
262
|
}, 3000);
|
|
274
263
|
}
|
|
275
|
-
|
|
276
264
|
};
|
|
277
265
|
export default MelCloudAta;
|
package/src/melcloudatw.js
CHANGED
|
@@ -52,10 +52,6 @@ class MelCloudAtw extends EventEmitter {
|
|
|
52
52
|
try {
|
|
53
53
|
//read device info from file
|
|
54
54
|
const devicesData = await this.functions.readData(this.devicesFile, true);
|
|
55
|
-
if (!Array.isArray(devicesData)) {
|
|
56
|
-
if (this.logWarn) this.emit('warn', `Device data not found`);
|
|
57
|
-
return null;
|
|
58
|
-
}
|
|
59
55
|
const deviceData = devicesData.find(device => device.DeviceID === this.deviceId);
|
|
60
56
|
|
|
61
57
|
if (this.accountType === 'melcloudhome') {
|
|
@@ -109,10 +105,7 @@ class MelCloudAtw extends EventEmitter {
|
|
|
109
105
|
|
|
110
106
|
//check state changes
|
|
111
107
|
const deviceDataHasNotChanged = JSON.stringify(devicesData) === JSON.stringify(this.devicesData);
|
|
112
|
-
if (deviceDataHasNotChanged)
|
|
113
|
-
if (this.logDebug) this.emit('debug', `Device state not changed`);
|
|
114
|
-
return;
|
|
115
|
-
}
|
|
108
|
+
if (deviceDataHasNotChanged) return;
|
|
116
109
|
this.devicesData = devicesData;
|
|
117
110
|
|
|
118
111
|
//emit info
|
|
@@ -148,93 +141,81 @@ class MelCloudAtw extends EventEmitter {
|
|
|
148
141
|
let path = '';
|
|
149
142
|
switch (accountType) {
|
|
150
143
|
case "melcloud":
|
|
151
|
-
|
|
144
|
+
deviceData.Device.EffectiveFlags = effectiveFlags;
|
|
145
|
+
payload = {
|
|
146
|
+
DeviceID: deviceData.Device.DeviceID,
|
|
147
|
+
EffectiveFlags: deviceData.Device.EffectiveFlags,
|
|
148
|
+
Power: deviceData.Device.Power,
|
|
149
|
+
SetTemperatureZone1: deviceData.Device.SetTemperatureZone1,
|
|
150
|
+
SetTemperatureZone2: deviceData.Device.SetTemperatureZone2,
|
|
151
|
+
OperationMode: deviceData.Device.OperationMode,
|
|
152
|
+
OperationModeZone1: deviceData.Device.OperationModeZone1,
|
|
153
|
+
OperationModeZone2: deviceData.Device.OperationModeZone2,
|
|
154
|
+
SetHeatFlowTemperatureZone1: deviceData.Device.SetHeatFlowTemperatureZone1,
|
|
155
|
+
SetHeatFlowTemperatureZone2: deviceData.Device.SetHeatFlowTemperatureZone2,
|
|
156
|
+
SetCoolFlowTemperatureZone1: deviceData.Device.SetCoolFlowTemperatureZone1,
|
|
157
|
+
SetCoolFlowTemperatureZone2: deviceData.Device.SetCoolFlowTemperatureZone2,
|
|
158
|
+
SetTankWaterTemperature: deviceData.Device.SetTankWaterTemperature,
|
|
159
|
+
ForcedHotWaterMode: deviceData.Device.ForcedHotWaterMode,
|
|
160
|
+
EcoHotWater: deviceData.Device.EcoHotWater,
|
|
161
|
+
HolidayMode: deviceData.Device.HolidayMode,
|
|
162
|
+
ProhibitZone1: deviceData.Device.ProhibitHeatingZone1,
|
|
163
|
+
ProhibitZone2: deviceData.Device.ProhibitHeatingZone2,
|
|
164
|
+
ProhibitHotWater: deviceData.Device.ProhibitHotWater,
|
|
165
|
+
HasPendingCommand: true
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (this.logDebug) this.emit('debug', `Send Data: ${JSON.stringify(payload, null, 2)}`);
|
|
169
|
+
await axios(ApiUrls.SetAtw, {
|
|
152
170
|
method: 'POST',
|
|
153
171
|
baseURL: ApiUrls.BaseURL,
|
|
154
172
|
timeout: 10000,
|
|
155
173
|
headers: deviceData.Headers,
|
|
156
|
-
|
|
174
|
+
data: payload
|
|
157
175
|
});
|
|
158
|
-
|
|
159
|
-
deviceData.Device.EffectiveFlags = effectiveFlags;
|
|
160
|
-
payload = {
|
|
161
|
-
data: {
|
|
162
|
-
DeviceID: deviceData.Device.DeviceID,
|
|
163
|
-
EffectiveFlags: deviceData.Device.EffectiveFlags,
|
|
164
|
-
Power: deviceData.Device.Power,
|
|
165
|
-
SetTemperatureZone1: deviceData.Device.SetTemperatureZone1,
|
|
166
|
-
SetTemperatureZone2: deviceData.Device.SetTemperatureZone2,
|
|
167
|
-
OperationMode: deviceData.Device.OperationMode,
|
|
168
|
-
OperationModeZone1: deviceData.Device.OperationModeZone1,
|
|
169
|
-
OperationModeZone2: deviceData.Device.OperationModeZone2,
|
|
170
|
-
SetHeatFlowTemperatureZone1: deviceData.Device.SetHeatFlowTemperatureZone1,
|
|
171
|
-
SetHeatFlowTemperatureZone2: deviceData.Device.SetHeatFlowTemperatureZone2,
|
|
172
|
-
SetCoolFlowTemperatureZone1: deviceData.Device.SetCoolFlowTemperatureZone1,
|
|
173
|
-
SetCoolFlowTemperatureZone2: deviceData.Device.SetCoolFlowTemperatureZone2,
|
|
174
|
-
SetTankWaterTemperature: deviceData.Device.SetTankWaterTemperature,
|
|
175
|
-
ForcedHotWaterMode: deviceData.Device.ForcedHotWaterMode,
|
|
176
|
-
EcoHotWater: deviceData.Device.EcoHotWater,
|
|
177
|
-
HolidayMode: deviceData.Device.HolidayMode,
|
|
178
|
-
ProhibitZone1: deviceData.Device.ProhibitHeatingZone1,
|
|
179
|
-
ProhibitZone2: deviceData.Device.ProhibitHeatingZone2,
|
|
180
|
-
ProhibitHotWater: deviceData.Device.ProhibitHotWater,
|
|
181
|
-
HasPendingCommand: true
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
await axiosInstancePost(ApiUrls.SetAtw, payload);
|
|
186
176
|
this.updateData(deviceData);
|
|
187
177
|
return true;
|
|
188
178
|
case "melcloudhome":
|
|
189
179
|
switch (effectiveFlags) {
|
|
190
180
|
case 'holidaymode':
|
|
191
|
-
payload = {
|
|
192
|
-
data: { enabled: deviceData.HolidayMode.Enabled, startDate: deviceData.HolidayMode.StartDate, endDate: deviceData.HolidayMode.EndDate, units: { "ATW": [deviceData.DeviceID] } }
|
|
193
|
-
};
|
|
181
|
+
payload = { enabled: deviceData.HolidayMode.Enabled, startDate: deviceData.HolidayMode.StartDate, endDate: deviceData.HolidayMode.EndDate, units: { "ATW": [deviceData.DeviceID] } };
|
|
194
182
|
method = 'POST';
|
|
195
183
|
path = ApiUrlsHome.PostHolidayMode;
|
|
196
184
|
break;
|
|
197
185
|
case 'schedule':
|
|
198
|
-
payload = {
|
|
199
|
-
data: {
|
|
200
|
-
enabled: deviceData.ScheduleEnabled
|
|
201
|
-
}
|
|
202
|
-
};
|
|
186
|
+
payload = { enabled: deviceData.ScheduleEnabled };
|
|
203
187
|
method = 'PUT';
|
|
204
188
|
path = ApiUrlsHome.PutScheduleEnable.replace('deviceid', deviceData.DeviceID);
|
|
205
189
|
break;
|
|
206
190
|
default:
|
|
207
191
|
payload = {
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
EcoHotWater: deviceData.Device.EcoHotWater,
|
|
222
|
-
}
|
|
192
|
+
Power: deviceData.Device.Power,
|
|
193
|
+
SetTemperatureZone1: deviceData.Device.SetTemperatureZone1,
|
|
194
|
+
SetTemperatureZone2: deviceData.Device.SetTemperatureZone2,
|
|
195
|
+
OperationMode: HeatPump.OperationModeMapEnumToString[deviceData.Device.OperationMode],
|
|
196
|
+
OperationModeZone1: HeatPump.OperationModeMapEnumToString[deviceData.Device.OperationModeZone1],
|
|
197
|
+
OperationModeZone2: HeatPump.OperationModeMapEnumToString[deviceData.Device.OperationModeZone2],
|
|
198
|
+
SetHeatFlowTemperatureZone1: deviceData.Device.SetHeatFlowTemperatureZone1,
|
|
199
|
+
SetHeatFlowTemperatureZone2: deviceData.Device.SetHeatFlowTemperatureZone2,
|
|
200
|
+
SetCoolFlowTemperatureZone1: deviceData.Device.SetCoolFlowTemperatureZone1,
|
|
201
|
+
SetCoolFlowTemperatureZone2: deviceData.Device.SetCoolFlowTemperatureZone2,
|
|
202
|
+
SetTankWaterTemperature: deviceData.Device.SetTankWaterTemperature,
|
|
203
|
+
ForcedHotWaterMode: deviceData.Device.ForcedHotWaterMode,
|
|
204
|
+
EcoHotWater: deviceData.Device.EcoHotWater,
|
|
223
205
|
};
|
|
224
206
|
method = 'PUT';
|
|
225
207
|
path = ApiUrlsHome.SetAtw.replace('deviceid', deviceData.DeviceID);
|
|
226
208
|
break
|
|
227
209
|
}
|
|
228
210
|
|
|
229
|
-
|
|
211
|
+
if (this.logDebug) this.emit('debug', `Send Data: ${JSON.stringify(payload, null, 2)}`);
|
|
212
|
+
await axios(path, {
|
|
230
213
|
method: method,
|
|
231
214
|
baseURL: ApiUrlsHome.BaseURL,
|
|
232
215
|
timeout: 10000,
|
|
233
|
-
headers: deviceData.Headers
|
|
216
|
+
headers: deviceData.Headers,
|
|
217
|
+
data: payload
|
|
234
218
|
});
|
|
235
|
-
|
|
236
|
-
if (this.logDebug) this.emit('debug', `Send Data: ${JSON.stringify(payload.data, null, 2)}`);
|
|
237
|
-
await axiosInstancePut(path, payload);
|
|
238
219
|
this.updateData(deviceData);
|
|
239
220
|
return true;
|
|
240
221
|
default:
|
package/src/melclouderv.js
CHANGED
|
@@ -52,10 +52,6 @@ class MelCloudErv extends EventEmitter {
|
|
|
52
52
|
try {
|
|
53
53
|
//read device info from file
|
|
54
54
|
const devicesData = await this.functions.readData(this.devicesFile, true);
|
|
55
|
-
if (!Array.isArray(devicesData)) {
|
|
56
|
-
if (this.logWarn) this.emit('warn', `Device data not found`);
|
|
57
|
-
return null;
|
|
58
|
-
}
|
|
59
55
|
const deviceData = devicesData.find(device => device.DeviceID === this.deviceId);
|
|
60
56
|
|
|
61
57
|
if (this.accountType === 'melcloudhome') {
|
|
@@ -112,10 +108,7 @@ class MelCloudErv extends EventEmitter {
|
|
|
112
108
|
|
|
113
109
|
//check state changes
|
|
114
110
|
const deviceDataHasNotChanged = JSON.stringify(devicesData) === JSON.stringify(this.devicesData);
|
|
115
|
-
if (deviceDataHasNotChanged)
|
|
116
|
-
if (this.logDebug) this.emit('debug', `Device state not changed`);
|
|
117
|
-
return;
|
|
118
|
-
}
|
|
111
|
+
if (deviceDataHasNotChanged) return;
|
|
119
112
|
this.devicesData = devicesData;
|
|
120
113
|
|
|
121
114
|
//emit info
|
|
@@ -137,14 +130,6 @@ class MelCloudErv extends EventEmitter {
|
|
|
137
130
|
let path = '';
|
|
138
131
|
switch (accountType) {
|
|
139
132
|
case "melcloud":
|
|
140
|
-
const axiosInstancePost = axios.create({
|
|
141
|
-
method: 'POST',
|
|
142
|
-
baseURL: ApiUrls.BaseURL,
|
|
143
|
-
timeout: 10000,
|
|
144
|
-
headers: deviceData.Headers,
|
|
145
|
-
withCredentials: true
|
|
146
|
-
});
|
|
147
|
-
|
|
148
133
|
//set target temp based on display mode and ventilation mode
|
|
149
134
|
switch (displayType) {
|
|
150
135
|
case 1: //Heather/Cooler
|
|
@@ -168,25 +153,30 @@ class MelCloudErv extends EventEmitter {
|
|
|
168
153
|
//device state
|
|
169
154
|
deviceData.Device.EffectiveFlags = effectiveFlags;
|
|
170
155
|
payload = {
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
HasPendingCommand: true
|
|
186
|
-
}
|
|
156
|
+
DeviceID: deviceData.Device.DeviceID,
|
|
157
|
+
EffectiveFlags: deviceData.Device.EffectiveFlags,
|
|
158
|
+
Power: deviceData.Device.Power,
|
|
159
|
+
SetTemperature: deviceData.Device.SetTemperature,
|
|
160
|
+
SetFanSpeed: deviceData.Device.SetFanSpeed,
|
|
161
|
+
OperationMode: deviceData.Device.OperationMode,
|
|
162
|
+
VentilationMode: deviceData.Device.VentilationMode,
|
|
163
|
+
DefaultCoolingSetTemperature: deviceData.Device.DefaultCoolingSetTemperature,
|
|
164
|
+
DefaultHeatingSetTemperature: deviceData.Device.DefaultHeatingSetTemperature,
|
|
165
|
+
HideRoomTemperature: deviceData.Device.HideRoomTemperature,
|
|
166
|
+
HideSupplyTemperature: deviceData.Device.HideSupplyTemperature,
|
|
167
|
+
HideOutdoorTemperature: deviceData.Device.HideOutdoorTemperature,
|
|
168
|
+
NightPurgeMode: deviceData.Device.NightPurgeMode,
|
|
169
|
+
HasPendingCommand: true
|
|
187
170
|
}
|
|
188
171
|
|
|
189
|
-
|
|
172
|
+
if (this.logDebug) this.emit('debug', `Send Data: ${JSON.stringify(payload, null, 2)}`);
|
|
173
|
+
await axios(ApiUrls.SetErv, {
|
|
174
|
+
method: 'POST',
|
|
175
|
+
baseURL: ApiUrls.BaseURL,
|
|
176
|
+
timeout: 10000,
|
|
177
|
+
headers: deviceData.Headers,
|
|
178
|
+
data: payload
|
|
179
|
+
});
|
|
190
180
|
this.updateData(deviceData);
|
|
191
181
|
return true;
|
|
192
182
|
case "melcloudhome":
|
|
@@ -204,45 +194,36 @@ class MelCloudErv extends EventEmitter {
|
|
|
204
194
|
|
|
205
195
|
switch (effectiveFlags) {
|
|
206
196
|
case 'holidaymode':
|
|
207
|
-
payload = {
|
|
208
|
-
data: { enabled: deviceData.HolidayMode.Enabled, startDate: deviceData.HolidayMode.StartDate, endDate: deviceData.HolidayMode.EndDate, units: { "ERV": [deviceData.DeviceID] } }
|
|
209
|
-
};
|
|
197
|
+
payload = { enabled: deviceData.HolidayMode.Enabled, startDate: deviceData.HolidayMode.StartDate, endDate: deviceData.HolidayMode.EndDate, units: { "ERV": [deviceData.DeviceID] } };
|
|
210
198
|
method = 'POST';
|
|
211
199
|
path = ApiUrlsHome.PostHolidayMode;
|
|
212
200
|
break;
|
|
213
201
|
case 'schedule':
|
|
214
|
-
payload = {
|
|
215
|
-
data: {
|
|
216
|
-
enabled: deviceData.ScheduleEnabled
|
|
217
|
-
}
|
|
218
|
-
};
|
|
202
|
+
payload = { enabled: deviceData.ScheduleEnabled };
|
|
219
203
|
method = 'PUT';
|
|
220
204
|
path = ApiUrlsHome.PutScheduleEnable.replace('deviceid', deviceData.DeviceID);
|
|
221
205
|
break;
|
|
222
206
|
default:
|
|
223
207
|
payload = {
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
VentilationMode: Ventilation.VentilationModeMapEnumToString[deviceData.Device.VentilationMode],
|
|
230
|
-
}
|
|
208
|
+
Power: deviceData.Device.Power,
|
|
209
|
+
SetTemperature: deviceData.Device.SetTemperature,
|
|
210
|
+
SetFanSpeed: String(deviceData.Device.SetFanSpeed),
|
|
211
|
+
OperationMode: Ventilation.OperationModeMapEnumToString[deviceData.Device.OperationMode],
|
|
212
|
+
VentilationMode: Ventilation.VentilationModeMapEnumToString[deviceData.Device.VentilationMode],
|
|
231
213
|
};
|
|
232
214
|
method = 'PUT';
|
|
233
215
|
path = ApiUrlsHome.SetErv.replace('deviceid', deviceData.DeviceID);
|
|
234
216
|
break
|
|
235
217
|
}
|
|
236
218
|
|
|
237
|
-
|
|
219
|
+
if (this.logDebug) this.emit('debug', `Send Data: ${JSON.stringify(payload, null, 2)}`);
|
|
220
|
+
await axios(path, {
|
|
238
221
|
method: method,
|
|
239
222
|
baseURL: ApiUrlsHome.BaseURL,
|
|
240
223
|
timeout: 10000,
|
|
241
|
-
headers: deviceData.Headers
|
|
224
|
+
headers: deviceData.Headers,
|
|
225
|
+
data: payload
|
|
242
226
|
});
|
|
243
|
-
|
|
244
|
-
if (this.logDebug) this.emit('debug', `Send Data: ${JSON.stringify(payload.data, null, 2)}`);
|
|
245
|
-
await axiosInstancePut(path, payload);
|
|
246
227
|
this.updateData(deviceData);
|
|
247
228
|
return true;
|
|
248
229
|
default:
|
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import axios from 'axios';
|
|
3
|
+
import { exec } from 'child_process';
|
|
4
|
+
import { promisify } from 'util';
|
|
5
|
+
import EventEmitter from 'events';
|
|
6
|
+
import puppeteer from 'puppeteer';
|
|
7
|
+
import ImpulseGenerator from './impulsegenerator.js';
|
|
8
|
+
import Functions from './functions.js';
|
|
9
|
+
import { ApiUrlsHome } from './constants.js';
|
|
10
|
+
const execPromise = promisify(exec);
|
|
11
|
+
|
|
12
|
+
class MelCloud extends EventEmitter {
|
|
13
|
+
constructor(account, accountFile, buildingsFile, devicesFile, pluginStart = false) {
|
|
14
|
+
super();
|
|
15
|
+
this.accountType = account.type;
|
|
16
|
+
this.user = account.user;
|
|
17
|
+
this.passwd = account.passwd;
|
|
18
|
+
this.language = account.language;
|
|
19
|
+
this.logWarn = account.log?.warn;
|
|
20
|
+
this.logError = account.log?.error;
|
|
21
|
+
this.logDebug = account.log?.debug;
|
|
22
|
+
this.accountFile = accountFile;
|
|
23
|
+
this.buildingsFile = buildingsFile;
|
|
24
|
+
this.devicesFile = devicesFile;
|
|
25
|
+
this.contextKey = null;
|
|
26
|
+
this.functions = new Functions(this.logWarn, this.logError, this.logDebug)
|
|
27
|
+
.on('warn', warn => this.emit('warn', warn))
|
|
28
|
+
.on('error', error => this.emit('error', error))
|
|
29
|
+
.on('debug', debug => this.emit('debug', debug));
|
|
30
|
+
|
|
31
|
+
if (pluginStart) {
|
|
32
|
+
//lock flags
|
|
33
|
+
this.locks = {
|
|
34
|
+
connect: false,
|
|
35
|
+
checkDevicesList: false
|
|
36
|
+
};
|
|
37
|
+
this.impulseGenerator = new ImpulseGenerator()
|
|
38
|
+
.on('connect', () => this.handleWithLock('connect', async () => {
|
|
39
|
+
await this.connect(true);
|
|
40
|
+
}))
|
|
41
|
+
.on('checkDevicesList', () => this.handleWithLock('checkDevicesList', async () => {
|
|
42
|
+
await this.checkDevicesList();
|
|
43
|
+
}))
|
|
44
|
+
.on('state', (state) => {
|
|
45
|
+
this.emit(state ? 'success' : 'warn', `Impulse generator ${state ? 'started' : 'stopped'}`);
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async handleWithLock(lockKey, fn) {
|
|
51
|
+
if (this.locks[lockKey]) return;
|
|
52
|
+
|
|
53
|
+
this.locks[lockKey] = true;
|
|
54
|
+
try {
|
|
55
|
+
await fn();
|
|
56
|
+
} catch (error) {
|
|
57
|
+
this.emit('error', `Inpulse generator error: ${error}`);
|
|
58
|
+
} finally {
|
|
59
|
+
this.locks[lockKey] = false;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// MELCloud Home
|
|
64
|
+
async checkDevicesList() {
|
|
65
|
+
try {
|
|
66
|
+
const devicesList = { State: false, Info: null, Devices: [] }
|
|
67
|
+
const headers = {
|
|
68
|
+
'Accept': '*/*',
|
|
69
|
+
'Accept-Language': 'en-US,en;q=0.9',
|
|
70
|
+
'Cookie': this.contextKey,
|
|
71
|
+
'User-Agent': 'homebridge-melcloud-control/4.0.0',
|
|
72
|
+
'DNT': '1',
|
|
73
|
+
'Origin': 'https://melcloudhome.com',
|
|
74
|
+
'Referer': 'https://melcloudhome.com/dashboard',
|
|
75
|
+
'Sec-Fetch-Dest': 'empty',
|
|
76
|
+
'Sec-Fetch-Mode': 'cors',
|
|
77
|
+
'Sec-Fetch-Site': 'same-origin',
|
|
78
|
+
'X-CSRF': '1'
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
if (this.logDebug) this.emit('debug', `Scanning for devices`);
|
|
82
|
+
const listDevicesData = await axios(ApiUrlsHome.GetUserContext, {
|
|
83
|
+
method: 'GET',
|
|
84
|
+
baseURL: ApiUrlsHome.BaseURL,
|
|
85
|
+
timeout: 25000,
|
|
86
|
+
headers: headers
|
|
87
|
+
});
|
|
88
|
+
const buildingsList = listDevicesData.data.buildings;
|
|
89
|
+
if (this.logDebug) this.emit('debug', `Buildings: ${JSON.stringify(buildingsList, null, 2)}`);
|
|
90
|
+
|
|
91
|
+
if (!buildingsList) {
|
|
92
|
+
devicesList.Info = 'No building found'
|
|
93
|
+
return devicesList;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
await this.functions.saveData(this.buildingsFile, buildingsList);
|
|
97
|
+
if (this.logDebug) this.emit('debug', `Buildings list saved`);
|
|
98
|
+
|
|
99
|
+
const devices = buildingsList.flatMap(building => {
|
|
100
|
+
// Funkcja kapitalizująca klucze obiektu
|
|
101
|
+
const capitalizeKeys = obj =>
|
|
102
|
+
Object.fromEntries(
|
|
103
|
+
Object.entries(obj).map(([key, value]) => [
|
|
104
|
+
key.charAt(0).toUpperCase() + key.slice(1),
|
|
105
|
+
value
|
|
106
|
+
])
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
// Rekurencyjna kapitalizacja kluczy w obiekcie lub tablicy
|
|
110
|
+
const capitalizeKeysDeep = obj => {
|
|
111
|
+
if (Array.isArray(obj)) return obj.map(capitalizeKeysDeep);
|
|
112
|
+
if (obj && typeof obj === 'object') {
|
|
113
|
+
return Object.fromEntries(
|
|
114
|
+
Object.entries(obj).map(([key, value]) => [
|
|
115
|
+
key.charAt(0).toUpperCase() + key.slice(1),
|
|
116
|
+
capitalizeKeysDeep(value)
|
|
117
|
+
])
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
return obj;
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
// Funkcja tworząca finalny obiekt Device
|
|
124
|
+
const createDevice = (device, type) => {
|
|
125
|
+
// Settings już kapitalizowane w nazwach
|
|
126
|
+
const settingsArray = device.Settings || [];
|
|
127
|
+
|
|
128
|
+
const settingsObject = Object.fromEntries(
|
|
129
|
+
settingsArray.map(({ name, value }) => {
|
|
130
|
+
let parsedValue = value;
|
|
131
|
+
if (value === "True") parsedValue = true;
|
|
132
|
+
else if (value === "False") parsedValue = false;
|
|
133
|
+
else if (!isNaN(value) && value !== "") parsedValue = Number(value);
|
|
134
|
+
|
|
135
|
+
const key = name.charAt(0).toUpperCase() + name.slice(1);
|
|
136
|
+
return [key, parsedValue];
|
|
137
|
+
})
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
// Scal Capabilities + Settings + DeviceType w Device
|
|
141
|
+
const deviceObject = {
|
|
142
|
+
...capitalizeKeys(device.Capabilities || {}),
|
|
143
|
+
...settingsObject,
|
|
144
|
+
DeviceType: type,
|
|
145
|
+
IsConnected: device.IsConnected
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
// Kapitalizacja brakujących obiektów/tablic
|
|
149
|
+
if (device.FrostProtection) device.FrostProtection = { ...capitalizeKeys(device.FrostProtection || {}) };
|
|
150
|
+
if (device.OverheatProtection) device.OverheatProtection = { ...capitalizeKeys(device.OverheatProtection || {}) };
|
|
151
|
+
if (device.HolidayMode) device.HolidayMode = { ...capitalizeKeys(device.HolidayMode || {}) };
|
|
152
|
+
if (Array.isArray(device.Schedule)) device.Schedule = device.Schedule.map(capitalizeKeysDeep);
|
|
153
|
+
|
|
154
|
+
// Usuń stare pola Settings i Capabilities
|
|
155
|
+
const { Settings, Capabilities, Id, GivenDisplayName, ...rest } = device;
|
|
156
|
+
|
|
157
|
+
return {
|
|
158
|
+
...rest,
|
|
159
|
+
Type: type,
|
|
160
|
+
DeviceID: Id,
|
|
161
|
+
DeviceName: GivenDisplayName,
|
|
162
|
+
Device: deviceObject,
|
|
163
|
+
Headers: headers
|
|
164
|
+
};
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
return [
|
|
168
|
+
...(building.airToAirUnits || []).map(d => createDevice(capitalizeKeys(d), 0)),
|
|
169
|
+
...(building.airToWaterUnits || []).map(d => createDevice(capitalizeKeys(d), 1)),
|
|
170
|
+
...(building.airToVentilationUnits || []).map(d => createDevice(capitalizeKeys(d), 3))
|
|
171
|
+
];
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
const devicesCount = devices.length;
|
|
175
|
+
if (devicesCount === 0) {
|
|
176
|
+
devicesList.Info = 'No devices found'
|
|
177
|
+
return devicesList;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
await this.functions.saveData(this.devicesFile, devices);
|
|
181
|
+
if (this.logDebug) this.emit('debug', `${devicesCount} devices saved`);
|
|
182
|
+
|
|
183
|
+
devicesList.State = true;
|
|
184
|
+
devicesList.Info = `Found ${devicesCount} devices`;
|
|
185
|
+
devicesList.Devices = devices;
|
|
186
|
+
return devicesList;
|
|
187
|
+
} catch (error) {
|
|
188
|
+
if (error.response?.status === 401) {
|
|
189
|
+
await connectToMelCloudHome();
|
|
190
|
+
if (this.logWarn) this.emit('warn', 'Check devices list not possible, cookies expired, trying to get new.');
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
throw new Error(`Check devices list error: ${error.message}`);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
async connect() {
|
|
199
|
+
if (this.logDebug) this.emit('debug', 'Connecting to MELCloud Home');
|
|
200
|
+
const GLOBAL_TIMEOUT = 90000;
|
|
201
|
+
|
|
202
|
+
let browser;
|
|
203
|
+
try {
|
|
204
|
+
const accountInfo = { State: false, Info: '', ContextKey: null, UseFahrenheit: false };
|
|
205
|
+
let chromiumPath = await this.functions.ensureChromiumInstalled();
|
|
206
|
+
|
|
207
|
+
// === Fallback to Puppeteer's built-in Chromium ===
|
|
208
|
+
if (!chromiumPath) {
|
|
209
|
+
try {
|
|
210
|
+
const puppeteerPath = puppeteer.executablePath();
|
|
211
|
+
if (puppeteerPath && fs.existsSync(puppeteerPath)) {
|
|
212
|
+
chromiumPath = puppeteerPath;
|
|
213
|
+
if (this.logDebug) this.emit('debug', `Using puppeteer Chromium at ${chromiumPath}`);
|
|
214
|
+
}
|
|
215
|
+
} catch { }
|
|
216
|
+
} else {
|
|
217
|
+
if (this.logDebug) this.emit('debug', `Using system Chromium at ${chromiumPath}`);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (!chromiumPath) {
|
|
221
|
+
accountInfo.Info = 'Chromium not found on Your device, please install it manually and try again';
|
|
222
|
+
return accountInfo;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Verify executable works
|
|
226
|
+
try {
|
|
227
|
+
const { stdout } = await execPromise(`"${chromiumPath}" --version`);
|
|
228
|
+
if (this.logDebug) this.emit('debug', `Chromium detected: ${stdout.trim()}`);
|
|
229
|
+
} catch (error) {
|
|
230
|
+
accountInfo.Info = `Chromium found at ${chromiumPath}, but cannot be executed: ${error.message}`;
|
|
231
|
+
return accountInfo;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (this.logDebug) this.emit('debug', `Launching Chromium...`);
|
|
235
|
+
browser = await puppeteer.launch({
|
|
236
|
+
headless: true,
|
|
237
|
+
executablePath: chromiumPath,
|
|
238
|
+
timeout: GLOBAL_TIMEOUT,
|
|
239
|
+
args: [
|
|
240
|
+
'--no-sandbox',
|
|
241
|
+
'--disable-setuid-sandbox',
|
|
242
|
+
'--disable-dev-shm-usage',
|
|
243
|
+
'--single-process',
|
|
244
|
+
'--disable-gpu',
|
|
245
|
+
'--no-zygote'
|
|
246
|
+
]
|
|
247
|
+
});
|
|
248
|
+
browser.on('disconnected', () => this.emit('debug', 'Browser disconnected'));
|
|
249
|
+
|
|
250
|
+
const page = await browser.newPage();
|
|
251
|
+
page.on('error', error => this.emit('error', `Page crashed: ${error.message}`));
|
|
252
|
+
page.on('pageerror', error => this.emit('error', `Browser error: ${error.message}`));
|
|
253
|
+
page.setDefaultTimeout(GLOBAL_TIMEOUT);
|
|
254
|
+
page.setDefaultNavigationTimeout(GLOBAL_TIMEOUT);
|
|
255
|
+
|
|
256
|
+
// Clear cookies before navigation
|
|
257
|
+
try {
|
|
258
|
+
const client = await page.createCDPSession();
|
|
259
|
+
await client.send('Network.clearBrowserCookies');
|
|
260
|
+
} catch (error) {
|
|
261
|
+
if (this.logError) this.emit('error', `Clear cookies error: ${error.message}`);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
try {
|
|
265
|
+
await page.goto(ApiUrlsHome.BaseURL, { waitUntil: ['domcontentloaded', 'networkidle2'], timeout: GLOBAL_TIMEOUT });
|
|
266
|
+
} catch (error) {
|
|
267
|
+
accountInfo.Info = `Navigation to ${ApiUrlsHome.BaseURL} failed: ${error.message}`;
|
|
268
|
+
return accountInfo;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Wait extra to ensure UI is rendered
|
|
272
|
+
await new Promise(r => setTimeout(r, 3000));
|
|
273
|
+
const loginBtn = await page.waitForSelector('button.btn--blue', { timeout: GLOBAL_TIMEOUT / 4 });
|
|
274
|
+
const loginText = await page.evaluate(el => el.textContent.trim(), loginBtn);
|
|
275
|
+
|
|
276
|
+
if (!['Zaloguj', 'Sign In', 'Login'].includes(loginText)) {
|
|
277
|
+
accountInfo.Info = `Login button ${loginText} not found`;
|
|
278
|
+
return accountInfo;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
await loginBtn.click();
|
|
282
|
+
await page.waitForNavigation({ waitUntil: 'networkidle2', timeout: GLOBAL_TIMEOUT / 3 });
|
|
283
|
+
|
|
284
|
+
const usernameInput = await page.$('input[name="username"]');
|
|
285
|
+
const passwordInput = await page.$('input[name="password"]');
|
|
286
|
+
if (!usernameInput || !passwordInput) {
|
|
287
|
+
accountInfo.Info = 'Username or password input not found';
|
|
288
|
+
return accountInfo;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
await page.type('input[name="username"]', this.user, { delay: 50 });
|
|
292
|
+
await page.type('input[name="password"]', this.passwd, { delay: 50 });
|
|
293
|
+
|
|
294
|
+
const submitButton = await page.$('input[type="submit"], button[type="submit"]');
|
|
295
|
+
if (!submitButton) {
|
|
296
|
+
accountInfo.Info = 'Submit button not found';
|
|
297
|
+
return accountInfo;
|
|
298
|
+
}
|
|
299
|
+
await Promise.race([Promise.all([submitButton.click(), page.waitForNavigation({ waitUntil: ['domcontentloaded', 'networkidle2'], timeout: GLOBAL_TIMEOUT / 4 })]), new Promise(r => setTimeout(r, GLOBAL_TIMEOUT / 3))]);
|
|
300
|
+
|
|
301
|
+
// Extract cookies
|
|
302
|
+
let c1 = null, c2 = null;
|
|
303
|
+
const start = Date.now();
|
|
304
|
+
while ((!c1 || !c2) && Date.now() - start < GLOBAL_TIMEOUT / 2) {
|
|
305
|
+
const cookies = await page.browserContext().cookies();
|
|
306
|
+
c1 = cookies.find(c => c.name === '__Secure-monitorandcontrolC1')?.value || c1;
|
|
307
|
+
c2 = cookies.find(c => c.name === '__Secure-monitorandcontrolC2')?.value || c2;
|
|
308
|
+
if (!c1 || !c2) await new Promise(r => setTimeout(r, 500));
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
if (!c1 || !c2) {
|
|
312
|
+
accountInfo.Info = 'Cookies C1/C2 missing';
|
|
313
|
+
return accountInfo;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const contextKey = [
|
|
317
|
+
'__Secure-monitorandcontrol=chunks-2',
|
|
318
|
+
`__Secure-monitorandcontrolC1=${c1}`,
|
|
319
|
+
`__Secure-monitorandcontrolC2=${c2}`
|
|
320
|
+
].join('; ');
|
|
321
|
+
this.contextKey = contextKey;
|
|
322
|
+
|
|
323
|
+
accountInfo.State = true;
|
|
324
|
+
accountInfo.Info = 'Connect to MELCloud Home Success';
|
|
325
|
+
accountInfo.ContextKey = contextKey;
|
|
326
|
+
await this.functions.saveData(this.accountFile, accountInfo);
|
|
327
|
+
|
|
328
|
+
return accountInfo;
|
|
329
|
+
} catch (error) {
|
|
330
|
+
throw new Error(`Connect error: ${error.message}`);
|
|
331
|
+
} finally {
|
|
332
|
+
if (browser) {
|
|
333
|
+
try { await browser.close(); }
|
|
334
|
+
catch (closeErr) {
|
|
335
|
+
if (this.logError) this.emit('error', `Failed to close Puppeteer: ${closeErr.message}`);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
async send(accountInfo) {
|
|
342
|
+
try {
|
|
343
|
+
const axiosInstance = axios.create({
|
|
344
|
+
method: 'POST',
|
|
345
|
+
baseURL: ApiUrlsHome.BaseURL,
|
|
346
|
+
timeout: 15000,
|
|
347
|
+
headers: {
|
|
348
|
+
'X-MitsContextKey': accountInfo.ContextKey,
|
|
349
|
+
'content-type': 'application/json'
|
|
350
|
+
}
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
const payload = { data: accountInfo.LoginData };
|
|
354
|
+
await axiosInstance(ApiUrlsHome.UpdateApplicationOptions, payload);
|
|
355
|
+
await this.functions.saveData(this.accountFile, accountInfo);
|
|
356
|
+
return true;
|
|
357
|
+
} catch (error) {
|
|
358
|
+
throw new Error(`Send data error: ${error.message}`);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
export default MelCloud;
|
|
364
|
+
|