homebridge-melcloud-control 4.2.3-beta.6 → 4.2.3-beta.61
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/config.schema.json +433 -25
- package/homebridge-ui/public/index.html +85 -15
- package/homebridge-ui/server.js +2 -1
- package/index.js +6 -5
- package/package.json +2 -2
- package/src/constants.js +48 -5
- package/src/deviceata.js +273 -155
- package/src/deviceatw.js +125 -119
- package/src/deviceerv.js +41 -42
- package/src/melcloud.js +27 -362
- package/src/melcloudata.js +35 -21
- package/src/melcloudatw.js +38 -29
- package/src/melclouderv.js +29 -22
- package/src/melcloudhome.js +403 -0
package/src/melcloudatw.js
CHANGED
|
@@ -52,12 +52,11 @@ 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
|
-
const deviceData = devicesData.find(device => device.DeviceID === this.deviceId);
|
|
55
|
+
const deviceData = devicesData.Devices.find(device => device.DeviceID === this.deviceId);
|
|
56
56
|
|
|
57
57
|
if (this.accountType === 'melcloudhome') {
|
|
58
|
-
deviceData.SerialNumber = deviceData.DeviceID || '4.0.0';
|
|
59
|
-
deviceData.Device.FirmwareAppVersion = deviceData.ConnectedInterfaceIdentifier || '4.0.0';
|
|
60
58
|
}
|
|
59
|
+
|
|
61
60
|
const safeConfig = {
|
|
62
61
|
...deviceData,
|
|
63
62
|
Headers: 'removed',
|
|
@@ -65,15 +64,15 @@ class MelCloudAtw extends EventEmitter {
|
|
|
65
64
|
if (this.logDebug) this.emit('debug', `Device Data: ${JSON.stringify(safeConfig, null, 2)}`);
|
|
66
65
|
|
|
67
66
|
//device
|
|
68
|
-
|
|
67
|
+
//device
|
|
68
|
+
const serialNumber = deviceData.SerialNumber || '4.0.0';
|
|
69
|
+
const firmwareAppVersion = deviceData.Device?.FirmwareAppVersion || '4.0.0';
|
|
69
70
|
const hasHotWaterTank = deviceData.Device?.HasHotWaterTank;
|
|
70
|
-
const firmwareAppVersion = deviceData.Device?.FirmwareAppVersion;
|
|
71
71
|
const hasZone2 = deviceData.Device?.HasZone2;
|
|
72
72
|
|
|
73
73
|
//units
|
|
74
74
|
const units = Array.isArray(deviceData.Device?.Units) ? deviceData.Device?.Units : [];
|
|
75
75
|
const unitsCount = units.length;
|
|
76
|
-
const manufacturer = 'Mitsubishi';
|
|
77
76
|
|
|
78
77
|
const { indoor, outdoor } = units.reduce((acc, unit) => {
|
|
79
78
|
const target = unit.IsIndoor ? 'indoor' : 'outdoor';
|
|
@@ -105,11 +104,11 @@ class MelCloudAtw extends EventEmitter {
|
|
|
105
104
|
|
|
106
105
|
//check state changes
|
|
107
106
|
const deviceDataHasNotChanged = JSON.stringify(devicesData) === JSON.stringify(this.devicesData);
|
|
108
|
-
if (deviceDataHasNotChanged)
|
|
107
|
+
if (deviceDataHasNotChanged) return;
|
|
109
108
|
this.devicesData = devicesData;
|
|
110
109
|
|
|
111
110
|
//emit info
|
|
112
|
-
this.emit('deviceInfo',
|
|
111
|
+
this.emit('deviceInfo', indoor.model, outdoor.model, serialNumber, firmwareAppVersion, hasHotWaterTank, hasZone2);
|
|
113
112
|
|
|
114
113
|
//emit state
|
|
115
114
|
this.emit('deviceState', deviceData);
|
|
@@ -120,7 +119,7 @@ class MelCloudAtw extends EventEmitter {
|
|
|
120
119
|
};
|
|
121
120
|
};
|
|
122
121
|
|
|
123
|
-
async send(accountType, displayType, deviceData,
|
|
122
|
+
async send(accountType, displayType, deviceData, flag) {
|
|
124
123
|
try {
|
|
125
124
|
|
|
126
125
|
//prevent to set out of range temp
|
|
@@ -141,7 +140,7 @@ class MelCloudAtw extends EventEmitter {
|
|
|
141
140
|
let path = '';
|
|
142
141
|
switch (accountType) {
|
|
143
142
|
case "melcloud":
|
|
144
|
-
deviceData.Device.EffectiveFlags =
|
|
143
|
+
deviceData.Device.EffectiveFlags = flag;
|
|
145
144
|
payload = {
|
|
146
145
|
DeviceID: deviceData.Device.DeviceID,
|
|
147
146
|
EffectiveFlags: deviceData.Device.EffectiveFlags,
|
|
@@ -176,39 +175,49 @@ class MelCloudAtw extends EventEmitter {
|
|
|
176
175
|
this.updateData(deviceData);
|
|
177
176
|
return true;
|
|
178
177
|
case "melcloudhome":
|
|
179
|
-
switch (
|
|
178
|
+
switch (flag) {
|
|
180
179
|
case 'holidaymode':
|
|
181
|
-
payload = {
|
|
180
|
+
payload = {
|
|
181
|
+
enabled: deviceData.HolidayMode.Enabled,
|
|
182
|
+
startDate: deviceData.HolidayMode.StartDate,
|
|
183
|
+
endDate: deviceData.HolidayMode.EndDate,
|
|
184
|
+
units: { "ATW": [deviceData.DeviceID] }
|
|
185
|
+
};
|
|
182
186
|
method = 'POST';
|
|
183
187
|
path = ApiUrlsHome.PostHolidayMode;
|
|
188
|
+
deviceData.Headers.Referer = ApiUrlsHome.Referers.PostHolidayMode.replace('deviceid', deviceData.DeviceID);
|
|
184
189
|
break;
|
|
185
190
|
case 'schedule':
|
|
186
191
|
payload = { enabled: deviceData.ScheduleEnabled };
|
|
187
192
|
method = 'PUT';
|
|
188
|
-
path = ApiUrlsHome.
|
|
193
|
+
path = ApiUrlsHome.PutScheduleEnabled.replace('deviceid', deviceData.DeviceID);
|
|
194
|
+
deviceData.Headers.Referer = ApiUrlsHome.Referers.PutScheduleEnabled.replace('deviceid', deviceData.DeviceID);
|
|
189
195
|
break;
|
|
190
196
|
default:
|
|
191
197
|
payload = {
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
198
|
+
power: deviceData.Device.Power,
|
|
199
|
+
setTemperatureZone1: deviceData.Device.SetTemperatureZone1,
|
|
200
|
+
setTemperatureZone2: deviceData.Device.SetTemperatureZone2,
|
|
201
|
+
operationMode: HeatPump.OperationModeMapEnumToString[deviceData.Device.OperationMode],
|
|
202
|
+
operationModeZone1: HeatPump.OperationModeMapEnumToString[deviceData.Device.OperationModeZone1],
|
|
203
|
+
operationModeZone2: HeatPump.OperationModeMapEnumToString[deviceData.Device.OperationModeZone2],
|
|
204
|
+
opetHeatFlowTemperatureZone1: deviceData.Device.SetHeatFlowTemperatureZone1,
|
|
205
|
+
setHeatFlowTemperatureZone2: deviceData.Device.SetHeatFlowTemperatureZone2,
|
|
206
|
+
setCoolFlowTemperatureZone1: deviceData.Device.SetCoolFlowTemperatureZone1,
|
|
207
|
+
setCoolFlowTemperatureZone2: deviceData.Device.SetCoolFlowTemperatureZone2,
|
|
208
|
+
setTankWaterTemperature: deviceData.Device.SetTankWaterTemperature,
|
|
209
|
+
forcedHotWaterMode: deviceData.Device.ForcedHotWaterMode,
|
|
210
|
+
ecoHotWater: deviceData.Device.EcoHotWater,
|
|
205
211
|
};
|
|
206
212
|
method = 'PUT';
|
|
207
|
-
path = ApiUrlsHome.
|
|
213
|
+
path = ApiUrlsHome.PutAtw.replace('deviceid', deviceData.DeviceID);
|
|
214
|
+
deviceData.Headers.Referer = ApiUrlsHome.Referers.PutDeviceSettings
|
|
208
215
|
break
|
|
209
216
|
}
|
|
210
217
|
|
|
211
|
-
|
|
218
|
+
deviceData.Headers['Content-Type'] = 'application/json; charset=utf-8';
|
|
219
|
+
deviceData.Headers.Origin = ApiUrlsHome.Origin;
|
|
220
|
+
if (this.logDebug) this.emit('debug', `Send Data: ${JSON.stringify(payload, null, 2)}, Headers: ${JSON.stringify(deviceData.Headers, null, 2)}`);
|
|
212
221
|
await axios(path, {
|
|
213
222
|
method: method,
|
|
214
223
|
baseURL: ApiUrlsHome.BaseURL,
|
|
@@ -233,7 +242,7 @@ class MelCloudAtw extends EventEmitter {
|
|
|
233
242
|
|
|
234
243
|
setTimeout(() => {
|
|
235
244
|
this.lock = false
|
|
236
|
-
},
|
|
245
|
+
}, 2500);
|
|
237
246
|
}
|
|
238
247
|
};
|
|
239
248
|
export default MelCloudAtw;
|
package/src/melclouderv.js
CHANGED
|
@@ -52,31 +52,28 @@ 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
|
-
const deviceData = devicesData.find(device => device.DeviceID === this.deviceId);
|
|
55
|
+
const deviceData = devicesData.Devices.find(device => device.DeviceID === this.deviceId);
|
|
56
56
|
|
|
57
57
|
if (this.accountType === 'melcloudhome') {
|
|
58
|
-
deviceData.SerialNumber = deviceData.DeviceID || '4.0.0';
|
|
59
|
-
deviceData.Device.FirmwareAppVersion = deviceData.ConnectedInterfaceIdentifier || '4.0.0';
|
|
60
|
-
|
|
61
58
|
//read default temps
|
|
62
59
|
const temps = await this.functions.readData(this.defaultTempsFile, true);
|
|
63
60
|
deviceData.Device.DefaultHeatingSetTemperature = temps?.defaultHeatingSetTemperature ?? 20;
|
|
64
61
|
deviceData.Device.DefaultCoolingSetTemperature = temps?.defaultCoolingSetTemperature ?? 24;
|
|
65
62
|
}
|
|
63
|
+
|
|
66
64
|
const safeConfig = {
|
|
67
65
|
...deviceData,
|
|
68
66
|
Headers: 'removed',
|
|
69
67
|
};
|
|
70
68
|
if (this.logDebug) this.emit('debug', `Device Data: ${JSON.stringify(safeConfig, null, 2)}`);
|
|
71
69
|
|
|
72
|
-
//
|
|
73
|
-
const serialNumber = deviceData.SerialNumber;
|
|
74
|
-
const firmwareAppVersion = deviceData.Device?.FirmwareAppVersion;
|
|
70
|
+
//device
|
|
71
|
+
const serialNumber = deviceData.SerialNumber || '4.0.0';
|
|
72
|
+
const firmwareAppVersion = deviceData.Device?.FirmwareAppVersion || '4.0.0';
|
|
75
73
|
|
|
76
74
|
//units
|
|
77
75
|
const units = Array.isArray(deviceData.Device?.Units) ? deviceData.Device?.Units : [];
|
|
78
76
|
const unitsCount = units.length;
|
|
79
|
-
const manufacturer = 'Mitsubishi';
|
|
80
77
|
|
|
81
78
|
const { indoor, outdoor } = units.reduce((acc, unit) => {
|
|
82
79
|
const target = unit.IsIndoor ? 'indoor' : 'outdoor';
|
|
@@ -112,7 +109,7 @@ class MelCloudErv extends EventEmitter {
|
|
|
112
109
|
this.devicesData = devicesData;
|
|
113
110
|
|
|
114
111
|
//emit info
|
|
115
|
-
this.emit('deviceInfo',
|
|
112
|
+
this.emit('deviceInfo', indoor.model, outdoor.model, serialNumber, firmwareAppVersion);
|
|
116
113
|
|
|
117
114
|
//emit state
|
|
118
115
|
this.emit('deviceState', deviceData);
|
|
@@ -123,7 +120,7 @@ class MelCloudErv extends EventEmitter {
|
|
|
123
120
|
};
|
|
124
121
|
};
|
|
125
122
|
|
|
126
|
-
async send(accountType, displayType, deviceData,
|
|
123
|
+
async send(accountType, displayType, deviceData, flag) {
|
|
127
124
|
try {
|
|
128
125
|
let method = null
|
|
129
126
|
let payload = {};
|
|
@@ -151,7 +148,7 @@ class MelCloudErv extends EventEmitter {
|
|
|
151
148
|
};
|
|
152
149
|
|
|
153
150
|
//device state
|
|
154
|
-
deviceData.Device.EffectiveFlags =
|
|
151
|
+
deviceData.Device.EffectiveFlags = flag;
|
|
155
152
|
payload = {
|
|
156
153
|
DeviceID: deviceData.Device.DeviceID,
|
|
157
154
|
EffectiveFlags: deviceData.Device.EffectiveFlags,
|
|
@@ -192,31 +189,41 @@ class MelCloudErv extends EventEmitter {
|
|
|
192
189
|
}
|
|
193
190
|
}
|
|
194
191
|
|
|
195
|
-
switch (
|
|
192
|
+
switch (flag) {
|
|
196
193
|
case 'holidaymode':
|
|
197
|
-
payload = {
|
|
194
|
+
payload = {
|
|
195
|
+
enabled: deviceData.HolidayMode.Enabled,
|
|
196
|
+
startDate: deviceData.HolidayMode.StartDate,
|
|
197
|
+
endDate: deviceData.HolidayMode.EndDate,
|
|
198
|
+
units: { "ERV": [deviceData.DeviceID] }
|
|
199
|
+
};
|
|
198
200
|
method = 'POST';
|
|
199
201
|
path = ApiUrlsHome.PostHolidayMode;
|
|
202
|
+
deviceData.Headers.Referer = ApiUrlsHome.Referers.PostHolidayMode.replace('deviceid', deviceData.DeviceID);
|
|
200
203
|
break;
|
|
201
204
|
case 'schedule':
|
|
202
205
|
payload = { enabled: deviceData.ScheduleEnabled };
|
|
203
206
|
method = 'PUT';
|
|
204
|
-
path = ApiUrlsHome.
|
|
207
|
+
path = ApiUrlsHome.PutScheduleEnabled.replace('deviceid', deviceData.DeviceID);
|
|
208
|
+
deviceData.Headers.Referer = ApiUrlsHome.Referers.PutScheduleEnabled.replace('deviceid', deviceData.DeviceID);
|
|
205
209
|
break;
|
|
206
210
|
default:
|
|
207
211
|
payload = {
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
212
|
+
power: deviceData.Device.Power,
|
|
213
|
+
setTemperature: deviceData.Device.SetTemperature,
|
|
214
|
+
setFanSpeed: String(deviceData.Device.SetFanSpeed),
|
|
215
|
+
operationMode: Ventilation.OperationModeMapEnumToString[deviceData.Device.OperationMode],
|
|
216
|
+
ventilationMode: Ventilation.VentilationModeMapEnumToString[deviceData.Device.VentilationMode],
|
|
213
217
|
};
|
|
214
218
|
method = 'PUT';
|
|
215
|
-
path = ApiUrlsHome.
|
|
219
|
+
path = ApiUrlsHome.PutErv.replace('deviceid', deviceData.DeviceID);
|
|
220
|
+
deviceData.Headers.Referer = ApiUrlsHome.Referers.PutDeviceSettings
|
|
216
221
|
break
|
|
217
222
|
}
|
|
218
223
|
|
|
219
|
-
|
|
224
|
+
deviceData.Headers['Content-Type'] = 'application/json; charset=utf-8';
|
|
225
|
+
deviceData.Headers.Origin = ApiUrlsHome.Origin;
|
|
226
|
+
if (this.logDebug) this.emit('debug', `Send Data: ${JSON.stringify(payload, null, 2)}, Headers: ${JSON.stringify(deviceData.Headers, null, 2)}`);
|
|
220
227
|
await axios(path, {
|
|
221
228
|
method: method,
|
|
222
229
|
baseURL: ApiUrlsHome.BaseURL,
|
|
@@ -241,7 +248,7 @@ class MelCloudErv extends EventEmitter {
|
|
|
241
248
|
|
|
242
249
|
setTimeout(() => {
|
|
243
250
|
this.lock = false
|
|
244
|
-
},
|
|
251
|
+
}, 2500);
|
|
245
252
|
}
|
|
246
253
|
};
|
|
247
254
|
export default MelCloudErv;
|
|
@@ -0,0 +1,403 @@
|
|
|
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, LanguageLocaleMap } 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.headers = {};
|
|
26
|
+
|
|
27
|
+
this.functions = new Functions(this.logWarn, this.logError, this.logDebug)
|
|
28
|
+
.on('warn', warn => this.emit('warn', warn))
|
|
29
|
+
.on('error', error => this.emit('error', error))
|
|
30
|
+
.on('debug', debug => this.emit('debug', debug));
|
|
31
|
+
|
|
32
|
+
if (pluginStart) {
|
|
33
|
+
//lock flags
|
|
34
|
+
this.locks = {
|
|
35
|
+
connect: false,
|
|
36
|
+
checkDevicesList: false
|
|
37
|
+
};
|
|
38
|
+
this.impulseGenerator = new ImpulseGenerator()
|
|
39
|
+
.on('connect', () => this.handleWithLock('connect', async () => {
|
|
40
|
+
await this.connect();
|
|
41
|
+
}))
|
|
42
|
+
.on('checkDevicesList', () => this.handleWithLock('checkDevicesList', async () => {
|
|
43
|
+
await this.checkDevicesList();
|
|
44
|
+
}))
|
|
45
|
+
.on('state', (state) => {
|
|
46
|
+
this.emit(state ? 'success' : 'warn', `Impulse generator ${state ? 'started' : 'stopped'}`);
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async handleWithLock(lockKey, fn) {
|
|
52
|
+
if (this.locks[lockKey]) return;
|
|
53
|
+
|
|
54
|
+
this.locks[lockKey] = true;
|
|
55
|
+
try {
|
|
56
|
+
await fn();
|
|
57
|
+
} catch (error) {
|
|
58
|
+
this.emit('error', `Inpulse generator error: ${error}`);
|
|
59
|
+
} finally {
|
|
60
|
+
this.locks[lockKey] = false;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// MELCloud Home
|
|
65
|
+
async checkScenesList() {
|
|
66
|
+
try {
|
|
67
|
+
if (this.logDebug) this.emit('debug', `Scanning for scenes`);
|
|
68
|
+
const listScenesData = await axios(ApiUrlsHome.GetUserScenes, {
|
|
69
|
+
method: 'GET',
|
|
70
|
+
baseURL: ApiUrlsHome.BaseURL,
|
|
71
|
+
timeout: 25000,
|
|
72
|
+
headers: this.headers
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
const scenesList = listScenesData.data;
|
|
76
|
+
if (this.logDebug) this.emit('debug', `Scenes: ${JSON.stringify(scenesList, null, 2)}`);
|
|
77
|
+
|
|
78
|
+
const capitalizeKeysDeep = obj => {
|
|
79
|
+
if (Array.isArray(obj)) {
|
|
80
|
+
return obj.map(item => capitalizeKeysDeep(item));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (obj && typeof obj === 'object') {
|
|
84
|
+
return Object.fromEntries(
|
|
85
|
+
Object.entries(obj).map(([key, value]) => [
|
|
86
|
+
key.charAt(0).toUpperCase() + key.slice(1),
|
|
87
|
+
capitalizeKeysDeep(value)
|
|
88
|
+
])
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return obj;
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
return capitalizeKeysDeep(scenesList);
|
|
96
|
+
} catch (error) {
|
|
97
|
+
throw new Error(`Check scenes list error: ${error.message}`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async checkDevicesList() {
|
|
102
|
+
try {
|
|
103
|
+
const devicesList = { State: false, Info: null, Devices: [], Scenes: [] }
|
|
104
|
+
if (this.logDebug) this.emit('debug', `Scanning for devices`);
|
|
105
|
+
const listDevicesData = await axios(ApiUrlsHome.GetUserContext, {
|
|
106
|
+
method: 'GET',
|
|
107
|
+
baseURL: ApiUrlsHome.BaseURL,
|
|
108
|
+
timeout: 25000,
|
|
109
|
+
headers: this.headers
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
const userContext = listDevicesData.data;
|
|
113
|
+
const buildings = userContext.buildings ?? [];
|
|
114
|
+
const guestBuildings = userContext.guestBuildings ?? [];
|
|
115
|
+
const buildingsList = [...buildings, ...guestBuildings];
|
|
116
|
+
if (this.logDebug) this.emit('debug', `Buildings: ${JSON.stringify(buildingsList, null, 2)}`);
|
|
117
|
+
|
|
118
|
+
if (!buildingsList) {
|
|
119
|
+
devicesList.Info = 'No building found'
|
|
120
|
+
return devicesList;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
await this.functions.saveData(this.buildingsFile, userContext);
|
|
124
|
+
if (this.logDebug) this.emit('debug', `Buildings list saved`);
|
|
125
|
+
|
|
126
|
+
const devices = buildingsList.flatMap(building => {
|
|
127
|
+
// Funkcja kapitalizująca klucze obiektu
|
|
128
|
+
const capitalizeKeys = obj =>
|
|
129
|
+
Object.fromEntries(
|
|
130
|
+
Object.entries(obj).map(([key, value]) => [
|
|
131
|
+
key.charAt(0).toUpperCase() + key.slice(1),
|
|
132
|
+
value
|
|
133
|
+
])
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
// Rekurencyjna kapitalizacja kluczy w obiekcie lub tablicy
|
|
137
|
+
const capitalizeKeysDeep = obj => {
|
|
138
|
+
if (Array.isArray(obj)) return obj.map(capitalizeKeysDeep);
|
|
139
|
+
if (obj && typeof obj === 'object') {
|
|
140
|
+
return Object.fromEntries(
|
|
141
|
+
Object.entries(obj).map(([key, value]) => [
|
|
142
|
+
key.charAt(0).toUpperCase() + key.slice(1),
|
|
143
|
+
capitalizeKeysDeep(value)
|
|
144
|
+
])
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
return obj;
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
// Funkcja tworząca finalny obiekt Device
|
|
151
|
+
const createDevice = (device, type) => {
|
|
152
|
+
// Settings już kapitalizowane w nazwach
|
|
153
|
+
const settingsArray = device.Settings || [];
|
|
154
|
+
|
|
155
|
+
const settingsObject = Object.fromEntries(
|
|
156
|
+
settingsArray.map(({ name, value }) => {
|
|
157
|
+
let parsedValue = value;
|
|
158
|
+
if (value === "True") parsedValue = true;
|
|
159
|
+
else if (value === "False") parsedValue = false;
|
|
160
|
+
else if (!isNaN(value) && value !== "") parsedValue = Number(value);
|
|
161
|
+
|
|
162
|
+
const key = name.charAt(0).toUpperCase() + name.slice(1);
|
|
163
|
+
return [key, parsedValue];
|
|
164
|
+
})
|
|
165
|
+
);
|
|
166
|
+
|
|
167
|
+
// Scal Capabilities + Settings + DeviceType w Device
|
|
168
|
+
const deviceObject = {
|
|
169
|
+
...capitalizeKeys(device.Capabilities || {}),
|
|
170
|
+
...settingsObject,
|
|
171
|
+
DeviceType: type,
|
|
172
|
+
FirmwareAppVersion: device.ConnectedInterfaceIdentifier,
|
|
173
|
+
IsConnected: device.IsConnected
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
// Kapitalizacja brakujących obiektów/tablic
|
|
177
|
+
if (device.FrostProtection) device.FrostProtection = { ...capitalizeKeys(device.FrostProtection || {}) };
|
|
178
|
+
if (device.OverheatProtection) device.OverheatProtection = { ...capitalizeKeys(device.OverheatProtection || {}) };
|
|
179
|
+
if (device.HolidayMode) device.HolidayMode = { ...capitalizeKeys(device.HolidayMode || {}) };
|
|
180
|
+
if (Array.isArray(device.Schedule)) device.Schedule = device.Schedule.map(capitalizeKeysDeep || []);
|
|
181
|
+
|
|
182
|
+
// Usuń stare pola Settings i Capabilities
|
|
183
|
+
const { Settings, Capabilities, Id, GivenDisplayName, ...rest } = device;
|
|
184
|
+
|
|
185
|
+
return {
|
|
186
|
+
...rest,
|
|
187
|
+
Type: type,
|
|
188
|
+
DeviceID: Id,
|
|
189
|
+
DeviceName: GivenDisplayName,
|
|
190
|
+
SerialNumber: Id,
|
|
191
|
+
Device: deviceObject,
|
|
192
|
+
Headers: this.headers
|
|
193
|
+
};
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
return [
|
|
197
|
+
...(building.airToAirUnits || []).map(d => createDevice(capitalizeKeys(d), 0)),
|
|
198
|
+
...(building.airToWaterUnits || []).map(d => createDevice(capitalizeKeys(d), 1)),
|
|
199
|
+
...(building.airToVentilationUnits || []).map(d => createDevice(capitalizeKeys(d), 3))
|
|
200
|
+
];
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
const devicesCount = devices.length;
|
|
204
|
+
if (devicesCount === 0) {
|
|
205
|
+
devicesList.Info = 'No devices found'
|
|
206
|
+
return devicesList;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const scenes = await this.checkScenesList();
|
|
210
|
+
|
|
211
|
+
devicesList.State = true;
|
|
212
|
+
devicesList.Info = `Found ${devicesCount} devices`;
|
|
213
|
+
devicesList.Devices = devices;
|
|
214
|
+
devicesList.Scenes = scenes;
|
|
215
|
+
|
|
216
|
+
await this.functions.saveData(this.devicesFile, devicesList);
|
|
217
|
+
if (this.logDebug) this.emit('debug', `${devicesCount} devices saved`);
|
|
218
|
+
|
|
219
|
+
return devicesList;
|
|
220
|
+
} catch (error) {
|
|
221
|
+
if (error.response?.status === 401) {
|
|
222
|
+
if (this.logWarn) this.emit('warn', 'Check devices list not possible, cookies expired, trying to get new.');
|
|
223
|
+
await this.connect();
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
throw new Error(`Check devices list error: ${error.message}`);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
async connect() {
|
|
232
|
+
if (this.logDebug) this.emit('debug', 'Connecting to MELCloud Home');
|
|
233
|
+
const GLOBAL_TIMEOUT = 90000;
|
|
234
|
+
|
|
235
|
+
let browser;
|
|
236
|
+
try {
|
|
237
|
+
const accountInfo = { State: false, Info: '', Headers: {}, UseFahrenheit: false };
|
|
238
|
+
let chromiumPath = await this.functions.ensureChromiumInstalled();
|
|
239
|
+
|
|
240
|
+
// === Fallback to Puppeteer's built-in Chromium ===
|
|
241
|
+
if (!chromiumPath) {
|
|
242
|
+
try {
|
|
243
|
+
const puppeteerPath = puppeteer.executablePath();
|
|
244
|
+
if (puppeteerPath && fs.existsSync(puppeteerPath)) {
|
|
245
|
+
chromiumPath = puppeteerPath;
|
|
246
|
+
if (this.logDebug) this.emit('debug', `Using puppeteer Chromium at ${chromiumPath}`);
|
|
247
|
+
}
|
|
248
|
+
} catch { }
|
|
249
|
+
} else {
|
|
250
|
+
if (this.logDebug) this.emit('debug', `Using system Chromium at ${chromiumPath}`);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
if (!chromiumPath) {
|
|
254
|
+
accountInfo.Info = 'Chromium not found on Your device, please install it manually and try again';
|
|
255
|
+
return accountInfo;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Verify executable works
|
|
259
|
+
try {
|
|
260
|
+
const { stdout } = await execPromise(`"${chromiumPath}" --version`);
|
|
261
|
+
if (this.logDebug) this.emit('debug', `Chromium detected: ${stdout.trim()}`);
|
|
262
|
+
} catch (error) {
|
|
263
|
+
accountInfo.Info = `Chromium found at ${chromiumPath}, but cannot be executed: ${error.message}`;
|
|
264
|
+
return accountInfo;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (this.logDebug) this.emit('debug', `Launching Chromium...`);
|
|
268
|
+
browser = await puppeteer.launch({
|
|
269
|
+
headless: true,
|
|
270
|
+
executablePath: chromiumPath,
|
|
271
|
+
timeout: GLOBAL_TIMEOUT,
|
|
272
|
+
args: [
|
|
273
|
+
'--no-sandbox',
|
|
274
|
+
'--disable-setuid-sandbox',
|
|
275
|
+
'--disable-dev-shm-usage',
|
|
276
|
+
'--single-process',
|
|
277
|
+
'--disable-gpu',
|
|
278
|
+
'--no-zygote'
|
|
279
|
+
]
|
|
280
|
+
});
|
|
281
|
+
browser.on('disconnected', () => this.emit('debug', 'Browser disconnected'));
|
|
282
|
+
|
|
283
|
+
const page = await browser.newPage();
|
|
284
|
+
page.on('error', error => this.emit('error', `Page crashed: ${error.message}`));
|
|
285
|
+
page.on('pageerror', error => this.emit('error', `Browser error: ${error.message}`));
|
|
286
|
+
page.setDefaultTimeout(GLOBAL_TIMEOUT);
|
|
287
|
+
page.setDefaultNavigationTimeout(GLOBAL_TIMEOUT);
|
|
288
|
+
|
|
289
|
+
// Clear cookies before navigation
|
|
290
|
+
try {
|
|
291
|
+
const client = await page.createCDPSession();
|
|
292
|
+
await client.send('Network.clearBrowserCookies');
|
|
293
|
+
} catch (error) {
|
|
294
|
+
if (this.logError) this.emit('error', `Clear cookies error: ${error.message}`);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
try {
|
|
298
|
+
await page.goto(ApiUrlsHome.BaseURL, { waitUntil: ['domcontentloaded', 'networkidle2'], timeout: GLOBAL_TIMEOUT });
|
|
299
|
+
} catch (error) {
|
|
300
|
+
accountInfo.Info = `Navigation to ${ApiUrlsHome.BaseURL} failed: ${error.message}`;
|
|
301
|
+
return accountInfo;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// Wait extra to ensure UI is rendered
|
|
305
|
+
await new Promise(r => setTimeout(r, 3000));
|
|
306
|
+
const loginBtn = await page.waitForSelector('button.btn--blue', { timeout: GLOBAL_TIMEOUT / 4 });
|
|
307
|
+
const loginText = await page.evaluate(el => el.textContent.trim(), loginBtn);
|
|
308
|
+
|
|
309
|
+
if (!['Zaloguj', 'Sign In', 'Login'].includes(loginText)) {
|
|
310
|
+
accountInfo.Info = `Login button ${loginText} not found`;
|
|
311
|
+
return accountInfo;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
await loginBtn.click();
|
|
315
|
+
await page.waitForNavigation({ waitUntil: 'networkidle2', timeout: GLOBAL_TIMEOUT / 3 });
|
|
316
|
+
|
|
317
|
+
const usernameInput = await page.$('input[name="username"]');
|
|
318
|
+
const passwordInput = await page.$('input[name="password"]');
|
|
319
|
+
if (!usernameInput || !passwordInput) {
|
|
320
|
+
accountInfo.Info = 'Username or password input not found';
|
|
321
|
+
return accountInfo;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
await page.type('input[name="username"]', this.user, { delay: 50 });
|
|
325
|
+
await page.type('input[name="password"]', this.passwd, { delay: 50 });
|
|
326
|
+
|
|
327
|
+
const submitButton = await page.$('input[type="submit"], button[type="submit"]');
|
|
328
|
+
if (!submitButton) {
|
|
329
|
+
accountInfo.Info = 'Submit button not found';
|
|
330
|
+
return accountInfo;
|
|
331
|
+
}
|
|
332
|
+
await Promise.race([Promise.all([submitButton.click(), page.waitForNavigation({ waitUntil: ['domcontentloaded', 'networkidle2'], timeout: GLOBAL_TIMEOUT / 4 })]), new Promise(r => setTimeout(r, GLOBAL_TIMEOUT / 3))]);
|
|
333
|
+
|
|
334
|
+
// Extract cookies
|
|
335
|
+
let c1 = null, c2 = null;
|
|
336
|
+
const start = Date.now();
|
|
337
|
+
while ((!c1 || !c2) && Date.now() - start < GLOBAL_TIMEOUT / 2) {
|
|
338
|
+
const cookies = await page.browserContext().cookies();
|
|
339
|
+
c1 = cookies.find(c => c.name === '__Secure-monitorandcontrolC1')?.value || c1;
|
|
340
|
+
c2 = cookies.find(c => c.name === '__Secure-monitorandcontrolC2')?.value || c2;
|
|
341
|
+
if (!c1 || !c2) await new Promise(r => setTimeout(r, 500));
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
if (!c1 || !c2) {
|
|
345
|
+
accountInfo.Info = 'Cookies C1/C2 missing';
|
|
346
|
+
return accountInfo;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
const cookies = [
|
|
350
|
+
'__Secure-monitorandcontrol=chunks-2',
|
|
351
|
+
`__Secure-monitorandcontrolC1=${c1}`,
|
|
352
|
+
`__Secure-monitorandcontrolC2=${c2}`
|
|
353
|
+
].join('; ');
|
|
354
|
+
|
|
355
|
+
this.headers = {
|
|
356
|
+
'Accept': '*/*',
|
|
357
|
+
'Accept-Encoding': 'gzip, deflate, br',
|
|
358
|
+
'Accept-Language': LanguageLocaleMap[this.language],
|
|
359
|
+
'Cookie': cookies,
|
|
360
|
+
'Priority': 'u=3, i',
|
|
361
|
+
'Referer': ApiUrlsHome.Dashboard,
|
|
362
|
+
'Sec-Fetch-Dest': 'empty',
|
|
363
|
+
'Sec-Fetch-Mode': 'cors',
|
|
364
|
+
'Sec-Fetch-Site': 'same-origin',
|
|
365
|
+
'x-csrf': '1'
|
|
366
|
+
};
|
|
367
|
+
|
|
368
|
+
accountInfo.State = true;
|
|
369
|
+
accountInfo.Info = 'Connect to MELCloud Home Success';
|
|
370
|
+
accountInfo.Headers = this.headers;
|
|
371
|
+
await this.functions.saveData(this.accountFile, accountInfo);
|
|
372
|
+
|
|
373
|
+
return accountInfo;
|
|
374
|
+
} catch (error) {
|
|
375
|
+
throw new Error(`Connect error: ${error.message}`);
|
|
376
|
+
} finally {
|
|
377
|
+
if (browser) {
|
|
378
|
+
try { await browser.close(); }
|
|
379
|
+
catch (closeErr) {
|
|
380
|
+
if (this.logError) this.emit('error', `Failed to close Puppeteer: ${closeErr.message}`);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
async send(accountInfo) {
|
|
387
|
+
try {
|
|
388
|
+
await axios(ApiUrlsHome.UpdateApplicationOptions, {
|
|
389
|
+
method: 'POST',
|
|
390
|
+
baseURL: ApiUrlsHome.BaseURL,
|
|
391
|
+
timeout: 15000,
|
|
392
|
+
headers: accountInfo.Headers
|
|
393
|
+
});
|
|
394
|
+
await this.functions.saveData(this.accountFile, accountInfo);
|
|
395
|
+
return true;
|
|
396
|
+
} catch (error) {
|
|
397
|
+
throw new Error(`Send data error: ${error.message}`);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
export default MelCloud;
|
|
403
|
+
|