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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.js CHANGED
@@ -194,7 +194,7 @@ class MelCloudPlatform {
194
194
 
195
195
  //start impulse generators\
196
196
  await configuredDevice.startStopImpulseGenerator(true, [{ name: 'checkState', sampling: deviceRefreshInterval }]);
197
- const timmers = accountType === 'melcloudhome' ? [{ name: 'connect', sampling: 3000000 }, { name: 'checkDevicesList', sampling: 3000 }] : [{ name: 'checkDevicesList', sampling: refreshInterval }];
197
+ const timmers = accountType === 'melcloudhome' ? [{ name: 'connect', sampling: 3600000 }, { name: 'checkDevicesList', sampling: 3000 }] : [{ name: 'checkDevicesList', sampling: refreshInterval }];
198
198
  await melCloud.impulseGenerator.state(true, timmers, false);
199
199
 
200
200
  //stop impulse generator
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "displayName": "MELCloud Control",
3
3
  "name": "homebridge-melcloud-control",
4
- "version": "4.3.0-beta.4",
4
+ "version": "4.3.0-beta.41",
5
5
  "description": "Homebridge plugin to control Mitsubishi Air Conditioner, Heat Pump and Energy Recovery Ventilation.",
6
6
  "license": "MIT",
7
7
  "author": "grzegorz914",
package/src/constants.js CHANGED
@@ -12,6 +12,7 @@ export const ApiUrls = {
12
12
  SetAta: "/Device/SetAta",
13
13
  SetAtw: "/Device/SetAtw",
14
14
  SetErv: "/Device/SetErv",
15
+ GetRefreshUnit: "/Device/RequestRefresh?id=deviceid",
15
16
  UpdateApplicationOptions: "/User/UpdateApplicationOptions",
16
17
  HolidayModeUpdate: "/HolidayMode/Update",
17
18
  EnergyCostReport: "/EnergyCost/Report",
@@ -42,7 +43,8 @@ export const ApiUrlsHome = {
42
43
  PutDeviceSettings: "https://melcloudhome.com/dashboard",
43
44
  PutScheduleEnabled: "https://melcloudhome.com/ata/deviceid/schedule",
44
45
  },
45
- Origin: "https://melcloudhome.com"
46
+ Origin: "https://melcloudhome.com",
47
+ WebSocketURL: "wss://ws.melcloudhome.com/?hash="
46
48
  };
47
49
 
48
50
  export const DeviceType = [
package/src/deviceata.js CHANGED
@@ -1079,7 +1079,7 @@ class DeviceAta extends EventEmitter {
1079
1079
  //control
1080
1080
  if (button.displayType > 3) {
1081
1081
  if (this.logDebug) this.emit('debug', `Prepare button control ${name} service`);
1082
- const buttonControlService = new serviceType(serviceName, `buttonControlService${deviceId} ${i}`);
1082
+ const buttonControlService = new Service.Switch(serviceName, `buttonControlService${deviceId} ${i}`);
1083
1083
  buttonControlService.addOptionalCharacteristic(Characteristic.ConfiguredName);
1084
1084
  buttonControlService.setCharacteristic(Characteristic.ConfiguredName, serviceName);
1085
1085
  buttonControlService.getCharacteristic(Characteristic.On)
package/src/deviceatw.js CHANGED
@@ -1346,7 +1346,7 @@ class DeviceAtw extends EventEmitter {
1346
1346
  //control
1347
1347
  if (button.displayType > 3) {
1348
1348
  if (this.logDebug) this.emit('debug', `Prepare button control ${name} service`);
1349
- const buttonControlService = new serviceType(serviceName1, `buttonControlService${deviceId} ${i}`);
1349
+ const buttonControlService = new Service.Switch(serviceName1, `buttonControlService${deviceId} ${i}`);
1350
1350
  buttonControlService.addOptionalCharacteristic(Characteristic.ConfiguredName);
1351
1351
  buttonControlService.setCharacteristic(Characteristic.ConfiguredName, serviceName1);
1352
1352
  buttonControlService.getCharacteristic(Characteristic.On)
package/src/deviceerv.js CHANGED
@@ -1006,7 +1006,7 @@ class DeviceErv extends EventEmitter {
1006
1006
  //control
1007
1007
  if (button.displayType > 3) {
1008
1008
  if (this.logDebug) this.emit('debug', `Prepare button control ${name} service`);
1009
- const buttonControlService = new serviceType(serviceName1, `buttonControlService${deviceId} ${i}`);
1009
+ const buttonControlService = new Service.Switch(serviceName1, `buttonControlService${deviceId} ${i}`);
1010
1010
  buttonControlService.addOptionalCharacteristic(Characteristic.ConfiguredName);
1011
1011
  buttonControlService.setCharacteristic(Characteristic.ConfiguredName, serviceName1);
1012
1012
  buttonControlService.getCharacteristic(Characteristic.On)
@@ -26,7 +26,10 @@ class MelCloudAta extends EventEmitter {
26
26
  //set default values
27
27
  this.deviceData = {};
28
28
  this.headers = {};
29
- this.start = true;
29
+ this.socket = null;
30
+ this.connecting = false;
31
+ this.socketConnected = false;
32
+ this.reconnectDelay = 1000;
30
33
 
31
34
  //lock flag
32
35
  this.locks = false;
@@ -52,6 +55,33 @@ class MelCloudAta extends EventEmitter {
52
55
  }
53
56
  }
54
57
 
58
+ cleanupSocket() {
59
+ if (this.heartbeat) {
60
+ clearInterval(this.heartbeat);
61
+ this.heartbeat = null;
62
+ }
63
+
64
+ if (this.socket) {
65
+ try { this.socket.close(); } catch { }
66
+ this.socket = null;
67
+ }
68
+
69
+ this.socketConnected = false;
70
+ }
71
+
72
+ reconnect() {
73
+ this.cleanupSocket();
74
+
75
+ const delay = Math.min(this.reconnectDelay, 30000); // max 30s
76
+ if (this.logDebug) this.emit('debug', `Reconnect in ${delay}ms`);
77
+
78
+ setTimeout(() => {
79
+ this.reconnectDelay *= 2;
80
+ this.reconnectDelay = Math.max(1000, this.reconnectDelay);
81
+ this.connectSocket(deviceData);
82
+ }, delay);
83
+ };
84
+
55
85
  async checkState() {
56
86
  try {
57
87
 
@@ -67,40 +97,83 @@ class MelCloudAta extends EventEmitter {
67
97
  deviceData.Device.ActualFanSpeed = AirConditioner.FanSpeedMapStringToEnum[deviceData.Device.ActualFanSpeed] ?? deviceData.Device.ActualFanSpeed;
68
98
  deviceData.Device.SetFanSpeed = AirConditioner.FanSpeedMapStringToEnum[deviceData.Device.SetFanSpeed] ?? deviceData.Device.SetFanSpeed;
69
99
  deviceData.Device.VaneVerticalDirection = AirConditioner.VaneVerticalDirectionMapStringToEnum[deviceData.Device.VaneVerticalDirection] ?? deviceData.Device.VaneVerticalDirection;
70
- deviceData.Device.VaneHorizontalDirection = AirConditioner.VaneHorizontalDirectionMapStringToEnum[deviceData.Device.VaneHorizontalDirection] ?? deviceData.Device.VaneHorizontalDirection;
100
+ deviceData.Device.VaneHorizontalDirection = AirConditioner.VaneHorizontalDirectionMapStringToEnum[deviceData.Device.VaneHorizontalDirection] ?? deviceData.Device.VaneHorizontalDirection
71
101
 
72
102
  //read default temps
73
103
  const temps = await this.functions.readData(this.defaultTempsFile, true);
74
104
  deviceData.Device.DefaultHeatingSetTemperature = temps?.defaultHeatingSetTemperature ?? 20;
75
105
  deviceData.Device.DefaultCoolingSetTemperature = temps?.defaultCoolingSetTemperature ?? 24;
76
- }
77
- if (this.logDebug) this.emit('debug', `Device Data: ${JSON.stringify(deviceData, null, 2)}`);
78
106
 
79
- if (this.start) {
80
- const hash = '2db32d6f-c19c-4b1f-a0dd-1915420a5152';
107
+ if (!this.connecting && !this.socketConnected) {
108
+ this.connecting = true;
109
+
110
+ const url = `${ApiUrlsHome.WebSocketURL}${devicesData.WsHeaders.hash}`;
111
+ try {
112
+ const socket = new WebSocket(url, { headers: devicesData.WsHeaders.headers })
113
+ .on('error', (error) => {
114
+ if (this.logError) this.emit('error', `Socket error: ${error}`);
115
+ })
116
+ .on('close', () => {
117
+ if (this.logDebug) this.emit('debug', `Socket closed`);
118
+ this.reconnect();
119
+ })
120
+ .on('open', () => {
121
+ this.socket = socket;
122
+ this.socketConnected = true;
123
+ this.connecting = false;
124
+ this.emit('success', `Socket Connect Success`);
125
+
126
+ // heartbeat
127
+ this.heartbeat = setInterval(() => {
128
+ if (socket.readyState === socket.OPEN) {
129
+ if (!this.logDebug) this.emit('warn', `Socket send heartbeat`);
130
+ socket.ping();
131
+ }
132
+ }, 30000);
133
+ })
134
+ .on('pong', () => {
135
+ if (!this.logDebug) this.emit('warn', `Socket received heartbeat`);
136
+ })
137
+ .on('message', (message) => {
138
+ const parsedMessage = JSON.parse(message);
139
+ if (!this.logDebug) this.emit('warn', `Incoming message: ${JSON.stringify(parsedMessage, null, 2)}`);
140
+
141
+ const messageData = parsedMessage?.[0]?.Data;
142
+ const unitId = messageData?.id;
143
+
144
+ switch (unitId) {
145
+ case this.deviceId:
146
+ const messageType = parsedMessage[0].messageType;
147
+ switch (messageType) {
148
+ case 'unitStateChanged':
149
+ const settings = Object.fromEntries(messageData.settings.map(({ name, value }) => {
150
+ let parsedValue = value;
151
+ if (value === "True") parsedValue = true;
152
+ else if (value === "False") parsedValue = false;
153
+ else if (!isNaN(value) && value !== "") parsedValue = Number(value);
154
+ return [name, parsedValue];
155
+ }));
156
+ Object.assign(deviceData.Device, settings);
157
+
158
+ this.emit('deviceState', deviceData);
159
+ break;
160
+ case 'unitWiFiChanged':
161
+ const state = Object.fromEntries(messageData.settings.map(s => [s.name, s.value]));
162
+ Object.assign(deviceData.Device, state);
81
163
 
82
- const ws = new WebSocket(`wss://ws.melcloudhome.com/?hash=${hash}`, {
83
- headers: {
84
- 'Origin': 'https://melcloudhome.com',
85
- 'Pragma': 'no-cache',
86
- 'Cache-Control': 'no-cache'
164
+ this.emit('deviceState', deviceData);
165
+ break;
166
+ }
167
+ break;
168
+ }
169
+ });
170
+ } catch (error) {
171
+ if (this.logError) this.emit('error', `Socket connection failed: ${error}`);
172
+ this.reconnect();
87
173
  }
88
- });
89
- ws.on('open', () => {
90
- if (!this.logDebug) this.emit('warn', `Connected to MelCloudHome WebSocket`);
91
- this.start = false;
92
- })
93
- .on('message', (data) => {
94
- if (!this.logDebug) this.emit('warn', `Incoming message:', ${data.toString()}`);
95
- })
96
- .on('close', () => {
97
- if (!this.logDebug) this.emit('warn', `Connection closed`);
98
- })
99
- .on('error', (error) => {
100
- if (!this.logDebug) this.emit('warn', `Connected error: ${error}`);
101
- });
174
+ }
102
175
  }
103
-
176
+ if (this.logDebug) this.emit('debug', `Device Data: ${JSON.stringify(deviceData, null, 2)}`);
104
177
 
105
178
  //device
106
179
  const serialNumber = deviceData.SerialNumber || '4.0.0';
@@ -163,6 +236,7 @@ class MelCloudAta extends EventEmitter {
163
236
  let method = null
164
237
  let payload = {};
165
238
  let path = '';
239
+ let updateState = true;
166
240
  switch (accountType) {
167
241
  case "melcloud":
168
242
  switch (flag) {
@@ -207,7 +281,7 @@ class MelCloudAta extends EventEmitter {
207
281
  headers: this.headers,
208
282
  data: payload
209
283
  });
210
- this.updateData(deviceData);
284
+ this.updateData(deviceData, updateState);
211
285
  return true;
212
286
  case "melcloudhome":
213
287
  switch (flag) {
@@ -281,7 +355,28 @@ class MelCloudAta extends EventEmitter {
281
355
  method = 'PUT';
282
356
  path = ApiUrlsHome.PutAta.replace('deviceid', deviceData.DeviceID);
283
357
  this.headers.Referer = ApiUrlsHome.Referers.PutDeviceSettings;
284
- break
358
+ updateState = false;
359
+
360
+ const settings = [
361
+ { "name": "Power", "value": deviceData.Device.Power },
362
+ { "name": "SetTemperature", "value": deviceData.Device.SetTemperature },
363
+ { "name": "SetFanSpeed", "value": deviceData.Device.SetFanSpeed },
364
+ { "name": "OperationMode", "value": deviceData.Device.OperationMode },
365
+ { "name": "VaneHorizontalDirection", "value": deviceData.Device.VaneHorizontalDirection },
366
+ { "name": "VaneVerticalDirection", "value": deviceData.Device.VaneVerticalDirection }
367
+ ]
368
+ const payload1 = [
369
+ {
370
+ messageType: "unitStateChanged",
371
+ Data: {
372
+ id: deviceData.DeviceID, // Twój ID jednostki
373
+ unitType: "ata", // tak jak incoming
374
+ settings: settings // czyli tablica: [{ name, value }, ...]
375
+ }
376
+ }
377
+ ];
378
+ this.socket.send(JSON.stringify(payload1));
379
+ return;
285
380
  }
286
381
 
287
382
  this.headers['Content-Type'] = 'application/json; charset=utf-8';
@@ -295,7 +390,7 @@ class MelCloudAta extends EventEmitter {
295
390
  data: payload
296
391
  });
297
392
 
298
- this.updateData(deviceData);
393
+ this.updateData(deviceData, updateState);
299
394
  return true;
300
395
  default:
301
396
  return;
@@ -306,9 +401,9 @@ class MelCloudAta extends EventEmitter {
306
401
  }
307
402
  }
308
403
 
309
- updateData(deviceData) {
404
+ updateData(deviceData, updateState = true) {
310
405
  this.locks = true;
311
- this.emit('deviceState', deviceData);
406
+ if (updateState) this.emit('deviceState', deviceData);
312
407
 
313
408
  setTimeout(() => {
314
409
  this.locks = false
@@ -23,6 +23,7 @@ class MelCloudHome extends EventEmitter {
23
23
  this.buildingsFile = buildingsFile;
24
24
  this.devicesFile = devicesFile;
25
25
  this.headers = {};
26
+ this.wsHeaders = {};
26
27
 
27
28
  this.functions = new Functions(this.logWarn, this.logError, this.logDebug)
28
29
  .on('warn', warn => this.emit('warn', warn))
@@ -208,6 +209,7 @@ class MelCloudHome extends EventEmitter {
208
209
  devicesList.Devices = devices;
209
210
  devicesList.Scenes = scenes;
210
211
  devicesList.Headers = this.headers;
212
+ devicesList.WsHeaders = this.wsHeaders;
211
213
 
212
214
  await this.functions.saveData(this.devicesFile, devicesList);
213
215
  if (this.logDebug) this.emit('debug', `${devicesCount} devices saved`);
@@ -273,21 +275,25 @@ class MelCloudHome extends EventEmitter {
273
275
  const page = await browser.newPage();
274
276
  page.on('error', error => this.emit('error', `Page crashed: ${error.message}`));
275
277
  page.on('pageerror', error => this.emit('error', `Browser error: ${error.message}`));
276
- // Zmienna na znaleziony hash
277
- let melcloudHash = null;
278
-
279
- // Nasłuchuj WebSocketów
280
- page.on('request', req => {
281
- if (req.url().startsWith('wss://ws.melcloudhome.com/?hash=')) {
282
- const url = req.url();
283
- const params = new URL(url).searchParams;
284
- melcloudHash = params.get('hash');
285
- if (!this.logDebug) this.emit('warn', `FOUND HASH: ${melcloudHash}`);
286
- }
287
- });
288
278
  page.setDefaultTimeout(GLOBAL_TIMEOUT);
289
279
  page.setDefaultNavigationTimeout(GLOBAL_TIMEOUT);
290
280
 
281
+ // === CDP session ===
282
+ let hash = null;
283
+ const client = await page.createCDPSession();
284
+ await client.send('Network.enable')
285
+ client.on('Network.webSocketCreated', ({ url }) => {
286
+ try {
287
+ if (url.startsWith('wss://ws.melcloudhome.com/?hash=')) {
288
+ const params = new URL(url).searchParams;
289
+ hash = params.get('hash');
290
+ if (this.logDebug) this.emit('debug', `MelCloudHome WS hash detected: ${hash}`);
291
+ }
292
+ } catch (err) {
293
+ this.emit('error', `CDP WebSocketCreated handler error: ${err.message}`);
294
+ }
295
+ });
296
+
291
297
  try {
292
298
  await page.goto(ApiUrlsHome.BaseURL, { waitUntil: ['domcontentloaded', 'networkidle2'], timeout: GLOBAL_TIMEOUT });
293
299
  } catch (error) {
@@ -368,6 +374,16 @@ class MelCloudHome extends EventEmitter {
368
374
  headers: headers
369
375
  })
370
376
 
377
+ this.wsHeaders = {
378
+ hash: hash,
379
+ headers: {
380
+ 'Origin': ApiUrlsHome.BaseURL,
381
+ 'Pragma': 'no-cache',
382
+ 'Cache-Control': 'no-cache'
383
+ }
384
+ };
385
+
386
+
371
387
  accountInfo.State = true;
372
388
  accountInfo.Info = 'Connect to MELCloud Home Success';
373
389
  await this.functions.saveData(this.accountFile, accountInfo);